@cosmicdrift/kumiko-framework 0.1.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 (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,56 @@
1
+ import { buildMetricName } from "./metric-validator";
2
+ import type { Meter, MetricLabels, MetricsHandle } from "./types";
3
+
4
+ // Feature-bound MetricsHandle: the short name a handler writes
5
+ // (e.g. "created_total") is resolved to the fully qualified name
6
+ // (e.g. "kumiko_orders_created_total") using the feature the current
7
+ // handler belongs to.
8
+ //
9
+ // The Meter enforces that the resolved name is registered — unregistered
10
+ // metrics throw, so typos surface at first call rather than drifting into
11
+ // dashboards. The feature name itself is validated via buildMetricName.
12
+
13
+ export function createMetricsHandle(meter: Meter, featureName: string): MetricsHandle {
14
+ return {
15
+ inc(shortName, labels, value) {
16
+ const name = buildMetricName(featureName, shortName);
17
+ meter.counter(name).inc(value, labels);
18
+ },
19
+ observe(shortName, value, labels) {
20
+ const name = buildMetricName(featureName, shortName);
21
+ meter.histogram(name).observe(value, labels);
22
+ },
23
+ set(shortName, value, labels) {
24
+ const name = buildMetricName(featureName, shortName);
25
+ meter.gauge(name).set(value, labels);
26
+ },
27
+ };
28
+ }
29
+
30
+ // Fallback for contexts where the feature is unknown (e.g. system-hooks,
31
+ // internal pipeline code). Short names are used verbatim — useful for
32
+ // framework-level usage, but rejected by the Meter unless pre-registered.
33
+ export function createUnboundMetricsHandle(meter: Meter): MetricsHandle {
34
+ return {
35
+ inc(name, labels, value) {
36
+ meter.counter(name).inc(value, labels);
37
+ },
38
+ observe(name, value, labels) {
39
+ meter.histogram(name).observe(value, labels);
40
+ },
41
+ set(name, value, labels) {
42
+ meter.gauge(name).set(value, labels);
43
+ },
44
+ };
45
+ }
46
+
47
+ // Noop fallback used when no provider is configured and for safety in
48
+ // contexts where we can't determine the feature. Every call is a no-op —
49
+ // tests and non-observability-aware features never crash.
50
+ export function createNoopMetricsHandle(): MetricsHandle {
51
+ return {
52
+ inc(_name: string, _labels?: MetricLabels, _value?: number): void {},
53
+ observe(_name: string, _value: number, _labels?: MetricLabels): void {},
54
+ set(_name: string, _value: number, _labels?: MetricLabels): void {},
55
+ };
56
+ }
@@ -0,0 +1,146 @@
1
+ import type {
2
+ Counter,
3
+ Gauge,
4
+ Histogram,
5
+ Meter,
6
+ MetricDefinition,
7
+ ObservabilityProvider,
8
+ SerializedTraceContext,
9
+ Span,
10
+ SpanStatus,
11
+ StartSpanOptions,
12
+ Tracer,
13
+ } from "./types";
14
+
15
+ // Default provider. Hot-path identical to "observability disabled" — every
16
+ // method is O(1), allocates a tiny object at most, and never calls any IO.
17
+ // Used in tests and as the safe default when no config is provided.
18
+
19
+ class NoopSpan implements Span {
20
+ readonly traceId = "";
21
+ readonly spanId = "";
22
+ readonly parentSpanId: string | undefined;
23
+ readonly name: string;
24
+ private _ended = false;
25
+
26
+ constructor(name: string, parentSpanId: string | undefined) {
27
+ this.name = name;
28
+ this.parentSpanId = parentSpanId;
29
+ }
30
+
31
+ setAttribute(_key: string, _value: unknown): void {}
32
+ setAttributes(_attrs: Record<string, unknown>): void {}
33
+ setStatus(_status: SpanStatus, _message?: string): void {}
34
+ recordException(_error: Error): void {}
35
+ end(_endTime?: number): void {
36
+ this._ended = true;
37
+ }
38
+ get ended(): boolean {
39
+ return this._ended;
40
+ }
41
+ }
42
+
43
+ class NoopTracer implements Tracer {
44
+ startSpan(name: string, options?: StartSpanOptions): Span {
45
+ // `parent` may be either a live Span or a SerializedTraceContext — both
46
+ // carry `spanId`, so a uniform read is safe.
47
+ const parentSpanId = options?.parent?.spanId;
48
+ return new NoopSpan(name, parentSpanId);
49
+ }
50
+
51
+ async withSpan<T>(
52
+ name: string,
53
+ optionsOrFn: StartSpanOptions | ((span: Span) => Promise<T>),
54
+ fn?: (span: Span) => Promise<T>,
55
+ ): Promise<T> {
56
+ const actualFn = typeof optionsOrFn === "function" ? optionsOrFn : fn;
57
+ if (!actualFn) {
58
+ throw new Error("withSpan called without callback");
59
+ }
60
+ const span = new NoopSpan(name, undefined);
61
+ try {
62
+ return await actualFn(span);
63
+ } finally {
64
+ span.end();
65
+ }
66
+ }
67
+
68
+ getActiveSpan(): Span | undefined {
69
+ return undefined;
70
+ }
71
+
72
+ startSpanFromContext(
73
+ name: string,
74
+ _context: SerializedTraceContext,
75
+ _options?: StartSpanOptions,
76
+ ): Span {
77
+ return new NoopSpan(name, undefined);
78
+ }
79
+ }
80
+
81
+ class NoopCounter implements Counter {
82
+ inc(_value?: number, _labels?: Record<string, unknown>): void {}
83
+ }
84
+
85
+ class NoopHistogram implements Histogram {
86
+ observe(_value: number, _labels?: Record<string, unknown>): void {}
87
+ }
88
+
89
+ class NoopGauge implements Gauge {
90
+ set(_value: number, _labels?: Record<string, unknown>): void {}
91
+ inc(_value?: number, _labels?: Record<string, unknown>): void {}
92
+ dec(_value?: number, _labels?: Record<string, unknown>): void {}
93
+ }
94
+
95
+ class NoopMeter implements Meter {
96
+ private readonly defs = new Map<string, MetricDefinition>();
97
+ private readonly counterInstance = new NoopCounter();
98
+ private readonly histogramInstance = new NoopHistogram();
99
+ private readonly gaugeInstance = new NoopGauge();
100
+
101
+ registerMetric(def: MetricDefinition): void {
102
+ if (this.defs.has(def.name)) {
103
+ throw new Error(`[Kumiko Observability] Metric "${def.name}" already registered.`);
104
+ }
105
+ this.defs.set(def.name, def);
106
+ }
107
+
108
+ counter(name: string): Counter {
109
+ const def = this.defs.get(name);
110
+ if (!def || def.type !== "counter") {
111
+ throw new Error(`[Kumiko Observability] Counter "${name}" not registered or wrong type.`);
112
+ }
113
+ return this.counterInstance;
114
+ }
115
+
116
+ histogram(name: string): Histogram {
117
+ const def = this.defs.get(name);
118
+ if (!def || def.type !== "histogram") {
119
+ throw new Error(`[Kumiko Observability] Histogram "${name}" not registered or wrong type.`);
120
+ }
121
+ return this.histogramInstance;
122
+ }
123
+
124
+ gauge(name: string): Gauge {
125
+ const def = this.defs.get(name);
126
+ if (!def || def.type !== "gauge") {
127
+ throw new Error(`[Kumiko Observability] Gauge "${name}" not registered or wrong type.`);
128
+ }
129
+ return this.gaugeInstance;
130
+ }
131
+
132
+ definitions(): ReadonlyMap<string, MetricDefinition> {
133
+ return this.defs;
134
+ }
135
+ }
136
+
137
+ export function createNoopProvider(): ObservabilityProvider {
138
+ const tracer = new NoopTracer();
139
+ const meter = new NoopMeter();
140
+ return {
141
+ name: "noop",
142
+ tracer,
143
+ meter,
144
+ async shutdown() {},
145
+ };
146
+ }
@@ -0,0 +1,284 @@
1
+ // Prometheus-scrapeable Meter implementation.
2
+ //
3
+ // The RecordingMeter emits events but doesn't keep rolling totals — it was
4
+ // designed as a pass-through to a user-side provider (console, OTLP,
5
+ // custom). A /metrics endpoint needs the totals materialised, so this
6
+ // module wires the same `Meter` interface to an in-memory accumulator:
7
+ //
8
+ // - counter: sum per labelset
9
+ // - gauge: current value per labelset
10
+ // - histogram: bucket counts + sum + count per labelset
11
+ //
12
+ // `serializeOpenMetrics(meter)` renders the accumulated state into the
13
+ // text format both Prometheus and the OpenMetrics standard accept.
14
+ //
15
+ // Scope limits:
16
+ // - No sliding windows (absolute counters only — the scraper diffs).
17
+ // - No exemplars (OpenMetrics feature, not used by most scrape configs).
18
+ // - No `_created` timestamps on counters — Prometheus-compatible, not
19
+ // fully OpenMetrics-conformant. Most dashboards don't care.
20
+ //
21
+ // If the caller wraps a different Meter alongside (e.g. ConsoleProvider
22
+ // for dev), they can build a composite meter that forwards to both —
23
+ // PrometheusMeter is a leaf, not an aggregator.
24
+
25
+ import { validateLabelKey } from "./metric-validator";
26
+ import type { Counter, Gauge, Histogram, Meter, MetricDefinition, MetricLabels } from "./types";
27
+
28
+ // Default buckets follow Prometheus' histogram convention (seconds-scale).
29
+ // Callers can override per-metric via MetricDefinition.buckets.
30
+ const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] as const;
31
+
32
+ // Canonicalise a labels object to a stable key — same labels in different
33
+ // insertion order must hash to the same slot.
34
+ function labelsKey(labels: MetricLabels | undefined): string {
35
+ if (!labels) return "";
36
+ const keys = Object.keys(labels).sort();
37
+ return keys.map((k) => `${k}=${String(labels[k])}`).join(",");
38
+ }
39
+
40
+ // Escape label values per OpenMetrics: backslash, double-quote, newline.
41
+ function escapeLabelValue(v: string | number | boolean): string {
42
+ return String(v).replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n");
43
+ }
44
+
45
+ function renderLabels(labels: MetricLabels | undefined): string {
46
+ if (!labels) return "";
47
+ const entries = Object.entries(labels).sort(([a], [b]) => a.localeCompare(b));
48
+ if (entries.length === 0) return "";
49
+ const inner = entries.map(([k, v]) => `${k}="${escapeLabelValue(v)}"`).join(",");
50
+ return `{${inner}}`;
51
+ }
52
+
53
+ function renderLabelsWithExtra(
54
+ labels: MetricLabels | undefined,
55
+ extra: readonly [string, string][],
56
+ ): string {
57
+ const entries: [string, string][] = labels
58
+ ? Object.entries(labels).map(([k, v]) => [k, String(v)])
59
+ : [];
60
+ for (const [k, v] of extra) entries.push([k, v]);
61
+ entries.sort(([a], [b]) => a.localeCompare(b));
62
+ if (entries.length === 0) return "";
63
+ const inner = entries.map(([k, v]) => `${k}="${escapeLabelValue(v)}"`).join(",");
64
+ return `{${inner}}`;
65
+ }
66
+
67
+ type CounterState = { labels: MetricLabels | undefined; value: number };
68
+ type GaugeState = { labels: MetricLabels | undefined; value: number };
69
+ type HistogramState = {
70
+ labels: MetricLabels | undefined;
71
+ buckets: number[]; // cumulative counts, indexed by boundary position
72
+ sum: number;
73
+ count: number;
74
+ boundaries: readonly number[]; // pinned at first observe so late-changes don't skew
75
+ };
76
+
77
+ // Shared slot-accumulator. counter.inc, gauge.inc, gauge.dec all boil
78
+ // down to "add `delta` to the existing slot or create a new slot with
79
+ // `delta`". The only variance is the sign — extracted once so counter
80
+ // and gauge don't each reimplement the same get-or-create-and-add.
81
+ // CounterState and GaugeState are structurally identical — if that
82
+ // ever diverges, this helper becomes per-type and the call-sites move
83
+ // to their own accumulator.
84
+ function addToSlot(
85
+ slots: Map<string, { labels: MetricLabels | undefined; value: number }>,
86
+ labels: MetricLabels | undefined,
87
+ delta: number,
88
+ ): void {
89
+ const key = labelsKey(labels);
90
+ const existing = slots.get(key);
91
+ if (existing) {
92
+ existing.value += delta;
93
+ } else {
94
+ slots.set(key, { labels, value: delta });
95
+ }
96
+ }
97
+
98
+ class PrometheusCounter implements Counter {
99
+ constructor(private readonly slots: Map<string, CounterState>) {}
100
+ inc(value?: number, labels?: MetricLabels): void {
101
+ addToSlot(this.slots, labels, value ?? 1);
102
+ }
103
+ }
104
+
105
+ class PrometheusGauge implements Gauge {
106
+ constructor(private readonly slots: Map<string, GaugeState>) {}
107
+ set(value: number, labels?: MetricLabels): void {
108
+ // set() overwrites wholesale — can't go through addToSlot which only
109
+ // knows about delta accumulation.
110
+ this.slots.set(labelsKey(labels), { labels, value });
111
+ }
112
+ inc(value?: number, labels?: MetricLabels): void {
113
+ addToSlot(this.slots, labels, value ?? 1);
114
+ }
115
+ dec(value?: number, labels?: MetricLabels): void {
116
+ addToSlot(this.slots, labels, -(value ?? 1));
117
+ }
118
+ }
119
+
120
+ class PrometheusHistogram implements Histogram {
121
+ constructor(
122
+ private readonly def: MetricDefinition,
123
+ private readonly slots: Map<string, HistogramState>,
124
+ ) {}
125
+ observe(value: number, labels?: MetricLabels): void {
126
+ const key = labelsKey(labels);
127
+ let state = this.slots.get(key);
128
+ if (!state) {
129
+ const boundaries = this.def.buckets ?? DEFAULT_BUCKETS;
130
+ state = {
131
+ labels,
132
+ buckets: new Array(boundaries.length).fill(0),
133
+ sum: 0,
134
+ count: 0,
135
+ boundaries,
136
+ };
137
+ this.slots.set(key, state);
138
+ }
139
+ state.sum += value;
140
+ state.count += 1;
141
+ for (let i = 0; i < state.boundaries.length; i++) {
142
+ // biome-ignore lint/style/noNonNullAssertion: bounded by loop guard
143
+ if (value <= state.boundaries[i]!) {
144
+ // biome-ignore lint/style/noNonNullAssertion: bounded by loop guard
145
+ state.buckets[i]!++;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ export type PrometheusMeterSnapshot = ReadonlyMap<
152
+ string,
153
+ { def: MetricDefinition; slots: ReadonlyArray<CounterState | GaugeState | HistogramState> }
154
+ >;
155
+
156
+ export interface PrometheusMeter extends Meter {
157
+ // Returns the current accumulated state. Used by serializeOpenMetrics
158
+ // — exposed separately so callers can inspect without parsing the text
159
+ // output (handy in tests).
160
+ snapshot(): PrometheusMeterSnapshot;
161
+ }
162
+
163
+ export function createPrometheusMeter(): PrometheusMeter {
164
+ const defs = new Map<string, MetricDefinition>();
165
+ const counterSlots = new Map<string, Map<string, CounterState>>();
166
+ const gaugeSlots = new Map<string, Map<string, GaugeState>>();
167
+ const histogramSlots = new Map<string, Map<string, HistogramState>>();
168
+ const counters = new Map<string, Counter>();
169
+ const gauges = new Map<string, Gauge>();
170
+ const histograms = new Map<string, Histogram>();
171
+
172
+ return {
173
+ registerMetric(def) {
174
+ if (defs.has(def.name)) {
175
+ throw new Error(`[Kumiko Observability] Metric "${def.name}" already registered.`);
176
+ }
177
+ for (const label of def.labels ?? []) validateLabelKey(label);
178
+ defs.set(def.name, def);
179
+ if (def.type === "counter") {
180
+ const slots = new Map<string, CounterState>();
181
+ counterSlots.set(def.name, slots);
182
+ counters.set(def.name, new PrometheusCounter(slots));
183
+ } else if (def.type === "gauge") {
184
+ const slots = new Map<string, GaugeState>();
185
+ gaugeSlots.set(def.name, slots);
186
+ gauges.set(def.name, new PrometheusGauge(slots));
187
+ } else {
188
+ const slots = new Map<string, HistogramState>();
189
+ histogramSlots.set(def.name, slots);
190
+ histograms.set(def.name, new PrometheusHistogram(def, slots));
191
+ }
192
+ },
193
+ counter(name) {
194
+ const c = counters.get(name);
195
+ if (!c)
196
+ throw new Error(`[Kumiko Observability] Counter "${name}" not registered or wrong type.`);
197
+ return c;
198
+ },
199
+ gauge(name) {
200
+ const g = gauges.get(name);
201
+ if (!g)
202
+ throw new Error(`[Kumiko Observability] Gauge "${name}" not registered or wrong type.`);
203
+ return g;
204
+ },
205
+ histogram(name) {
206
+ const h = histograms.get(name);
207
+ if (!h)
208
+ throw new Error(`[Kumiko Observability] Histogram "${name}" not registered or wrong type.`);
209
+ return h;
210
+ },
211
+ definitions() {
212
+ return defs;
213
+ },
214
+ snapshot() {
215
+ const out = new Map<
216
+ string,
217
+ { def: MetricDefinition; slots: (CounterState | GaugeState | HistogramState)[] }
218
+ >();
219
+ for (const [name, def] of defs) {
220
+ let slots: (CounterState | GaugeState | HistogramState)[];
221
+ if (def.type === "counter") {
222
+ slots = [...(counterSlots.get(name)?.values() ?? [])];
223
+ } else if (def.type === "gauge") {
224
+ slots = [...(gaugeSlots.get(name)?.values() ?? [])];
225
+ } else {
226
+ slots = [...(histogramSlots.get(name)?.values() ?? [])];
227
+ }
228
+ out.set(name, { def, slots });
229
+ }
230
+ return out;
231
+ },
232
+ };
233
+ }
234
+
235
+ // --- OpenMetrics text-format serializer -----------------------------------
236
+
237
+ export function serializeOpenMetrics(meter: PrometheusMeter): string {
238
+ const lines: string[] = [];
239
+ const snap = meter.snapshot();
240
+ // Sort metric names for deterministic output — diff-friendly in tests,
241
+ // Prometheus doesn't care but humans do.
242
+ const names = [...snap.keys()].sort();
243
+
244
+ for (const name of names) {
245
+ const entry = snap.get(name);
246
+ if (!entry) continue;
247
+ const { def, slots } = entry;
248
+ if (def.description) lines.push(`# HELP ${name} ${def.description}`);
249
+ lines.push(`# TYPE ${name} ${def.type}`);
250
+
251
+ // @cast-boundary engine-bridge — slots union narrows by def.type
252
+ if (def.type === "counter") {
253
+ for (const s of slots as CounterState[]) {
254
+ lines.push(`${name}${renderLabels(s.labels)} ${s.value}`);
255
+ }
256
+ } else if (def.type === "gauge") {
257
+ for (const s of slots as GaugeState[]) {
258
+ lines.push(`${name}${renderLabels(s.labels)} ${s.value}`);
259
+ }
260
+ } else {
261
+ for (const s of slots as HistogramState[]) {
262
+ // Cumulative bucket counts + +Inf terminator + sum/count suffixes.
263
+ let cumulative = 0;
264
+ for (let i = 0; i < s.boundaries.length; i++) {
265
+ // biome-ignore lint/style/noNonNullAssertion: bounded by loop guard
266
+ cumulative = s.buckets[i]!;
267
+ // biome-ignore lint/style/noNonNullAssertion: bounded by loop guard
268
+ const le = String(s.boundaries[i]!);
269
+ lines.push(
270
+ `${name}_bucket${renderLabelsWithExtra(s.labels, [["le", le]])} ${cumulative}`,
271
+ );
272
+ }
273
+ lines.push(`${name}_bucket${renderLabelsWithExtra(s.labels, [["le", "+Inf"]])} ${s.count}`);
274
+ lines.push(`${name}_sum${renderLabels(s.labels)} ${s.sum}`);
275
+ lines.push(`${name}_count${renderLabels(s.labels)} ${s.count}`);
276
+ }
277
+ }
278
+ }
279
+
280
+ // OpenMetrics requires a trailing newline + `# EOF` — Prometheus ignores
281
+ // but conformant scrapers rely on it.
282
+ lines.push("# EOF");
283
+ return `${lines.join("\n")}\n`;
284
+ }
@@ -0,0 +1,156 @@
1
+ import { assertUnreachable } from "../utils";
2
+ import { validateLabelKey } from "./metric-validator";
3
+ import type { Counter, Gauge, Histogram, Meter, MetricDefinition, MetricLabels } from "./types";
4
+
5
+ // Event type emitted when any metric changes — feeds into provider emitters.
6
+ export type MetricEvent =
7
+ | {
8
+ readonly type: "counter.inc";
9
+ readonly name: string;
10
+ readonly value: number;
11
+ readonly labels: MetricLabels | undefined;
12
+ }
13
+ | {
14
+ readonly type: "histogram.observe";
15
+ readonly name: string;
16
+ readonly value: number;
17
+ readonly labels: MetricLabels | undefined;
18
+ }
19
+ | {
20
+ readonly type: "gauge.set" | "gauge.inc" | "gauge.dec";
21
+ readonly name: string;
22
+ readonly value: number;
23
+ readonly labels: MetricLabels | undefined;
24
+ };
25
+
26
+ export type MetricEventHandler = (event: MetricEvent) => void;
27
+
28
+ // Validate provided labels against the declared label keys.
29
+ // Unknown or missing keys throw — typed metrics only.
30
+ function validateLabels(def: MetricDefinition, labels?: MetricLabels): void {
31
+ const declared = new Set(def.labels ?? []);
32
+ if (def.tenantLabel) declared.add("tenant_id");
33
+ if (!labels) {
34
+ if (declared.size > 0) {
35
+ throw new Error(
36
+ `[Kumiko Observability] Metric "${def.name}" expects labels ${[...declared].join(", ")} but got none.`,
37
+ );
38
+ }
39
+ // skip: metric has no declared labels and none were passed — valid call
40
+ return;
41
+ }
42
+ for (const key of Object.keys(labels)) {
43
+ if (!declared.has(key)) {
44
+ throw new Error(
45
+ `[Kumiko Observability] Metric "${def.name}" got unknown label "${key}". ` +
46
+ `Allowed: ${[...declared].join(", ") || "(none)"}.`,
47
+ );
48
+ }
49
+ }
50
+ for (const key of declared) {
51
+ if (!(key in labels)) {
52
+ throw new Error(`[Kumiko Observability] Metric "${def.name}" missing label "${key}".`);
53
+ }
54
+ }
55
+ }
56
+
57
+ class RecordingCounter implements Counter {
58
+ constructor(
59
+ private readonly def: MetricDefinition,
60
+ private readonly emit: MetricEventHandler,
61
+ ) {}
62
+ inc(value?: number, labels?: MetricLabels): void {
63
+ validateLabels(this.def, labels);
64
+ this.emit({ type: "counter.inc", name: this.def.name, value: value ?? 1, labels });
65
+ }
66
+ }
67
+
68
+ class RecordingHistogram implements Histogram {
69
+ constructor(
70
+ private readonly def: MetricDefinition,
71
+ private readonly emit: MetricEventHandler,
72
+ ) {}
73
+ observe(value: number, labels?: MetricLabels): void {
74
+ validateLabels(this.def, labels);
75
+ this.emit({ type: "histogram.observe", name: this.def.name, value, labels });
76
+ }
77
+ }
78
+
79
+ class RecordingGauge implements Gauge {
80
+ constructor(
81
+ private readonly def: MetricDefinition,
82
+ private readonly emit: MetricEventHandler,
83
+ ) {}
84
+ set(value: number, labels?: MetricLabels): void {
85
+ validateLabels(this.def, labels);
86
+ this.emit({ type: "gauge.set", name: this.def.name, value, labels });
87
+ }
88
+ inc(value?: number, labels?: MetricLabels): void {
89
+ validateLabels(this.def, labels);
90
+ this.emit({ type: "gauge.inc", name: this.def.name, value: value ?? 1, labels });
91
+ }
92
+ dec(value?: number, labels?: MetricLabels): void {
93
+ validateLabels(this.def, labels);
94
+ this.emit({ type: "gauge.dec", name: this.def.name, value: value ?? 1, labels });
95
+ }
96
+ }
97
+
98
+ export class RecordingMeter implements Meter {
99
+ private readonly defs = new Map<string, MetricDefinition>();
100
+ private readonly counters = new Map<string, Counter>();
101
+ private readonly histograms = new Map<string, Histogram>();
102
+ private readonly gauges = new Map<string, Gauge>();
103
+
104
+ constructor(private readonly emit: MetricEventHandler) {}
105
+
106
+ registerMetric(def: MetricDefinition): void {
107
+ if (this.defs.has(def.name)) {
108
+ throw new Error(`[Kumiko Observability] Metric "${def.name}" already registered.`);
109
+ }
110
+ for (const label of def.labels ?? []) {
111
+ validateLabelKey(label);
112
+ }
113
+ this.defs.set(def.name, def);
114
+ switch (def.type) {
115
+ case "counter":
116
+ this.counters.set(def.name, new RecordingCounter(def, this.emit));
117
+ break;
118
+ case "histogram":
119
+ this.histograms.set(def.name, new RecordingHistogram(def, this.emit));
120
+ break;
121
+ case "gauge":
122
+ this.gauges.set(def.name, new RecordingGauge(def, this.emit));
123
+ break;
124
+ default:
125
+ assertUnreachable(def.type, "metric type");
126
+ }
127
+ }
128
+
129
+ counter(name: string): Counter {
130
+ const c = this.counters.get(name);
131
+ if (!c) {
132
+ throw new Error(`[Kumiko Observability] Counter "${name}" not registered or wrong type.`);
133
+ }
134
+ return c;
135
+ }
136
+
137
+ histogram(name: string): Histogram {
138
+ const h = this.histograms.get(name);
139
+ if (!h) {
140
+ throw new Error(`[Kumiko Observability] Histogram "${name}" not registered or wrong type.`);
141
+ }
142
+ return h;
143
+ }
144
+
145
+ gauge(name: string): Gauge {
146
+ const g = this.gauges.get(name);
147
+ if (!g) {
148
+ throw new Error(`[Kumiko Observability] Gauge "${name}" not registered or wrong type.`);
149
+ }
150
+ return g;
151
+ }
152
+
153
+ definitions(): ReadonlyMap<string, MetricDefinition> {
154
+ return this.defs;
155
+ }
156
+ }