@sincpro/client-framework 0.2.0 → 0.4.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 (39) hide show
  1. package/AGENTS.md +51 -23
  2. package/MANIFESTO.md +44 -26
  3. package/README.md +304 -29
  4. package/dist/adapters/opentelemetry.d.ts +1 -1
  5. package/dist/adapters/opentelemetry.js +1 -1
  6. package/dist/adapters/sentry.d.ts +2 -2
  7. package/dist/adapters/sentry.js +2 -2
  8. package/dist/client/memory.js +14 -6
  9. package/dist/common/decimal.d.ts +49 -0
  10. package/dist/common/decimal.js +146 -0
  11. package/dist/{client → common}/ids.d.ts +7 -0
  12. package/dist/{client → common}/ids.js +12 -0
  13. package/dist/common/money.d.ts +74 -0
  14. package/dist/common/money.js +168 -0
  15. package/dist/common/serialization.d.ts +49 -0
  16. package/dist/common/serialization.js +155 -0
  17. package/dist/common/time.d.ts +40 -0
  18. package/dist/common/time.js +93 -0
  19. package/dist/context/codec.d.ts +24 -0
  20. package/dist/context/codec.js +28 -0
  21. package/dist/context/context.d.ts +45 -0
  22. package/dist/context/context.js +70 -0
  23. package/dist/context/store.d.ts +51 -0
  24. package/dist/context/store.js +59 -0
  25. package/dist/index.d.ts +17 -5
  26. package/dist/index.js +17 -5
  27. package/dist/observability/ports.d.ts +2 -2
  28. package/dist/services/bounded-context.d.ts +79 -0
  29. package/dist/services/bounded-context.js +272 -0
  30. package/dist/services/execution.d.ts +8 -8
  31. package/dist/services/execution.js +8 -6
  32. package/dist/services/feature.d.ts +70 -48
  33. package/dist/services/feature.js +57 -191
  34. package/dist/services/process.d.ts +60 -0
  35. package/dist/services/process.js +57 -0
  36. package/dist/testing/index.d.ts +3 -3
  37. package/package.json +2 -2
  38. package/dist/services/runtime.d.ts +0 -48
  39. package/dist/services/runtime.js +0 -30
package/AGENTS.md CHANGED
@@ -8,24 +8,30 @@ it says what this package is responsible for and what it is not. This file is ho
8
8
  - **Read the manifesto.** A piece that serves nothing there does not go in; a piece it forbids
9
9
  does not either. If you think it should, change the manifesto first and say why.
10
10
  - **Look at the backend.** This package mirrors sincpro_framework (`~/dev/sincpro_framework`):
11
- `Feature`, `ApplicationService`, the context, the span per use case. When in doubt about a
11
+ the bounded context and its dependencies, `Feature`, `ApplicationService`, the context and its
12
+ store, the process, the span per use case. When in doubt about a
12
13
  word or a behaviour, read how the backend does it, not memory.
14
+ - **Use the shared words.** [`docs/UBIQUITOUS_LANGUAGE.md`](docs/UBIQUITOUS_LANGUAGE.md) is the
15
+ vocabulary of every Sincpro framework. A new word, or a word that changes, goes there in the
16
+ same pull request; a word in its «do not use» table never enters code or docs.
13
17
  - **Look at who uses it.** @sincpro/web-framework (`~/dev/sincpro_web_framework`) and
14
18
  @sincpro/mobile (`~/dev/mobile/sincpro_mobile`). A piece only one of them needs, and that
15
19
  needs a platform, belongs there.
16
20
 
17
21
  ## Where things go
18
22
 
19
- | Folder | What |
20
- | -------------------- | ------------------------------------------------------------------------------------------- |
21
- | `src/client/` | The seam with a server: `Transport`, REST, memory, `entity()`, the client contract, ids |
22
- | `src/model/` | Model definitions: `defineMeta`, from a JSON Schema, the store of known ones |
23
- | `src/services/` | `Feature`, `ApplicationService`, the runtime, the execution and what binds to it |
24
- | `src/observability/` | The ports a platform plugs: `Tracer`, `ErrorReporter` |
25
- | `src/codegen/` | The generator and its CLI (`sincpro-client generate`) — the only place Node is allowed |
26
- | `src/adapters/` | Optional entries over a vendor (`./opentelemetry`, `./sentry`): the only place one is named |
27
- | `src/testing/` | `memoryTracer`, `memoryReporter` — the ports in memory, for tests |
28
- | `tests/` | vitest in Node; `tests/support/` holds the fake domains and forge's real contracts |
23
+ | Folder | What |
24
+ | -------------------- | --------------------------------------------------------------------------------------------- |
25
+ | `src/common/` | The values of the wire: ids, a day and an instant, `Decimal`, `Money`, `valuesOf` / `rebuilt` |
26
+ | `src/client/` | The seam with a server: `Transport`, REST, memory, `entity()`, the client contract |
27
+ | `src/model/` | Model definitions: `defineMeta`, from a JSON Schema, the store of known ones |
28
+ | `src/services/` | `UseBoundedContext`, `Feature`, `ApplicationService`, the process, the execution |
29
+ | `src/context/` | The `ContextStore` port, `memoryContexts`, and `Context` — one level of it |
30
+ | `src/observability/` | The ports a platform plugs: `Tracer`, `ErrorReporter` |
31
+ | `src/codegen/` | The generator and its CLI (`sincpro-client generate`) — the only place Node is allowed |
32
+ | `src/adapters/` | Optional entries over a vendor (`./opentelemetry`, `./sentry`): the only place one is named |
33
+ | `src/testing/` | `memoryTracer`, `memoryReporter` — the ports in memory, for tests |
34
+ | `tests/` | vitest in Node; `tests/support/` holds the fake domains and forge's real contracts |
29
35
 
30
36
  ## Hard rules (the build enforces them — `make doctor`)
31
37
 
@@ -33,7 +39,7 @@ it says what this package is responsible for and what it is not. This file is ho
33
39
  no `document`, `window`, `localStorage`, `navigator`. What a platform must give, it gives
34
40
  through a port.
35
41
  - **Node only in the generator.** `node:` imports, `process` and `Buffer` live in `src/codegen/`
36
- and nowhere else; nothing in the runtime imports `codegen/`.
42
+ and nowhere else; nothing an application runs imports `codegen/`.
37
43
  - **Self-imports, no extensions.** Inside `src/`, import `@sincpro/client-framework/<path>` —
38
44
  never a relative path, never `.ts`.
39
45
 
@@ -48,16 +54,28 @@ it says what this package is responsible for and what it is not. This file is ho
48
54
 
49
55
  ## Known traps
50
56
 
51
- - **A run sees the instance through a view of its own.** Inside a method, `this.deps`,
52
- `this.context` and `this.features` are that run's — bound to its execution. So a Feature keeps
53
- nothing of a request on `this`, and declares no `#private` fields (they refuse the view); use
54
- TypeScript's `private`.
55
- - **A span is named by the key, not the class** (`bindServices(runtime, "project", { sendIssue })`
56
- → `project/sendIssue`; an ApplicationService's Features by their keys too): a production build
57
- renames classes. Only an unbound Feature falls back to its class name.
58
- - **The trace reaches the wire through `deps`.** A dependency is bound to the run when it is a
59
- `Transport` or carries `BIND` (every generated client does, through `bindable`); anything else
60
- is handed as it is. A client built outside `deps` and closed over sends no trace.
57
+ - **A run sees the instance through a view of its own.** Inside a method, each dependency
58
+ (`this.client`), `this.context` and `this.feature` are that run's — bound to its execution. So a
59
+ service declares no `#private` fields (they refuse the view); use TypeScript's `private`.
60
+ - **A service is a singleton of its bounded context.** What a method writes on `this` lands on the
61
+ instance, shared by the next call, the call beside it and every `with`/`scoped` variant — as in
62
+ sincpro_framework. What belongs to one request goes in its arguments or its context.
63
+ - **A span is named by the registered name, not the class** (`register({ features: { Issues } })`
64
+ → `project/Issues.close`, identity `project.Issues.close`): a production build renames classes,
65
+ never the keys of an object.
66
+ - **The dependencies are typed, not declared.** `BaseFeature<ProjectDependencies>` types
67
+ `this.client` from an interface, which does not exist at run time; the bounded context answers
68
+ whatever was added with `addDependency`. A dependency nobody added reads `undefined`.
69
+ - **The trace reaches the wire through the dependencies.** A dependency is bound to the run when it
70
+ is a `Transport` or carries `BIND` (every generated client does, through `bindable`); anything
71
+ else is handed as it is. A client built elsewhere and closed over sends no trace.
72
+ - **The process is read on every call, never at import.** `project` is defined when its module
73
+ loads, before `main` runs `configureProcess`; nothing may capture the tracer, the reporter or the
74
+ store at definition time. `configureProcess` replaces everything at once — a test calls it in
75
+ `beforeEach` and starts with an empty context.
76
+ - **A store's `load` may answer a new object each time** (one that parses JSON does): `Context`
77
+ keeps one object per `version`, so a screen can hand `values` to `useSyncExternalStore`. A store
78
+ that changes without moving the version is a broken store.
61
79
  - **What the generator writes is compared byte for byte** (`tests/support/forge-contract/`, out
62
80
  of prettier and eslint). After changing the generator on purpose, run `make codegen-fixtures`
63
81
  and read the diff.
@@ -71,7 +89,17 @@ it says what this package is responsible for and what it is not. This file is ho
71
89
  all-zeros span: the adapter gives no ids, so no `traceparent` of zeros reaches the server.
72
90
  - **Reported means unexpected.** A `TransportError` the server answered with a 4xx is `refused`,
73
91
  an `AbortError` is `aborted`; neither is reported (`outcomeOf`). Change that with
74
- `RuntimeOptions.outcomeOf`, never by catching in a Feature.
92
+ `ProcessOptions.outcomeOf`, never by catching in a Feature.
93
+
94
+ - **Only what is transversal comes here.** A value's wire form (a decimal as text, a day never
95
+ an instant, a money with its currency) is the backend's contract, the same on every platform,
96
+ so it lives in `src/common/`. How a value is shown to a person, typed in an input, or kept in
97
+ SQLite or IndexedDB is the platform's — @sincpro/web-framework's or @sincpro/mobile's.
98
+ - **A decimal never becomes a `number`.** It travels as text and is computed with `Decimal` or
99
+ `Money` (exact, on `BigInt`); `Number(...)` or `parseFloat` on an amount is the float the
100
+ backend sends text to avoid. And a day is never a `Date`: a `Date` is an instant.
101
+ - **The in-memory server copies by the wire's rules** (`valuesOf` → `readValues`): what it receives
102
+ and answers is what a real server would — a `Money` as `{ amount, currency }`, a `Date` as text.
75
103
 
76
104
  ## Before you hand it back
77
105
 
package/MANIFESTO.md CHANGED
@@ -25,28 +25,37 @@ server-side render, in a script and in a test, unchanged.**
25
25
 
26
26
  ## What it is responsible for
27
27
 
28
- 1. **Use cases.** `Feature` is one use case, with its dependencies injected; `ApplicationService`
29
- is a Feature that also executes the Features of its own domain — never another
30
- ApplicationService. All of a client's logic lives in them.
28
+ 1. **Services.** A `Feature` is a facade over one part of the domain, each public method a use
29
+ case; an `ApplicationService` composes the Features of its own bounded context — never another
30
+ ApplicationService. All of a client's logic lives in them, with their dependencies typed and
31
+ injected by their bounded context (`this.client`, `this.session`).
31
32
  2. **Interception.** Every public method of a use case passes through interceptors, and that is
32
33
  what makes the defaults free: the trace, the error report, the context. The author of a
33
34
  Feature writes none of it.
34
35
  3. **Observability, as ports.** `Tracer` and `ErrorReporter` are the contract: one span per use
35
- case, named `<domain>/<Feature>` like the backend's; the unexpected reported with its trace id;
36
- what the server already explained (a validation, a conflict) not reported. The platform plugs
37
- the implementation — OpenTelemetry, GlitchTip, a queue on the phone, memory in a test.
38
- 4. **Context.** The levels a call sees — the application, its domain, the call itself — and what
39
- travels to the server with it (`traceparent`, `sincpro-context`). Carried by each call, never
40
- kept in a global: two calls at once never mix their parents.
41
- 5. **Domains and the application.** A bounded context is declared once — its client, its use
42
- cases, what they depend on — and an application is the set of domains it loads. Domains never
43
- import each other; they talk by reference.
36
+ case, named `<bounded context>/<Service>.<method>` like the backend's; the unexpected reported
37
+ with its trace id; what the server already explained (a validation, a conflict) not reported.
38
+ The platform plugs the implementation — OpenTelemetry, GlitchTip, a queue on the phone, memory
39
+ in a test — once, for the process (`configureProcess`).
40
+ 4. **Context.** The levels a call sees — the process, its bounded context, a scope — read once
41
+ where a gesture starts and inherited by everything it runs, and what travels to the server with
42
+ it (`traceparent`, `sincpro-context`). Where the values live is a port (`ContextStore`) each
43
+ platform implements with its own technology; the execution is carried by each call, never kept
44
+ in a global stack: two calls at once never mix their parents.
45
+ 5. **Bounded contexts.** Set up like the backend's — its dependencies (`addDependency`), its
46
+ Features and ApplicationServices, registered once — and used through one light object:
47
+ `project.Issues.close(input)`. An application is the set of bounded contexts it loads; they
48
+ never import each other's insides.
44
49
  6. **The client.** The `Transport` port and its implementations (REST, memory), `entity()` with
45
50
  typed commands and answers, the service's client contract, model definitions read once from
46
- the server, client-side ids (UUID v7) and the server's `Problem`.
47
- 7. **The generator.** From a service's client contract to each domain's types, client and
51
+ the server and the server's `Problem`.
52
+ 7. **The values of the wire.** How every value travels, as the backend writes it — a decimal as
53
+ text, a money with its currency, a day that is never an instant, bytes tagged, ids as UUID v7 —
54
+ and the exact arithmetic a client needs on them (`Decimal`, `Money`). Only the wire form and
55
+ what follows from it: showing a value to a person is each platform's.
56
+ 8. **The generator.** From a service's client contract to each domain's types, client and
48
57
  entities — generated, never edited, and as platform-neutral as everything else here.
49
- 8. **Testing.** An in-memory transport, tracer and reporter, so a use case is proven in Node with
58
+ 9. **Testing.** An in-memory transport, tracer and reporter, so a use case is proven in Node with
50
59
  nothing else.
51
60
 
52
61
  ## What it is not
@@ -55,7 +64,8 @@ server-side render, in a script and in a test, unchanged.**
55
64
  the web and to @sincpro/mobile on a phone.
56
65
  - **Not a shell, a router or a window manager.** Where things open is the platform's.
57
66
  - **Not a cache or a state manager.** Holding server state for screens is the platform's job,
58
- with the libraries that do it best.
67
+ with the libraries that do it best. The context is not one either: a handful of values every
68
+ use case runs with — the tenant, the company — kept wherever the platform says.
59
69
  - **Not offline storage.** SQLite on a phone or IndexedDB in a point of sale are platform
60
70
  adapters.
61
71
  - **Not a telemetry or error-tracking SDK.** It defines the ports and calls them. For the
@@ -72,14 +82,16 @@ server-side render, in a script and in a test, unchanged.**
72
82
  1. **Platform-neutral, or it does not go in.** It runs on Node, in a browser and on React Native.
73
83
  What it needs from a platform it asks through a port, never by reaching for it. The build
74
84
  enforces it.
75
- 2. **The backend's words.** Feature, ApplicationService, domain, context, the span name — the
76
- same as sincpro_framework, so whoever knows the backend already knows the client.
77
- 3. **Nothing escapes the interceptors.** Reusing a use case is executing its Feature. A helper
78
- that wraps one skips the trace and the report — that is how an incident ends up with nothing
79
- to look at.
85
+ 2. **The backend's words.** Bounded context, Feature, ApplicationService, dependencies, context,
86
+ process, the span name — the same as sincpro_framework, so whoever knows the backend already
87
+ knows the client.
88
+ 3. **Nothing escapes the interceptors.** Reusing a use case is calling it through its bounded
89
+ context. A helper that wraps one skips the trace and the report — that is how an incident ends
90
+ up with nothing to look at.
80
91
  4. **Context travels with the call.** Never in a global stack: clients run many things at once.
81
92
  5. **A port where there are two implementations, a class where there is one.** `Transport` has
82
- REST and memory; `Tracer` has a platform's and a test's.
93
+ REST and memory; `Tracer` has a platform's and a test's; `ContextStore` has the browser's, the
94
+ phone's and memory.
83
95
  6. **Generated, never edited.** Applications extend by composition; regenerating never loses
84
96
  work.
85
97
  7. **The server is the source of truth.** A client reads the model, the preview and the refusal,
@@ -95,8 +107,8 @@ server-side render, in a script and in a test, unchanged.**
95
107
  @sincpro/criteria the contract with the server: Criteria, Meta, Page, Problem
96
108
  ▲
97
109
  @sincpro/client-framework this package — platform-neutral
98
- │ Feature · ApplicationService · interceptors · context per call
99
- │ Domain · application · Tracer and ErrorReporter ports
110
+ │ UseBoundedContext · Feature · ApplicationService · interceptors
111
+ │ the process · the context · Tracer, ErrorReporter and ContextStore ports
100
112
  │ Transport · entity() · client contract · model definitions · ids
101
113
  │ the generator · in-memory doubles for tests
102
114
  ▲
@@ -115,8 +127,14 @@ where they already ran without the DOM. `Feature` and `ApplicationService` came
115
127
  ports for tracing and error reporting are in, with the trace and the context written on every
116
128
  request a use case sends.
117
129
 
118
- **Next:** the domains and the application. The plan is written in @sincpro/web-framework
119
- (`docs/prd/PRD_02_marco-de-trabajo.md`, in Spanish like the rest of Sincpro's design records).
130
+ **Then the bounded context**, set up as the backend sets one up: a dependency type that types every
131
+ service's `this.client`, `addDependency`, the services registered by name and used as
132
+ `project.Issues.close(input)` and `this.feature.Issues`; the process configured once in `main`
133
+ (`configureProcess`); and the context as a port, so the browser and the phone keep it each with
134
+ their own technology behind the same door.
135
+
136
+ **Next:** @sincpro/mobile on this package, and the screens' side in @sincpro/web-framework — the
137
+ cache of reads, invalidated by the writes a use case sends.
120
138
 
121
139
  ## How we decide
122
140
 
package/README.md CHANGED
@@ -5,44 +5,252 @@ every platform.** Write a use case once — a `Feature`, or an `ApplicationServi
5
5
  Features — and run it in a browser, on a phone, in a server-side render, in a script or in a test.
6
6
  Every use case is traced and every unexpected error reported, continuing the backend's trace,
7
7
  without its author writing either. A service's client contract becomes typed code for each of
8
- its domains.
8
+ its bounded contexts.
9
9
 
10
- > **Status: 0.x, the first cut.** The API may still change before 1.0. In: the client, the
11
- > generator, `Feature` and `ApplicationService` with their interceptors, and the ports for tracing
12
- > and error reporting. Read the [manifesto](MANIFESTO.md) for what this package is responsible
13
- > for, what it is not and where it is going.
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.
14
15
 
15
- ## A use case, traced and reported by default
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
+ ```
16
111
 
17
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
+ ```
125
+
126
+ ```ts
127
+ // domains/project/infrastructure/bounded_context.ts
18
128
  import {
19
- ApplicationService,
20
- bindServices,
21
- createRuntime,
22
- Feature,
129
+ ApplicationService as BaseApplicationService,
130
+ Feature as BaseFeature,
131
+ UseBoundedContext,
23
132
  } from "@sincpro/client-framework";
24
133
 
25
- class SendIssue extends Feature<{ project: ProjectClient }> {
26
- execute(input: dto.CommandSendIssue) {
27
- return this.deps.project.sendIssue(input); // carries this run's traceparent
134
+ import type { ProjectFeatures } from "../services";
135
+
136
+ export class Feature extends BaseFeature<ProjectDependencies> {}
137
+ export class ApplicationService extends BaseApplicationService<
138
+ ProjectDependencies,
139
+ ProjectFeatures
140
+ > {}
141
+
142
+ export function configProjectBoundedContext(name: string) {
143
+ return registerDependencies(new UseBoundedContext<ProjectDependencies>(name));
144
+ }
145
+ ```
146
+
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
28
153
  }
29
154
  }
30
155
 
31
- class CloseIssue extends ApplicationService<{ preview: PreviewClose; send: SendIssue }> {
32
- async execute(input: CloseIssueInput) {
33
- const preview = await this.features.preview.execute(input); // a child span
34
- if (preview.warnings.length > 0) return { blocked: preview };
35
- return this.features.send.execute({ ...input, message: "closed" }); // another child
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);
36
161
  }
37
162
  }
38
163
 
39
- const runtime = createRuntime({ tracer, reporter, context: () => ({ tenant_id: "acme" }) });
40
- const project = bindServices(runtime, "project", {
41
- closeIssue: new CloseIssue({ preview: new PreviewClose(deps), send: new SendIssue(deps) }),
164
+ // domains/project/services/index.ts
165
+ export const features = { Issues, Notices };
166
+ export const applicationServices = { CloseAndNotify };
167
+ export type ProjectFeatures = typeof features;
168
+ ```
169
+
170
+ ```ts
171
+ // domains/project/index.ts
172
+ export const project = configProjectBoundedContext("project").register({
173
+ features,
174
+ applicationServices,
42
175
  });
43
- await project.closeIssue.execute(input); // project/closeIssue → project/preview, project/send
176
+
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
44
180
  ```
45
181
 
182
+ Every public method is a use case: a span named as the backend names its own
183
+ (`project/Issues.close`, identity `project.Issues.close`), and a report when it fails in a way
184
+ nobody expected. One gesture is one trace, from the click to the database:
185
+
186
+ ```mermaid
187
+ sequenceDiagram
188
+ participant caller as Caller<br/>(a screen, a test)
189
+ participant app as project.CloseAndNotify<br/>(ApplicationService)
190
+ participant feature as this.feature.Issues<br/>(Feature)
191
+ participant client as this.client<br/>(generated, bound to the run)
192
+ participant process as Process<br/>(Tracer · ErrorReporter)
193
+ participant server as sincpro_framework
194
+
195
+ caller->>app: execute(input)
196
+ app->>process: span project/CloseAndNotify — the context read once
197
+ app->>feature: close(input)
198
+ feature->>process: child span project/Issues.close
199
+ feature->>client: closeIssue(input)
200
+ client->>server: POST /issues/{issue_id}/close · traceparent · sincpro-context
201
+ server-->>client: ResponseCloseIssue — or a Problem
202
+ client-->>caller: the answer, through each run
203
+ Note over process: each run ends ok · refused (shown, never reported) · aborted · failed (reported once, with its trace id)
204
+ ```
205
+
206
+ `project` holds references only — each service is made once, the first time it is
207
+ asked for — and it is never frozen: `project.with({ client: fake })` and
208
+ `project.scoped({ company_id: 2 })` derive a variant that shares everything else.
209
+
210
+ ## The process: tracing, errors and the context
211
+
212
+ What every bounded context runs with is set once, where the application starts — the way the
213
+ backend's process is. All of it is optional:
214
+
215
+ ```ts
216
+ // entrypoints/main.ts
217
+ configureProcess({
218
+ tracer, // the platform's Tracer
219
+ reporter, // the platform's ErrorReporter
220
+ context: memoryContexts(), // where the context lives (this is the default)
221
+ });
222
+
223
+ processContext.set({ tenant_id: "acme" }); // every use case runs with it and sends it
224
+ project.context.set({ warehouse_id: 4 }); // only what «project» runs
225
+ ```
226
+
227
+ A service reads it as `this.context` — the process's values, under them its bounded context's,
228
+ under them a scope's, read once where the gesture started — and every request it sends carries
229
+ it in `sincpro-context`. A screen hears it change with `processContext.watch(…)`.
230
+
231
+ ```mermaid
232
+ flowchart LR
233
+ p["processContext<br/>tenant_id · company_id"]
234
+ b["project.context<br/>warehouse_id"]
235
+ s["project.scoped({ company_id: 2 })<br/>one window"]
236
+ e["this.context<br/>read once where the gesture starts"]
237
+ c["this.context of each Feature it runs<br/>the same values"]
238
+ h["sincpro-context<br/>on every request"]
239
+ store[("ContextStore<br/>the platform's")]
240
+ p --> e
241
+ b --> e
242
+ s --> e
243
+ e --> c
244
+ e --> h
245
+ c --> h
246
+ p -.-> store
247
+ b -.-> store
248
+ ```
249
+
250
+ The context lives in a `ContextStore`, the port each platform implements with its own technology:
251
+ memory by default, `sessionStorage` or the screens' own store in a browser, MMKV on a phone,
252
+ `AsyncLocalStorage` in a server that renders. The services never know which.
253
+
46
254
  `tracer` and `reporter` are injected by the application — this package initialises nothing.
47
255
  Any object that fulfils the port works; for the common vendors there are optional adapters that
48
256
  wrap what the application built:
@@ -59,18 +267,85 @@ Nothing is installed or shipped unless it is imported, and without a tracer or a
59
267
  case runs the same — nothing traces, nothing reports. In a test, `memoryTracer()` and
60
268
  `memoryReporter()` (`@sincpro/client-framework/testing`).
61
269
 
270
+ ## Values on the wire
271
+
272
+ The backend writes every message with one set of rules, and every client reads and writes them
273
+ the same — so they live here, not in each platform:
274
+
275
+ | Value | Travels as | Here |
276
+ | ----------- | ------------------------------------------ | ------------------------------------------------ |
277
+ | a decimal | text, every digit: `"10.50"` | `Decimal` — exact, on `BigInt`; rounds half up |
278
+ | a money | `{ "amount": "10.50", "currency": "BOB" }` | `Money` — never adds two currencies |
279
+ | a day | `"2026-10-02"`, never converted | `isDay`, `dayOf(instant, zone)` for «today» |
280
+ | an instant | ISO text: `"2026-10-02T14:30:00Z"` | `wallTimeOf` / `instantOf` on the company's zone |
281
+ | an id | a UUID v7 as 32 hex | `newEntityId`, `isEntityId`, `momentOf` |
282
+ | bytes | `{ "$bytes": "<base64>" }` | `valuesOf` / `readValues` |
283
+ | the context | only what travels: text, numbers, lists | `plainCodec`, `travelling` |
284
+
285
+ ```ts
286
+ import { Money, pack, unpack, valuesOf } from "@sincpro/client-framework";
287
+
288
+ const line = new Money("19.90", "BOB").times("3"); // 59.70 BOB, exact
289
+ const total = Money.total([line, delivery], "BOB").rounded(); // to the currency's decimals
290
+ valuesOf({ total, at: new Date() }); // { total: { amount, currency }, at: "…Z" }
291
+ const answer = unpack(text, Money); // a typed read when asked for — never imposed
292
+ ```
293
+
294
+ Not restrictive, like the backend: reading keeps every key it finds, a typed read is asked for
295
+ (`rebuilt(values, Money)`), any currency code is read, and an application adds its own decimals
296
+ (`Money.decimals["USDT"] = 6`). What JSON cannot write (`NaN`, a function, a cycle) is refused at
297
+ once, naming where. Showing a value to a person — `Intl.NumberFormat`, a date picker — is each
298
+ platform's, never this package's.
299
+
62
300
  ## Where it sits
63
301
 
64
- ```text
65
- @sincpro/criteria the contract with the server
66
- ▲
67
- @sincpro/client-framework use cases · interceptors · context · ports · domains · the client
68
- ▲
69
- @sincpro/web-framework the web @sincpro/mobile a phone
302
+ ```mermaid
303
+ flowchart TB
304
+ backend["sincpro_framework<br/>the service: its bounded contexts, Commands, the client contract"]
305
+ criteria["@sincpro/criteria<br/>Criteria · Meta · Page · Problem"]
306
+ cf["@sincpro/client-framework<br/>bounded contexts · services · process · context · ports · the client · the generator"]
307
+ web["@sincpro/web-framework<br/>hooks · screens · the browser's tracer and reporter"]
308
+ mobile["@sincpro/mobile<br/>React Native · SQLite · its queue"]
309
+ rest["a script · a server-side render · a test"]
310
+ web --> cf
311
+ mobile --> cf
312
+ rest --> cf
313
+ cf --> criteria
314
+ backend -. "client contract → sincpro-client generate" .-> cf
315
+ cf -- "HTTP · traceparent · sincpro-context" --> backend
70
316
  ```
71
317
 
72
318
  No React, no DOM, no platform: what a platform must provide — how a span is exported, where an
73
- error is reported — it plugs through a port.
319
+ error is reported, where the context lives — it plugs through a port.
320
+
321
+ ## On each platform
322
+
323
+ This package asks the platform for little: `fetch`, `AbortSignal`, `URL` / `URLSearchParams`,
324
+ `crypto.getRandomValues` (client-side ids, retry keys), `Proxy` / `WeakMap`. Browsers and Node 26
325
+ have them all. **React Native** (Hermes) needs two polyfills, installed once where the app
326
+ starts, before anything imports this package:
327
+
328
+ ```ts
329
+ import "react-native-get-random-values"; // or expo-crypto's getRandomValues polyfill
330
+ import "react-native-url-polyfill/auto"; // RN's own URL resolves relative paths poorly
331
+ ```
332
+
333
+ `memoryTransport` copies with `structuredClone` where the engine has it and a JSON round trip
334
+ where it does not. The OpenTelemetry adapter takes `@opentelemetry/api` from 1.9, the version
335
+ `@sincpro/mobile` runs.
336
+
337
+ The services are the same everywhere; each platform plugs its own technology behind the same
338
+ three ports, in its `main`:
339
+
340
+ | Port | Browser (@sincpro/web-framework) | Phone (@sincpro/mobile) | Test |
341
+ | --------------- | ----------------------------------------------- | ------------------------------------------ | ----------------- |
342
+ | `Tracer` | OpenTelemetry + OTLP/HTTP (`./telemetry`) | OpenTelemetry with its queue and exporter | `memoryTracer` |
343
+ | `ErrorReporter` | GlitchTip / Sentry browser client (`./errors`) | Sentry React Native client | `memoryReporter` |
344
+ | `ContextStore` | memory, `sessionStorage`, or the screens' store | MMKV, or memory hydrated from AsyncStorage | `memoryContexts` |
345
+ | `Transport` | `restTransport` over `fetch` | `restTransport`, or SQLite offline | `memoryTransport` |
346
+
347
+ A store's reads are synchronous — a use case reads its context where a gesture starts and never
348
+ waits for it — so an asynchronous storage is held in memory and written through.
74
349
 
75
350
  ## Development
76
351
 
@@ -6,7 +6,7 @@ import type { Tracer } from "../observability/ports.js";
6
6
  *
7
7
  * // a browser (@sincpro/web-framework/telemetry does this), Node, React Native…
8
8
  * const tracer = openTelemetryTracer(provider.getTracer("forge-web"));
9
- * const runtime = createRuntime({ tracer });
9
+ * configureProcess({ tracer });
10
10
  *
11
11
  * Context: only `@opentelemetry/api`, which runs the same on every platform — the provider, its
12
12
  * exporter and where it sends are the platform's (the browser's OTLP over fetch, a phone's
@@ -5,7 +5,7 @@ import { isSpanContextValid, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, Trac
5
5
  *
6
6
  * // a browser (@sincpro/web-framework/telemetry does this), Node, React Native…
7
7
  * const tracer = openTelemetryTracer(provider.getTracer("forge-web"));
8
- * const runtime = createRuntime({ tracer });
8
+ * configureProcess({ tracer });
9
9
  *
10
10
  * Context: only `@opentelemetry/api`, which runs the same on every platform — the provider, its
11
11
  * exporter and where it sends are the platform's (the browser's OTLP over fetch, a phone's
@@ -17,11 +17,11 @@ export type ExceptionClient = {
17
17
  *
18
18
  * // @sincpro/web-framework/errors builds the browser's client with GlitchTip's DSN
19
19
  * const reporter = sentryReporter(client);
20
- * const runtime = createRuntime({ reporter });
20
+ * configureProcess({ reporter });
21
21
  *
22
22
  * Context: it imports nothing — it takes the client the platform made (with its DSN, release
23
23
  * and data rules), so it costs no dependency and works on every platform. What is reported is
24
- * already decided by the runtime (`outcomeOf`: only `failed`); this only says where it happened.
24
+ * already decided by the process (`outcomeOf`: only `failed`); this only says where it happened.
25
25
  * The context goes as a context, not as tags: a tag is indexed and searched, and an execution's
26
26
  * values are not the place for that.
27
27
  */