@sincpro/client-framework 0.5.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 (134) hide show
  1. package/AGENTS.md +54 -25
  2. package/MANIFESTO.md +59 -39
  3. package/README.md +329 -328
  4. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  5. package/dist/client/{memory.js → adapters/memory.js} +7 -5
  6. package/dist/client/adapters/parquet.d.ts +50 -0
  7. package/dist/client/adapters/parquet.js +151 -0
  8. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +7 -1
  9. package/dist/client/{rest.js → adapters/rest.js} +48 -3
  10. package/dist/client/{entity.d.ts → domain/entity.d.ts} +22 -11
  11. package/dist/client/{entity.js → domain/entity.js} +16 -1
  12. package/dist/client/{transport.d.ts → domain/transport.d.ts} +37 -1
  13. package/dist/client/{transport.js → domain/transport.js} +6 -0
  14. package/dist/client/index.d.ts +16 -0
  15. package/dist/client/index.js +16 -0
  16. package/dist/client/services/propagation.d.ts +32 -0
  17. package/dist/{services/execution.js → client/services/propagation.js} +8 -21
  18. package/dist/client/{retry.js → services/retry.js} +1 -1
  19. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +21 -2
  20. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  21. package/dist/codegen/{names.js → domain/names.js} +3 -3
  22. package/dist/codegen/{cli.js → entrypoint/cli.js} +9 -8
  23. package/dist/codegen/index.d.ts +7 -7
  24. package/dist/codegen/index.js +5 -10
  25. package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
  26. package/dist/codegen/{client.js → services/client.js} +34 -5
  27. package/dist/codegen/services/domain.d.ts +20 -0
  28. package/dist/codegen/{domain.js → services/domain.js} +121 -23
  29. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -2
  30. package/dist/codegen/{entities.js → services/entities.js} +34 -4
  31. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  32. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
  33. package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
  34. package/dist/common/collection.d.ts +110 -0
  35. package/dist/common/collection.js +169 -0
  36. package/dist/common/index.d.ts +10 -0
  37. package/dist/common/index.js +10 -0
  38. package/dist/context/adapters/memory.d.ts +6 -0
  39. package/dist/context/adapters/memory.js +34 -0
  40. package/dist/context/adapters/plain-codec.d.ts +3 -0
  41. package/dist/context/adapters/plain-codec.js +16 -0
  42. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  43. package/dist/context/{codec.js → domain/codec.js} +0 -14
  44. package/dist/context/domain/execution.d.ts +33 -0
  45. package/dist/context/domain/execution.js +11 -0
  46. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  47. package/dist/context/{store.js → domain/store.js} +1 -34
  48. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  49. package/dist/context/index.d.ts +17 -0
  50. package/dist/context/index.js +15 -0
  51. package/dist/context/infrastructure/process.d.ts +13 -0
  52. package/dist/context/infrastructure/process.js +35 -0
  53. package/dist/core/domain/bus.d.ts +18 -0
  54. package/dist/core/domain/bus.js +18 -0
  55. package/dist/core/domain/dto.d.ts +32 -0
  56. package/dist/core/domain/dto.js +20 -0
  57. package/dist/core/domain/errors.d.ts +18 -0
  58. package/dist/core/domain/errors.js +1 -0
  59. package/dist/core/domain/services.d.ts +37 -0
  60. package/dist/core/domain/services.js +28 -0
  61. package/dist/core/entrypoint/bounded-context.d.ts +70 -0
  62. package/dist/core/entrypoint/bounded-context.js +252 -0
  63. package/dist/core/entrypoint/screens.d.ts +6 -0
  64. package/dist/core/entrypoint/screens.js +136 -0
  65. package/dist/core/index.d.ts +17 -0
  66. package/dist/core/index.js +16 -0
  67. package/dist/core/infrastructure/process.d.ts +56 -0
  68. package/dist/core/infrastructure/process.js +35 -0
  69. package/dist/core/services/binding.d.ts +6 -0
  70. package/dist/core/services/binding.js +13 -0
  71. package/dist/core/services/error-handlers.d.ts +11 -0
  72. package/dist/core/services/error-handlers.js +21 -0
  73. package/dist/core/services/execution.d.ts +35 -0
  74. package/dist/core/services/execution.js +137 -0
  75. package/dist/index.d.ts +16 -36
  76. package/dist/index.js +16 -36
  77. package/dist/model/{meta.d.ts → domain/meta.d.ts} +1 -0
  78. package/dist/model/index.d.ts +10 -0
  79. package/dist/model/index.js +10 -0
  80. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  81. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  82. package/dist/model/{schema.js → services/schema.js} +1 -1
  83. package/dist/observability/adapters/console.d.ts +12 -0
  84. package/dist/observability/adapters/console.js +35 -0
  85. package/dist/observability/adapters/memory.d.ts +41 -0
  86. package/dist/observability/adapters/memory.js +49 -0
  87. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  88. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  89. package/dist/observability/domain/errors.d.ts +23 -0
  90. package/dist/observability/domain/errors.js +8 -0
  91. package/dist/observability/domain/logging.d.ts +42 -0
  92. package/dist/observability/domain/logging.js +1 -0
  93. package/dist/observability/domain/tracing.d.ts +49 -0
  94. package/dist/observability/domain/tracing.js +20 -0
  95. package/dist/observability/index.d.ts +16 -0
  96. package/dist/observability/index.js +4 -0
  97. package/dist/observability/infrastructure/registry.d.ts +21 -0
  98. package/dist/observability/infrastructure/registry.js +18 -0
  99. package/dist/observability/services/logging.d.ts +27 -0
  100. package/dist/observability/services/logging.js +105 -0
  101. package/dist/observability/services/outcome.d.ts +12 -0
  102. package/dist/observability/services/outcome.js +20 -0
  103. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  104. package/dist/testing/index.d.ts +3 -33
  105. package/dist/testing/index.js +5 -37
  106. package/package.json +25 -8
  107. package/dist/codegen/domain.d.ts +0 -15
  108. package/dist/observability/ports.d.ts +0 -65
  109. package/dist/observability/ports.js +0 -21
  110. package/dist/services/bounded-context.d.ts +0 -79
  111. package/dist/services/bounded-context.js +0 -261
  112. package/dist/services/execution.d.ts +0 -63
  113. package/dist/services/feature.d.ts +0 -85
  114. package/dist/services/feature.js +0 -86
  115. package/dist/services/process.d.ts +0 -60
  116. package/dist/services/process.js +0 -57
  117. package/dist/services/screens.d.ts +0 -2
  118. package/dist/services/screens.js +0 -67
  119. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  120. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  121. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  122. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  123. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  124. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  125. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  126. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  127. /package/dist/codegen/{check.js → services/check.js} +0 -0
  128. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  129. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  130. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  131. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  132. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  133. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  134. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Model definitions: written by hand (`defineMeta`), read from a JSON Schema, kept once known.
3
+ *
4
+ * domain/ `defineMeta`, `field`
5
+ * services/ `metaFromSchema`, `completeMeta`
6
+ * infrastructure/ `metaStore`: the definitions already known
7
+ */
8
+ export { defineMeta, field, } from "./domain/meta.js";
9
+ export { metaStore, } from "./infrastructure/metas.js";
10
+ export { completeMeta, metaFromSchema, } from "./services/schema.js";
@@ -3,7 +3,7 @@ import type { Meta } from "@sincpro/criteria";
3
3
  * The model definitions already known, by entity key (`"billing:invoice"`).
4
4
  *
5
5
  * Context: sincpro_framework publishes each model once, in the discovery document the
6
- * transport reads (`core/discovery`); an answer carries only its version. This keeps the
6
+ * transport reads (`client/services/discovery`); an answer carries only its version. This keeps the
7
7
  * definitions ACROSS readings, so the second list of the same entity, the record opened from it
8
8
  * and the select that points at it draw their labels before the rows arrive.
9
9
  *
@@ -3,7 +3,7 @@ import type { Meta } from "@sincpro/criteria";
3
3
  * A model definition read from a JSON Schema — what an OpenAPI document holds.
4
4
  *
5
5
  * Context: a model a READ answers is published by sincpro_framework as its contract — JSON
6
- * Schema with `x-sincpro` — and read by the transport (`metaFromContract`, `core/discovery`).
6
+ * Schema with `x-sincpro` — and read by the transport (`metaFromContract`, `client/services/discovery`).
7
7
  * What has no `x-sincpro` is a plain JSON Schema: a COMMAND's body, a value object it carries.
8
8
  *
9
9
  * So this reads such a component schema into a `Meta` — the form of a Command, a value object —
@@ -1,4 +1,4 @@
1
- import { field } from "./meta.js";
1
+ import { field } from "../domain/meta.js";
2
2
  const UNKNOWN = {
3
3
  type: "unknown",
4
4
  kind: "scalar",
@@ -0,0 +1,12 @@
1
+ import type { Logger, LogLayer, LogLevel } from "../domain/logging.js";
2
+ export type ConsoleLoggerOptions = {
3
+ mode: "development" | "production";
4
+ /** Development INFO/DEBUG threshold. WARN/ERROR are never hidden by layer/level masks. */
5
+ level?: LogLevel;
6
+ layers?: Partial<Record<LogLayer, boolean>>;
7
+ };
8
+ /**
9
+ * Built-in console engine: silent in production, configurable
10
+ * development layers, and WARN/ERROR preserved. Other sinks are filtered independently.
11
+ */
12
+ export declare function consoleLogger(options: ConsoleLoggerOptions): Logger;
@@ -0,0 +1,35 @@
1
+ const RANK = { debug: 0, info: 1, warn: 2, error: 3 };
2
+ /**
3
+ * Built-in console engine: silent in production, configurable
4
+ * development layers, and WARN/ERROR preserved. Other sinks are filtered independently.
5
+ */
6
+ export function consoleLogger(options) {
7
+ const mode = options.mode;
8
+ if (mode !== "development" && mode !== "production") {
9
+ throw new TypeError("Console logger mode must be development or production");
10
+ }
11
+ const level = options.level ?? "debug";
12
+ if (!Object.hasOwn(RANK, level)) {
13
+ throw new TypeError("Console logger level must be debug, info, warn or error");
14
+ }
15
+ const layers = { ...options.layers };
16
+ return {
17
+ log(record) {
18
+ if (mode === "production")
19
+ return;
20
+ if (record.level !== "warn" && record.level !== "error") {
21
+ if (layers[record.layer] === false || RANK[record.level] < RANK[level])
22
+ return;
23
+ }
24
+ const prefix = `${record.timestamp} [${record.layer}] ${record.level.toUpperCase()}`;
25
+ const method = record.level === "debug"
26
+ ? console.debug
27
+ : record.level === "warn"
28
+ ? console.warn
29
+ : record.level === "error"
30
+ ? console.error
31
+ : console.log;
32
+ method.call(console, prefix, record.message, record);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,41 @@
1
+ import type { ErrorAbout, ErrorReporter } from "../domain/errors.js";
2
+ import type { Logger, LogRecord } from "../domain/logging.js";
3
+ import type { Outcome, Tracer } from "../domain/tracing.js";
4
+ /**
5
+ * Observability adapters that keep what they were told, so a test reads it back.
6
+ *
7
+ * const tracer = memoryTracer();
8
+ * configureProcess({ tracer });
9
+ * await project.execute(new CommandCloseIssue("issue-1"));
10
+ * tracer.spans.map((one) => one.name); // ["project/CloseIssue", "project/Store", …]
11
+ *
12
+ * Context: a use case is proven in Node with nothing of a platform; these are the platform's
13
+ * part, held in memory. Not test doubles of this package — real implementations of its ports.
14
+ */
15
+ /** A span as the memory tracer kept it. */
16
+ export type RecordedSpan = {
17
+ name: string;
18
+ traceId: string;
19
+ spanId: string;
20
+ /** The span it was opened under, when it was. */
21
+ parentSpanId: string | undefined;
22
+ attributes: Record<string, string | number | boolean>;
23
+ /** How it ended; `undefined` while it is open. */
24
+ outcome: Outcome | undefined;
25
+ error: unknown;
26
+ };
27
+ export type MemoryTracer = Tracer & {
28
+ readonly spans: RecordedSpan[];
29
+ };
30
+ export declare function memoryTracer(): MemoryTracer;
31
+ export type MemoryReporter = ErrorReporter & {
32
+ readonly reports: {
33
+ error: unknown;
34
+ about: ErrorAbout;
35
+ }[];
36
+ };
37
+ export declare function memoryReporter(): MemoryReporter;
38
+ export type MemoryLogger = Logger & {
39
+ readonly records: LogRecord[];
40
+ };
41
+ export declare function memoryLogger(): MemoryLogger;
@@ -0,0 +1,49 @@
1
+ function hex(bytes) {
2
+ return Array.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
3
+ }
4
+ export function memoryTracer() {
5
+ const spans = [];
6
+ return {
7
+ spans,
8
+ start(name, options) {
9
+ const recorded = {
10
+ name,
11
+ traceId: options.parent?.traceId || hex(16),
12
+ spanId: hex(8),
13
+ parentSpanId: options.parent?.spanId || undefined,
14
+ attributes: { ...options.attributes },
15
+ outcome: undefined,
16
+ error: undefined,
17
+ };
18
+ spans.push(recorded);
19
+ return {
20
+ traceId: recorded.traceId,
21
+ spanId: recorded.spanId,
22
+ sampled: true,
23
+ set: (attributes) => void Object.assign(recorded.attributes, attributes),
24
+ end: (outcome, error) => {
25
+ if (recorded.outcome !== undefined)
26
+ return;
27
+ recorded.outcome = outcome;
28
+ recorded.error = error;
29
+ },
30
+ };
31
+ },
32
+ };
33
+ }
34
+ export function memoryReporter() {
35
+ const reports = [];
36
+ return { reports, report: (error, about) => void reports.push({ error, about }) };
37
+ }
38
+ export function memoryLogger() {
39
+ const records = [];
40
+ return {
41
+ records,
42
+ log(record) {
43
+ records.push({
44
+ ...record,
45
+ ...(record.attributes ? { attributes: { ...record.attributes } } : {}),
46
+ });
47
+ },
48
+ };
49
+ }
@@ -1,5 +1,5 @@
1
1
  import { type Tracer as OtelTracer } from "@opentelemetry/api";
2
- import type { Tracer } from "../observability/ports.js";
2
+ import type { Tracer } from "../domain/tracing.js";
3
3
  /**
4
4
  * The `Tracer` port over OpenTelemetry's API: every use case a span of whatever provider the
5
5
  * platform registered.
@@ -1,4 +1,4 @@
1
- import type { ErrorReporter } from "../observability/ports.js";
1
+ import type { ErrorReporter } from "../domain/errors.js";
2
2
  /**
3
3
  * What this adapter needs of a Sentry-compatible client: its `captureException`. A browser's
4
4
  * `BrowserClient`, Node's, React Native's — and GlitchTip, which speaks Sentry's protocol.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What a platform plugs to report what nobody expected.
3
+ *
4
+ * Context: the backend reports each unexpected failure once, with the use case and the trace it
5
+ * belongs to; GlitchTip, Sentry or memory in a test receive it here. A reporter that fails is
6
+ * ignored: telling about a failure never becomes a second one.
7
+ */
8
+ /** What a report says about where the error happened. */
9
+ export type ErrorAbout = {
10
+ /** `<bounded context>.<Service>`: the use case's identity, as the backend's — or, for what
11
+ * failed outside one (`reportError`), where it happened (`view project:issue/acme-3`). */
12
+ useCase: string;
13
+ /** The trace the error belongs to — the same id the server logged, when it got that far. */
14
+ traceId: string;
15
+ /** The execution's own id. */
16
+ executionId: string;
17
+ /** What the use case ran with: the process's context, its bounded context's, its scope's. */
18
+ context: Readonly<Record<string, unknown>>;
19
+ };
20
+ /** Reports what nobody expected. One per platform; none when nothing reports. */
21
+ export interface ErrorReporter {
22
+ report(error: unknown, about: ErrorAbout): void;
23
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What a platform plugs to report what nobody expected.
3
+ *
4
+ * Context: the backend reports each unexpected failure once, with the use case and the trace it
5
+ * belongs to; GlitchTip, Sentry or memory in a test receive it here. A reporter that fails is
6
+ * ignored: telling about a failure never becomes a second one.
7
+ */
8
+ export {};
@@ -0,0 +1,42 @@
1
+ import type { Attributes, Outcome } from "./tracing.js";
2
+ /**
3
+ * What a platform plugs to receive structured logs: a sink (`Logger`) that owns filtering and
4
+ * delivery — the console by default, Loki, a phone's queue, memory in a test.
5
+ *
6
+ * Context: a line says which layer wrote it, the way sincpro_framework's architecture names
7
+ * them: the application layer (a bounded context and its ApplicationServices and Features,
8
+ * through `project.logger` and `this.logger`), its adapters (`loggerAdapter`) and the
9
+ * infrastructure around them (`logger`). A line written inside a use case carries that
10
+ * execution's identity; nothing writes a DTO, a response, an error body or the session's
11
+ * context on its own.
12
+ */
13
+ export type LogLevel = "debug" | "info" | "warn" | "error";
14
+ /** The layer a line comes from. */
15
+ export type LogLayer = "infrastructure" | "adapter" | "bounded_context" | "application_service" | "feature";
16
+ /** Structured logging never implicitly includes a DTO, response, error body or session context. */
17
+ export type LogRecord = {
18
+ readonly timestamp: string;
19
+ readonly level: LogLevel;
20
+ readonly layer: LogLayer;
21
+ readonly message: string;
22
+ readonly attributes?: Attributes;
23
+ readonly event?: "execution.started" | "execution.finished" | "error.handled";
24
+ readonly boundedContext?: string;
25
+ readonly useCase?: string;
26
+ readonly executionId?: string;
27
+ readonly traceId?: string;
28
+ readonly spanId?: string;
29
+ readonly outcome?: Outcome;
30
+ readonly durationMs?: number;
31
+ };
32
+ /** A sink owns filtering and delivery. Logging is best-effort and never awaited by business work. */
33
+ export interface Logger {
34
+ log(record: LogRecord): void | Promise<void>;
35
+ }
36
+ /** How code writes a line: the logger of a layer, of a bounded context, or of a run. */
37
+ export interface LayerLogger {
38
+ debug(message: string, attributes?: Attributes): void;
39
+ info(message: string, attributes?: Attributes): void;
40
+ warn(message: string, attributes?: Attributes): void;
41
+ error(message: string, attributes?: Attributes): void;
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * What a platform plugs for execution traces: one span per use case.
3
+ *
4
+ * Context: sincpro_framework opens one span per use case; a client has to do the same, and
5
+ * continue the backend's trace. The vendor — OpenTelemetry in a browser, a queue on a phone,
6
+ * memory in a test — is the platform's, so this package defines the contract and calls it, and
7
+ * bundles none. Two implementations at least (a platform's and a test's) are why it is a port.
8
+ *
9
+ * Nothing here may break a use case: a tracer that fails is ignored, and the use case answers as
10
+ * it would have.
11
+ */
12
+ /** What is said about a span: plain values only, the way OpenTelemetry takes them. */
13
+ export type Attributes = Readonly<Record<string, string | number | boolean>>;
14
+ /**
15
+ * How a use case ended.
16
+ *
17
+ * - `ok`: it answered.
18
+ * - `refused`: expected — the server said no and explained why (a validation, a conflict, a
19
+ * permission), or the bounded context declared the error expected (`expectErrors`). Never
20
+ * reported.
21
+ * - `aborted`: whoever asked stopped waiting (a screen that closed). Never reported.
22
+ * - `failed`: anything else — a bug, a server that broke, a server nobody could reach. Reported.
23
+ */
24
+ export type Outcome = "ok" | "refused" | "aborted" | "failed";
25
+ /** One span: a use case, open while it runs. */
26
+ export interface Span {
27
+ /** 32 hex — the id the server continues and a person quotes as a support code. Empty when
28
+ * nothing traces. */
29
+ readonly traceId: string;
30
+ /** 16 hex. Empty when nothing traces. */
31
+ readonly spanId: string;
32
+ /** Whether this trace is kept: the server follows the same decision. */
33
+ readonly sampled: boolean;
34
+ /** Vendor state that travels beside the trace (`tracestate`), when there is any. */
35
+ readonly state?: string;
36
+ /** Says more about the use case while it runs. */
37
+ set(attributes: Attributes): void;
38
+ /** Closes it, once. `error` is what it failed or was refused with. */
39
+ end(outcome: Outcome, error?: unknown): void;
40
+ }
41
+ /** Opens spans. One per platform; `NO_TRACER` when nothing traces. */
42
+ export interface Tracer {
43
+ start(name: string, options: {
44
+ parent?: Span;
45
+ attributes?: Attributes;
46
+ }): Span;
47
+ }
48
+ /** Traces nothing: what a use case runs with until a platform plugs a tracer. */
49
+ export declare const NO_TRACER: Tracer;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * What a platform plugs for execution traces: one span per use case.
3
+ *
4
+ * Context: sincpro_framework opens one span per use case; a client has to do the same, and
5
+ * continue the backend's trace. The vendor — OpenTelemetry in a browser, a queue on a phone,
6
+ * memory in a test — is the platform's, so this package defines the contract and calls it, and
7
+ * bundles none. Two implementations at least (a platform's and a test's) are why it is a port.
8
+ *
9
+ * Nothing here may break a use case: a tracer that fails is ignored, and the use case answers as
10
+ * it would have.
11
+ */
12
+ const EMPTY_SPAN = {
13
+ traceId: "",
14
+ spanId: "",
15
+ sampled: false,
16
+ set: () => undefined,
17
+ end: () => undefined,
18
+ };
19
+ /** Traces nothing: what a use case runs with until a platform plugs a tracer. */
20
+ export const NO_TRACER = { start: () => EMPTY_SPAN };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Observability: the ports a platform plugs (`Tracer`, `ErrorReporter`, `Logger`), the console
3
+ * engine logs go to by default, and how code writes and reports.
4
+ *
5
+ * domain/ the ports: tracing, errors, logging
6
+ * adapters/ console (default), memory (`./testing`), OpenTelemetry and Sentry (optional
7
+ * entries `./opentelemetry`, `./sentry`)
8
+ * infrastructure/ the ports this process runs with (`configureProcess` plugs them)
9
+ * services/ the loggers, `reportError`, the default `outcomeOf`
10
+ */
11
+ export type { ErrorAbout, ErrorReporter, } from "./domain/errors.js";
12
+ export type { LayerLogger, Logger, LogLayer, LogLevel, LogRecord, } from "./domain/logging.js";
13
+ export { type Attributes, NO_TRACER, type Outcome, type Span, type Tracer, } from "./domain/tracing.js";
14
+ export { combineLoggers, logger, loggerAdapter, } from "./services/logging.js";
15
+ export { outcomeOf } from "./services/outcome.js";
16
+ export { reportError } from "./services/reporting.js";
@@ -0,0 +1,4 @@
1
+ export { NO_TRACER, } from "./domain/tracing.js";
2
+ export { combineLoggers, logger, loggerAdapter, } from "./services/logging.js";
3
+ export { outcomeOf } from "./services/outcome.js";
4
+ export { reportError } from "./services/reporting.js";
@@ -0,0 +1,21 @@
1
+ import type { ErrorReporter } from "../domain/errors.js";
2
+ import type { Logger } from "../domain/logging.js";
3
+ import { type Outcome, type Tracer } from "../domain/tracing.js";
4
+ /**
5
+ * The observability this process runs with: who traces, who reports, where logs go and how an
6
+ * error ended a use case — the ports `configureProcess` plugged, read on every call.
7
+ *
8
+ * Context: sincpro_framework keeps the same state in its `observability_registry`, outside the
9
+ * buses, so a test resets everything with one call. Until a process configures, nothing traces,
10
+ * nothing reports, and logs go to the console in development mode.
11
+ */
12
+ export type ObservabilityPorts = {
13
+ readonly tracer: Tracer;
14
+ readonly reporter: ErrorReporter | undefined;
15
+ readonly logger: Logger | undefined;
16
+ readonly outcomeOf: (error: unknown) => Outcome;
17
+ };
18
+ /** What the process observes with now. */
19
+ export declare function observabilityNow(): ObservabilityPorts;
20
+ /** @internal — `configureProcess` and `configureLogger` are the way in. */
21
+ export declare function replaceObservability(ports: Partial<ObservabilityPorts>): void;
@@ -0,0 +1,18 @@
1
+ import { consoleLogger } from "../adapters/console.js";
2
+ import { NO_TRACER, } from "../domain/tracing.js";
3
+ import { outcomeOf } from "../services/outcome.js";
4
+ const DEFAULTS = {
5
+ tracer: NO_TRACER,
6
+ reporter: undefined,
7
+ logger: consoleLogger({ mode: "development" }),
8
+ outcomeOf,
9
+ };
10
+ let current = DEFAULTS;
11
+ /** What the process observes with now. */
12
+ export function observabilityNow() {
13
+ return current;
14
+ }
15
+ /** @internal — `configureProcess` and `configureLogger` are the way in. */
16
+ export function replaceObservability(ports) {
17
+ current = { ...current, ...ports };
18
+ }
@@ -0,0 +1,27 @@
1
+ import { type Execution } from "../../context/domain/execution.js";
2
+ import type { LayerLogger, Logger, LogLayer } from "../domain/logging.js";
3
+ import type { Outcome } from "../domain/tracing.js";
4
+ /** The infrastructure's logger — the global one: the code that starts and holds the application. */
5
+ export declare const logger: LayerLogger;
6
+ /** The adapters' logger: what talks to a printer, a database, another system. */
7
+ export declare const loggerAdapter: LayerLogger;
8
+ /** @internal — a bounded context's own logger (`project.logger`). */
9
+ export declare function boundedContextLogger(name: string): LayerLogger;
10
+ /** @internal — what a Feature or ApplicationService finds as `this.logger` while it runs. */
11
+ export declare function executionLogger(layer: "feature" | "application_service", execution: Execution): LayerLogger;
12
+ /** Each destination keeps its own policy; one failing sink does not hold back the others. */
13
+ export declare function combineLoggers(...loggers: readonly Logger[]): Logger;
14
+ /** @internal — lifecycle records contain correlation and outcome, never business payloads. */
15
+ export declare function logExecution(layer: "feature" | "application_service", execution: Execution, completed?: {
16
+ outcome: Outcome;
17
+ durationMs: number;
18
+ }): void;
19
+ /**
20
+ * @internal — an error a handler of the bounded context answered instead of raising: said once,
21
+ * with where it was handled, never with the error itself.
22
+ */
23
+ export declare function logHandled(layer: LogLayer, correlation: {
24
+ execution: Execution;
25
+ } | {
26
+ boundedContext: string;
27
+ }): void;
@@ -0,0 +1,105 @@
1
+ import { BIND } from "../../context/domain/execution.js";
2
+ import { observabilityNow } from "../infrastructure/registry.js";
3
+ const processSink = () => observabilityNow().logger;
4
+ /** Sink failures cannot replace a business outcome or cause unhandled async rejections. */
5
+ function deliver(logger, record) {
6
+ if (!logger)
7
+ return;
8
+ try {
9
+ const pending = logger.log(record);
10
+ if (pending)
11
+ void Promise.resolve(pending).catch(() => { });
12
+ }
13
+ catch {
14
+ // Logging adapters are best-effort, as are tracing and error-reporting adapters.
15
+ }
16
+ }
17
+ function correlationOf(execution) {
18
+ return {
19
+ boundedContext: execution.boundedContext,
20
+ useCase: execution.useCase,
21
+ executionId: execution.id,
22
+ ...(execution.span.traceId ? { traceId: execution.span.traceId } : {}),
23
+ ...(execution.span.spanId ? { spanId: execution.span.spanId } : {}),
24
+ };
25
+ }
26
+ function layerLogger(layer, sink, correlation = {}) {
27
+ const write = (level, message, attributes) => {
28
+ deliver(sink(), {
29
+ timestamp: new Date().toISOString(),
30
+ level,
31
+ layer,
32
+ message,
33
+ ...(attributes ? { attributes: { ...attributes } } : {}),
34
+ ...correlation,
35
+ });
36
+ };
37
+ const logger = {
38
+ debug: (message, attributes) => write("debug", message, attributes),
39
+ info: (message, attributes) => write("info", message, attributes),
40
+ warn: (message, attributes) => write("warn", message, attributes),
41
+ error: (message, attributes) => write("error", message, attributes),
42
+ };
43
+ Object.defineProperty(logger, BIND, {
44
+ value: (run) => layerLogger(layer, sink, correlationOf(run)),
45
+ enumerable: false,
46
+ });
47
+ return logger;
48
+ }
49
+ /** The infrastructure's logger — the global one: the code that starts and holds the application. */
50
+ export const logger = layerLogger("infrastructure", processSink);
51
+ /** The adapters' logger: what talks to a printer, a database, another system. */
52
+ export const loggerAdapter = layerLogger("adapter", processSink);
53
+ /** @internal — a bounded context's own logger (`project.logger`). */
54
+ export function boundedContextLogger(name) {
55
+ return layerLogger("bounded_context", processSink, { boundedContext: name });
56
+ }
57
+ /** @internal — what a Feature or ApplicationService finds as `this.logger` while it runs. */
58
+ export function executionLogger(layer, execution) {
59
+ return layerLogger(layer, processSink, correlationOf(execution));
60
+ }
61
+ /** Each destination keeps its own policy; one failing sink does not hold back the others. */
62
+ export function combineLoggers(...loggers) {
63
+ return {
64
+ log: (record) => {
65
+ for (const logger of loggers)
66
+ deliver(logger, record);
67
+ },
68
+ };
69
+ }
70
+ /** @internal — lifecycle records contain correlation and outcome, never business payloads. */
71
+ export function logExecution(layer, execution, completed) {
72
+ const outcome = completed?.outcome;
73
+ const level = outcome === "failed"
74
+ ? "error"
75
+ : outcome === "refused"
76
+ ? "warn"
77
+ : completed
78
+ ? "info"
79
+ : "debug";
80
+ deliver(processSink(), {
81
+ timestamp: new Date().toISOString(),
82
+ level,
83
+ layer,
84
+ event: completed ? "execution.finished" : "execution.started",
85
+ message: completed ? `Execution ${completed.outcome}` : "Execution started",
86
+ ...correlationOf(execution),
87
+ ...(completed ? completed : {}),
88
+ });
89
+ }
90
+ /**
91
+ * @internal — an error a handler of the bounded context answered instead of raising: said once,
92
+ * with where it was handled, never with the error itself.
93
+ */
94
+ export function logHandled(layer, correlation) {
95
+ deliver(processSink(), {
96
+ timestamp: new Date().toISOString(),
97
+ level: "info",
98
+ layer,
99
+ event: "error.handled",
100
+ message: "Error handled",
101
+ ...("execution" in correlation
102
+ ? correlationOf(correlation.execution)
103
+ : { boundedContext: correlation.boundedContext }),
104
+ });
105
+ }
@@ -0,0 +1,12 @@
1
+ import type { Outcome } from "../domain/tracing.js";
2
+ /**
3
+ * How an error ended a use case, by default.
4
+ *
5
+ * Context: the backend reports what nobody expected and only that — a refusal it explained is
6
+ * the business answering, not a fault. Here the same: a `TransportError` the server answered
7
+ * with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
8
+ * else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported. A bounded
9
+ * context adds its own expected errors (`expectErrors`); a process replaces this whole
10
+ * (`configureProcess({ outcomeOf })`).
11
+ */
12
+ export declare function outcomeOf(error: unknown): Outcome;
@@ -0,0 +1,20 @@
1
+ import { TransportError } from "../../client/domain/transport.js";
2
+ /**
3
+ * How an error ended a use case, by default.
4
+ *
5
+ * Context: the backend reports what nobody expected and only that — a refusal it explained is
6
+ * the business answering, not a fault. Here the same: a `TransportError` the server answered
7
+ * with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
8
+ * else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported. A bounded
9
+ * context adds its own expected errors (`expectErrors`); a process replaces this whole
10
+ * (`configureProcess({ outcomeOf })`).
11
+ */
12
+ export function outcomeOf(error) {
13
+ if (error instanceof TransportError && error.status !== null)
14
+ return error.status >= 400 && error.status < 500 ? "refused" : "failed";
15
+ if (error !== null &&
16
+ typeof error === "object" &&
17
+ error.name === "AbortError")
18
+ return "aborted";
19
+ return "failed";
20
+ }
@@ -1,5 +1,6 @@
1
- import { newEntityId } from "../common/ids.js";
2
- import { processContext, processNow } from "./process.js";
1
+ import { newEntityId } from "../../common/ids.js";
2
+ import { processContext } from "../../context/infrastructure/process.js";
3
+ import { observabilityNow } from "../infrastructure/registry.js";
3
4
  /**
4
5
  * Whether this is the first time this error is reported. A Feature that fails inside an
5
6
  * ApplicationService throws through it, and a screen that draws the failure may break on it:
@@ -28,7 +29,7 @@ export function firstReport(error) {
28
29
  * ignored: telling about a failure never becomes a second one.
29
30
  */
30
31
  export function reportError(error, where) {
31
- const settings = processNow();
32
+ const settings = observabilityNow();
32
33
  try {
33
34
  // What the server explained, or what the caller stopped waiting for, is not a fault here either.
34
35
  if (settings.outcomeOf(error) !== "failed")
@@ -1,35 +1,5 @@
1
- import type { ErrorAbout, ErrorReporter, Outcome, Tracer } from "../observability/ports.js";
2
1
  /**
3
- * A tracer and a reporter that keep what they were told, so a test reads it back.
4
- *
5
- * const tracer = memoryTracer();
6
- * configureProcess({ tracer });
7
- * await project.CloseIssue.execute(input);
8
- * tracer.spans.map((one) => one.name); // ["project/CloseIssue", "project/Issues.close", …]
9
- *
10
- * Context: a use case is proven in Node with nothing of a platform; these are the platform's
11
- * part, held in memory. Not test doubles of this package — real implementations of its ports.
2
+ * `@sincpro/client-framework/testing`: the observability ports in memory, so a use case is
3
+ * proven in Node with nothing of a platform. Not part of what an application runs.
12
4
  */
13
- /** A span as the memory tracer kept it. */
14
- export type RecordedSpan = {
15
- name: string;
16
- traceId: string;
17
- spanId: string;
18
- /** The span it was opened under, when it was. */
19
- parentSpanId: string | undefined;
20
- attributes: Record<string, string | number | boolean>;
21
- /** How it ended; `undefined` while it is open. */
22
- outcome: Outcome | undefined;
23
- error: unknown;
24
- };
25
- export type MemoryTracer = Tracer & {
26
- readonly spans: RecordedSpan[];
27
- };
28
- export declare function memoryTracer(): MemoryTracer;
29
- export type MemoryReporter = ErrorReporter & {
30
- readonly reports: {
31
- error: unknown;
32
- about: ErrorAbout;
33
- }[];
34
- };
35
- export declare function memoryReporter(): MemoryReporter;
5
+ export { type MemoryLogger, memoryLogger, type MemoryReporter, memoryReporter, type MemoryTracer, memoryTracer, type RecordedSpan, } from "../observability/adapters/memory.js";