@cosmicdrift/kumiko-types 0.159.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.
package/LICENSE ADDED
@@ -0,0 +1,57 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Marc Frost
6
+
7
+ Licensed Work: @cosmicdrift/kumiko-framework
8
+ The Licensed Work is © 2026 Marc Frost.
9
+
10
+ Additional Use Grant:
11
+ You may use the Licensed Work in production for any purpose, including
12
+ commercially, EXCEPT for the Restricted Use.
13
+
14
+ "Restricted Use" is defined as using the Licensed Work to provide a platform
15
+ or service to third parties that allows them to host, deploy, or run their
16
+ own applications built with the Licensed Work. This includes, but is not
17
+ limited to: managed hosting services, software-as-a-service (SaaS) platforms,
18
+ platform-as-a-service (PaaS), developer platforms, or any multi-tenant
19
+ managed offering of the Licensed Work.
20
+
21
+ This restriction does not apply to the Licensor, any entity controlled by,
22
+ controlling, or under common control with the Licensor ("Affiliates"), or
23
+ contractors acting on their behalf. The Licensor remains free to use the
24
+ Licensed Work for any purpose, including for the operation of kumiko.rocks.
25
+
26
+ Change Date: 2030-05-05
27
+ Change License: Apache License, Version 2.0
28
+
29
+
30
+ Terms
31
+
32
+ The Licensor hereby grants you the right to copy, modify, create derivative works,
33
+ redistribute, and make non-production use of the Licensed Work. The Licensor may
34
+ make an Additional Use Grant, above, permitting limited production use.
35
+
36
+ Effective on the Change Date, or the fourth anniversary of the first publicly
37
+ available distribution of the Licensed Work under this License, whichever comes
38
+ first, this License will convert to the Change License.
39
+
40
+ This Business Source License governs use of the Licensed Work in all cases, except
41
+ as to any use that is explicitly granted in the Additional Use Grant above or
42
+ under the Change License after the Change Date.
43
+
44
+ If your use of the Licensed Work does not comply with the requirements of this
45
+ License, you must cease use of the Licensed Work immediately.
46
+
47
+ All copies of the Licensed Work, and all derivative works thereof, must include
48
+ this License.
49
+
50
+ This License does not grant you any right, title, or interest in any trademark,
51
+ logo, or branding of the Licensor, except as required to comply with this License.
52
+
53
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
54
+ “AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
55
+ WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
56
+ TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
57
+ ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-types",
3
+ "version": "0.159.0",
4
+ "description": "Framework-Type-Definitions für Kumiko — FeatureDefinition, BootCheck-Types und die reinen Engine-Types, ohne Runtime-Code. Erlaubt Downstream-Konsumenten, gegen die Type-Contracts zu bauen, ohne das ganze Framework-Package zu importieren.",
5
+ "license": "BUSL-1.1",
6
+ "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/CosmicDriftGameStudio/kumiko-framework.git",
10
+ "directory": "packages/types"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
14
+ },
15
+ "homepage": "https://kumiko.rocks",
16
+ "type": "module",
17
+ "kumiko": {
18
+ "runtime": "client"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./src/index.ts",
23
+ "default": "./src/index.ts"
24
+ },
25
+ "./identifiers": {
26
+ "types": "./src/identifiers.ts",
27
+ "default": "./src/identifiers.ts"
28
+ },
29
+ "./target-ref": {
30
+ "types": "./src/target-ref.ts",
31
+ "default": "./src/target-ref.ts"
32
+ },
33
+ "./event-type-map": {
34
+ "types": "./src/event-type-map.ts",
35
+ "default": "./src/event-type-map.ts"
36
+ },
37
+ "./http-route": {
38
+ "types": "./src/http-route.ts",
39
+ "default": "./src/http-route.ts"
40
+ },
41
+ "./relations": {
42
+ "types": "./src/relations.ts",
43
+ "default": "./src/relations.ts"
44
+ },
45
+ "./tree-node": {
46
+ "types": "./src/tree-node.ts",
47
+ "default": "./src/tree-node.ts"
48
+ },
49
+ "./logger": {
50
+ "types": "./src/logger.ts",
51
+ "default": "./src/logger.ts"
52
+ },
53
+ "./search-adapter": {
54
+ "types": "./src/search-adapter.ts",
55
+ "default": "./src/search-adapter.ts"
56
+ },
57
+ "./geo-tz": {
58
+ "types": "./src/geo-tz.ts",
59
+ "default": "./src/geo-tz.ts"
60
+ },
61
+ "./observability": {
62
+ "types": "./src/observability/index.ts",
63
+ "default": "./src/observability/index.ts"
64
+ },
65
+ "./observability/metric": {
66
+ "types": "./src/observability/metric.ts",
67
+ "default": "./src/observability/metric.ts"
68
+ },
69
+ "./observability/provider": {
70
+ "types": "./src/observability/provider.ts",
71
+ "default": "./src/observability/provider.ts"
72
+ },
73
+ "./observability/span": {
74
+ "types": "./src/observability/span.ts",
75
+ "default": "./src/observability/span.ts"
76
+ },
77
+ "./db-connection": {
78
+ "types": "./src/db-connection.ts",
79
+ "default": "./src/db-connection.ts"
80
+ }
81
+ },
82
+ "dependencies": {
83
+ "hono": "^4.12.18",
84
+ "postgres": "^3.4.9"
85
+ },
86
+ "publishConfig": {
87
+ "registry": "https://registry.npmjs.org",
88
+ "access": "public"
89
+ },
90
+ "files": [
91
+ "src",
92
+ "README.md",
93
+ "LICENSE"
94
+ ]
95
+ }
@@ -0,0 +1,11 @@
1
+ import type postgres from "postgres";
2
+
3
+ // biome-ignore lint/suspicious/noExplicitAny: Bun.SQL global type
4
+ export type DbConnection = ReturnType<typeof postgres> | any;
5
+ // biome-ignore lint/suspicious/noExplicitAny: postgres-js namespace lookup
6
+ export type DbTx = postgres.TransactionSql<any> | any;
7
+ export type DbRunner = DbConnection | DbTx;
8
+ export type DbRow = Record<string, unknown>;
9
+ export type PgClient = ReturnType<typeof postgres>;
10
+
11
+ export type PgListenClient = ReturnType<typeof postgres>;
@@ -0,0 +1,37 @@
1
+ // Cross-Feature Compile-Time-Type-Map.
2
+ //
3
+ // Zweck: ctx.appendEvent / ctx.queryProjection / dispatcher.write gegen ein
4
+ // statisch bekanntes Schema-Bild prüfen, statt erst zur Boot- oder Runtime
5
+ // (zod-validate) zu scheitern. Designer/AI-Layer profitiert dadurch sofort:
6
+ // Autocomplete kennt alle Event-Typen aller geladenen Features, payload-
7
+ // Shape-Mismatches werden im Editor angezeigt, nicht erst beim Boot.
8
+ //
9
+ // Befüllung erfolgt per Feature über `declare module "@cosmicdrift/kumiko-framework/engine"`
10
+ // — entweder hand-geschrieben (für stabile Frameworks-Internals) oder vom
11
+ // Codegen-Skript erzeugt (für apps/bundled-features). Empty defaults sind
12
+ // kein Bug: ein Feature ohne Augmentation ist runtime-pluggable und nutzt
13
+ // die Fallback-Overload mit `unknown` payload.
14
+ //
15
+ // Pattern für hand-geschriebene Augmentation am File-Top:
16
+ //
17
+ // declare module "@cosmicdrift/kumiko-framework/engine" {
18
+ // interface KumikoEventTypeMap {
19
+ // "users:user.created": z.infer<typeof userCreatedSchema>;
20
+ // }
21
+ // }
22
+
23
+ // MUST be `interface` (not `type`): only interfaces support TS declaration-
24
+ // merging. Apps/features extend these via `declare module "@cosmicdrift/kumiko-framework/engine"`
25
+ // blocks. A `type X = {}` alias would silently break that augmentation channel.
26
+
27
+ // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker — augmented per feature
28
+ export interface KumikoEventTypeMap {}
29
+
30
+ // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
31
+ export interface KumikoEntityTypeMap {}
32
+
33
+ // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
34
+ export interface KumikoHandlerPayloadMap {}
35
+
36
+ // biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
37
+ export interface KumikoHandlerResultMap {}
package/src/geo-tz.ts ADDED
@@ -0,0 +1,32 @@
1
+ // GeoTzProvider — optional adapter for geo-position → IANA zone. The framework
2
+ // defines ONLY the interface + injection seam (ctx.tz.fromCoordinates /
3
+ // fromAddress); a concrete implementation ships in a separate package
4
+ // (e.g. a geo-tz-based offline package). v1 default: no provider — the
5
+ // fromCoordinates/fromAddress methods throw clearly instead of silently guessing.
6
+ //
7
+ // `fromCoordinates` is the PRIMARY method: offline geo-tz libs resolve
8
+ // lat/lng → zone (exact, offline, free) — they don't know postal addresses.
9
+ // `fromAddress` is optional, for providers backed by a geocoding API (address →
10
+ // zone, online). This split keeps the interface compatible with both
11
+ // provider classes instead of forcing an address shape the offline case
12
+ // can't serve at all.
13
+
14
+ export type GeoCoordinates = {
15
+ readonly latitude: number;
16
+ readonly longitude: number;
17
+ };
18
+
19
+ export type GeoAddress = {
20
+ readonly street?: string;
21
+ readonly city?: string;
22
+ readonly region?: string;
23
+ readonly postalCode?: string;
24
+ readonly country?: string;
25
+ };
26
+
27
+ export type GeoTzProvider = {
28
+ /** Geo-coordinates → IANA zone (offline lat/lng → tz). */
29
+ readonly fromCoordinates: (coords: GeoCoordinates) => string | Promise<string>;
30
+ /** Optional: postal address → IANA zone (geocoding API provider). */
31
+ readonly fromAddress?: (address: GeoAddress) => string | Promise<string>;
32
+ };
@@ -0,0 +1,72 @@
1
+ // HTTP-Route-Definition — feature-deklarierte HTTP-Endpoints außerhalb
2
+ // der /api/write|query|batch-Pipeline. Use-Case: RSS/Atom-Feeds, OpenAPI-
3
+ // Specs, OG-Image-Generators, Webhook-Receiver — alles wo der Feature-
4
+ // Author das Wire-Format selbst kontrolliert.
5
+ //
6
+ // Pattern symmetrisch zu r.queryHandler / r.writeHandler: Definition als
7
+ // Teil des Features (nicht des App-Bootstrapping). Phase-3 Multi-Tenant
8
+ // wird trivial weil tenant-context via host-resolution greift.
9
+ //
10
+ // Escape-hatch bleibt: runProdApp.extraRoutes für hand-rolled Routes die
11
+ // nichts mit einem Feature zu tun haben (z.B. plattform-spezifische
12
+ // Static-Serving-Logic).
13
+
14
+ import type { Context } from "hono";
15
+
16
+ /** Subset von HTTP-Methoden den wir aktiv unterstützen. Hono spricht
17
+ * alle, aber das hier sind die einzigen die ein Feature-Author
18
+ * realistisch deklariert. */
19
+ export type HttpRouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
20
+
21
+ /** Dependencies die der Handler vom Framework bekommt. App-Author kann
22
+ * die App selbst aufrufen (`deps.app.fetch(...)` für intern-call) oder
23
+ * direkt per dispatcher Daten ziehen. Db/Redis sind die rohen Connections
24
+ * — wer Tenant-Scope braucht muss durch dispatcher.query gehen.
25
+ *
26
+ * Hono-typing: `Context<any, any>` weil das Hono-Type-Param-Setup nur
27
+ * intern relevant ist. Concrete Hono-app wird im Boot-Path zugewiesen. */
28
+ export type HttpRouteHandlerDeps = {
29
+ /** Die Hono-app — Handler kann via app.fetch(...) interne Routes
30
+ * ansprechen (z.B. /api/query mit der vollen Auth-/Anonymous-Chain). */
31
+ // biome-ignore lint/suspicious/noExplicitAny: Hono's generic-Param ist im Framework-Boundary unsichtbar
32
+ readonly app: import("hono").Hono<any, any>;
33
+ /** Run a query handler in-process, forcing a SPECIFIC tenant — WITHOUT
34
+ * going through the public /api/query HTTP layer (no header parsing, no
35
+ * anonymousAccess tenant resolution). The synthesized caller carries
36
+ * anonymous-level access ONLY (same role a real anonymous request would
37
+ * have, no more) — the primitive forces the tenant, not the privilege
38
+ * level, so it stays safe to call from any `anonymous: true` route
39
+ * without risking a field-level disclosure a real anonymous caller
40
+ * couldn't already get. Use this whenever the route needs a tenant
41
+ * other than the one the request resolves to (e.g. always
42
+ * SYSTEM_TENANT_ID regardless of the visited host) — spoofing that via
43
+ * an internal X-Tenant header on `app.fetch(...)` is indistinguishable
44
+ * from an external client and gets rejected by resolverTrust:
45
+ * "authoritative" anonymousAccess configs (see auth-middleware.ts). */
46
+ readonly systemQuery: (
47
+ type: string,
48
+ payload: unknown,
49
+ tenantId: import("./identifiers").TenantId,
50
+ ) => Promise<unknown>;
51
+ };
52
+
53
+ export type HttpRouteHandler = (
54
+ // biome-ignore lint/suspicious/noExplicitAny: Hono Context-Generics sind im Framework-Boundary unsichtbar
55
+ c: Context<any, any>,
56
+ deps: HttpRouteHandlerDeps,
57
+ ) => Response | Promise<Response>;
58
+
59
+ export type HttpRouteDefinition = {
60
+ /** HTTP-Methode — bei Hono-Mount via app.{get,post,...}(path). */
61
+ readonly method: HttpRouteMethod;
62
+ /** URL-Pfad (Hono-Pattern, z.B. "/feed.xml" oder "/og/:tenantId.png"). */
63
+ readonly path: string;
64
+ /** Wenn true, bypasses die /api/*-Auth-Middleware. Default false —
65
+ * Routes liegen außerhalb /api/* und sehen die Auth-Middleware
66
+ * ohnehin nicht; das Flag ist semantisch (= "diese Route ist
67
+ * bewusst öffentlich") für Boot-Validator + Doku. */
68
+ readonly anonymous?: boolean;
69
+ /** Hono-Handler. Bekommt Hono-Context + Framework-Deps; returnt
70
+ * Response (sync oder async). */
71
+ readonly handler: HttpRouteHandler;
72
+ };
@@ -0,0 +1,47 @@
1
+ // Domain-identifier type aliases. Used everywhere a tenantId/userId/aggregateId
2
+ // travels through the framework. One declaration per concept so future
3
+ // representation changes (branded types, UUID validation, opaque wrappers)
4
+ // land in a single place.
5
+
6
+ // Tenant identifier — UUID string today. May become branded/opaque later
7
+ // without touching call sites.
8
+ export type TenantId = string;
9
+
10
+ // Lowercase UUID (any RFC-4122 variant). Strict enough to keep client-
11
+ // supplied junk (e.g. SQL fragments, path-traversal probes) out of the
12
+ // pipeline; loose enough that v4 / v7 / nil all match. Any caller that
13
+ // already holds a TenantId from a trusted source (JWT payload, server
14
+ // config) skips this — the helper is for **untrusted input** crossing
15
+ // the system boundary.
16
+ const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
17
+
18
+ // Validates a candidate string against the tenantId format and returns it
19
+ // as a TenantId, or `null` when it doesn't match. Use at every system
20
+ // boundary that admits untrusted input (HTTP headers, cookies, query
21
+ // params). Returning null instead of throwing keeps the caller in charge
22
+ // of the rejection shape — middleware returns 400, batch jobs may filter
23
+ // + log, and unit tests don't need a try/catch.
24
+ export function parseTenantId(value: unknown): TenantId | null {
25
+ if (typeof value !== "string") return null;
26
+ if (!TENANT_ID_REGEX.test(value)) return null;
27
+ return value;
28
+ }
29
+
30
+ // "System-scope" tenant marker: handlers carry this tenantId when the event
31
+ // doesn't belong to any particular tenant (reference data, cross-tenant
32
+ // jobs, global config). The concrete UUID is a valid v4 (not all-zeroes —
33
+ // Postgres' UUID type rejects invalid variants), chosen to be easy to
34
+ // eyeball in logs. Central constant so call sites don't re-type the string
35
+ // and the isSystemTenant() check stays in sync.
36
+ export const SYSTEM_TENANT_ID: TenantId = "00000000-0000-4000-8000-000000000000";
37
+
38
+ export function isSystemTenant(tenantId: TenantId | null | undefined): boolean {
39
+ return !tenantId || tenantId === SYSTEM_TENANT_ID;
40
+ }
41
+
42
+ // Primary-key identifier for any entity row. Two shapes coexist because of
43
+ // the entity-def `idType` switch: classic CRUD entities keep `serial` (number),
44
+ // while tenant + ES aggregates run on `uuid` (string). Call sites that pass
45
+ // the id through to the DB layer stay agnostic; only code that formats ids
46
+ // for URLs, logs, or cache keys needs `String(id)` — JS coerces both safely.
47
+ export type EntityId = number | string;
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/src/logger.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type Logger = {
2
+ info(msg: string, data?: Record<string, unknown>): void;
3
+ warn(msg: string, data?: Record<string, unknown>): void;
4
+ error(msg: string, data?: Record<string, unknown>): void;
5
+ debug(msg: string, data?: Record<string, unknown>): void;
6
+ child(context: Record<string, unknown>): Logger;
7
+ };
@@ -0,0 +1,29 @@
1
+ // Barrel for observability types. Split into span/metric/provider files so
2
+ // each module stays focused; consumers still import from "./types".
3
+
4
+ export type {
5
+ Counter,
6
+ Gauge,
7
+ Histogram,
8
+ Meter,
9
+ MetricDefinition,
10
+ MetricLabels,
11
+ MetricsHandle,
12
+ MetricType,
13
+ } from "./metric";
14
+ export type {
15
+ ObservabilityOptions,
16
+ ObservabilityProvider,
17
+ SamplingConfig,
18
+ SensitiveFilterConfig,
19
+ } from "./provider";
20
+ export type {
21
+ SerializedTraceContext,
22
+ Span,
23
+ SpanAttributes,
24
+ SpanAttributeValue,
25
+ SpanKind,
26
+ SpanStatus,
27
+ StartSpanOptions,
28
+ Tracer,
29
+ } from "./span";
@@ -0,0 +1,56 @@
1
+ // Metric + Meter contract. Feature code interacts through ctx.metrics
2
+ // (MetricsHandle); the framework wires Counter/Histogram/Gauge behind that.
3
+
4
+ export type MetricLabels = Record<string, string | number | boolean>;
5
+
6
+ export type MetricType = "counter" | "histogram" | "gauge";
7
+
8
+ export type MetricDefinition = {
9
+ // Fully-qualified name (with kumiko_<feature>_ prefix already applied).
10
+ readonly name: string;
11
+ readonly type: MetricType;
12
+ readonly description?: string;
13
+ // Declared label keys. Inc/observe calls with unknown label keys throw.
14
+ readonly labels?: readonly string[];
15
+ // Buckets only for histogram. If omitted, provider-default is used.
16
+ readonly buckets?: readonly number[];
17
+ readonly unit?: string;
18
+ // If true, the framework auto-injects tenant_id from the active ctx.
19
+ // Default false — adding tenant_id multiplies cardinality by tenant count.
20
+ readonly tenantLabel?: boolean;
21
+ };
22
+
23
+ export interface Counter {
24
+ inc(value?: number, labels?: MetricLabels): void;
25
+ }
26
+
27
+ export interface Histogram {
28
+ observe(value: number, labels?: MetricLabels): void;
29
+ }
30
+
31
+ export interface Gauge {
32
+ set(value: number, labels?: MetricLabels): void;
33
+ inc(value?: number, labels?: MetricLabels): void;
34
+ dec(value?: number, labels?: MetricLabels): void;
35
+ }
36
+
37
+ export interface Meter {
38
+ // Called once per metric during boot. Duplicate names throw.
39
+ registerMetric(def: MetricDefinition): void;
40
+ // Lookup by fully-qualified name. Unknown names throw — typed access only.
41
+ counter(name: string): Counter;
42
+ histogram(name: string): Histogram;
43
+ gauge(name: string): Gauge;
44
+ // List of registered definitions — used by ctx.metrics to validate labels.
45
+ definitions(): ReadonlyMap<string, MetricDefinition>;
46
+ }
47
+
48
+ // Public handle for feature code — ctx.metrics.
49
+ // Name resolution uses the fully-qualified name (kumiko_<feature>_<short>).
50
+ // The registrar resolves the short name from the calling feature context
51
+ // at boot time; at handler time the map is ready.
52
+ export interface MetricsHandle {
53
+ inc(name: string, labels?: MetricLabels, value?: number): void;
54
+ observe(name: string, value: number, labels?: MetricLabels): void;
55
+ set(name: string, value: number, labels?: MetricLabels): void;
56
+ }
@@ -0,0 +1,32 @@
1
+ // Observability provider contract — the "plug" that implementations (noop,
2
+ // console, otlp, prometheus) fulfil. Configuration types that consumers
3
+ // (buildServer, setupTestStack) hand in also live here.
4
+
5
+ import type { Meter } from "./metric";
6
+ import type { Tracer } from "./span";
7
+
8
+ export type SamplingConfig = {
9
+ // Base sampling rate 0..1. Default 1 in v1 (sample everything).
10
+ readonly tracing?: number;
11
+ readonly alwaysOnError?: boolean;
12
+ readonly alwaysOnSlow?: { readonly thresholdMs: number };
13
+ };
14
+
15
+ export type SensitiveFilterConfig = {
16
+ readonly redactedHeaders: readonly string[];
17
+ readonly redactedQueryParams: readonly string[];
18
+ readonly redactedAttributeKeyPatterns: readonly RegExp[];
19
+ };
20
+
21
+ export type ObservabilityOptions = {
22
+ readonly sampling?: SamplingConfig;
23
+ readonly sensitiveFilter?: Partial<SensitiveFilterConfig>;
24
+ };
25
+
26
+ export interface ObservabilityProvider {
27
+ readonly name: string;
28
+ readonly tracer: Tracer;
29
+ readonly meter: Meter;
30
+ // Graceful flush. Called from framework lifecycle shutdown.
31
+ shutdown(): Promise<void>;
32
+ }
@@ -0,0 +1,58 @@
1
+ // Span + Tracer contract. Provider implementations (noop, recording → console/otlp)
2
+ // all speak this — everything else (middleware, dispatcher, db, redis, jobs)
3
+ // is provider-agnostic.
4
+
5
+ export type SpanAttributeValue = string | number | boolean;
6
+ export type SpanAttributes = Record<string, SpanAttributeValue>;
7
+
8
+ export type SpanStatus = "unset" | "ok" | "error";
9
+
10
+ export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer";
11
+
12
+ // Serialized form of a trace context — what we pass across process boundaries
13
+ // (outbox row, BullMQ job payload). Matches W3C trace-context spec loosely,
14
+ // but staying minimal — a full W3C parser is v2.
15
+ export type SerializedTraceContext = {
16
+ readonly traceId: string;
17
+ readonly spanId: string;
18
+ readonly baggage?: Readonly<Record<string, string>>;
19
+ };
20
+
21
+ export type StartSpanOptions = {
22
+ // Either a live Span (normal in-process parent) or a serialized trace
23
+ // context (cross-process — outbox row, job payload). Both carry traceId
24
+ // and spanId, so the tracer reads them uniformly. Omitted → fall back to
25
+ // the AsyncLocalStorage active span.
26
+ readonly parent?: Span | SerializedTraceContext;
27
+ readonly attributes?: SpanAttributes;
28
+ readonly kind?: SpanKind;
29
+ readonly startTime?: number;
30
+ };
31
+
32
+ export interface Span {
33
+ readonly traceId: string;
34
+ readonly spanId: string;
35
+ readonly parentSpanId: string | undefined;
36
+ readonly name: string;
37
+ setAttribute(key: string, value: SpanAttributeValue): void;
38
+ setAttributes(attrs: SpanAttributes): void;
39
+ setStatus(status: SpanStatus, message?: string): void;
40
+ recordException(error: Error): void;
41
+ end(endTime?: number): void;
42
+ // Whether end() has been called. Idempotency guard for auto-wrappers.
43
+ readonly ended: boolean;
44
+ }
45
+
46
+ export interface Tracer {
47
+ startSpan(name: string, options?: StartSpanOptions): Span;
48
+ // Runs fn inside the span context (AsyncLocalStorage), ends the span
49
+ // automatically — including on thrown errors, where the error is recorded
50
+ // and status set to "error" before re-throwing.
51
+ withSpan<T>(
52
+ name: string,
53
+ optionsOrFn: StartSpanOptions | ((span: Span) => Promise<T>),
54
+ fn?: (span: Span) => Promise<T>,
55
+ ): Promise<T>;
56
+ // Current active span from AsyncLocalStorage, or undefined.
57
+ getActiveSpan(): Span | undefined;
58
+ }
@@ -0,0 +1,52 @@
1
+ // Keep in sync with OnDeleteStrategies in packages/framework/src/engine/constants.ts.
2
+ type OnDeleteStrategy = "cascade" | "restrict" | "setNull" | "nothing";
3
+
4
+ // --- Relations ---
5
+
6
+ export type BelongsToRelation = {
7
+ readonly type: "belongsTo";
8
+ readonly target: string;
9
+ readonly foreignKey: string;
10
+ readonly searchInclude?: readonly string[];
11
+ // onDelete is declared on the parent-side (hasMany / manyToMany) because
12
+ // that's where the "what happens to my children?" decision lives. A
13
+ // belongsTo node just points at a parent — the parent's onDelete drives
14
+ // the cleanup.
15
+ };
16
+
17
+ export type HasManyRelation = {
18
+ readonly type: "hasMany";
19
+ readonly target: string;
20
+ readonly foreignKey: string;
21
+ readonly onDelete?: OnDeleteStrategy;
22
+ // When true, a nested payload under this relation's key (e.g.
23
+ // `{ tasks: [{ ... }] }` on a `project:create` write) is auto-expanded
24
+ // into child writes: parent first, then one child-write per entry with
25
+ // the foreign key set to the parent's new id — all in the same TX.
26
+ // Opt-in (default false) so legacy hasMany relations that were declared
27
+ // purely for cascade-delete or UI-nav semantics don't silently gain a
28
+ // client-writable path. Children are never inferred from payload-shape
29
+ // alone; only relations with this flag unlock nested-write.
30
+ //
31
+ // Scope v1: depth=1, create-only, hasMany-only. Update-nested,
32
+ // delete-nested, and belongsTo/m2m auto-expansion are explicit future
33
+ // work — when they arrive, they'll take the same flag so the opt-in
34
+ // stays a single, consistent surface.
35
+ readonly nestedWrite?: boolean;
36
+ };
37
+
38
+ export type ManyToManyRelation = {
39
+ readonly type: "manyToMany";
40
+ readonly target: string;
41
+ readonly through: {
42
+ readonly table: string;
43
+ readonly sourceKey: string;
44
+ readonly targetKey: string;
45
+ };
46
+ readonly searchInclude?: readonly string[];
47
+ readonly onDelete?: OnDeleteStrategy;
48
+ };
49
+
50
+ export type RelationDefinition = BelongsToRelation | HasManyRelation | ManyToManyRelation;
51
+
52
+ export type EntityRelations = Readonly<Record<string, RelationDefinition>>;
@@ -0,0 +1,39 @@
1
+ import type { EntityId, TenantId } from "./identifiers";
2
+
3
+ export type SearchAdapterConfig = {
4
+ searchableFields: readonly string[];
5
+ rankingFields?: readonly string[];
6
+ };
7
+
8
+ export type SearchDocument = {
9
+ entityType: string;
10
+ entityId: EntityId;
11
+ weight: number;
12
+ fields: Record<string, unknown>;
13
+ };
14
+
15
+ export type SearchResult = {
16
+ entityType: string;
17
+ entityId: EntityId;
18
+ };
19
+
20
+ export type SearchOptions = {
21
+ limit?: number;
22
+ filterType?: string;
23
+ };
24
+
25
+ export type SearchAdapter = {
26
+ configure(tenantId: TenantId, config: SearchAdapterConfig): Promise<void>;
27
+ index(tenantId: TenantId, doc: SearchDocument): Promise<void>;
28
+ search(tenantId: TenantId, query: string, options?: SearchOptions): Promise<SearchResult[]>;
29
+ remove(tenantId: TenantId, entityType: string, entityId: EntityId): Promise<void>;
30
+ // Bulk variants. Default implementations loop over the single-doc methods —
31
+ // adapters should override when the backend supports a real batch call
32
+ // (Meilisearch, Elasticsearch, Typesense all do). Cuts a batch-write from
33
+ // N sequential HTTP + waitTask round-trips to one.
34
+ indexBatch?(tenantId: TenantId, docs: readonly SearchDocument[]): Promise<void>;
35
+ removeBatch?(
36
+ tenantId: TenantId,
37
+ items: readonly { entityType: string; entityId: EntityId }[],
38
+ ): Promise<void>;
39
+ };
@@ -0,0 +1,21 @@
1
+ // TargetRef — runtime-Repräsentation eines typed buildTarget-Outputs.
2
+ // Wird vom Visual-Tree-Component (renderer-web) an einen Target-Resolver
3
+ // dispatcht; der Resolver findet die Editor-Maske via featureId.
4
+ //
5
+ // **Compile-time-Safety:** TargetRef wird niemals hand-getippt. Stattdessen
6
+ // erzeugt der typed buildTarget-Builder (engine/build-target.ts) einen
7
+ // TargetRef, dessen action + args gegen die treeActions-Map des Ziel-
8
+ // Features validiert sind.
9
+ //
10
+ // **Runtime:** args sind hier untyped (Record<string, unknown>), weil
11
+ // TargetRef die erased-runtime-Version ist. Der Resolver kennt das
12
+ // Ziel-Feature und kann args entsprechend casten — ähnlich wie Event-
13
+ // Payloads im Event-Store.
14
+ //
15
+ // Siehe docs/plans/architecture/visual-tree.md A5.
16
+
17
+ export type TargetRef = {
18
+ readonly featureId: string;
19
+ readonly action: string;
20
+ readonly args?: Readonly<Record<string, unknown>>;
21
+ };
@@ -0,0 +1,129 @@
1
+ // TreeNode — single Knoten im Client-navProvider-Tree. Provider liefern
2
+ // entweder statische readonly TreeNode[] oder dynamische TreeChildrenSubscribe.
3
+ //
4
+ // **Mental-Modell** (VS-Code-Explorer):
5
+ // [icon] [label] [...hover-actions]
6
+ // optional ein target zum Klicken (öffnet Editor-Maske via
7
+ // Target-Resolver) und optional children als nested tree.
8
+ //
9
+ // **State** markiert Visual-Modus für Skeleton-Pattern:
10
+ // - "filled" (default) — schwarz, Knoten hat Inhalt
11
+ // - "stub" — hellgrau, existing aber leer (Designer-Stub-File)
12
+ // - "empty" — Platzhalter für "+ create"-Affordance
13
+ // - "loading" — Children werden gerade aufgelöst
14
+ // - "error" — Provider hat Fehler emittiert
15
+ // Provider die kein Skeleton-Pattern brauchen müssen state nicht setzen.
16
+ //
17
+ // **Subscribe-Form** für dynamic Children: Provider erhält emit(),
18
+ // gibt unsubscribe() zurück. Initial-Emit synchron oder async, weitere
19
+ // Emits beliebig oft (z.B. wenn Entity-Row neu erscheint via SSE).
20
+ // Spielt natürlich mit existing SSE-Frame: ein Provider kann intern
21
+ // auf Entity-Update-Events abonnieren und bei Änderung emit() aufrufen.
22
+
23
+ import type { TargetRef } from "./target-ref";
24
+
25
+ export type TreeNodeState = "filled" | "stub" | "empty" | "loading" | "error";
26
+
27
+ export type TreeAction = {
28
+ // Icon-Key — vom Renderer-Icon-Registry interpretiert. Konvention
29
+ // matched NavDefinition.icon: unbekannte Icons surface als missing-icon
30
+ // im UI, nicht als Boot-Failure.
31
+ readonly icon: string;
32
+ // i18n-Translation-Key oder roher String. Vom Renderer aufgelöst, Engine
33
+ // behandelt opak (mirrors NavDefinition.label, WorkspaceDefinition.label).
34
+ readonly label: string;
35
+ // Klick-Ziel der Action. Pflicht — Action ohne target ist semantisch
36
+ // sinnlos (Hover-Icon das nichts tut).
37
+ readonly target: TargetRef;
38
+ };
39
+
40
+ export type TreeNode = {
41
+ // i18n-Translation-Key oder roher String. Vom Renderer beim Rendern
42
+ // aufgelöst (siehe TreeAction.label).
43
+ readonly label: string;
44
+ // Optional. Icon links neben dem Label. Selbe Konvention wie
45
+ // TreeAction.icon — Renderer-Icon-Registry-Lookup.
46
+ readonly icon?: string;
47
+ // Visueller State für Skeleton-Pattern. Default "filled" (kein Eintrag
48
+ // ⇒ schwarz/normal). Wert-Semantik im Header-Comment dieser Datei.
49
+ readonly state?: TreeNodeState;
50
+ // Optional Klick-Ziel. Fehlt → reiner Container-Knoten (nur ausklappbar,
51
+ // nicht klickbar). Vorhanden → Klick öffnet die Editor-Maske via
52
+ // Target-Resolver in renderer-web.
53
+ readonly target?: TargetRef;
54
+ // Hover-Actions rechts (Add/Refresh/Delete/etc.). Werden in der
55
+ // Sidebar-Row erst bei Hover sichtbar — VS-Code-Pattern. Engine
56
+ // ordnet die Actions in der Reihenfolge an, in der sie hier stehen.
57
+ readonly actions?: readonly TreeAction[];
58
+ // Statische Children oder dynamic Subscribe-Function. Subscribe wird
59
+ // erst beim Ausklappen aufgerufen (lazy); die Function-Form erlaubt
60
+ // SSE-gefütterte Live-Updates wenn neue Entity-Rows reinkommen.
61
+ readonly children?: readonly TreeNode[] | TreeChildrenSubscribe;
62
+ // Provider-deklarierte „+ create"-Action für Knoten mit `state: "empty"`.
63
+ // Tree-Component zeigt automatisch ein „+"-Icon und dispatcht
64
+ // `createAction.target` bei Klick — Provider weiß was „leer befüllen"
65
+ // für ihn bedeutet (z.B. „neuer Page-Slug" vs „neue Entity-Row"),
66
+ // Convention könnte das nicht raten. Konsistent zu `state` (auch
67
+ // Provider-explizit). Siehe visual-tree.md V.1.1-Decision D3.
68
+ readonly createAction?: TreeAction;
69
+ };
70
+
71
+ // Subscribe<T> — Provider implementiert: emit(initial); ...emit(updated);
72
+ // und gibt unsubscribe-Function zurück. Caller (Tree-Component) ruft
73
+ // unsubscribe auf wenn Knoten unmounted/eingeklappt wird.
74
+ //
75
+ // **V.1.4 emitError**: optional callback für async-error-Pfade (fetch-
76
+ // fail, SSE-disconnect). Provider die explizit Errors signalisieren
77
+ // wollen rufen `emitError(e)` statt empty-emit; VisualTree zeigt
78
+ // Error-Banner mit Retry-Button. Sync-Throws im Provider-Body werden
79
+ // vom useEffect-try/catch abgefangen — emitError ist nur für async.
80
+ export type Subscribe<T> = (
81
+ emit: (value: T) => void,
82
+ emitError?: (error: Error) => void,
83
+ ) => () => void;
84
+
85
+ // TreeChildrenSubscribe — Lazy-Variante für dynamic Children. Wird
86
+ // erst aufgerufen wenn der Knoten im UI ausgeklappt wird. Kein ctx-
87
+ // Argument: Provider sind session-bound; Backend liest tenantId aus
88
+ // session bei jedem fetch/dispatch. V.1.1 hatte ein ctx mit tenantId,
89
+ // das aber im Browser nie echten Tenant trug (war auf SYSTEM_TENANT_ID
90
+ // gepinnt) und vom einzigen V.1.2-Consumer (text-content) ignoriert
91
+ // wurde. SR2-Rip 2026-05-18: Dead-API entfernt; wenn später ein
92
+ // Provider tenant-aware-rendern muss (z.B. cross-tenant-Dashboards
93
+ // für SystemAdmin), wird ctx mit echtem Tenant-Source aus dem Auth-
94
+ // Layer re-introduziert. YAGNI bis dahin.
95
+ export type TreeChildrenSubscribe = () => Subscribe<readonly TreeNode[]>;
96
+
97
+ // TreeActionDef — Schema-Eintrag pro Action in der treeActions-Map
98
+ // eines Features. Phase 0: Args sind ein optionales Type-Sample
99
+ // (kein Validator zur Laufzeit — Validation passiert compile-time
100
+ // via buildTarget-Generic, runtime via Editor-Panel-Schema).
101
+ //
102
+ // Lebt hier (nicht in build-target.ts) weil es konzeptuell zur
103
+ // Visual-Tree-Domäne gehört, nicht zum Builder. build-target.ts
104
+ // importiert den Type von hier.
105
+ export type TreeActionDef<TArgs = Record<string, unknown>> = {
106
+ readonly args?: TArgs;
107
+ };
108
+
109
+ // TreeActionsHandle<T> — Return-Type von r.treeActions(...). Trägt
110
+ // den literal-typed Action-Map durch das Feature-Export-System
111
+ // (siehe FeatureDefinition.exports + Memory `[EventDef-Exports-
112
+ // Pattern]`). Das ist die compile-time Bridge zu buildTarget:
113
+ //
114
+ // const handle = r.treeActions({ edit: { args: { slug: "" as string } } });
115
+ // // handle.id → TFeature (literal feature name)
116
+ // // handle.treeActions → { edit: { args: { slug: string } } } (literal-typed)
117
+ // buildTarget({ target: handle, action: "edit", args: { slug: "x" } });
118
+ // // ^^^^^^^^^^^^^^ ^^^^^^^^
119
+ // // literal-validated typed-validated
120
+ //
121
+ // Runtime-Lookup geht über FeatureDefinition.treeActions (erased Map),
122
+ // Compile-Time-Validation über diesen Handle.
123
+ export type TreeActionsHandle<
124
+ TFeature extends string,
125
+ TActions extends Record<string, TreeActionDef>,
126
+ > = {
127
+ readonly id: TFeature;
128
+ readonly treeActions: TActions;
129
+ };