@sincpro/client-framework 0.0.0-stage → 0.2.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 (59) hide show
  1. package/AGENTS.md +85 -0
  2. package/LICENSE +21 -0
  3. package/MANIFESTO.md +127 -0
  4. package/README.md +100 -2
  5. package/dist/adapters/opentelemetry.d.ts +21 -0
  6. package/dist/adapters/opentelemetry.js +68 -0
  7. package/dist/adapters/sentry.d.ts +28 -0
  8. package/dist/adapters/sentry.js +36 -0
  9. package/dist/client/contract.d.ts +34 -0
  10. package/dist/client/contract.js +41 -0
  11. package/dist/client/discovery.d.ts +47 -0
  12. package/dist/client/discovery.js +68 -0
  13. package/dist/client/entity.d.ts +242 -0
  14. package/dist/client/entity.js +187 -0
  15. package/dist/client/ids.d.ts +17 -0
  16. package/dist/client/ids.js +52 -0
  17. package/dist/client/memory.d.ts +46 -0
  18. package/dist/client/memory.js +108 -0
  19. package/dist/client/rest.d.ts +49 -0
  20. package/dist/client/rest.js +143 -0
  21. package/dist/client/retry.d.ts +24 -0
  22. package/dist/client/retry.js +25 -0
  23. package/dist/client/transport.d.ts +89 -0
  24. package/dist/client/transport.js +26 -0
  25. package/dist/codegen/cli.d.ts +2 -0
  26. package/dist/codegen/cli.js +82 -0
  27. package/dist/codegen/client.d.ts +14 -0
  28. package/dist/codegen/client.js +180 -0
  29. package/dist/codegen/domain.d.ts +14 -0
  30. package/dist/codegen/domain.js +195 -0
  31. package/dist/codegen/entities.d.ts +14 -0
  32. package/dist/codegen/entities.js +158 -0
  33. package/dist/codegen/index.d.ts +10 -0
  34. package/dist/codegen/index.js +8 -0
  35. package/dist/codegen/metamodel.d.ts +89 -0
  36. package/dist/codegen/metamodel.js +1 -0
  37. package/dist/codegen/names.d.ts +39 -0
  38. package/dist/codegen/names.js +113 -0
  39. package/dist/codegen/rest.d.ts +31 -0
  40. package/dist/codegen/rest.js +27 -0
  41. package/dist/index.d.ts +31 -0
  42. package/dist/index.js +31 -0
  43. package/dist/model/meta.d.ts +121 -0
  44. package/dist/model/meta.js +130 -0
  45. package/dist/model/metas.d.ts +17 -0
  46. package/dist/model/metas.js +7 -0
  47. package/dist/model/schema.d.ts +39 -0
  48. package/dist/model/schema.js +128 -0
  49. package/dist/observability/ports.d.ts +64 -0
  50. package/dist/observability/ports.js +21 -0
  51. package/dist/services/execution.d.ts +63 -0
  52. package/dist/services/execution.js +88 -0
  53. package/dist/services/feature.d.ts +60 -0
  54. package/dist/services/feature.js +220 -0
  55. package/dist/services/runtime.d.ts +48 -0
  56. package/dist/services/runtime.js +30 -0
  57. package/dist/testing/index.d.ts +35 -0
  58. package/dist/testing/index.js +37 -0
  59. package/package.json +92 -4
package/AGENTS.md ADDED
@@ -0,0 +1,85 @@
1
+ # AGENTS.md — working on @sincpro/client-framework
2
+
3
+ Rules for any agent (or person) changing this package. Read [`MANIFESTO.md`](MANIFESTO.md) first:
4
+ it says what this package is responsible for and what it is not. This file is how to keep it.
5
+
6
+ ## Before you change anything
7
+
8
+ - **Read the manifesto.** A piece that serves nothing there does not go in; a piece it forbids
9
+ does not either. If you think it should, change the manifesto first and say why.
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
12
+ word or a behaviour, read how the backend does it, not memory.
13
+ - **Look at who uses it.** @sincpro/web-framework (`~/dev/sincpro_web_framework`) and
14
+ @sincpro/mobile (`~/dev/mobile/sincpro_mobile`). A piece only one of them needs, and that
15
+ needs a platform, belongs there.
16
+
17
+ ## Where things go
18
+
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 |
29
+
30
+ ## Hard rules (the build enforces them — `make doctor`)
31
+
32
+ - **Platform-neutral.** No React, no `react-native`, no web-ui, no composer, no @sincpro/mobile;
33
+ no `document`, `window`, `localStorage`, `navigator`. What a platform must give, it gives
34
+ through a port.
35
+ - **Node only in the generator.** `node:` imports, `process` and `Buffer` live in `src/codegen/`
36
+ and nowhere else; nothing in the runtime imports `codegen/`.
37
+ - **Self-imports, no extensions.** Inside `src/`, import `@sincpro/client-framework/<path>` —
38
+ never a relative path, never `.ts`.
39
+
40
+ ## Conventions
41
+
42
+ - Code, comments and docs in English: this package is open source.
43
+ - `Context:` blocks at the top of a file are the design record: why it is the way it is. Keep
44
+ them true.
45
+ - A change ships with a test in `tests/` that fails without it, run in Node.
46
+ - Public means exported from `src/index.ts`. No side effects at import (`sideEffects: false`).
47
+ - A port (an `interface`) only where there are two implementations; a class or a type otherwise.
48
+
49
+ ## Known traps
50
+
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.
61
+ - **What the generator writes is compared byte for byte** (`tests/support/forge-contract/`, out
62
+ of prettier and eslint). After changing the generator on purpose, run `make codegen-fixtures`
63
+ and read the diff.
64
+ - **A module name the generator writes is built in parts** (`codegen/names.ts`, `FRAMEWORK`): the
65
+ build's `tsc-alias` rewrites this package's specifiers inside strings too.
66
+ - **An adapter wraps what the application built, never builds it.** `openTelemetryTracer` takes
67
+ the app's tracer, `sentryReporter` the app's client: no `register()`, no `Sentry.init()`, no
68
+ endpoint or DSN here, nothing read from the environment. A vendor's SDK is imported only in
69
+ `src/adapters/`, and nothing else imports an adapter (`make doctor`).
70
+ - **A span nothing records claims no trace.** OpenTelemetry's API with no provider answers an
71
+ all-zeros span: the adapter gives no ids, so no `traceparent` of zeros reaches the server.
72
+ - **Reported means unexpected.** A `TransportError` the server answered with a 4xx is `refused`,
73
+ an `AbortError` is `aborted`; neither is reported (`outcomeOf`). Change that with
74
+ `RuntimeOptions.outcomeOf`, never by catching in a Feature.
75
+
76
+ ## Before you hand it back
77
+
78
+ ```bash
79
+ make verify-format # eslint --fix + prettier + typecheck + doctor
80
+ make test
81
+ make build
82
+ ```
83
+
84
+ Never bump `version` in `package.json` — the release pipeline does. Never commit; a person
85
+ does.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SINCPRO S.R.L
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MANIFESTO.md ADDED
@@ -0,0 +1,127 @@
1
+ # Manifesto
2
+
3
+ > **@sincpro/client-framework is the client side of
4
+ > [sincpro_framework](https://github.com/Sincpro-SRL/sincpro_framework), for every platform:
5
+ > where a client's logic lives, how every use case is traced and reported, how an application is
6
+ > made of domains, and how it talks to a service through a client generated from that service's
7
+ > contract. No React, no DOM, no platform.**
8
+
9
+ This is the founding statement of the repository: what it is responsible for, what it is not,
10
+ how it is composed and where it is going. Every change is measured against it. If a change needs
11
+ something this page does not say, change this page first, and say why.
12
+
13
+ ## Why it exists
14
+
15
+ Sincpro builds business software with one backend framework and several clients: the web
16
+ ([@sincpro/web-framework](https://github.com/Sincpro-SRL/sincpro_web_framework)), mobile
17
+ ([@sincpro/mobile](https://github.com/Sincpro-SRL/sincpro_mobile)), server-side renders, scripts,
18
+ tests. Each one was growing the same core on its own — mobile wrote `Feature` and
19
+ `ApplicationService` with interceptors; the web wrote the transport, the entities and the
20
+ generator from the contract — and a use case written for one could not run on the other.
21
+
22
+ The backend already answers how a use case is written, traced and reported. This package gives
23
+ every client the same answer, once: **a Feature written here runs in a browser, on a phone, in a
24
+ server-side render, in a script and in a test, unchanged.**
25
+
26
+ ## What it is responsible for
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.
31
+ 2. **Interception.** Every public method of a use case passes through interceptors, and that is
32
+ what makes the defaults free: the trace, the error report, the context. The author of a
33
+ Feature writes none of it.
34
+ 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.
44
+ 6. **The client.** The `Transport` port and its implementations (REST, memory), `entity()` with
45
+ 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
48
+ 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
50
+ nothing else.
51
+
52
+ ## What it is not
53
+
54
+ - **Not a UI.** No React, no components, no hooks: drawing belongs to @sincpro/web-framework on
55
+ the web and to @sincpro/mobile on a phone.
56
+ - **Not a shell, a router or a window manager.** Where things open is the platform's.
57
+ - **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.
59
+ - **Not offline storage.** SQLite on a phone or IndexedDB in a point of sale are platform
60
+ adapters.
61
+ - **Not a telemetry or error-tracking SDK.** It defines the ports and calls them. For the
62
+ common vendors it offers optional adapters (`./opentelemetry`, `./sentry`) that wrap what the
63
+ application built — its provider, its client, with its endpoint and its DSN. It initialises
64
+ nothing and registers nothing global; an application that does not import them installs
65
+ nothing and ships nothing, and without a tracer or a reporter everything runs the same.
66
+ - **Not where business rules live.** The server keeps them; a client reads what it publishes.
67
+ - **Not an event bus**, and not a dispatcher: a use case is executed by calling it, and the
68
+ interceptors are what make that call traced and reported.
69
+
70
+ ## What we believe
71
+
72
+ 1. **Platform-neutral, or it does not go in.** It runs on Node, in a browser and on React Native.
73
+ What it needs from a platform it asks through a port, never by reaching for it. The build
74
+ 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.
80
+ 4. **Context travels with the call.** Never in a global stack: clients run many things at once.
81
+ 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.
83
+ 6. **Generated, never edited.** Applications extend by composition; regenerating never loses
84
+ work.
85
+ 7. **The server is the source of truth.** A client reads the model, the preview and the refusal,
86
+ and never re-implements a rule.
87
+ 8. **Small, and paid for only when used.** No side effects at import, tree-shakable, and no
88
+ dependency it can avoid.
89
+ 9. **Grow by evidence.** Nothing before its second use; every change ships with a test that fails
90
+ without it.
91
+
92
+ ## How it is composed
93
+
94
+ ```text
95
+ @sincpro/criteria the contract with the server: Criteria, Meta, Page, Problem
96
+ ▲
97
+ @sincpro/client-framework this package — platform-neutral
98
+ │ Feature · ApplicationService · interceptors · context per call
99
+ │ Domain · application · Tracer and ErrorReporter ports
100
+ │ Transport · entity() · client contract · model definitions · ids
101
+ │ the generator · in-memory doubles for tests
102
+ ▲
103
+ @sincpro/web-framework the web: hooks, components on @sincpro/web-ui, the shell on
104
+ @sincpro/web-composer, the browser's tracing and error reporting
105
+ @sincpro/mobile mobile: the React Native shell, SQLite, its queue, its exporter
106
+ ```
107
+
108
+ Dependencies point down only. This package knows nothing above it.
109
+
110
+ ## Where it stands
111
+
112
+ **The first cut is here.** The client and the generator came out of @sincpro/web-framework,
113
+ where they already ran without the DOM. `Feature` and `ApplicationService` came from
114
+ @sincpro/mobile, with each call carrying its own execution instead of a global stack. And the
115
+ ports for tracing and error reporting are in, with the trace and the context written on every
116
+ request a use case sends.
117
+
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).
120
+
121
+ ## How we decide
122
+
123
+ This page is the yardstick. A piece that serves nothing here does not go in. A piece this page
124
+ forbids does not go in either. To change what this page says, change the page first — in the
125
+ open, with the reason.
126
+
127
+ Open source, under the [MIT license](LICENSE).
package/README.md CHANGED
@@ -1,3 +1,101 @@
1
- # Temporary Holding Version
1
+ # @sincpro/client-framework
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 domains.
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.
14
+
15
+ ## A use case, traced and reported by default
16
+
17
+ ```ts
18
+ import {
19
+ ApplicationService,
20
+ bindServices,
21
+ createRuntime,
22
+ Feature,
23
+ } from "@sincpro/client-framework";
24
+
25
+ class SendIssue extends Feature<{ project: ProjectClient }> {
26
+ execute(input: dto.CommandSendIssue) {
27
+ return this.deps.project.sendIssue(input); // carries this run's traceparent
28
+ }
29
+ }
30
+
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
36
+ }
37
+ }
38
+
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) }),
42
+ });
43
+ await project.closeIssue.execute(input); // project/closeIssue → project/preview, project/send
44
+ ```
45
+
46
+ `tracer` and `reporter` are injected by the application — this package initialises nothing.
47
+ Any object that fulfils the port works; for the common vendors there are optional adapters that
48
+ wrap what the application built:
49
+
50
+ ```ts
51
+ import { openTelemetryTracer } from "@sincpro/client-framework/opentelemetry"; // peer: @opentelemetry/api
52
+ import { sentryReporter } from "@sincpro/client-framework/sentry"; // no dependency
53
+
54
+ const tracer = openTelemetryTracer(provider.getTracer("my-app")); // the app's provider and exporter
55
+ const reporter = sentryReporter(client); // the app's Sentry-compatible client (GlitchTip too)
56
+ ```
57
+
58
+ Nothing is installed or shipped unless it is imported, and without a tracer or a reporter every use
59
+ case runs the same — nothing traces, nothing reports. In a test, `memoryTracer()` and
60
+ `memoryReporter()` (`@sincpro/client-framework/testing`).
61
+
62
+ ## Where it sits
63
+
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
70
+ ```
71
+
72
+ 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.
74
+
75
+ ## Development
76
+
77
+ Node 26 (`nvm use`).
78
+
79
+ ```bash
80
+ make init # pre-commit + dependencies
81
+ make test # vitest, in Node
82
+ make verify-format # format + lint + doctor; fails if the tree is left dirty (what CI runs)
83
+ make build # tsc into dist/
84
+ ```
85
+
86
+ `make doctor` enforces what the type checker cannot: the source imports itself, never a relative
87
+ path; no React, no UI, no browser global; Node only inside the generator. Read
88
+ [`AGENTS.md`](AGENTS.md) before changing anything, and [`CONTRIBUTING.md`](CONTRIBUTING.md)
89
+ before opening a pull request.
90
+
91
+ ## CI/CD
92
+
93
+ Every workflow in `.github/workflows/` delegates to
94
+ [`Sincpro-SRL/.github`](https://github.com/Sincpro-SRL/.github) and only calls `make` targets:
95
+ checks on every pull request and push to `main`, a release draft on `main`, publication to npm
96
+ when a release is published, and notifications. The version is written by the release pipeline,
97
+ never by hand.
98
+
99
+ ## License
100
+
101
+ [MIT](LICENSE)
@@ -0,0 +1,21 @@
1
+ import { type Tracer as OtelTracer } from "@opentelemetry/api";
2
+ import type { Tracer } from "../observability/ports.js";
3
+ /**
4
+ * The `Tracer` port over OpenTelemetry's API: every use case a span of whatever provider the
5
+ * platform registered.
6
+ *
7
+ * // a browser (@sincpro/web-framework/telemetry does this), Node, React Native…
8
+ * const tracer = openTelemetryTracer(provider.getTracer("forge-web"));
9
+ * const runtime = createRuntime({ tracer });
10
+ *
11
+ * Context: only `@opentelemetry/api`, which runs the same on every platform — the provider, its
12
+ * exporter and where it sends are the platform's (the browser's OTLP over fetch, a phone's
13
+ * queue, a server's SDK). An optional entry with an optional peer: an application that does not
14
+ * import it installs nothing and ships nothing.
15
+ *
16
+ * The parent is given, never read from OpenTelemetry's context: a run carries its own execution
17
+ * (`Execution`), so two use cases at once never take each other's span, and no context manager
18
+ * (Zone.js) is needed. A span nothing records (a sampler said no, no provider registered)
19
+ * answers no ids — so no `traceparent` claims a trace that does not exist.
20
+ */
21
+ export declare function openTelemetryTracer(tracer: OtelTracer): Tracer;
@@ -0,0 +1,68 @@
1
+ import { isSpanContextValid, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, TraceFlags, } from "@opentelemetry/api";
2
+ /**
3
+ * The `Tracer` port over OpenTelemetry's API: every use case a span of whatever provider the
4
+ * platform registered.
5
+ *
6
+ * // a browser (@sincpro/web-framework/telemetry does this), Node, React Native…
7
+ * const tracer = openTelemetryTracer(provider.getTracer("forge-web"));
8
+ * const runtime = createRuntime({ tracer });
9
+ *
10
+ * Context: only `@opentelemetry/api`, which runs the same on every platform — the provider, its
11
+ * exporter and where it sends are the platform's (the browser's OTLP over fetch, a phone's
12
+ * queue, a server's SDK). An optional entry with an optional peer: an application that does not
13
+ * import it installs nothing and ships nothing.
14
+ *
15
+ * The parent is given, never read from OpenTelemetry's context: a run carries its own execution
16
+ * (`Execution`), so two use cases at once never take each other's span, and no context manager
17
+ * (Zone.js) is needed. A span nothing records (a sampler said no, no provider registered)
18
+ * answers no ids — so no `traceparent` claims a trace that does not exist.
19
+ */
20
+ export function openTelemetryTracer(tracer) {
21
+ return {
22
+ start(name, options) {
23
+ const parent = options.parent;
24
+ const parentContext = parent !== undefined && parent.traceId !== "" && parent.spanId !== ""
25
+ ? trace.setSpanContext(ROOT_CONTEXT, {
26
+ traceId: parent.traceId,
27
+ spanId: parent.spanId,
28
+ traceFlags: parent.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE,
29
+ isRemote: false,
30
+ })
31
+ : ROOT_CONTEXT;
32
+ const span = tracer.startSpan(name, {
33
+ kind: SpanKind.INTERNAL,
34
+ ...(options.attributes ? { attributes: options.attributes } : {}),
35
+ }, parentContext);
36
+ const own = span.spanContext();
37
+ const valid = isSpanContextValid(own);
38
+ const state = own.traceState?.serialize();
39
+ return {
40
+ traceId: valid ? own.traceId : "",
41
+ spanId: valid ? own.spanId : "",
42
+ sampled: valid && (own.traceFlags & TraceFlags.SAMPLED) === TraceFlags.SAMPLED,
43
+ ...(state ? { state } : {}),
44
+ set: (attributes) => void span.setAttributes(attributes),
45
+ end: (outcome, error) => close(span, outcome, error),
46
+ };
47
+ },
48
+ };
49
+ }
50
+ /**
51
+ * How the use case ended, said the way OpenTelemetry reads it: `failed` is an error with its
52
+ * exception; `refused` and `aborted` are expected answers — said in `sincpro.outcome`, never as
53
+ * an error, the way the backend leaves a refusal out of its error rate.
54
+ */
55
+ function close(span, outcome, error) {
56
+ span.setAttribute("sincpro.outcome", outcome);
57
+ if (outcome === "failed") {
58
+ if (error instanceof Error)
59
+ span.recordException(error);
60
+ span.setStatus({
61
+ code: SpanStatusCode.ERROR,
62
+ ...(error instanceof Error ? { message: error.message } : {}),
63
+ });
64
+ }
65
+ else if (outcome === "ok")
66
+ span.setStatus({ code: SpanStatusCode.OK });
67
+ span.end();
68
+ }
@@ -0,0 +1,28 @@
1
+ import type { ErrorReporter } from "../observability/ports.js";
2
+ /**
3
+ * What this adapter needs of a Sentry-compatible client: its `captureException`. A browser's
4
+ * `BrowserClient`, Node's, React Native's — and GlitchTip, which speaks Sentry's protocol.
5
+ */
6
+ export type ExceptionClient = {
7
+ captureException(error: unknown, hint?: {
8
+ captureContext?: {
9
+ tags?: Record<string, string>;
10
+ contexts?: Record<string, Record<string, unknown>>;
11
+ };
12
+ }): unknown;
13
+ };
14
+ /**
15
+ * The `ErrorReporter` port over a Sentry-compatible client: an unexpected error reported with the
16
+ * use case it happened in, its trace — the id the backend logged too — and its context.
17
+ *
18
+ * // @sincpro/web-framework/errors builds the browser's client with GlitchTip's DSN
19
+ * const reporter = sentryReporter(client);
20
+ * const runtime = createRuntime({ reporter });
21
+ *
22
+ * Context: it imports nothing — it takes the client the platform made (with its DSN, release
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.
25
+ * The context goes as a context, not as tags: a tag is indexed and searched, and an execution's
26
+ * values are not the place for that.
27
+ */
28
+ export declare function sentryReporter(client: ExceptionClient): ErrorReporter;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The `ErrorReporter` port over a Sentry-compatible client: an unexpected error reported with the
3
+ * use case it happened in, its trace — the id the backend logged too — and its context.
4
+ *
5
+ * // @sincpro/web-framework/errors builds the browser's client with GlitchTip's DSN
6
+ * const reporter = sentryReporter(client);
7
+ * const runtime = createRuntime({ reporter });
8
+ *
9
+ * Context: it imports nothing — it takes the client the platform made (with its DSN, release
10
+ * and data rules), so it costs no dependency and works on every platform. What is reported is
11
+ * already decided by the runtime (`outcomeOf`: only `failed`); this only says where it happened.
12
+ * The context goes as a context, not as tags: a tag is indexed and searched, and an execution's
13
+ * values are not the place for that.
14
+ */
15
+ export function sentryReporter(client) {
16
+ return {
17
+ report(error, about) {
18
+ client.captureException(error, {
19
+ captureContext: {
20
+ tags: {
21
+ "sincpro.use_case": about.useCase,
22
+ ...(about.traceId === "" ? {} : { trace_id: about.traceId }),
23
+ },
24
+ contexts: {
25
+ sincpro: {
26
+ use_case: about.useCase,
27
+ execution_id: about.executionId,
28
+ ...(about.traceId === "" ? {} : { trace_id: about.traceId }),
29
+ context: about.context,
30
+ },
31
+ },
32
+ },
33
+ });
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,34 @@
1
+ import { type Contract, type Meta } from "@sincpro/criteria";
2
+ /**
3
+ * A bounded context's client contract, as the application keeps it: the snapshot
4
+ * `sincpro-web generate` writes (`contract.json`), or the document a service serves
5
+ * (`GET /client-contract/<context>.json`). Only its models are read here.
6
+ *
7
+ * Context: the generated entities are keyed `<domain>:<kind>` (`project:issue`) — the key the
8
+ * workspace refers to them by and the meta store keeps their definitions under —, so the rule
9
+ * that turns a model's name into its kind lives once, here, and both the generator and the
10
+ * preload read it.
11
+ */
12
+ export type ClientContract = {
13
+ models: Readonly<Record<string, Contract>>;
14
+ $defs?: Readonly<Record<string, Contract>>;
15
+ };
16
+ /** `ProjectEvent` → `project_event`: the kind in an entity's key. */
17
+ export declare function kindOf(model: string): string;
18
+ /** The key of a model's entity: `entityKeyOf("project", "ProjectEvent")` → `"project:project_event"`. */
19
+ export declare function entityKeyOf(domain: string, model: string): string;
20
+ /**
21
+ * The version of every entity of `contract`, by its model's name — what `restTransport`
22
+ * compares each answer with (`snapshot`) to say when the generated code is behind the server.
23
+ *
24
+ * restTransport({ base: "/v1", snapshot: versionsOf(contract), onVersionDrift: warn })
25
+ */
26
+ export declare function versionsOf(contract: ClientContract): Record<string, string>;
27
+ /**
28
+ * The definition of every model of `contract`, by its entity's key — what `FrameworkProvider
29
+ * metas` preloads, so the first screen draws its labels with no request at all. The transport
30
+ * still reads the live definition and asks again when an answer says another version.
31
+ *
32
+ * <FrameworkProvider transport={transport} metas={metasOf(contract, "project")}>
33
+ */
34
+ export declare function metasOf(contract: ClientContract, domain: string): Record<string, Meta>;
@@ -0,0 +1,41 @@
1
+ import { metaFromContract } from "@sincpro/criteria";
2
+ /** `ProjectEvent` → `project_event`: the kind in an entity's key. */
3
+ export function kindOf(model) {
4
+ return model
5
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
6
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
7
+ .toLowerCase();
8
+ }
9
+ /** The key of a model's entity: `entityKeyOf("project", "ProjectEvent")` → `"project:project_event"`. */
10
+ export function entityKeyOf(domain, model) {
11
+ return `${domain}:${kindOf(model)}`;
12
+ }
13
+ /**
14
+ * The version of every entity of `contract`, by its model's name — what `restTransport`
15
+ * compares each answer with (`snapshot`) to say when the generated code is behind the server.
16
+ *
17
+ * restTransport({ base: "/v1", snapshot: versionsOf(contract), onVersionDrift: warn })
18
+ */
19
+ export function versionsOf(contract) {
20
+ return Object.fromEntries(Object.entries(contract.models).flatMap(([name, model]) => {
21
+ const version = model["x-sincpro"]?.version;
22
+ return model["x-sincpro"]?.identity != null && typeof version === "string"
23
+ ? [[name, version]]
24
+ : [];
25
+ }));
26
+ }
27
+ /**
28
+ * The definition of every model of `contract`, by its entity's key — what `FrameworkProvider
29
+ * metas` preloads, so the first screen draws its labels with no request at all. The transport
30
+ * still reads the live definition and asks again when an answer says another version.
31
+ *
32
+ * <FrameworkProvider transport={transport} metas={metasOf(contract, "project")}>
33
+ */
34
+ export function metasOf(contract, domain) {
35
+ // A related model, or a value a record holds, is pointed at by its name (`#/models/Run`).
36
+ const related = { ...(contract.$defs ?? {}), ...contract.models };
37
+ return Object.fromEntries(Object.entries(contract.models)
38
+ // Only an entity has a key of its own; a value it holds is described inside it.
39
+ .filter(([, model]) => model["x-sincpro"]?.identity != null)
40
+ .map(([name, model]) => [entityKeyOf(domain, name), metaFromContract(model, related)]));
41
+ }
@@ -0,0 +1,47 @@
1
+ import { type Meta } from "@sincpro/criteria";
2
+ /**
3
+ * The discovery document of a bounded context, as far as this reads it: its paths, each
4
+ * operation with the entities it belongs to, and the published contract of every model.
5
+ *
6
+ * Context: sincpro_framework describes a model once — JSON Schema with `x-sincpro`, under the
7
+ * document's `components.schemas` — and every read answers only the version of it
8
+ * (`model_version`). So the definition a list, a form and a select are built from is read here,
9
+ * once, and asked for again only when an answer says another version.
10
+ */
11
+ export type DiscoveryDocument = {
12
+ paths?: Readonly<Record<string, Readonly<Record<string, DiscoveryOperation>>>>;
13
+ components?: {
14
+ schemas?: Readonly<Record<string, DocumentSchema>>;
15
+ };
16
+ };
17
+ export type DiscoveryOperation = {
18
+ "x-sincpro-entities"?: readonly string[];
19
+ responses?: Readonly<Record<string, {
20
+ content?: Readonly<Record<string, {
21
+ schema?: DocumentSchema;
22
+ }>>;
23
+ }>>;
24
+ };
25
+ type DocumentSchema = {
26
+ $ref?: string;
27
+ items?: DocumentSchema;
28
+ anyOf?: readonly DocumentSchema[];
29
+ properties?: Readonly<Record<string, DocumentSchema>>;
30
+ "x-sincpro"?: {
31
+ entity?: string;
32
+ version?: string;
33
+ };
34
+ };
35
+ /**
36
+ * The model a request to `path` answers, by the name of its contract in the document: the
37
+ * entity the operation says it belongs to, else the one its answer holds.
38
+ *
39
+ * modelAt(published, "/v1/projects/p1", "get") // "Project"
40
+ */
41
+ export declare function modelAt(published: DiscoveryDocument, path: string, method: string): string | undefined;
42
+ /** The definition of the model named `name`, read from its contract, and its version. */
43
+ export declare function definitionOf(published: DiscoveryDocument, name: string): {
44
+ meta: Meta;
45
+ version: string | null;
46
+ } | undefined;
47
+ export {};