@sincpro/client-framework 0.6.0 → 1.0.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 (132) hide show
  1. package/AGENTS.md +54 -25
  2. package/MANIFESTO.md +59 -39
  3. package/README.md +324 -345
  4. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  5. package/dist/client/{memory.js → adapters/memory.js} +2 -2
  6. package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
  7. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
  8. package/dist/client/{rest.js → adapters/rest.js} +2 -2
  9. package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
  10. package/dist/client/index.d.ts +16 -0
  11. package/dist/client/index.js +16 -0
  12. package/dist/client/services/propagation.d.ts +32 -0
  13. package/dist/{services/execution.js → client/services/propagation.js} +5 -21
  14. package/dist/client/{retry.js → services/retry.js} +1 -1
  15. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
  16. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  17. package/dist/codegen/{names.js → domain/names.js} +3 -3
  18. package/dist/codegen/{cli.js → entrypoint/cli.js} +9 -8
  19. package/dist/codegen/index.d.ts +7 -7
  20. package/dist/codegen/index.js +5 -10
  21. package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
  22. package/dist/codegen/{client.js → services/client.js} +9 -1
  23. package/dist/codegen/services/domain.d.ts +20 -0
  24. package/dist/codegen/{domain.js → services/domain.js} +82 -25
  25. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
  26. package/dist/codegen/{entities.js → services/entities.js} +18 -4
  27. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  28. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
  29. package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
  30. package/dist/common/index.d.ts +10 -0
  31. package/dist/common/index.js +10 -0
  32. package/dist/context/adapters/memory.d.ts +6 -0
  33. package/dist/context/adapters/memory.js +34 -0
  34. package/dist/context/adapters/plain-codec.d.ts +3 -0
  35. package/dist/context/adapters/plain-codec.js +16 -0
  36. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  37. package/dist/context/{codec.js → domain/codec.js} +0 -14
  38. package/dist/context/domain/execution.d.ts +33 -0
  39. package/dist/context/domain/execution.js +11 -0
  40. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  41. package/dist/context/{store.js → domain/store.js} +1 -34
  42. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  43. package/dist/context/index.d.ts +17 -0
  44. package/dist/context/index.js +15 -0
  45. package/dist/context/infrastructure/process.d.ts +13 -0
  46. package/dist/context/infrastructure/process.js +35 -0
  47. package/dist/core/domain/bus.d.ts +18 -0
  48. package/dist/core/domain/bus.js +18 -0
  49. package/dist/core/domain/dto.d.ts +32 -0
  50. package/dist/core/domain/dto.js +20 -0
  51. package/dist/core/domain/errors.d.ts +18 -0
  52. package/dist/core/domain/errors.js +1 -0
  53. package/dist/core/domain/services.d.ts +37 -0
  54. package/dist/core/domain/services.js +28 -0
  55. package/dist/core/entrypoint/bounded-context.d.ts +70 -0
  56. package/dist/core/entrypoint/bounded-context.js +252 -0
  57. package/dist/core/entrypoint/screens.d.ts +6 -0
  58. package/dist/core/entrypoint/screens.js +136 -0
  59. package/dist/core/index.d.ts +17 -0
  60. package/dist/core/index.js +16 -0
  61. package/dist/core/infrastructure/process.d.ts +56 -0
  62. package/dist/core/infrastructure/process.js +35 -0
  63. package/dist/core/services/binding.d.ts +6 -0
  64. package/dist/core/services/binding.js +13 -0
  65. package/dist/core/services/error-handlers.d.ts +11 -0
  66. package/dist/core/services/error-handlers.js +21 -0
  67. package/dist/core/services/execution.d.ts +35 -0
  68. package/dist/core/services/execution.js +137 -0
  69. package/dist/index.d.ts +16 -38
  70. package/dist/index.js +16 -38
  71. package/dist/model/index.d.ts +10 -0
  72. package/dist/model/index.js +10 -0
  73. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  74. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  75. package/dist/model/{schema.js → services/schema.js} +1 -1
  76. package/dist/observability/adapters/console.d.ts +12 -0
  77. package/dist/observability/adapters/console.js +35 -0
  78. package/dist/observability/adapters/memory.d.ts +41 -0
  79. package/dist/observability/adapters/memory.js +49 -0
  80. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  81. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  82. package/dist/observability/domain/errors.d.ts +23 -0
  83. package/dist/observability/domain/errors.js +8 -0
  84. package/dist/observability/domain/logging.d.ts +42 -0
  85. package/dist/observability/domain/logging.js +1 -0
  86. package/dist/observability/domain/tracing.d.ts +49 -0
  87. package/dist/observability/domain/tracing.js +20 -0
  88. package/dist/observability/index.d.ts +16 -0
  89. package/dist/observability/index.js +4 -0
  90. package/dist/observability/infrastructure/registry.d.ts +21 -0
  91. package/dist/observability/infrastructure/registry.js +18 -0
  92. package/dist/observability/services/logging.d.ts +27 -0
  93. package/dist/observability/services/logging.js +105 -0
  94. package/dist/observability/services/outcome.d.ts +12 -0
  95. package/dist/observability/services/outcome.js +20 -0
  96. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  97. package/dist/testing/index.d.ts +3 -33
  98. package/dist/testing/index.js +5 -37
  99. package/package.json +16 -8
  100. package/dist/codegen/domain.d.ts +0 -15
  101. package/dist/observability/ports.d.ts +0 -65
  102. package/dist/observability/ports.js +0 -21
  103. package/dist/services/bounded-context.d.ts +0 -79
  104. package/dist/services/bounded-context.js +0 -261
  105. package/dist/services/execution.d.ts +0 -63
  106. package/dist/services/feature.d.ts +0 -85
  107. package/dist/services/feature.js +0 -86
  108. package/dist/services/process.d.ts +0 -60
  109. package/dist/services/process.js +0 -57
  110. package/dist/services/screens.d.ts +0 -2
  111. package/dist/services/screens.js +0 -76
  112. /package/dist/{adapters → client/adapters}/parquet.js +0 -0
  113. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  114. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  115. /package/dist/client/{entity.js → domain/entity.js} +0 -0
  116. /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
  117. /package/dist/client/{transport.js → domain/transport.js} +0 -0
  118. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  119. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  120. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  121. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  122. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  123. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  124. /package/dist/codegen/{check.js → services/check.js} +0 -0
  125. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  126. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  127. /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
  128. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  129. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  130. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  131. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  132. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
package/README.md CHANGED
@@ -1,425 +1,404 @@
1
1
  # @sincpro/client-framework
2
2
 
3
- **The client side of [sincpro_framework](https://github.com/Sincpro-SRL/sincpro_framework), for
4
- every platform.** Write a use case once — a `Feature`, or an `ApplicationService` that composes
5
- Features — and run it in a browser, on a phone, in a server-side render, in a script or in a test.
6
- Every use case is traced and every unexpected error reported, continuing the backend's trace,
7
- without its author writing either. A service's client contract becomes typed code for each of
8
- its bounded contexts.
9
-
10
- > **Status: 0.x.** The API may still change before 1.0. In: the client, the generator, the
11
- > bounded context with its `Feature` and `ApplicationService`, the process (tracing, error
12
- > reporting, the context) and its ports. Read the [manifesto](MANIFESTO.md) for what this package
13
- > is responsible for, what it is not and where it is going, and the
14
- > [ubiquitous language](docs/UBIQUITOUS_LANGUAGE.md) every Sincpro framework shares.
15
-
16
- ## What it gives, at a glance
17
-
18
- ```mermaid
19
- classDiagram
20
- direction LR
21
- class UseBoundedContext {
22
- +name
23
- +context : Context
24
- +addDependency(name, value)
25
- +register(features, applicationServices) BoundedContext
26
- }
27
- class BoundedContext {
28
- +Issues : the methods of Issues
29
- +CloseAndNotify : the methods of CloseAndNotify
30
- +context : Context
31
- +with(dependencies) BoundedContext
32
- +scoped(context) BoundedContext
33
- }
34
- class Feature {
35
- #client, session : each dependency
36
- #context : what this run executes with
37
- }
38
- class ApplicationService {
39
- #feature : Issues, Notices
40
- }
41
- class Context {
42
- +values
43
- +get(name)
44
- +set(values)
45
- +clear(names)
46
- +watch(listener)
47
- }
48
- class ContextStore {
49
- <<port>>
50
- +load(key)
51
- +save(key, values)
52
- +delete(key)
53
- +version(key)
54
- +watch(key, listener)
55
- }
56
- class Tracer {
57
- <<port>>
58
- +start(name, parent, attributes) Span
59
- }
60
- class ErrorReporter {
61
- <<port>>
62
- +report(error, about)
63
- }
64
- class Transport {
65
- <<port>>
66
- +list(route, criteria)
67
- +one(route)
68
- +send(method, route, body)
69
- }
70
- UseBoundedContext --> BoundedContext : register
71
- BoundedContext o-- Feature : each by its name
72
- Feature <|-- ApplicationService
73
- BoundedContext --> Context : its level
74
- Context --> ContextStore : kept in
75
- BoundedContext ..> Tracer : a span per use case
76
- BoundedContext ..> ErrorReporter : what nobody expected
77
- Feature ..> Transport : through its generated client
78
- ```
79
-
80
- `configureProcess({ tracer, reporter, context })` plugs the ports once; `processContext` is the
81
- process's level of the context. Beside them: the generated client (`sincpro-client generate`),
82
- `entity()`, model definitions (`defineMeta`, `metaFromSchema`) and client-side ids.
83
-
84
- ## A bounded context, set up like the backend's
85
-
86
- Each bounded context of an application has the layout sincpro_framework gives a service: what its
87
- services can count on, its bases, its services, and the one object the rest of the application
88
- uses it through.
89
-
90
- ```mermaid
91
- flowchart LR
92
- subgraph py["sincpro_framework · Python"]
93
- direction TB
94
- pd["infrastructure/dependencies.py<br/>DependencyContextType · add_dependency"]
95
- pf["infrastructure/framework.py<br/>Feature · ApplicationService · UseFramework"]
96
- ps["services/<br/>@bus.feature · @bus.app_service"]
97
- pi["__init__.py<br/>the bounded context"]
98
- pd --> pf --> ps --> pi
99
- end
100
- subgraph ts["@sincpro/client-framework · TypeScript"]
101
- direction TB
102
- td["infrastructure/dependencies.ts<br/>ProjectDependencies · addDependency"]
103
- tf["infrastructure/bounded_context.ts<br/>Feature · ApplicationService · UseBoundedContext"]
104
- tsv["services/<br/>features · applicationServices"]
105
- ti["index.ts<br/>export const project"]
106
- td --> tf --> tsv --> ti
107
- end
108
- ti --> users["a screen · another bounded context · a test<br/>project.Issues.close(input)"]
109
- pi -. "the client contract" .-> td
110
- ```
111
-
112
- ```ts
113
- // domains/project/infrastructure/dependencies.ts — what the services can count on
114
- export interface ProjectDependencies {
115
- client: ProjectClient; // generated from the service's contract
116
- session: Session;
117
- }
118
-
119
- export function registerDependencies(bc: UseBoundedContext<ProjectDependencies>) {
120
- bc.addDependency("client", clientOf(transport));
121
- bc.addDependency("session", session);
122
- return bc;
123
- }
124
- ```
3
+ The client side of [sincpro_framework](https://github.com/Sincpro-SRL/sincpro_framework), for
4
+ the web, React Native, scripts and tests: the same application layer as the backend —
5
+ Commands, Features, ApplicationServices and a bounded context that is their bus —, observed
6
+ the same way, and a typed client generated from a service's contract. No React, no DOM, no
7
+ platform storage: what a platform must give, it gives through a port.
8
+
9
+ - [A bounded context](#a-bounded-context)
10
+ - [Errors](#errors)
11
+ - [Observability](#observability)
12
+ - [Context](#context)
13
+ - [Talking to a server](#talking-to-a-server)
14
+ - [Generated code](#generated-code)
15
+ - [Values on the wire](#values-on-the-wire)
16
+ - [Package layout](#package-layout)
17
+ - [Development](#development)
18
+
19
+ ## A bounded context
125
20
 
126
21
  ```ts
127
- // domains/project/infrastructure/bounded_context.ts
128
22
  import {
129
- ApplicationService as BaseApplicationService,
130
- Feature as BaseFeature,
131
- UseBoundedContext,
23
+ ApplicationService,
24
+ BoundedContext,
25
+ Command,
26
+ Feature,
27
+ Response,
132
28
  } from "@sincpro/client-framework";
133
29
 
134
- import type { ProjectFeatures } from "../services";
30
+ class ResponseTotal extends Response {
31
+ constructor(readonly total: number) {
32
+ super();
33
+ }
34
+ }
135
35
 
136
- export class Feature extends BaseFeature<ProjectDependencies> {}
137
- export class ApplicationService extends BaseApplicationService<
138
- ProjectDependencies,
139
- ProjectFeatures
140
- > {}
36
+ class CommandTotal extends Command<ResponseTotal> {
37
+ constructor(readonly quantity: number) {
38
+ super();
39
+ }
40
+ }
141
41
 
142
- export function configProjectBoundedContext(name: string) {
143
- return registerDependencies(new UseBoundedContext<ProjectDependencies>(name));
42
+ class CommandQuote extends Command<ResponseTotal> {
43
+ constructor(readonly quantity: number) {
44
+ super();
45
+ }
144
46
  }
145
- ```
146
47
 
147
- ```ts
148
- // domains/project/services/issues.ts — a Feature is a facade: one part of the domain
149
- export class Issues extends Feature {
150
- close(input: dto.CommandCloseIssue) {
151
- this.session.require(); // each dependency is a typed property
152
- return this.client.closeIssue(input); // carries this run's traceparent
48
+ class Total extends Feature<CommandTotal, ResponseTotal> {
49
+ static readonly serviceName = "Total";
50
+ declare readonly unitPrice: number; // a dependency, typed
51
+
52
+ execute(command: CommandTotal): ResponseTotal {
53
+ this.logger.info("Totalling", { quantity: command.quantity });
54
+ return new ResponseTotal(command.quantity * this.unitPrice);
153
55
  }
154
56
  }
155
57
 
156
- // domains/project/services/close_and_notify.ts — composes the Features of its bounded context
157
- export class CloseAndNotify extends ApplicationService {
158
- async execute(input: dto.CommandCloseIssue) {
159
- await this.feature.Issues.close(input); // a child span, the same trace
160
- return this.feature.Notices.tell(input);
58
+ class Quote extends ApplicationService<CommandQuote, ResponseTotal> {
59
+ static readonly serviceName = "Quote";
60
+
61
+ execute(command: CommandQuote): Promise<ResponseTotal> {
62
+ return this.featureBus.execute(new CommandTotal(command.quantity));
161
63
  }
162
64
  }
163
65
 
164
- // domains/project/services/index.ts
165
- export const features = { Issues, Notices };
166
- export const applicationServices = { CloseAndNotify };
167
- export type ProjectFeatures = typeof features;
66
+ const sales = new BoundedContext<{ unitPrice: number }>("sales");
67
+ sales.addDependency("unitPrice", 10);
68
+ sales.feature(CommandTotal)(Total);
69
+ sales.appService(CommandQuote)(Quote);
70
+
71
+ const answer = await sales.execute(new CommandQuote(3)); // ResponseTotal — total 30
168
72
  ```
169
73
 
170
- ```ts
171
- // domains/project/index.ts
172
- export const project = configProjectBoundedContext("project").register({
173
- features,
174
- applicationServices,
175
- });
74
+ A Command names, by its type, the Response it answers: `execute` infers it, with no second
75
+ argument (the backend's `bus(dto, ResponseType)` is accepted too, and checked in TypeScript
76
+ only). A Response may be a class, a plain object, a page, a primitive or `void`.
77
+
78
+ - **One Command, one service.** A Command class is registered once, across both layers, and
79
+ dispatched by its exact constructor. `serviceName` names the service in spans and logs —
80
+ stable, where a minifier renames classes.
81
+ - **Dependencies are attributes.** `addDependency` supplies them; a service declares their types
82
+ (`declare readonly client: …`), usually once on a base class per bounded context, as
83
+ `sincpro-client scaffold` writes it. Declaring registers nothing; reading a missing one fails.
84
+ A replacement names what it replaces: `addDependency(name, next, { replaces: previous })`.
85
+ - **A service is a singleton of its generation.** What belongs to one request goes in the
86
+ Command or the context, never on `this`. Use TypeScript `private`, not `#private`: a run sees
87
+ the instance through a view of its own.
88
+ - **Generations.** The first call (or `buildRootBus()`) builds one; what is registered later
89
+ answers from the next call, and a running call keeps its own. `fresh()` copies the
90
+ registrations; `scoped({ company_id: 2 })` runs with more context and the same instances.
91
+ - **Composition.** An ApplicationService runs its context's Features through `this.featureBus`,
92
+ as children of its own execution; never another ApplicationService.
93
+
94
+ ### Decorators, when the project has them
176
95
 
177
- // anywhere: a screen, a script, a test
178
- await project.Issues.close(input); // span project/Issues.close
179
- await project.CloseAndNotify.execute(input); // project/CloseAndNotify → its Features under it
96
+ ```ts
97
+ @sales.feature(CommandTotal)
98
+ class Total extends Feature<CommandTotal, ResponseTotal> { … }
180
99
  ```
181
100
 
182
- The generator writes this skeleton for you, once, from the service's contract — and never touches
183
- a file that is already there, because from then on it is yours:
101
+ The same functions work as standard decorators and as legacy ones (`experimentalDecorators`);
102
+ without a decorator transform, call them: `sales.feature(CommandTotal)(Total)`. No DI library
103
+ and no `reflect-metadata`. The tests compile both decorator forms with TypeScript; a bundler's
104
+ transform (Vite, Metro/Babel) is each application's to configure and test.
184
105
 
185
- ```bash
186
- sincpro-client generate project.contract.json --out src/domains/project/generated --domain project
187
- sincpro-client scaffold project.contract.json --out src/domains/project # dependencies, bases, services, index
188
- ```
106
+ ## Errors
189
107
 
190
- And the layers stay layers: `sincpro-client check` resolves every import of the application with
191
- its own TypeScript and `tsconfig` (aliases included) and fails when a bounded context reaches into
192
- another's insides, a service imports React or web-ui, `common` imports a bounded context, or the
193
- entrypoints skip a bounded context's `index.ts` — put it in `make doctor`:
108
+ As the backend's, a bounded context has three chains of error handlers, run
109
+ first-registered-first. A handler returns the answer, or throws to hand the error on; the last
110
+ one's throw reaches the caller:
194
111
 
195
- ```bash
196
- sincpro-client check --root src
197
- ```
112
+ ```ts
113
+ sales.addFeatureErrorHandler((error) => {
114
+ if (error instanceof PriceNotFound) return new ResponseTotal(0); // the answer
115
+ throw error; // the next handler, or the caller
116
+ });
117
+ sales.addAppServiceErrorHandler(toBusinessError);
118
+ sales.addGlobalErrorHandler(toProblem); // what reaches sales.execute or a scope
198
119
 
199
- Every public method is a use case: a span named as the backend names its own
200
- (`project/Issues.close`, identity `project.Issues.close`), and a report when it fails in a way
201
- nobody expected. One gesture is one trace, from the click to the database:
202
-
203
- ```mermaid
204
- sequenceDiagram
205
- participant caller as Caller<br/>(a screen, a test)
206
- participant app as project.CloseAndNotify<br/>(ApplicationService)
207
- participant feature as this.feature.Issues<br/>(Feature)
208
- participant client as this.client<br/>(generated, bound to the run)
209
- participant process as Process<br/>(Tracer · ErrorReporter)
210
- participant server as sincpro_framework
211
-
212
- caller->>app: execute(input)
213
- app->>process: span project/CloseAndNotify — the context read once
214
- app->>feature: close(input)
215
- feature->>process: child span project/Issues.close
216
- feature->>client: closeIssue(input)
217
- client->>server: POST /issues/{issue_id}/close · traceparent · sincpro-context
218
- server-->>client: ResponseCloseIssue — or a Problem
219
- client-->>caller: the answer, through each run
220
- Note over process: each run ends ok · refused (shown, never reported) · aborted · failed (reported once, with its trace id)
120
+ sales.expectErrors(QuoteExpired); // the business answering: `refused`, never reported
221
121
  ```
222
122
 
223
- `project` holds references only — each service is made once, the first time it is
224
- asked for — and it is never frozen: `project.with({ client: fake })` and
225
- `project.scoped({ company_id: 2 })` derive a variant that shares everything else.
123
+ A failure is observed where it happened, before any handler runs: its span closes with its
124
+ outcome, its log line is written and, when unexpected, it is reported once. A handler decides
125
+ the answer, not whether the failure is seen; an answered one adds an `error.handled` line. A
126
+ feature handler answers wherever the Feature ran, inside an ApplicationService too; the global
127
+ chain wraps only `execute` and `scoped(...).execute`.
128
+
129
+ How an error ended a use case:
130
+
131
+ | Outcome | When | Reported |
132
+ | --------- | ---------------------------------------------------------------------------- | -------- |
133
+ | `ok` | it answered | — |
134
+ | `refused` | the server answered a 4xx (`TransportError`), or the class is `expectErrors` | no |
135
+ | `aborted` | whoever asked stopped waiting (`AbortError`) | no |
136
+ | `failed` | anything else: a bug, a 5xx, a server nobody reached | yes |
226
137
 
227
- ## The process: tracing, errors and the context
138
+ `configureProcess({ outcomeOf })` replaces the process-wide rule. `reportError(error, where)`
139
+ reports what fails outside a use case — a view, a worker —, with the same rule.
228
140
 
229
- What every bounded context runs with is set once, where the application starts — the way the
230
- backend's process is. All of it is optional:
141
+ ## Observability
142
+
143
+ The platform plugs the implementations once, at startup; nothing breaks without them:
231
144
 
232
145
  ```ts
233
- // entrypoints/main.ts
146
+ import { configureProcess, memoryContexts } from "@sincpro/client-framework";
147
+ import { openTelemetryTracer } from "@sincpro/client-framework/opentelemetry";
148
+ import { sentryReporter } from "@sincpro/client-framework/sentry";
149
+
234
150
  configureProcess({
235
- tracer, // the platform's Tracer
236
- reporter, // the platform's ErrorReporter
237
- context: memoryContexts(), // where the context lives (this is the default)
151
+ tracer: openTelemetryTracer(provider.getTracer("forge-web")), // the app's provider
152
+ reporter: sentryReporter(client), // the app's Sentry or GlitchTip client
153
+ logger: { mode: production ? "production" : "development" },
154
+ context: memoryContexts(),
238
155
  });
239
-
240
- processContext.set({ tenant_id: "acme" }); // every use case runs with it and sends it
241
- project.context.set({ warehouse_id: 4 }); // only what «project» runs
242
156
  ```
243
157
 
244
- What a screen reads and writes is a use case too when it goes through `screensTransport(transport)`:
245
- each request is `screens/EntityReads.list`, `…/EntityWrites.send` — traced, with the context on
246
- it, and reported when it failed.
247
-
248
- A service reads it as `this.context` — the process's values, under them its bounded context's,
249
- under them a scope's, read once where the gesture started — and every request it sends carries
250
- it in `sincpro-context`. A screen hears it change with `processContext.watch(…)`.
251
-
252
- ```mermaid
253
- flowchart LR
254
- p["processContext<br/>tenant_id · company_id"]
255
- b["project.context<br/>warehouse_id"]
256
- s["project.scoped({ company_id: 2 })<br/>one window"]
257
- e["this.context<br/>read once where the gesture starts"]
258
- c["this.context of each Feature it runs<br/>the same values"]
259
- h["sincpro-context<br/>on every request"]
260
- store[("ContextStore<br/>the platform's")]
261
- p --> e
262
- b --> e
263
- s --> e
264
- e --> c
265
- e --> h
266
- c --> h
267
- p -.-> store
268
- b -.-> store
269
- ```
158
+ Every execution opens a span `<bounded context>/<serviceName>`; an ApplicationService's Features
159
+ are its children, on the same trace. A dependency that is a `Transport` or a generated client is
160
+ bound to the run, so each request carries `traceparent` (and `tracestate`) and
161
+ `sincpro-context` — the backend continues the trace. A tracer, reporter or logger that fails is
162
+ ignored: telling about a use case never changes its answer.
270
163
 
271
- The context lives in a `ContextStore`, the port each platform implements with its own technology:
272
- memory by default, `sessionStorage` or the screens' own store in a browser, MMKV on a phone,
273
- `AsyncLocalStorage` in a server that renders. The services never know which.
164
+ The vendor entries wrap what the application built — they initialize nothing, read no
165
+ environment and register nothing global. There is no context manager: the trace reaches the
166
+ wire through the transport. On the web, leave the backend's origin out of OpenTelemetry's
167
+ automatic `fetch` instrumentation (`ignoreUrls`), or it writes a `traceparent` of a trace of its
168
+ own over this one.
274
169
 
275
- `tracer` and `reporter` are injected by the application — this package initialises nothing.
276
- Any object that fulfils the port works; for the common vendors there are optional adapters that
277
- wrap what the application built:
170
+ ### Logging
171
+
172
+ The application layer imports no logger. Two are predefined, for the code around the use cases:
278
173
 
279
174
  ```ts
280
- import { openTelemetryTracer } from "@sincpro/client-framework/opentelemetry"; // peer: @opentelemetry/api
281
- import { sentryReporter } from "@sincpro/client-framework/sentry"; // no dependency
175
+ import { logger, loggerAdapter } from "@sincpro/client-framework";
282
176
 
283
- const tracer = openTelemetryTracer(provider.getTracer("my-app")); // the app's provider and exporter
284
- const reporter = sentryReporter(client); // the app's Sentry-compatible client (GlitchTip too)
177
+ logger.info("Database opened"); // infrastructure — the global logger
178
+ loggerAdapter.warn("Printer is slow", { ms: 900 }); // an adapter
179
+ sales.logger.info("Catalog preloaded"); // a bounded context, outside a use case
180
+ this.logger.info("Totalling"); // inside a Feature or ApplicationService
285
181
  ```
286
182
 
287
- What fails outside a use case — a screen that broke while drawing, a subscriber, a worker — goes to
288
- the same reporter with `reportError(error, where)`: once (an error a use case already reported is
289
- not sent again), with the process's context.
183
+ | Logger | Layer | Each line carries |
184
+ | ------------------------- | --------------------- | ---------------------------------------------- |
185
+ | `logger` | `infrastructure` | — |
186
+ | `loggerAdapter` | `adapter` | the run, when injected as a dependency |
187
+ | `sales.logger` | `bounded_context` | its bounded context |
188
+ | `this.logger`, Feature | `feature` | bounded context, use case, execution and trace |
189
+ | `this.logger`, AppService | `application_service` | the same |
290
190
 
291
- Nothing is installed or shipped unless it is imported, and without a tracer or a reporter every use
292
- case runs the same — nothing traces, nothing reports. In a test, `memoryTracer()` and
293
- `memoryReporter()` (`@sincpro/client-framework/testing`).
191
+ `this.logger` keeps its run across awaits and concurrent calls, without a global stack. An
192
+ adapter that imports `loggerAdapter` at module level writes without the run; inject it
193
+ (`sales.addDependency("log", loggerAdapter)`) or pass `this.logger` when a line must carry it.
294
194
 
295
- ## Values on the wire
195
+ One configuration, two doors — `configureProcess({ logger })` replaces every process port,
196
+ `configureLogger(...)` only logging (an in-app debug switch):
197
+
198
+ ```ts
199
+ configureLogger({ mode: "development", level: "info", layers: { adapter: false } }); // the console
200
+ configureLogger({ mode: "production" }); // the console, silent
201
+ configureLogger({ engine: combineLoggers(consoleLogger({ mode }), lokiLogger) }); // other sinks
202
+ configureLogger({ engine: false }); // no logs
203
+ ```
204
+
205
+ The console is the default engine. In production it is silent; in development it prints
206
+ everything, and `level` and `layers` hide only INFO/DEBUG. A supplied `engine` owns its own
207
+ policy, and each sink of `combineLoggers` keeps its own — the console's silence never silences
208
+ another. Every execution writes `execution.started` (DEBUG) and `execution.finished` with its
209
+ outcome and duration (INFO, WARN when refused, ERROR when failed). No line carries a DTO, a
210
+ result, an error body or the session's context unless the author writes it. Delivery, buffering
211
+ and credentials (Loki, a phone's queue) are the platform's.
296
212
 
297
- The backend writes every message with one set of rules, and every client reads and writes them
298
- the same — so they live here, not in each platform:
299
-
300
- | Value | Travels as | Here |
301
- | ------------ | ------------------------------------------ | -------------------------------------------------- |
302
- | a decimal | text, every digit: `"10.50"` | `Decimal` — exact, on `BigInt`; rounds half up |
303
- | a money | `{ "amount": "10.50", "currency": "BOB" }` | `Money` — never adds two currencies |
304
- | a day | `"2026-10-02"`, never converted | `isDay`, `dayOf(instant, zone)` for «today» |
305
- | an instant | ISO text: `"2026-10-02T14:30:00Z"` | `wallTimeOf` / `instantOf` on the company's zone |
306
- | an id | a UUID v7 as 32 hex | `newEntityId`, `isEntityId`, `momentOf` |
307
- | bytes | `{ "$bytes": "<base64>" }` | `valuesOf` / `readValues` |
308
- | the context | only what travels: text, numbers, lists | `plainCodec`, `travelling` |
309
- | a to-many | `{ "items": […], "count": {…}, "cursor" }` | `EntityCollection<T>`, `collectionOf`, `isPartial` |
310
- | an export | Parquet, Arrow or JSON Lines, streamed | `downloadRoute`, `exportRoute`, `Download` |
311
- | a large page | Parquet, when the search says so | `restTransport({ decoders: [parquetDecoder] })` |
213
+ ## Context
214
+
215
+ The values every use case runs with — the tenant, the company —, in levels: the process's, the
216
+ bounded context's, a scope's. A root execution reads them once; its Features inherit that
217
+ snapshot, and what travels goes to the server in `sincpro-context`.
312
218
 
313
219
  ```ts
314
- import { Money, pack, unpack, valuesOf } from "@sincpro/client-framework";
220
+ processContext.set({ tenant_id: "acme" });
221
+ sales.context.set({ warehouse_id: 4 });
222
+ await sales.scoped({ company_id: 2 }).execute(new CommandQuote(1));
223
+ ```
315
224
 
316
- const line = new Money("19.90", "BOB").times("3"); // 59.70 BOB, exact
317
- const total = Money.total([line, delivery], "BOB").rounded(); // to the currency's decimals
318
- valuesOf({ total, at: new Date() }); // { total: { amount, currency }, at: "…Z" }
319
- const answer = unpack(text, Money); // a typed read when asked for — never imposed
225
+ Where the values live is a port (`ContextStore`): synchronous reads, a version per change and an
226
+ optional `watch`. `memoryContexts()` is the default; a browser, a phone or a server render plugs
227
+ its own. `values` is the same object until it changes, so a screen can hand it to React's
228
+ `useSyncExternalStore`.
229
+
230
+ ## Talking to a server
231
+
232
+ | Piece | What it does |
233
+ | ------------------------------ | ------------------------------------------------------------------------------------- |
234
+ | `restTransport({ base })` | REST over `fetch`: `Idempotency-Key`, the server's `Problem`, model versions, Parquet |
235
+ | `memoryTransport(tables)` | the same contract in memory, copying by the wire's rules — for tests and demos |
236
+ | `entity(key, routes)` | a model's routes and Commands, typed — what a generated `entities.ts` declares |
237
+ | `retryKeys()` | one `Idempotency-Key` per gesture, kept while a retry may repeat it |
238
+ | `screensTransport(transport)` | screen reads and writes run as use cases of `screens`: traced and reported |
239
+ | `parquetDecoder` (`./parquet`) | large pages read as Parquet through the same transport |
240
+
241
+ Inside a Feature, inject the transport or the generated client as they are — the run already
242
+ observes them. `screensTransport` is for what a screen asks directly, outside any Feature.
243
+
244
+ ## Generated code
245
+
246
+ ```bash
247
+ python -m sincpro_framework.entrypoints export-client-contract app.http:gateways contracts [--events]
248
+ sincpro-client generate contracts/project.contract.json --out src/domains/project/generated --domain project
249
+ sincpro-client scaffold contracts/project.contract.json --out src/domains/project
250
+ sincpro-client check --root src
320
251
  ```
321
252
 
322
- Not restrictive, like the backend: reading keeps every key it finds, a typed read is asked for
323
- (`rebuilt(values, Money)`), any currency code is read, and an application adds its own decimals
324
- (`Money.decimals["USDT"] = 6`). What JSON cannot write (`NaN`, a function, a cycle) is refused at
325
- once, naming where. Showing a value to a person — `Intl.NumberFormat`, a date picker — is each
326
- platform's, never this package's.
253
+ `generate` writes, from the contract the backend exports — never edited, written again whole:
327
254
 
328
- A relation that holds several records — the invoices of a customer — comes as a page of them,
329
- never an array: forty records, «at least 1000», and the cursor to go on. Read it with
330
- `collectionOf` (an array from a hand-written domain reads the same) — a `HeldCollection`, edited
331
- the way a string is: `append`, `extend`, `remove`, `replace` answer a new one. Say «1000+» when
332
- `count.exact` is false, and ask for the rest as a list of the related entity
333
- (`relatedCriteria(meta.fields.invoices, customer)`). A part of the record — the lines of an
334
- invoice — is written back only whole: the backend refuses a page of it.
255
+ | File | What |
256
+ | ------------- | ------------------------------------------------------------------------------------------ |
257
+ | `domain.ts` | a type per model, per `<Name>Input`, per operation's input and answer, per domain event |
258
+ | `client.ts` | `clientOf(transport)`: a method per operation REST reaches; `operations` and `events` data |
259
+ | `entities.ts` | each entity's list, record, edit, export, Commands and history, for screens |
335
260
 
336
- A search asked for millions of records answers Parquet instead of JSON to a client that reads it.
337
- Give the transport the decoder — an optional entry over `hyparquet` — and nothing above it
338
- changes: the same page, each value as the JSON wire writes it.
261
+ `scaffold` writes, once, the application's own files: its dependencies, its service bases and
262
+ `registerServices`. `check` enforces the application's layers and bounded-context boundaries.
263
+
264
+ **Domain events, on demand.** A backend leaves its events out of the contract unless it is
265
+ exported with `--events`. Then each event is a type as it is received — every field present,
266
+ `name` its wire name as a literal — and `DomainEvent`, their union, narrows by `name`:
339
267
 
340
268
  ```ts
341
- import { restTransport } from "@sincpro/client-framework";
342
- import { parquetDecoder } from "@sincpro/client-framework/parquet";
269
+ import type * as remote from "./generated/domain";
343
270
 
344
- const transport = restTransport({ base: "/v1", decoders: [parquetDecoder] });
271
+ function subjectOf(event: remote.DomainEvent): string {
272
+ switch (event.name) {
273
+ case "project.issue.v1.closed":
274
+ return event.issue_id; // remote.IssueClosed
275
+ // …
276
+ }
277
+ }
345
278
  ```
346
279
 
347
- ## Where it sits
348
-
349
- ```mermaid
350
- flowchart TB
351
- backend["sincpro_framework<br/>the service: its bounded contexts, Commands, the client contract"]
352
- criteria["@sincpro/criteria<br/>Criteria · Meta · Page · Problem"]
353
- cf["@sincpro/client-framework<br/>bounded contexts · services · process · context · ports · the client · the generator"]
354
- web["@sincpro/web-framework<br/>hooks · screens · the browser's tracer and reporter"]
355
- mobile["@sincpro/mobile<br/>React Native · SQLite · its queue"]
356
- rest["a script · a server-side render · a test"]
357
- web --> cf
358
- mobile --> cf
359
- rest --> cf
360
- cf --> criteria
361
- backend -. "client contract → sincpro-client generate" .-> cf
362
- cf -- "HTTP · traceparent · sincpro-context" --> backend
363
- ```
280
+ `events` in `client.ts` says, per event, its wire name and whether the bounded context records it
281
+ (its aggregates' own) or handles it (a Feature subscribes).
364
282
 
365
- No React, no DOM, no platform: what a platform must provide — how a span is exported, where an
366
- error is reported, where the context lives — it plugs through a port.
283
+ ### Types, not classes
367
284
 
368
- ## On each platform
285
+ What is generated is the remote service's contract and an adapter over it, so it is written as
286
+ types; the application's own Commands are classes, written by hand.
369
287
 
370
- This package asks the platform for little: `fetch`, `AbortSignal`, `URL` / `URLSearchParams`,
371
- `crypto.getRandomValues` (client-side ids, retry keys), `Proxy` / `WeakMap`. Browsers and Node 26
372
- have them all. **React Native** (Hermes) needs two polyfills, installed once where the app
373
- starts, before anything imports this package:
288
+ - **The wire is JSON, and a type says it exactly.** A decimal is text, a day is text, a page is
289
+ `{ items, count, cursor }`. A class around that JSON would claim more than it holds unless
290
+ every field had a reader; a type claims nothing it does not hold.
291
+ - **The local bus routes by class, and the remote contract must not reach it.** A generated
292
+ class extending `Command` could be registered and executed locally — the confusion between a
293
+ remote operation and a local use case this package keeps apart. A remote call happens inside
294
+ a local Feature, through the injected client.
295
+ - **Events read best as a union.** A stream of facts — a queue, a history page — is narrowed by
296
+ `name`, with no registry of constructors to rebuild each one.
297
+ - **Nothing runs.** `domain.ts` adds no byte to a bundle and no side effect; a contract of three
298
+ hundred models costs nothing at runtime.
299
+
300
+ A local class takes the contract's shape and stays checked against it:
374
301
 
375
302
  ```ts
376
- import "react-native-get-random-values"; // or expo-crypto's getRandomValues polyfill
377
- import "react-native-url-polyfill/auto"; // RN's own URL resolves relative paths poorly
303
+ import type * as remote from "../generated/domain";
304
+
305
+ class CommandCancelIssue
306
+ extends Command<ResponseCancelIssue>
307
+ implements remote.CommandCancelIssue
308
+ {
309
+ constructor(readonly issue_id: string) {
310
+ super();
311
+ }
312
+ }
378
313
  ```
379
314
 
380
- `memoryTransport` copies with `structuredClone` where the engine has it and a JSON round trip
381
- where it does not. The OpenTelemetry adapter takes `@opentelemetry/api` from 1.9, the version
382
- `@sincpro/mobile` runs.
383
-
384
- The services are the same everywhere; each platform plugs its own technology behind the same
385
- three ports, in its `main`:
315
+ The generated name and the local one may be the same — the backend's —, so the generated ones
316
+ are imported under their own (`remote`). Validating and converting what arrives (a decimal into
317
+ `Decimal`) is a reader's job, not a class's: generated readers are the next step, when a use
318
+ needs them.
386
319
 
387
- | Port | Browser (@sincpro/web-framework) | Phone (@sincpro/mobile) | Test |
388
- | --------------- | ----------------------------------------------- | ------------------------------------------ | ----------------- |
389
- | `Tracer` | OpenTelemetry + OTLP/HTTP (`./telemetry`) | OpenTelemetry with its queue and exporter | `memoryTracer` |
390
- | `ErrorReporter` | GlitchTip / Sentry browser client (`./errors`) | Sentry React Native client | `memoryReporter` |
391
- | `ContextStore` | memory, `sessionStorage`, or the screens' store | MMKV, or memory hydrated from AsyncStorage | `memoryContexts` |
392
- | `Transport` | `restTransport` over `fetch` | `restTransport`, or SQLite offline | `memoryTransport` |
320
+ ## Values on the wire
393
321
 
394
- How @sincpro/mobile adopts all of this — what maps to what, the MMKV store, the order to move
395
- in — is [`docs/MOBILE.md`](docs/MOBILE.md).
322
+ | Value | Travels as | API |
323
+ | ---------- | -------------------------------------- | ---------------------------------------------- |
324
+ | Decimal | exact text, `"10.50"` | `Decimal` |
325
+ | Money | `{ amount: "10.50", currency: "BOB" }` | `Money` |
326
+ | Day | `"2026-10-02"`, never an instant | `isDay`, `dayOf` |
327
+ | Instant | ISO text | `wallTimeOf`, `instantOf` |
328
+ | ID | UUID v7 as 32 hex | `newEntityId`, `isEntityId`, `momentOf` |
329
+ | Bytes | `{ "$bytes": "<base64>" }` | `valuesOf`, `readValues` |
330
+ | Collection | items, count and cursor | `collectionOf`, `isPartial`, `relatedCriteria` |
331
+ | Export | Parquet, Arrow or JSON Lines | `downloadRoute`, `exportRoute` |
332
+
333
+ `Decimal` and `Money` compute exactly, on `BigInt`; a decimal never becomes a `number` and a day
334
+ never a `Date`. `valuesOf`/`pack` write; `readValues` reads tagged values back; `rebuilt`/`unpack`
335
+ use a reader given explicitly. Cycles, functions and non-finite numbers are refused. How a value
336
+ is shown, typed or stored is the platform's.
337
+
338
+ ## Package layout
339
+
340
+ Each module is laid out as sincpro_framework's are, and says what it offers in its `index.ts`:
341
+
342
+ ```text
343
+ src/
344
+ common/ the values of the wire
345
+ context/
346
+ domain/ ContextStore and ContextCodec ports, what travels, Execution
347
+ adapters/ memoryContexts, plainCodec
348
+ entrypoint/ Context: one door over a level
349
+ infrastructure/ the process's store and processContext
350
+ observability/
351
+ domain/ Tracer, ErrorReporter, Logger ports
352
+ adapters/ console (default), memory, opentelemetry and sentry (optional entries)
353
+ infrastructure/ the ports this process runs with
354
+ services/ logger, loggerAdapter, reportError, outcomeOf
355
+ client/
356
+ domain/ Transport, TransportError, entity(), the client contract
357
+ adapters/ rest, memory, parquet (optional entry)
358
+ services/ discovery, retry keys, propagation of trace and context
359
+ model/ defineMeta, metaFromSchema, metaStore
360
+ core/
361
+ domain/ Command, Response, Feature, ApplicationService, FeatureBus, ErrorHandler
362
+ services/ the execution pipeline, dependency binding, the handler chain
363
+ entrypoint/ BoundedContext, screensTransport
364
+ infrastructure/ configureProcess, configureLogger
365
+ codegen/ the generator and its CLI — the only place Node is allowed
366
+ testing/ memoryTracer, memoryReporter, memoryLogger
367
+ ```
396
368
 
397
- A store's reads are synchronous — a use case reads its context where a gesture starts and never
398
- waits for it — so an asynchronous storage is held in memory and written through.
369
+ | Entry | What |
370
+ | ------------------------------------------------------- | ------------------------------------------- |
371
+ | `@sincpro/client-framework` | everything an application runs |
372
+ | `@sincpro/client-framework/core` | the application layer alone |
373
+ | `@sincpro/client-framework/testing` | the observability ports in memory |
374
+ | `@sincpro/client-framework/codegen` | the generator, as functions |
375
+ | `./console`, `./opentelemetry`, `./sentry`, `./parquet` | optional entries; a vendor's SDK only there |
399
376
 
400
377
  ## Development
401
378
 
402
- Node 26 (`nvm use`).
379
+ Node 26 (`nvm use`):
403
380
 
404
381
  ```bash
405
- make init # pre-commit + dependencies
406
- make test # vitest, in Node
407
- make verify-format # format + lint + doctor; fails if the tree is left dirty (what CI runs)
408
- make build # tsc into dist/
382
+ make init
383
+ make typecheck
384
+ make test
385
+ make verify-format # eslint --fix + prettier + typecheck + doctor; fails on a dirty tree
386
+ make build
387
+ make codegen-fixtures # after changing the generator on purpose; read the diff
409
388
  ```
410
389
 
411
- `make doctor` enforces what the type checker cannot: the source imports itself, never a relative
412
- path; no React, no UI, no browser global; Node only inside the generator. Read
413
- [`AGENTS.md`](AGENTS.md) before changing anything, and [`CONTRIBUTING.md`](CONTRIBUTING.md)
414
- before opening a pull request.
390
+ `make doctor` enforces what the compiler does not: self-imports, no platform (React, DOM, Node
391
+ outside `codegen/`), a vendor's SDK only in its optional entry, and `domain/` importing only
392
+ `domain/`. Tests run in Node, one folder per module; `tests/support/forge-contract/` holds real
393
+ contracts and what the generator writes from them, compared byte for byte.
415
394
 
416
- ## CI/CD
395
+ The runtime uses `fetch`, `AbortSignal`, `URL`, `crypto.getRandomValues`, `Proxy`, `WeakMap` and
396
+ `BigInt`; React Native must provide URL and random values before ids or transports are used.
417
397
 
418
- Every workflow in `.github/workflows/` delegates to
419
- [`Sincpro-SRL/.github`](https://github.com/Sincpro-SRL/.github) and only calls `make` targets:
420
- checks on every pull request and push to `main`, a release draft on `main`, publication to npm
421
- when a release is published, and notifications. The version is written by the release pipeline,
422
- never by hand.
398
+ Read [`MANIFESTO.md`](MANIFESTO.md) (what this package is and is not), [`AGENTS.md`](AGENTS.md)
399
+ (how to change it), [`CONTRIBUTING.md`](CONTRIBUTING.md),
400
+ [`docs/UBIQUITOUS_LANGUAGE.md`](docs/UBIQUITOUS_LANGUAGE.md) and
401
+ [`docs/MOBILE.md`](docs/MOBILE.md). Versions are written by the release pipeline.
423
402
 
424
403
  ## License
425
404