@cosmicdrift/kumiko-framework 0.2.2 → 0.3.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 (191) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +124 -38
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/auth/__tests__/roles.test.ts +24 -0
  10. package/src/auth/index.ts +7 -0
  11. package/src/auth/roles.ts +42 -0
  12. package/src/compliance/__tests__/duration-spec.test.ts +72 -0
  13. package/src/compliance/__tests__/profiles.test.ts +308 -0
  14. package/src/compliance/__tests__/sub-processors.test.ts +139 -0
  15. package/src/compliance/duration-spec.ts +44 -0
  16. package/src/compliance/index.ts +31 -0
  17. package/src/compliance/override-schema.ts +136 -0
  18. package/src/compliance/profiles.ts +427 -0
  19. package/src/compliance/sub-processors.ts +152 -0
  20. package/src/db/__tests__/big-int-field.test.ts +131 -0
  21. package/src/db/assert-exists-in.ts +2 -2
  22. package/src/db/cursor.ts +3 -3
  23. package/src/db/event-store-executor.ts +19 -13
  24. package/src/db/located-timestamp.ts +1 -1
  25. package/src/db/money.ts +12 -2
  26. package/src/db/pg-error.ts +1 -1
  27. package/src/db/row-helpers.ts +1 -1
  28. package/src/db/table-builder.ts +20 -5
  29. package/src/db/tenant-db.ts +9 -9
  30. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  31. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
  32. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
  33. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
  34. package/src/engine/__tests__/build-target.test.ts +135 -0
  35. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  36. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  37. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  38. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  39. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  40. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  41. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  42. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  43. package/src/engine/__tests__/raw-table.test.ts +2 -2
  44. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  45. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  46. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  47. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  48. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  49. package/src/engine/__tests__/steps-read.test.ts +142 -0
  50. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  51. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  52. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  53. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  54. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  55. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  56. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  57. package/src/engine/boot-validator/api-ext.ts +77 -0
  58. package/src/engine/boot-validator/config-deps.ts +163 -0
  59. package/src/engine/boot-validator/entity-handler.ts +466 -0
  60. package/src/engine/boot-validator/index.ts +159 -0
  61. package/src/engine/boot-validator/ownership.ts +198 -0
  62. package/src/engine/boot-validator/pii-retention.ts +155 -0
  63. package/src/engine/boot-validator/screens-nav.ts +624 -0
  64. package/src/engine/boot-validator.ts +1 -1528
  65. package/src/engine/build-app-schema.ts +1 -1
  66. package/src/engine/build-target.ts +99 -0
  67. package/src/engine/codemod/index.ts +15 -0
  68. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  69. package/src/engine/config-helpers.ts +9 -19
  70. package/src/engine/constants.ts +1 -1
  71. package/src/engine/define-feature.ts +127 -9
  72. package/src/engine/define-handler.ts +89 -3
  73. package/src/engine/define-roles.ts +2 -2
  74. package/src/engine/define-step.ts +28 -0
  75. package/src/engine/define-workflow.ts +110 -0
  76. package/src/engine/entity-handlers.ts +10 -9
  77. package/src/engine/event-helpers.ts +4 -4
  78. package/src/engine/extension-names.ts +105 -0
  79. package/src/engine/extensions/user-data.ts +106 -0
  80. package/src/engine/factories.ts +26 -16
  81. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  82. package/src/engine/feature-ast/extractors/index.ts +74 -0
  83. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  84. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  85. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  86. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  87. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  88. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  89. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  90. package/src/engine/feature-ast/parse.ts +13 -0
  91. package/src/engine/feature-ast/patch.ts +9 -1
  92. package/src/engine/feature-ast/patcher.ts +10 -3
  93. package/src/engine/feature-ast/patterns.ts +71 -1
  94. package/src/engine/feature-ast/render.ts +31 -1
  95. package/src/engine/index.ts +66 -2
  96. package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
  97. package/src/engine/pattern-library/library.ts +78 -2
  98. package/src/engine/pipeline.ts +88 -0
  99. package/src/engine/projection-helpers.ts +1 -1
  100. package/src/engine/read-claim.ts +1 -1
  101. package/src/engine/registry.ts +30 -2
  102. package/src/engine/resolve-config-or-param.ts +4 -0
  103. package/src/engine/run-pipeline.ts +162 -0
  104. package/src/engine/schema-builder.ts +10 -4
  105. package/src/engine/state-machine.ts +1 -1
  106. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  107. package/src/engine/steps/_duration-utils.ts +33 -0
  108. package/src/engine/steps/_no-return-guard.ts +21 -0
  109. package/src/engine/steps/_resolver-utils.ts +42 -0
  110. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  111. package/src/engine/steps/aggregate-append-event.ts +56 -0
  112. package/src/engine/steps/aggregate-create.ts +56 -0
  113. package/src/engine/steps/aggregate-update.ts +68 -0
  114. package/src/engine/steps/branch.ts +84 -0
  115. package/src/engine/steps/call-feature.ts +49 -0
  116. package/src/engine/steps/compute.ts +41 -0
  117. package/src/engine/steps/for-each.ts +111 -0
  118. package/src/engine/steps/mail-send.ts +44 -0
  119. package/src/engine/steps/read-find-many.ts +51 -0
  120. package/src/engine/steps/read-find-one.ts +58 -0
  121. package/src/engine/steps/retry.ts +87 -0
  122. package/src/engine/steps/return.ts +34 -0
  123. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  124. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  125. package/src/engine/steps/wait-for-event.ts +71 -0
  126. package/src/engine/steps/wait.ts +69 -0
  127. package/src/engine/steps/webhook-send.ts +71 -0
  128. package/src/engine/system-user.ts +1 -1
  129. package/src/engine/types/feature.ts +143 -1
  130. package/src/engine/types/fields.ts +134 -10
  131. package/src/engine/types/handlers.ts +18 -10
  132. package/src/engine/types/identifiers.ts +1 -0
  133. package/src/engine/types/index.ts +15 -1
  134. package/src/engine/types/step.ts +334 -0
  135. package/src/engine/types/target-ref.ts +21 -0
  136. package/src/engine/types/tree-node.ts +130 -0
  137. package/src/engine/types/workspace.ts +7 -0
  138. package/src/engine/validate-projection-allowlist.ts +161 -0
  139. package/src/event-store/snapshot.ts +1 -1
  140. package/src/event-store/upcaster-dead-letter.ts +1 -1
  141. package/src/event-store/upcaster.ts +1 -1
  142. package/src/files/__tests__/read-stream.test.ts +105 -0
  143. package/src/files/__tests__/write-stream.test.ts +233 -0
  144. package/src/files/__tests__/zip-stream.test.ts +357 -0
  145. package/src/files/file-routes.ts +1 -1
  146. package/src/files/in-memory-provider.ts +38 -0
  147. package/src/files/index.ts +3 -0
  148. package/src/files/local-provider.ts +58 -1
  149. package/src/files/types.ts +36 -8
  150. package/src/files/zip-stream.ts +251 -0
  151. package/src/jobs/job-runner.ts +10 -10
  152. package/src/lifecycle/lifecycle.ts +0 -3
  153. package/src/logging/index.ts +1 -0
  154. package/src/logging/pino-logger.ts +11 -7
  155. package/src/logging/utils.ts +24 -0
  156. package/src/observability/prometheus-meter.ts +7 -5
  157. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  158. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  159. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  160. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  161. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  162. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  163. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  164. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  165. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  166. package/src/pipeline/append-event-core.ts +22 -6
  167. package/src/pipeline/dispatcher-utils.ts +188 -0
  168. package/src/pipeline/dispatcher.ts +63 -283
  169. package/src/pipeline/distributed-lock.ts +1 -1
  170. package/src/pipeline/entity-cache.ts +2 -2
  171. package/src/pipeline/event-consumer-state.ts +0 -13
  172. package/src/pipeline/event-dispatcher.ts +4 -4
  173. package/src/pipeline/index.ts +0 -2
  174. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  175. package/src/pipeline/msp-rebuild.ts +5 -5
  176. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  177. package/src/pipeline/projection-rebuild.ts +2 -2
  178. package/src/pipeline/projection-state.ts +0 -12
  179. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  180. package/src/rate-limit/resolver.ts +1 -1
  181. package/src/search/in-memory-adapter.ts +1 -1
  182. package/src/search/meilisearch-adapter.ts +3 -3
  183. package/src/search/types.ts +1 -1
  184. package/src/secrets/leak-guard.ts +2 -2
  185. package/src/stack/request-helper.ts +9 -5
  186. package/src/stack/test-stack.ts +1 -1
  187. package/src/testing/handler-context.ts +4 -4
  188. package/src/testing/http-cookies.ts +1 -1
  189. package/src/time/tz-context.ts +1 -2
  190. package/src/ui-types/index.ts +4 -0
  191. package/src/engine/feature-ast/extractors.ts +0 -2562
@@ -41,12 +41,21 @@ function eventOwnerFeature(qualifiedName: string): string | undefined {
41
41
  return idx > 0 ? qualifiedName.slice(0, idx) : undefined;
42
42
  }
43
43
 
44
+ // System-event prefix: events under this namespace bypass the registry +
45
+ // ownership checks. Reserved for framework-internal coordination (step-
46
+ // engine deferred dispatch, lifecycle signals). The matching MSP filters
47
+ // on the literal type-string. Apps cannot write into "kumiko:system:*"
48
+ // directly — only framework step implementations call unsafeAppendEvent
49
+ // with these types.
50
+ const SYSTEM_EVENT_PREFIX = "kumiko:system:";
51
+
44
52
  export async function appendDomainEventCore(
45
53
  deps: AppendDomainEventCoreDeps,
46
54
  args: AppendEventArgs,
47
55
  ): Promise<StoredEvent> {
56
+ const isSystemEvent = args.type.startsWith(SYSTEM_EVENT_PREFIX);
48
57
  const eventDef = deps.registry.getEvent(args.type);
49
- if (!eventDef) {
58
+ if (!eventDef && !isSystemEvent) {
50
59
  throw new InternalError({
51
60
  message: `${deps.callSiteLabel}("${args.type}") — event not registered. Call r.defineEvent(shortName, schema) in a feature; appendEvent expects the qualified name returned by defineEvent (e.g. "<feature>:event:<short>").`,
52
61
  });
@@ -61,7 +70,7 @@ export async function appendDomainEventCore(
61
70
  // Feature names are registered case-preserving (pubsubOrders) but qualified
62
71
  // into kebab-case for the event/handler names (pubsub-orders:event:…) — so
63
72
  // we compare the kebab form on both sides.
64
- if (deps.callerFeature) {
73
+ if (deps.callerFeature && !isSystemEvent) {
65
74
  const owner = eventOwnerFeature(args.type);
66
75
  const callerKebab = toKebab(deps.callerFeature);
67
76
  if (owner && owner !== callerKebab) {
@@ -70,9 +79,16 @@ export async function appendDomainEventCore(
70
79
  });
71
80
  }
72
81
  }
73
- const parsed = eventDef.schema.safeParse(args.payload ?? {});
74
- if (!parsed.success) throw validationErrorFromZod(parsed.error);
75
- const validatedPayload = parsed.data as Record<string, unknown>; // @cast-boundary engine-payload
82
+ // System events skip schema validation — payload shape is owned by the
83
+ // framework step that emits + the bundled MSP that consumes.
84
+ let validatedPayload: Record<string, unknown>;
85
+ if (eventDef) {
86
+ const parsed = eventDef.schema.safeParse(args.payload ?? {});
87
+ if (!parsed.success) throw validationErrorFromZod(parsed.error);
88
+ validatedPayload = parsed.data as Record<string, unknown>;
89
+ } else {
90
+ validatedPayload = (args.payload ?? {}) as Record<string, unknown>;
91
+ }
76
92
 
77
93
  // Archive guard: block writes on archived streams. Without this an append
78
94
  // would produce an "invisible" row that loadAggregate filters out by default
@@ -97,7 +113,7 @@ export async function appendDomainEventCore(
97
113
  tenantId: deps.tenantId,
98
114
  expectedVersion,
99
115
  type: args.type,
100
- eventVersion: eventDef.version,
116
+ eventVersion: eventDef?.version ?? 1,
101
117
  payload: validatedPayload,
102
118
  metadata: {
103
119
  userId: deps.userId,
@@ -0,0 +1,188 @@
1
+ import { parseQn, qn } from "../engine/qualified-name";
2
+ import type {
3
+ HandlerRef,
4
+ LifecycleResult,
5
+ Registry,
6
+ SessionUser,
7
+ WriteResult,
8
+ } from "../engine/types";
9
+ import { InternalError, isKumikoError, type KumikoError, type WriteErrorInfo } from "../errors";
10
+
11
+ export type FailedWriteResult = Extract<WriteResult, { isSuccess: false }>;
12
+
13
+ export function isFailedWriteResult(result: unknown): result is FailedWriteResult {
14
+ return (
15
+ !!result && typeof result === "object" && "isSuccess" in result && result.isSuccess === false
16
+ );
17
+ }
18
+
19
+ export function isLifecycleResult(data: unknown): data is LifecycleResult {
20
+ return !!data && typeof data === "object" && "kind" in data;
21
+ }
22
+
23
+ export function isWriteResultShape(result: unknown): boolean {
24
+ return (
25
+ !!result &&
26
+ typeof result === "object" &&
27
+ "isSuccess" in result &&
28
+ typeof result.isSuccess === "boolean"
29
+ );
30
+ }
31
+
32
+ export function describeShape(result: unknown): string {
33
+ if (result === null) return "null";
34
+ if (result === undefined) return "undefined";
35
+ if (typeof result !== "object") return typeof result;
36
+ return `object with keys [${Object.keys(result).slice(0, 6).join(", ")}]`;
37
+ }
38
+
39
+ export function dispatcherSpanAttributes(
40
+ type: string,
41
+ operation: "query" | "write",
42
+ user: SessionUser,
43
+ feature: string | undefined,
44
+ ) {
45
+ const attrs: Record<string, string | number | boolean> = {
46
+ "kumiko.handler": type,
47
+ "kumiko.operation": operation,
48
+ "kumiko.user_id": user.id,
49
+ "kumiko.tenant_id": user.tenantId,
50
+ };
51
+ if (feature) attrs["kumiko.feature"] = feature;
52
+ return attrs;
53
+ }
54
+
55
+ export type AfterCommitHook = () => Promise<void>;
56
+
57
+ export type NestedSpec = {
58
+ readonly key: string;
59
+ readonly subType: string;
60
+ readonly foreignKey: string;
61
+ readonly items: readonly unknown[];
62
+ };
63
+
64
+ export type NestedTypeIssue = {
65
+ readonly path: string;
66
+ readonly code: string;
67
+ readonly i18nKey: string;
68
+ };
69
+
70
+ export function extractNestedSpecs(
71
+ parentType: string,
72
+ payload: unknown,
73
+ registry: Registry,
74
+ ): {
75
+ cleanPayload: Record<string, unknown>;
76
+ specs: readonly NestedSpec[];
77
+ typeIssues: readonly NestedTypeIssue[];
78
+ } | null {
79
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
80
+
81
+ let parsed: ReturnType<typeof parseQn>;
82
+ try {
83
+ parsed = parseQn(parentType);
84
+ } catch {
85
+ return null;
86
+ }
87
+ // v1 scope: only create. Update/delete-nested are explicit future work —
88
+ // they'd need different sub-types and id-handling semantics.
89
+ if (!parsed.name.endsWith(":create")) return null;
90
+
91
+ const entityName = registry.getHandlerEntity(parentType);
92
+ if (!entityName) return null;
93
+
94
+ const relations = registry.getRelations(entityName);
95
+ const source = payload as Record<string, unknown>; // @cast-boundary engine-payload — generic dispatch über alle Entity-Types
96
+ const clean: Record<string, unknown> = { ...source };
97
+ const specs: NestedSpec[] = [];
98
+ const typeIssues: NestedTypeIssue[] = [];
99
+
100
+ for (const [relKey, rel] of Object.entries(relations)) {
101
+ if (rel.type !== "hasMany" || !rel.nestedWrite) continue;
102
+ if (!(relKey in source)) continue;
103
+ const value = source[relKey];
104
+
105
+ // Non-array under a nested-write key is a client shape error. Silent
106
+ // strip (via default zod stripping) would hide it — a client sending
107
+ // `tasks: "bogus"` or `tasks: null` has to know the field was ignored,
108
+ // or they'll wonder why their data never showed up. Fail loud.
109
+ if (!Array.isArray(value)) {
110
+ typeIssues.push({
111
+ path: relKey,
112
+ code: "invalid_type",
113
+ i18nKey: "errors.validation.invalid_type",
114
+ });
115
+ // Still strip from clean payload — we're not letting the parent handler
116
+ // see a malformed value either.
117
+ delete clean[relKey];
118
+ continue;
119
+ }
120
+
121
+ // Strip the relation key from the clean payload — the parent handler
122
+ // only sees columns it actually owns.
123
+ delete clean[relKey];
124
+
125
+ // Sub-type composition: derive scope + operation from the parent qn,
126
+ // swap the entity segment. "feat:write:project:create" → "feat:write:task:create".
127
+ // Assumes target entity has a `:create` handler in the SAME feature scope
128
+ // as the parent. Cross-feature nested-writes are out of scope for v1;
129
+ // when needed, the registry would have to carry a back-pointer from
130
+ // entity → defining feature.
131
+ const subType = qn(parsed.scope, parsed.type, `${rel.target}:create`);
132
+
133
+ specs.push({
134
+ key: relKey,
135
+ subType,
136
+ foreignKey: rel.foreignKey,
137
+ items: value,
138
+ });
139
+ }
140
+
141
+ if (specs.length === 0 && typeIssues.length === 0) return null;
142
+ return { cleanPayload: clean, specs, typeIssues };
143
+ }
144
+
145
+ export function prefixValidationPath(info: WriteErrorInfo, prefix: string): WriteErrorInfo {
146
+ if (info.code !== "validation_error") return info;
147
+ const details = info.details as // @cast-boundary error-details
148
+ | {
149
+ fields?: readonly {
150
+ path: string;
151
+ code: string;
152
+ i18nKey: string;
153
+ params?: Readonly<Record<string, unknown>>;
154
+ }[];
155
+ }
156
+ | undefined;
157
+ const fields = details?.fields;
158
+ if (!fields) return info;
159
+ return {
160
+ ...info,
161
+ details: {
162
+ ...details,
163
+ fields: fields.map((f) => ({ ...f, path: `${prefix}.${f.path}` })),
164
+ },
165
+ };
166
+ }
167
+
168
+ export class BatchRollback extends Error {
169
+ constructor(
170
+ readonly failedIndex: number,
171
+ readonly failureError: WriteErrorInfo,
172
+ ) {
173
+ super(`batch rollback at command ${failedIndex}: ${failureError.code}`);
174
+ this.name = "BatchRollback";
175
+ }
176
+ }
177
+
178
+ export type HandlerType = string | HandlerRef;
179
+
180
+ export function resolveType(type: HandlerType): string {
181
+ return typeof type === "string" ? type : type.name;
182
+ }
183
+
184
+ export function wrapToKumiko(e: unknown): KumikoError {
185
+ if (isKumikoError(e)) return e;
186
+ if (e instanceof Error) return new InternalError({ cause: e });
187
+ return new InternalError({ message: String(e) });
188
+ }