pipework 0.251.0 → 0.252.1

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 (730) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/config/load.d.ts +2 -0
  3. package/dist/core/config/load.d.ts.map +1 -1
  4. package/dist/core/config/load.js +1 -0
  5. package/dist/core/config/load.js.map +1 -1
  6. package/dist/core/config/namespace.d.ts +1 -0
  7. package/dist/core/config/namespace.d.ts.map +1 -1
  8. package/dist/core/config/schema.d.ts +2 -0
  9. package/dist/core/config/schema.d.ts.map +1 -1
  10. package/dist/core/config/schema.js +1 -0
  11. package/dist/core/config/schema.js.map +1 -1
  12. package/dist/core/surface/types.d.ts +1 -0
  13. package/dist/core/surface/types.d.ts.map +1 -1
  14. package/dist/data/db/namespace.d.ts +3 -0
  15. package/dist/data/db/namespace.d.ts.map +1 -1
  16. package/dist/data/db/namespace.js +3 -0
  17. package/dist/data/db/namespace.js.map +1 -1
  18. package/dist/data/db/pool.d.ts.map +1 -1
  19. package/dist/data/db/pool.js +6 -1
  20. package/dist/data/db/pool.js.map +1 -1
  21. package/dist/data/db/query-label.d.ts +5 -0
  22. package/dist/data/db/query-label.d.ts.map +1 -0
  23. package/dist/data/db/query-label.js +11 -0
  24. package/dist/data/db/query-label.js.map +1 -0
  25. package/dist/data/db/query-watch.d.ts +21 -0
  26. package/dist/data/db/query-watch.d.ts.map +1 -0
  27. package/dist/data/db/query-watch.js +90 -0
  28. package/dist/data/db/query-watch.js.map +1 -0
  29. package/dist/data/query/postgres-js/driver.js +1 -1
  30. package/dist/data/query/postgres-js/driver.js.map +1 -1
  31. package/dist/data/query/postgres-js/session.d.ts +5 -1
  32. package/dist/data/query/postgres-js/session.d.ts.map +1 -1
  33. package/dist/data/query/postgres-js/session.js +16 -11
  34. package/dist/data/query/postgres-js/session.js.map +1 -1
  35. package/dist/data/query/utils.d.ts +2 -0
  36. package/dist/data/query/utils.d.ts.map +1 -1
  37. package/dist/data/query/utils.js.map +1 -1
  38. package/dist/infra/credits/schema.d.ts +1 -1
  39. package/dist/test/plugin.d.ts.map +1 -1
  40. package/dist/test/plugin.js +10 -4
  41. package/dist/test/plugin.js.map +1 -1
  42. package/package.json +4 -3
  43. package/src/async/jobs/abortable-delay.ts +18 -0
  44. package/src/async/jobs/claims.ts +52 -0
  45. package/src/async/jobs/cron.ts +179 -0
  46. package/src/async/jobs/errors.ts +56 -0
  47. package/src/async/jobs/exclusion.ts +117 -0
  48. package/src/async/jobs/execute.ts +219 -0
  49. package/src/async/jobs/index.ts +10 -0
  50. package/src/async/jobs/listener.ts +135 -0
  51. package/src/async/jobs/namespace.ts +20 -0
  52. package/src/async/jobs/queue-door.ts +44 -0
  53. package/src/async/jobs/queue.ts +1012 -0
  54. package/src/async/jobs/run-script.ts +67 -0
  55. package/src/async/jobs/scheduler.ts +218 -0
  56. package/src/async/jobs/schedules-table.ts +37 -0
  57. package/src/async/jobs/serialize-error.ts +48 -0
  58. package/src/async/jobs/table.ts +191 -0
  59. package/src/async/pipeline/define.ts +53 -0
  60. package/src/async/pipeline/errors.ts +35 -0
  61. package/src/async/pipeline/execute.ts +53 -0
  62. package/src/async/pipeline/index.ts +13 -0
  63. package/src/async/pipeline/namespace.ts +12 -0
  64. package/src/async/pipeline/resume.ts +67 -0
  65. package/src/async/pipeline/run-steps.ts +98 -0
  66. package/src/async/pipeline/types.ts +57 -0
  67. package/src/async/state-machine/define.ts +64 -0
  68. package/src/async/state-machine/errors.ts +29 -0
  69. package/src/async/state-machine/index.ts +3 -0
  70. package/src/async/state-machine/namespace.ts +12 -0
  71. package/src/async/state-machine/transition.ts +90 -0
  72. package/src/async/state-machine/types.ts +37 -0
  73. package/src/async/steps/errors.ts +31 -0
  74. package/src/async/steps/graph.ts +1214 -0
  75. package/src/async/steps/hash.ts +40 -0
  76. package/src/async/steps/index.ts +7 -0
  77. package/src/async/steps/namespace.ts +7 -0
  78. package/src/async/steps/table.ts +97 -0
  79. package/src/async/steps/types.ts +318 -0
  80. package/src/audit/chain/index.ts +9 -0
  81. package/src/audit/chain/install.ts +113 -0
  82. package/src/audit/chain/retire.ts +52 -0
  83. package/src/audit/chain/verify.ts +257 -0
  84. package/src/audit/query/cursor.ts +37 -0
  85. package/src/audit/query/index.ts +9 -0
  86. package/src/audit/query/query.ts +223 -0
  87. package/src/audit/schema/audit-record-fields.ts +68 -0
  88. package/src/audit/schema/audit-record.ts +23 -0
  89. package/src/auth/auth/audit.ts +31 -0
  90. package/src/auth/auth/chain.ts +84 -0
  91. package/src/auth/auth/cookie.ts +136 -0
  92. package/src/auth/auth/email-links.ts +311 -0
  93. package/src/auth/auth/guard.ts +128 -0
  94. package/src/auth/auth/index.ts +18 -0
  95. package/src/auth/auth/multi-org.ts +337 -0
  96. package/src/auth/auth/namespace.ts +34 -0
  97. package/src/auth/auth/parse-expiry.ts +13 -0
  98. package/src/auth/auth/passwords.ts +110 -0
  99. package/src/auth/auth/schema.ts +110 -0
  100. package/src/auth/auth/sessions.ts +565 -0
  101. package/src/auth/auth/sign-in.ts +156 -0
  102. package/src/auth/auth/signing-keys.ts +69 -0
  103. package/src/auth/auth/types.ts +26 -0
  104. package/src/auth/auth/uuid.ts +10 -0
  105. package/src/auth/rbac/cache.ts +55 -0
  106. package/src/auth/rbac/check.ts +30 -0
  107. package/src/auth/rbac/create.ts +35 -0
  108. package/src/auth/rbac/index.ts +5 -0
  109. package/src/auth/rbac/middleware.ts +20 -0
  110. package/src/auth/rbac/namespace.ts +15 -0
  111. package/src/auth/rbac/resolve.ts +31 -0
  112. package/src/auth/rbac/schema.sql +31 -0
  113. package/src/auth/rbac/seed.ts +67 -0
  114. package/src/auth/rbac/tables.ts +32 -0
  115. package/src/auth/rbac/types.ts +65 -0
  116. package/src/auth/tenant/extract.ts +33 -0
  117. package/src/auth/tenant/in-tenant.ts +49 -0
  118. package/src/auth/tenant/index.ts +7 -0
  119. package/src/auth/tenant/namespace.ts +23 -0
  120. package/src/auth/tenant/propagate.ts +20 -0
  121. package/src/auth/tenant/rls.ts +120 -0
  122. package/src/auth/tenant/scope.ts +33 -0
  123. package/src/auth/tenant/scoped-db.ts +581 -0
  124. package/src/auth/tenant/types.ts +14 -0
  125. package/src/auth/tenant/validate.ts +27 -0
  126. package/src/cli/commands/add.ts +158 -0
  127. package/src/cli/commands/alerts.ts +98 -0
  128. package/src/cli/commands/check.ts +1418 -0
  129. package/src/cli/commands/codemod.ts +78 -0
  130. package/src/cli/commands/db.ts +356 -0
  131. package/src/cli/commands/dev.ts +101 -0
  132. package/src/cli/commands/generate.ts +75 -0
  133. package/src/cli/commands/init.ts +36 -0
  134. package/src/cli/commands/install.ts +28 -0
  135. package/src/cli/commands/lint.ts +62 -0
  136. package/src/cli/commands/migrate.ts +132 -0
  137. package/src/cli/commands/openapi.ts +63 -0
  138. package/src/cli/commands/remove.ts +72 -0
  139. package/src/cli/commands/run.ts +50 -0
  140. package/src/cli/commands/scope.ts +54 -0
  141. package/src/cli/commands/serve.ts +68 -0
  142. package/src/cli/commands/shard.ts +31 -0
  143. package/src/cli/commands/shared/config-pipework-version.ts +28 -0
  144. package/src/cli/commands/shared/find-config.ts +22 -0
  145. package/src/cli/commands/shared/load-config.ts +35 -0
  146. package/src/cli/commands/shared/pnpm-install.ts +95 -0
  147. package/src/cli/commands/shared/report-removed.ts +8 -0
  148. package/src/cli/commands/sync.ts +32 -0
  149. package/src/cli/commands/test.ts +307 -0
  150. package/src/cli/commands/upgrade.ts +48 -0
  151. package/src/cli/dev-bootstrap.ts +24 -0
  152. package/src/cli/errors.ts +10 -0
  153. package/src/cli/index.ts +303 -0
  154. package/src/cli/output.ts +17 -0
  155. package/src/cli/port.ts +15 -0
  156. package/src/cli/surface-kinds.ts +20 -0
  157. package/src/client/index.ts +4 -0
  158. package/src/client/poll.ts +130 -0
  159. package/src/client/report-fault.ts +119 -0
  160. package/src/core/config/app-pool.ts +19 -0
  161. package/src/core/config/aws-env.ts +29 -0
  162. package/src/core/config/base-dir.ts +24 -0
  163. package/src/core/config/discover.ts +64 -0
  164. package/src/core/config/env-resolve.ts +87 -0
  165. package/src/core/config/env-types.ts +116 -0
  166. package/src/core/config/env.ts +55 -0
  167. package/src/core/config/errors.ts +9 -0
  168. package/src/core/config/import-config.ts +40 -0
  169. package/src/core/config/index.ts +8 -0
  170. package/src/core/config/load.ts +417 -0
  171. package/src/core/config/namespace.ts +14 -0
  172. package/src/core/config/resolve.ts +195 -0
  173. package/src/core/config/schema.ts +488 -0
  174. package/src/core/config/statement-observer.ts +48 -0
  175. package/src/core/config/ts-inline-hooks.ts +75 -0
  176. package/src/core/config/ts-register.ts +4 -0
  177. package/src/core/config/ts-resolve-hooks.ts +101 -0
  178. package/src/core/errors/base.ts +49 -0
  179. package/src/core/errors/http.ts +146 -0
  180. package/src/core/errors/index.ts +2 -0
  181. package/src/core/errors/namespace.ts +31 -0
  182. package/src/core/lifecycle/drain.ts +33 -0
  183. package/src/core/lifecycle/health.ts +72 -0
  184. package/src/core/lifecycle/index.ts +4 -0
  185. package/src/core/lifecycle/namespace.ts +7 -0
  186. package/src/core/lifecycle/startup.ts +102 -0
  187. package/src/core/lifecycle/types.ts +36 -0
  188. package/src/core/logging/context.ts +60 -0
  189. package/src/core/logging/error-chain.ts +68 -0
  190. package/src/core/logging/index.ts +5 -0
  191. package/src/core/logging/logger.ts +65 -0
  192. package/src/core/logging/namespace.ts +16 -0
  193. package/src/core/logging/proxy.ts +22 -0
  194. package/src/core/logging/redact-match.ts +24 -0
  195. package/src/core/logging/types.ts +47 -0
  196. package/src/core/pipework.ts +566 -0
  197. package/src/core/runtime/broker.ts +69 -0
  198. package/src/core/surface/http.ts +133 -0
  199. package/src/core/surface/index.ts +11 -0
  200. package/src/core/surface/job-timeout.ts +40 -0
  201. package/src/core/surface/namespace.ts +13 -0
  202. package/src/core/surface/script.ts +33 -0
  203. package/src/core/surface/start.ts +142 -0
  204. package/src/core/surface/types.ts +225 -0
  205. package/src/core/surface/validate.ts +88 -0
  206. package/src/core/surface/worker.ts +528 -0
  207. package/src/core/validation/connectivity.ts +49 -0
  208. package/src/core/validation/domain.ts +99 -0
  209. package/src/core/validation/index.ts +5 -0
  210. package/src/core/validation/queue-tables.ts +312 -0
  211. package/src/core/validation/rls-role.ts +138 -0
  212. package/src/core/validation/startup.ts +51 -0
  213. package/src/core/validation/surfaces.ts +65 -0
  214. package/src/core/validation/types.ts +25 -0
  215. package/src/data/db/advisory-lock.ts +55 -0
  216. package/src/data/db/aggregates.ts +172 -0
  217. package/src/data/db/array-elements.ts +111 -0
  218. package/src/data/db/bulk-set.ts +53 -0
  219. package/src/data/db/col/index.ts +14 -0
  220. package/src/data/db/composed.ts +53 -0
  221. package/src/data/db/db.ts +276 -0
  222. package/src/data/db/excluded.ts +16 -0
  223. package/src/data/db/expressions.ts +473 -0
  224. package/src/data/db/filter.ts +90 -0
  225. package/src/data/db/function-source.ts +114 -0
  226. package/src/data/db/identifiers.ts +16 -0
  227. package/src/data/db/idx/index.ts +9 -0
  228. package/src/data/db/in-list.ts +49 -0
  229. package/src/data/db/index.ts +9 -0
  230. package/src/data/db/insert-split.ts +137 -0
  231. package/src/data/db/instrumentation.ts +82 -0
  232. package/src/data/db/maintenance.ts +28 -0
  233. package/src/data/db/namespace.ts +107 -0
  234. package/src/data/db/owner-labels.ts +9 -0
  235. package/src/data/db/pool.ts +187 -0
  236. package/src/data/db/query-label.ts +13 -0
  237. package/src/data/db/query-watch.ts +116 -0
  238. package/src/data/db/raw-client-warning.ts +37 -0
  239. package/src/data/db/readonly.ts +23 -0
  240. package/src/data/db/reservation.ts +151 -0
  241. package/src/data/db/schema-features.ts +14 -0
  242. package/src/data/db/serializable.ts +83 -0
  243. package/src/data/db/sql.ts +1 -0
  244. package/src/data/db/timeouts.ts +76 -0
  245. package/src/data/db/types.ts +18 -0
  246. package/src/data/db/unbound.ts +33 -0
  247. package/src/data/db/values.ts +29 -0
  248. package/src/data/domain/brand.ts +15 -0
  249. package/src/data/domain/build-factory.ts +193 -0
  250. package/src/data/domain/build-schema.ts +161 -0
  251. package/src/data/domain/build-table.ts +355 -0
  252. package/src/data/domain/define.ts +239 -0
  253. package/src/data/domain/external-registry.ts +34 -0
  254. package/src/data/domain/external.ts +77 -0
  255. package/src/data/domain/field.ts +279 -0
  256. package/src/data/domain/index.ts +13 -0
  257. package/src/data/domain/naming.ts +6 -0
  258. package/src/data/domain/projections.ts +57 -0
  259. package/src/data/domain/trace-safe-projection.ts +31 -0
  260. package/src/data/domain/traits.ts +108 -0
  261. package/src/data/domain/types.ts +289 -0
  262. package/src/data/jsonb/composed.ts +18 -0
  263. package/src/data/jsonb/index.ts +1 -0
  264. package/src/data/jsonb/namespace.ts +30 -0
  265. package/src/data/jsonb/query.ts +86 -0
  266. package/src/data/migrate/append-only-generate.ts +47 -0
  267. package/src/data/migrate/applied.ts +47 -0
  268. package/src/data/migrate/apply.ts +309 -0
  269. package/src/data/migrate/check.ts +119 -0
  270. package/src/data/migrate/diff.ts +357 -0
  271. package/src/data/migrate/door-generate.ts +130 -0
  272. package/src/data/migrate/framework-rls.ts +58 -0
  273. package/src/data/migrate/generate.ts +201 -0
  274. package/src/data/migrate/history.ts +215 -0
  275. package/src/data/migrate/index.ts +9 -0
  276. package/src/data/migrate/internal-definitions.ts +175 -0
  277. package/src/data/migrate/post-process.ts +73 -0
  278. package/src/data/migrate/pragma.ts +73 -0
  279. package/src/data/migrate/provision-framework.ts +147 -0
  280. package/src/data/migrate/rls-generate.ts +61 -0
  281. package/src/data/migrate/run.ts +124 -0
  282. package/src/data/migrate/safety.ts +167 -0
  283. package/src/data/migrate/snapshot.ts +368 -0
  284. package/src/data/migrate/sql-emit.ts +392 -0
  285. package/src/data/migrate/statements.ts +312 -0
  286. package/src/data/query/alias.ts +126 -0
  287. package/src/data/query/batch.ts +8 -0
  288. package/src/data/query/cache/core/cache.ts +78 -0
  289. package/src/data/query/cache/core/index.ts +1 -0
  290. package/src/data/query/cache/core/types.ts +29 -0
  291. package/src/data/query/cache/readme.md +230 -0
  292. package/src/data/query/casing.ts +76 -0
  293. package/src/data/query/collation.ts +11 -0
  294. package/src/data/query/column-builder.ts +370 -0
  295. package/src/data/query/column.ts +150 -0
  296. package/src/data/query/entity.ts +42 -0
  297. package/src/data/query/errors.ts +33 -0
  298. package/src/data/query/index.ts +14 -0
  299. package/src/data/query/logger.ts +47 -0
  300. package/src/data/query/operations.ts +45 -0
  301. package/src/data/query/pg-core/alias.ts +12 -0
  302. package/src/data/query/pg-core/checks.ts +32 -0
  303. package/src/data/query/pg-core/columns/all.ts +71 -0
  304. package/src/data/query/pg-core/columns/bigint.ts +110 -0
  305. package/src/data/query/pg-core/columns/bigserial.ts +128 -0
  306. package/src/data/query/pg-core/columns/boolean.ts +43 -0
  307. package/src/data/query/pg-core/columns/char.ts +85 -0
  308. package/src/data/query/pg-core/columns/cidr.ts +43 -0
  309. package/src/data/query/pg-core/columns/common.ts +366 -0
  310. package/src/data/query/pg-core/columns/custom.ts +232 -0
  311. package/src/data/query/pg-core/columns/date.common.ts +15 -0
  312. package/src/data/query/pg-core/columns/date.ts +112 -0
  313. package/src/data/query/pg-core/columns/double-precision.ts +55 -0
  314. package/src/data/query/pg-core/columns/enum.ts +201 -0
  315. package/src/data/query/pg-core/columns/index.ts +35 -0
  316. package/src/data/query/pg-core/columns/inet.ts +43 -0
  317. package/src/data/query/pg-core/columns/int.common.ts +57 -0
  318. package/src/data/query/pg-core/columns/integer.ts +53 -0
  319. package/src/data/query/pg-core/columns/interval.ts +83 -0
  320. package/src/data/query/pg-core/columns/json.ts +58 -0
  321. package/src/data/query/pg-core/columns/jsonb.ts +56 -0
  322. package/src/data/query/pg-core/columns/line.ts +117 -0
  323. package/src/data/query/pg-core/columns/macaddr.ts +43 -0
  324. package/src/data/query/pg-core/columns/macaddr8.ts +43 -0
  325. package/src/data/query/pg-core/columns/numeric.ts +227 -0
  326. package/src/data/query/pg-core/columns/point.ts +127 -0
  327. package/src/data/query/pg-core/columns/postgis_extension/geometry.ts +119 -0
  328. package/src/data/query/pg-core/columns/postgis_extension/utils.ts +47 -0
  329. package/src/data/query/pg-core/columns/real.ts +58 -0
  330. package/src/data/query/pg-core/columns/serial.ts +55 -0
  331. package/src/data/query/pg-core/columns/smallint.ts +53 -0
  332. package/src/data/query/pg-core/columns/smallserial.ts +60 -0
  333. package/src/data/query/pg-core/columns/text.ts +67 -0
  334. package/src/data/query/pg-core/columns/time.ts +72 -0
  335. package/src/data/query/pg-core/columns/timestamp.ts +163 -0
  336. package/src/data/query/pg-core/columns/uuid.ts +51 -0
  337. package/src/data/query/pg-core/columns/varchar.ts +89 -0
  338. package/src/data/query/pg-core/columns/vector_extension/bit.ts +68 -0
  339. package/src/data/query/pg-core/columns/vector_extension/halfvec.ts +80 -0
  340. package/src/data/query/pg-core/columns/vector_extension/sparsevec.ts +67 -0
  341. package/src/data/query/pg-core/columns/vector_extension/vector.ts +80 -0
  342. package/src/data/query/pg-core/db.ts +674 -0
  343. package/src/data/query/pg-core/dialect.ts +1404 -0
  344. package/src/data/query/pg-core/expressions.ts +25 -0
  345. package/src/data/query/pg-core/foreign-keys.ts +130 -0
  346. package/src/data/query/pg-core/index.ts +21 -0
  347. package/src/data/query/pg-core/indexes.ts +276 -0
  348. package/src/data/query/pg-core/policies.ts +56 -0
  349. package/src/data/query/pg-core/primary-keys.ts +62 -0
  350. package/src/data/query/pg-core/query-builders/count.ts +86 -0
  351. package/src/data/query/pg-core/query-builders/delete.ts +293 -0
  352. package/src/data/query/pg-core/query-builders/index.ts +7 -0
  353. package/src/data/query/pg-core/query-builders/insert.ts +452 -0
  354. package/src/data/query/pg-core/query-builders/query-builder.ts +123 -0
  355. package/src/data/query/pg-core/query-builders/query.ts +157 -0
  356. package/src/data/query/pg-core/query-builders/raw.ts +49 -0
  357. package/src/data/query/pg-core/query-builders/refresh-materialized-view.ts +108 -0
  358. package/src/data/query/pg-core/query-builders/select.ts +1346 -0
  359. package/src/data/query/pg-core/query-builders/select.types.ts +437 -0
  360. package/src/data/query/pg-core/query-builders/update.ts +626 -0
  361. package/src/data/query/pg-core/roles.ts +41 -0
  362. package/src/data/query/pg-core/schema.ts +72 -0
  363. package/src/data/query/pg-core/sequence.ts +41 -0
  364. package/src/data/query/pg-core/session.ts +292 -0
  365. package/src/data/query/pg-core/subquery.ts +46 -0
  366. package/src/data/query/pg-core/table.ts +252 -0
  367. package/src/data/query/pg-core/unique-constraint.ts +73 -0
  368. package/src/data/query/pg-core/utils/array.ts +95 -0
  369. package/src/data/query/pg-core/utils/index.ts +1 -0
  370. package/src/data/query/pg-core/utils.ts +104 -0
  371. package/src/data/query/pg-core/view-base.ts +14 -0
  372. package/src/data/query/pg-core/view-common.ts +1 -0
  373. package/src/data/query/pg-core/view.ts +435 -0
  374. package/src/data/query/pg-core/with-builder.ts +41 -0
  375. package/src/data/query/postgres-js/README.md +46 -0
  376. package/src/data/query/postgres-js/array-parser.ts +45 -0
  377. package/src/data/query/postgres-js/driver-error-boundary.ts +149 -0
  378. package/src/data/query/postgres-js/driver.ts +163 -0
  379. package/src/data/query/postgres-js/index.ts +2 -0
  380. package/src/data/query/postgres-js/session.ts +234 -0
  381. package/src/data/query/primary-key.ts +11 -0
  382. package/src/data/query/query-builders/query-builder.ts +19 -0
  383. package/src/data/query/query-builders/select.types.ts +175 -0
  384. package/src/data/query/query-promise.ts +35 -0
  385. package/src/data/query/relations.ts +725 -0
  386. package/src/data/query/runnable-query.ts +12 -0
  387. package/src/data/query/selection-proxy.ts +121 -0
  388. package/src/data/query/session.ts +8 -0
  389. package/src/data/query/sql/expressions/conditions.ts +741 -0
  390. package/src/data/query/sql/expressions/index.ts +2 -0
  391. package/src/data/query/sql/expressions/select.ts +45 -0
  392. package/src/data/query/sql/functions/aggregate.ts +129 -0
  393. package/src/data/query/sql/functions/index.ts +2 -0
  394. package/src/data/query/sql/functions/vector.ts +178 -0
  395. package/src/data/query/sql/index.ts +3 -0
  396. package/src/data/query/sql/sql.ts +794 -0
  397. package/src/data/query/subquery.ts +53 -0
  398. package/src/data/query/table.ts +208 -0
  399. package/src/data/query/table.utils.ts +2 -0
  400. package/src/data/query/tracing-utils.ts +3 -0
  401. package/src/data/query/tracing.ts +11 -0
  402. package/src/data/query/utils.ts +357 -0
  403. package/src/data/query/version.ts +4 -0
  404. package/src/data/query/view-common.ts +1 -0
  405. package/src/data/schema/branded.ts +13 -0
  406. package/src/data/schema/generated/coerce.ts +5 -0
  407. package/src/data/schema/generated/combinator.ts +6 -0
  408. package/src/data/schema/generated/encoding.ts +5 -0
  409. package/src/data/schema/generated/error.ts +6 -0
  410. package/src/data/schema/generated/format.ts +5 -0
  411. package/src/data/schema/generated/type-constructor.ts +10 -0
  412. package/src/data/schema/generated/type.ts +6 -0
  413. package/src/data/schema/index.ts +6 -0
  414. package/src/data/schema/json-schema.ts +33 -0
  415. package/src/data/schema/namespace.ts +55 -0
  416. package/src/data/schema/parse.ts +39 -0
  417. package/src/data/schema/types.ts +39 -0
  418. package/src/data/temporal/append-only.ts +134 -0
  419. package/src/data/temporal/close.ts +27 -0
  420. package/src/data/temporal/columns.ts +10 -0
  421. package/src/data/temporal/definition-queries.ts +70 -0
  422. package/src/data/temporal/errors.ts +10 -0
  423. package/src/data/temporal/get-current.ts +22 -0
  424. package/src/data/temporal/index.ts +9 -0
  425. package/src/data/temporal/namespace.ts +36 -0
  426. package/src/data/temporal/query.ts +25 -0
  427. package/src/data/temporal/resolve.ts +17 -0
  428. package/src/data/temporal/revise.ts +67 -0
  429. package/src/data/temporal/types.ts +22 -0
  430. package/src/data/valve/artifact.ts +33 -0
  431. package/src/data/valve/contract-diff.ts +21 -0
  432. package/src/data/valve/cross.ts +57 -0
  433. package/src/data/valve/crossing-ledger.ts +50 -0
  434. package/src/data/valve/decision.ts +44 -0
  435. package/src/data/valve/durable-crossing.ts +52 -0
  436. package/src/data/valve/execute-crossing.ts +77 -0
  437. package/src/data/valve/faucet.ts +56 -0
  438. package/src/data/valve/gate.ts +114 -0
  439. package/src/data/valve/graph.ts +77 -0
  440. package/src/data/valve/index.ts +53 -0
  441. package/src/data/valve/namespace.ts +57 -0
  442. package/src/data/valve/registry.ts +50 -0
  443. package/src/data/valve/schema/valve-crossing-fields.ts +23 -0
  444. package/src/data/valve/schema/valve-crossing.ts +9 -0
  445. package/src/data/valve/supply.ts +154 -0
  446. package/src/data/valve/types.ts +143 -0
  447. package/src/data/vector/columns.ts +27 -0
  448. package/src/data/vector/index.ts +4 -0
  449. package/src/data/vector/namespace.ts +33 -0
  450. package/src/data/vector/query.ts +85 -0
  451. package/src/data/vector/validate.ts +16 -0
  452. package/src/data/write/audit/actor.ts +15 -0
  453. package/src/data/write/audit/audit-columns.ts +18 -0
  454. package/src/data/write/audit/audit-select.ts +111 -0
  455. package/src/data/write/audit/bound-transaction.ts +14 -0
  456. package/src/data/write/audit/cte-compile.ts +32 -0
  457. package/src/data/write/audit/decode-returning.ts +47 -0
  458. package/src/data/write/audit/final-select.ts +43 -0
  459. package/src/data/write/audit/infra-audit.ts +36 -0
  460. package/src/data/write/audit/primary-key.ts +65 -0
  461. package/src/data/write/audit/projection.ts +41 -0
  462. package/src/data/write/audit/read-touches.ts +86 -0
  463. package/src/data/write/audit/touch-match.ts +78 -0
  464. package/src/data/write/audit/verb-delete.ts +62 -0
  465. package/src/data/write/audit/verb-insert.ts +36 -0
  466. package/src/data/write/audit/verb-select.ts +47 -0
  467. package/src/data/write/audit/verb-update.ts +60 -0
  468. package/src/data/write/audit/verb-upsert.ts +104 -0
  469. package/src/data/write/conflict.ts +43 -0
  470. package/src/data/write/delete.ts +38 -0
  471. package/src/data/write/execute.ts +243 -0
  472. package/src/data/write/index.ts +11 -0
  473. package/src/data/write/insert.ts +63 -0
  474. package/src/data/write/state.ts +43 -0
  475. package/src/data/write/update.ts +54 -0
  476. package/src/data/write/upsert.ts +58 -0
  477. package/src/data/write/values.ts +19 -0
  478. package/src/index.ts +230 -0
  479. package/src/infra/access-links/code.ts +70 -0
  480. package/src/infra/access-links/handle.ts +892 -0
  481. package/src/infra/access-links/index.ts +9 -0
  482. package/src/infra/access-links/namespace.ts +30 -0
  483. package/src/infra/access-links/person-check.ts +177 -0
  484. package/src/infra/access-links/schema.ts +71 -0
  485. package/src/infra/alerts/async-sources.ts +231 -0
  486. package/src/infra/alerts/audit.ts +36 -0
  487. package/src/infra/alerts/beat.ts +123 -0
  488. package/src/infra/alerts/client-faults.ts +111 -0
  489. package/src/infra/alerts/faults.ts +156 -0
  490. package/src/infra/alerts/handle.ts +778 -0
  491. package/src/infra/alerts/http.ts +192 -0
  492. package/src/infra/alerts/index.ts +12 -0
  493. package/src/infra/alerts/infra-sources.ts +180 -0
  494. package/src/infra/alerts/namespace.ts +81 -0
  495. package/src/infra/alerts/redact.ts +21 -0
  496. package/src/infra/alerts/request-faults.ts +41 -0
  497. package/src/infra/alerts/schema.ts +124 -0
  498. package/src/infra/alerts/sinks.ts +125 -0
  499. package/src/infra/alerts/source-options.ts +26 -0
  500. package/src/infra/alerts/sources.ts +503 -0
  501. package/src/infra/alerts/subsystem-sources.ts +137 -0
  502. package/src/infra/audit/emit.ts +87 -0
  503. package/src/infra/audit/index.ts +2 -0
  504. package/src/infra/audit/namespace.ts +22 -0
  505. package/src/infra/audit/schema.sql +35 -0
  506. package/src/infra/audit/types.ts +18 -0
  507. package/src/infra/aws/credentials.ts +147 -0
  508. package/src/infra/aws/sigv4.ts +140 -0
  509. package/src/infra/cache/create.ts +58 -0
  510. package/src/infra/cache/entry.ts +81 -0
  511. package/src/infra/cache/index.ts +4 -0
  512. package/src/infra/cache/namespace.ts +10 -0
  513. package/src/infra/cache/tenant.ts +93 -0
  514. package/src/infra/cache/types.ts +24 -0
  515. package/src/infra/crawl/audit.ts +35 -0
  516. package/src/infra/crawl/errors.ts +25 -0
  517. package/src/infra/crawl/handle.ts +693 -0
  518. package/src/infra/crawl/index.ts +9 -0
  519. package/src/infra/crawl/links.ts +160 -0
  520. package/src/infra/crawl/namespace.ts +36 -0
  521. package/src/infra/crawl/robots.ts +100 -0
  522. package/src/infra/crawl/schema.ts +89 -0
  523. package/src/infra/credit-line/handle.ts +883 -0
  524. package/src/infra/credit-line/index.ts +6 -0
  525. package/src/infra/credit-line/namespace.ts +7 -0
  526. package/src/infra/credit-line/schema.ts +96 -0
  527. package/src/infra/credits/handle.ts +706 -0
  528. package/src/infra/credits/index.ts +5 -0
  529. package/src/infra/credits/namespace.ts +7 -0
  530. package/src/infra/credits/schema.ts +83 -0
  531. package/src/infra/email/audit.ts +36 -0
  532. package/src/infra/email/inbound.ts +622 -0
  533. package/src/infra/email/index.ts +11 -0
  534. package/src/infra/email/mail.ts +517 -0
  535. package/src/infra/email/mime.ts +447 -0
  536. package/src/infra/email/namespace.ts +22 -0
  537. package/src/infra/email/providers.ts +111 -0
  538. package/src/infra/email/schema.ts +96 -0
  539. package/src/infra/email/sns.ts +200 -0
  540. package/src/infra/email/unsubscribe.ts +44 -0
  541. package/src/infra/fetch/address.ts +88 -0
  542. package/src/infra/fetch/connect-retry.ts +87 -0
  543. package/src/infra/fetch/handle.ts +329 -0
  544. package/src/infra/fetch/index.ts +7 -0
  545. package/src/infra/fetch/namespace.ts +70 -0
  546. package/src/infra/idempotency/index.ts +3 -0
  547. package/src/infra/idempotency/middleware.ts +87 -0
  548. package/src/infra/idempotency/store.ts +92 -0
  549. package/src/infra/idempotency/types.ts +30 -0
  550. package/src/infra/join-requests/handle.ts +312 -0
  551. package/src/infra/join-requests/index.ts +2 -0
  552. package/src/infra/join-requests/namespace.ts +7 -0
  553. package/src/infra/join-requests/schema.ts +33 -0
  554. package/src/infra/outbox/core.ts +165 -0
  555. package/src/infra/quota/errors.ts +32 -0
  556. package/src/infra/quota/handle.ts +215 -0
  557. package/src/infra/quota/index.ts +5 -0
  558. package/src/infra/quota/namespace.ts +7 -0
  559. package/src/infra/quota/schema.ts +15 -0
  560. package/src/infra/storage/backend.ts +34 -0
  561. package/src/infra/storage/handle.ts +513 -0
  562. package/src/infra/storage/index.ts +9 -0
  563. package/src/infra/storage/local.ts +104 -0
  564. package/src/infra/storage/namespace.ts +13 -0
  565. package/src/infra/storage/s3.ts +257 -0
  566. package/src/infra/storage/schema.ts +24 -0
  567. package/src/infra/support/handle.ts +854 -0
  568. package/src/infra/support/http.ts +229 -0
  569. package/src/infra/support/index.ts +7 -0
  570. package/src/infra/support/namespace.ts +12 -0
  571. package/src/infra/support/schema.ts +96 -0
  572. package/src/infra/webhook/inbound.ts +45 -0
  573. package/src/infra/webhook/index.ts +5 -0
  574. package/src/infra/webhook/namespace.ts +24 -0
  575. package/src/infra/webhook/outbound.ts +315 -0
  576. package/src/infra/webhook/sign.ts +32 -0
  577. package/src/infra/webhook/verify.ts +82 -0
  578. package/src/invariants/assert.ts +26 -0
  579. package/src/invariants/index.ts +1 -0
  580. package/src/lint/config.ts +56 -0
  581. package/src/lint/doctrine.ts +163 -0
  582. package/src/lint/include.ts +97 -0
  583. package/src/lint/index.ts +3 -0
  584. package/src/lint/preset.ts +10 -0
  585. package/src/lint/resolver.ts +218 -0
  586. package/src/lint/rules/no-reexports.ts +46 -0
  587. package/src/request/behavior/audited.ts +85 -0
  588. package/src/request/behavior/cached.ts +62 -0
  589. package/src/request/behavior/compose.ts +28 -0
  590. package/src/request/behavior/index.ts +5 -0
  591. package/src/request/behavior/namespace.ts +16 -0
  592. package/src/request/behavior/types.ts +26 -0
  593. package/src/request/behavior/versioned.ts +54 -0
  594. package/src/request/context/create.ts +103 -0
  595. package/src/request/context/guard.ts +49 -0
  596. package/src/request/context/index.ts +4 -0
  597. package/src/request/context/run-in-transaction.ts +200 -0
  598. package/src/request/context/store.ts +28 -0
  599. package/src/request/context/types.ts +50 -0
  600. package/src/request/di/builder.ts +535 -0
  601. package/src/request/di/index.ts +8 -0
  602. package/src/request/di/resolve.ts +271 -0
  603. package/src/request/di/types.ts +262 -0
  604. package/src/request/fitting/namespace.ts +7 -0
  605. package/src/request/fixture/batch.ts +71 -0
  606. package/src/request/fixture/builder.ts +26 -0
  607. package/src/request/fixture/errors.ts +10 -0
  608. package/src/request/fixture/index.ts +8 -0
  609. package/src/request/fixture/namespace.ts +23 -0
  610. package/src/request/fixture/paginate.ts +44 -0
  611. package/src/request/fixture/to-handlers.ts +260 -0
  612. package/src/request/fixture/types.ts +79 -0
  613. package/src/request/flow/act-as.ts +78 -0
  614. package/src/request/flow/namespace.ts +17 -0
  615. package/src/request/http/error-step.ts +42 -0
  616. package/src/request/http/file.ts +141 -0
  617. package/src/request/http/index.ts +4 -0
  618. package/src/request/http/middleware.ts +141 -0
  619. package/src/request/http/namespace.ts +5 -0
  620. package/src/request/http/rate-limit-classes.ts +170 -0
  621. package/src/request/http/response.ts +82 -0
  622. package/src/request/http/server.ts +916 -0
  623. package/src/request/http/sniff.ts +261 -0
  624. package/src/request/http/types.ts +250 -0
  625. package/src/request/http/upload.ts +284 -0
  626. package/src/request/internal/text.ts +6 -0
  627. package/src/request/openapi/generate.ts +205 -0
  628. package/src/request/openapi/index.ts +2 -0
  629. package/src/request/openapi/namespace.ts +7 -0
  630. package/src/test/auth.ts +13 -0
  631. package/src/test/auto-setup.ts +45 -0
  632. package/src/test/context.ts +91 -0
  633. package/src/test/crawl-fixture.ts +157 -0
  634. package/src/test/discover-fork.ts +17 -0
  635. package/src/test/fingerprint.ts +135 -0
  636. package/src/test/globals.d.ts +7 -0
  637. package/src/test/index.ts +19 -0
  638. package/src/test/namespace.ts +87 -0
  639. package/src/test/plugin.ts +189 -0
  640. package/src/test/progress.ts +185 -0
  641. package/src/test/reap-after-run.ts +28 -0
  642. package/src/test/reap.ts +286 -0
  643. package/src/test/reset.ts +253 -0
  644. package/src/test/savepoints.ts +172 -0
  645. package/src/test/setup.ts +1422 -0
  646. package/src/test/system-channel.ts +50 -0
  647. package/src/test/vitest-plugin.ts +142 -0
  648. package/src/test/vitest-workspace.ts +507 -0
  649. package/src/test/vitest.ts +333 -0
  650. package/src/test/watch.ts +105 -0
  651. package/src/trace/capture/bounded-serializer.ts +146 -0
  652. package/src/trace/cardinality/analyze.ts +173 -0
  653. package/src/trace/cardinality/branch-context.ts +214 -0
  654. package/src/trace/cardinality/check.ts +33 -0
  655. package/src/trace/cardinality/classify-edge.ts +64 -0
  656. package/src/trace/cardinality/structural-max.ts +107 -0
  657. package/src/trace/check/coverage-check.ts +188 -0
  658. package/src/trace/check/flow-check.ts +103 -0
  659. package/src/trace/check/flow-step-site.ts +234 -0
  660. package/src/trace/check/io-pattern.ts +116 -0
  661. package/src/trace/codemod/add-cardinality.ts +152 -0
  662. package/src/trace/codemod/run.ts +80 -0
  663. package/src/trace/entry/child-trace.ts +21 -0
  664. package/src/trace/entry/close-trace.ts +87 -0
  665. package/src/trace/entry/finalize-trace.ts +28 -0
  666. package/src/trace/entry/open-trace.ts +138 -0
  667. package/src/trace/entry/parent-trace.ts +20 -0
  668. package/src/trace/entry/parse-traceparent.ts +39 -0
  669. package/src/trace/entry/process-start.ts +38 -0
  670. package/src/trace/entry/trace-context.ts +26 -0
  671. package/src/trace/entry/tracing-enabled.ts +10 -0
  672. package/src/trace/external/record.ts +195 -0
  673. package/src/trace/external/report.ts +66 -0
  674. package/src/trace/namespace.ts +19 -0
  675. package/src/trace/partition/day.ts +47 -0
  676. package/src/trace/partition/index.ts +14 -0
  677. package/src/trace/partition/partition-name.ts +23 -0
  678. package/src/trace/partition/plan.ts +82 -0
  679. package/src/trace/partition/schedule.ts +26 -0
  680. package/src/trace/partition/startup-hook.ts +39 -0
  681. package/src/trace/partition/sync.ts +154 -0
  682. package/src/trace/partition/tables.ts +53 -0
  683. package/src/trace/read/tree.ts +83 -0
  684. package/src/trace/retain/copy-on-error.ts +76 -0
  685. package/src/trace/runtime/expire-stale-traces.ts +54 -0
  686. package/src/trace/runtime/flow-step.ts +147 -0
  687. package/src/trace/runtime/flush-steps.ts +31 -0
  688. package/src/trace/runtime/pending-writes.ts +54 -0
  689. package/src/trace/runtime/step-buffer.ts +298 -0
  690. package/src/trace/runtime/step-error.ts +21 -0
  691. package/src/trace/runtime/step-meta.ts +55 -0
  692. package/src/trace/runtime/thenable.ts +12 -0
  693. package/src/trace/runtime/truncate-trace.ts +62 -0
  694. package/src/trace/schema/trace-fields.ts +52 -0
  695. package/src/trace/schema/trace-retained.ts +22 -0
  696. package/src/trace/schema/trace-step-fields.ts +44 -0
  697. package/src/trace/schema/trace-step-retained.ts +25 -0
  698. package/src/trace/schema/trace-step.ts +18 -0
  699. package/src/trace/schema/trace.ts +12 -0
  700. package/src/trace/transformer/apply.ts +79 -0
  701. package/src/trace/transformer/build-meta-literal.ts +30 -0
  702. package/src/trace/transformer/find-flow-step-sites.ts +85 -0
  703. package/src/trace/transformer/inject-flow-step-import.ts +39 -0
  704. package/src/trace/transformer/transform-error.ts +14 -0
  705. package/src/trace/transformer/transform-flow-steps.ts +123 -0
  706. package/src/trace/transformer/wrap-function-declaration.ts +62 -0
  707. package/src/trace/transformer/wrap-variable-declaration.ts +67 -0
  708. package/src/workspace/affected.ts +110 -0
  709. package/src/workspace/changed-files.ts +163 -0
  710. package/src/workspace/check-deps.ts +213 -0
  711. package/src/workspace/checksum.ts +20 -0
  712. package/src/workspace/dependency-graph.ts +67 -0
  713. package/src/workspace/detect-pipework-copies.ts +93 -0
  714. package/src/workspace/discover.ts +206 -0
  715. package/src/workspace/generate.ts +401 -0
  716. package/src/workspace/index.ts +7 -0
  717. package/src/workspace/module-imports-pipework.ts +39 -0
  718. package/src/workspace/module-package-name.ts +12 -0
  719. package/src/workspace/scrub-git-env.ts +23 -0
  720. package/src/workspace/source-paths.ts +36 -0
  721. package/src/workspace/sync.ts +182 -0
  722. package/dist/REFERENCE.md +0 -1477
  723. package/dist/infra/access-links/rate-limit.d.ts +0 -10
  724. package/dist/infra/access-links/rate-limit.d.ts.map +0 -1
  725. package/dist/infra/access-links/rate-limit.js +0 -28
  726. package/dist/infra/access-links/rate-limit.js.map +0 -1
  727. package/dist/infra/email/sigv4.d.ts +0 -24
  728. package/dist/infra/email/sigv4.d.ts.map +0 -1
  729. package/dist/infra/email/sigv4.js +0 -55
  730. package/dist/infra/email/sigv4.js.map +0 -1
@@ -0,0 +1,99 @@
1
+ import type { DefinedTable } from '../../data/domain/types.js'
2
+ import { resolveDefinitionRef } from '../../data/domain/types.js'
3
+ import type { ValidationIssue } from './types.js'
4
+
5
+ function inheritsBrandFromCompositeFK(def: DefinedTable, fieldName: string, brand: string): boolean {
6
+ for (const fk of def.$options.foreignKeys ?? []) {
7
+ const i = fk.columns.indexOf(fieldName)
8
+ if (i === -1) continue
9
+ const foreignKey = fk.references.columns[i]
10
+ if (foreignKey === undefined) continue
11
+ if (resolveDefinitionRef(fk.references.definition).$fields[foreignKey]?._meta.brand === brand) return true
12
+ }
13
+ return false
14
+ }
15
+
16
+ export function validateDomainIntegrity(
17
+ definitions: ReadonlyMap<string, DefinedTable>,
18
+ configuredDatabases: readonly string[],
19
+ ): ValidationIssue[] {
20
+ const issues: ValidationIssue[] = []
21
+
22
+ const tableNames = new Map<string, string>()
23
+ const brandNames = new Map<string, string>()
24
+
25
+ for (const [name, def] of definitions) {
26
+ if (def.$options.virtual === true) continue
27
+
28
+ if (tableNames.has(name)) {
29
+ issues.push({
30
+ phase: 'domain',
31
+ message: `"${name}" table name conflicts with "${tableNames.get(name)!}"`,
32
+ })
33
+ } else {
34
+ tableNames.set(name, name)
35
+ }
36
+
37
+ if (def.$options.database !== undefined) {
38
+ if (!configuredDatabases.includes(def.$options.database)) {
39
+ issues.push({
40
+ phase: 'domain',
41
+ message: `${name}.database "${def.$options.database}" is not a configured database`,
42
+ })
43
+ }
44
+ }
45
+
46
+ let tenantFieldCount = 0
47
+
48
+ for (const [fieldName, descriptor] of Object.entries(def.$fields)) {
49
+ const meta = descriptor._meta
50
+
51
+ if (meta.tenant) tenantFieldCount++
52
+
53
+ if (meta.reference !== undefined) {
54
+ const refDef = resolveDefinitionRef(meta.reference.definition)
55
+ if (!definitions.has(refDef.$name)) {
56
+ issues.push({
57
+ phase: 'domain',
58
+ message: `${name}.${fieldName} references "${refDef.$name}" — no matching domain definition`,
59
+ })
60
+ }
61
+
62
+ const refField = refDef.$fields[meta.reference.field]
63
+ if (refField === undefined) {
64
+ issues.push({
65
+ phase: 'domain',
66
+ message: `${name}.${fieldName} references "${refDef.$name}.${meta.reference.field}" — field does not exist`,
67
+ })
68
+ }
69
+ }
70
+
71
+ if (meta.brand !== undefined) {
72
+ const isInheritedFromFK = (meta.reference !== undefined
73
+ && resolveDefinitionRef(meta.reference.definition).$fields[meta.reference.field]?._meta.brand === meta.brand)
74
+ || inheritsBrandFromCompositeFK(def, fieldName, meta.brand)
75
+
76
+ if (!isInheritedFromFK) {
77
+ const existing = brandNames.get(meta.brand)
78
+ if (existing !== undefined && existing !== `${name}.${fieldName}`) {
79
+ issues.push({
80
+ phase: 'domain',
81
+ message: `brand "${meta.brand}" used by both ${existing} and ${name}.${fieldName}`,
82
+ })
83
+ } else {
84
+ brandNames.set(meta.brand, `${name}.${fieldName}`)
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if (tenantFieldCount > 1) {
91
+ issues.push({
92
+ phase: 'domain',
93
+ message: `${name} has ${String(tenantFieldCount)} tenant fields — exactly one expected`,
94
+ })
95
+ }
96
+ }
97
+
98
+ return issues
99
+ }
@@ -0,0 +1,5 @@
1
+ export { validateStartup, type ValidationContext } from './startup.js'
2
+ export { validateDomainIntegrity } from './domain.js'
3
+ export { collectSurfaceIssues } from './surfaces.js'
4
+ export { validateConnectivity } from './connectivity.js'
5
+ export { StartupValidationError, type ValidationIssue, type ValidationPhase } from './types.js'
@@ -0,0 +1,312 @@
1
+ import type { ValidationIssue } from './types.js'
2
+ import type { ResolvedDatabase } from '../config/load.js'
3
+ import { probeConnectTimeout } from './connectivity.js'
4
+ import { queueDefinitions, queueProgressColumns } from '../../async/jobs/table.js'
5
+ import { getTableColumns } from '../../data/query/index.js'
6
+
7
+ // D2 residual risk (subseam audit 2026-07-16): a queue whose module is in no
8
+ // schema glob and no import chain reaches the registry at runtime (createQueue
9
+ // runs when the worker imports it) but never reached a migration — so the
10
+ // first enqueue in production failed on a missing relation. This check makes
11
+ // that state loud at startup and at test bootstrap instead.
12
+ //
13
+ // The same story one level down: a table that exists but predates the current
14
+ // queue definition (0.26.0 added claim_token) fails at the first claim() with
15
+ // a raw 42703. So the shape is compared too. Extra columns are ignored on
16
+ // purpose — a consumer may add their own columns to a queue table; only
17
+ // columns the definition requires and the table lacks are reported.
18
+ //
19
+ // One level down again: a column can be present under the right name and
20
+ // still be the wrong column. `ALTER COLUMN ... SET DATA TYPE` / `DROP NOT
21
+ // NULL` are migration steps a consumer can skip exactly as they can skip an
22
+ // ADD COLUMN, and the result is quieter: claim_token as text compares wrong
23
+ // against a uuid rather than erroring, so a fence built on it silently never
24
+ // matches. See columnDrift() for what counts as compatible.
25
+
26
+ /** A required column whose type or nullability in the database contradicts the definition. */
27
+ export interface QueueColumnDrift {
28
+ readonly column: string
29
+ /** Present when the type differs; the definition's spelling and the database's. */
30
+ readonly requiredType?: string
31
+ readonly foundType?: string
32
+ /** True when the definition requires NOT NULL and the column is nullable. */
33
+ readonly nullableButRequired?: boolean
34
+ }
35
+
36
+ /** A registered queue table that is absent, or present with columns missing or drifted. */
37
+ export interface QueueTableProblem {
38
+ readonly table: string
39
+ /** Empty when the table itself is absent. */
40
+ readonly missingColumns: readonly string[]
41
+ /** Omitted entirely when no present column has drifted. */
42
+ readonly driftedColumns?: readonly QueueColumnDrift[]
43
+ /** The configured `progressColumn` when the table lacks it. */
44
+ readonly missingProgressColumn?: string
45
+ }
46
+
47
+ export function queueProgressColumnMessage(table: string, database: string, column: string): string {
48
+ return (
49
+ `queue table "${table}" in database "${database}" is missing column "${column}", which ` +
50
+ `createQueue({ progressColumn: '${column}' }) records as each claim's progressMark. ` +
51
+ `It is a column of your own: add it with a migration of your schema, then run \`pipework migrate\`.`
52
+ )
53
+ }
54
+
55
+ export function queueTableMessage(table: string, database: string): string {
56
+ return (
57
+ `queue table "${table}" is registered (createQueue) but does not exist in database "${database}". ` +
58
+ `The module calling createQueue({ table: '${table}' }) must be reachable from the database's ` +
59
+ `schema glob (or imported by a schema file) so \`pipework generate\` emits its migration. ` +
60
+ `Add it to the schema patterns, run \`pipework generate\`, then \`pipework migrate\`.`
61
+ )
62
+ }
63
+
64
+ export function queueColumnsMessage(table: string, database: string, missing: readonly string[]): string {
65
+ const columns = missing.map(c => `"${c}"`).join(', ')
66
+ const plural = missing.length === 1 ? 'column' : 'columns'
67
+ return (
68
+ `queue table "${table}" in database "${database}" is missing ${plural} ${columns}, which the queue ` +
69
+ `definition requires. The table predates the current definition — its migration has not been ` +
70
+ `regenerated or applied. Run \`pipework generate\`, then \`pipework migrate\`. ` +
71
+ `(Extra columns of your own are fine; only missing ones are reported.)`
72
+ )
73
+ }
74
+
75
+ function driftClause(drift: QueueColumnDrift): string {
76
+ const parts: string[] = []
77
+ if (drift.requiredType !== undefined && drift.foundType !== undefined) {
78
+ parts.push(`requires ${drift.requiredType} but the database has ${drift.foundType}`)
79
+ }
80
+ if (drift.nullableButRequired === true) {
81
+ parts.push('requires NOT NULL but the database allows nulls')
82
+ }
83
+ return `"${drift.column}" ${parts.join(', and ')}`
84
+ }
85
+
86
+ export function queueColumnDriftMessage(
87
+ table: string,
88
+ database: string,
89
+ drifted: readonly QueueColumnDrift[],
90
+ ): string {
91
+ const plural = drifted.length === 1 ? 'column' : 'columns'
92
+ return (
93
+ `queue table "${table}" in database "${database}" has ${plural} the queue definition disagrees with: ` +
94
+ `${drifted.map(driftClause).join('; ')}. The table predates the current definition — the migration ` +
95
+ `that alters ${drifted.length === 1 ? 'it' : 'them'} has not been regenerated or applied. ` +
96
+ `Run \`pipework generate\`, then \`pipework migrate\`. ` +
97
+ `(Length and precision are not compared, and a column stricter than the definition asks is fine.)`
98
+ )
99
+ }
100
+
101
+ /** The message for one problem, whichever kinds it carries. */
102
+ export function queueTableProblemMessage(problem: QueueTableProblem, database: string): string {
103
+ if (problem.missingColumns.length === 0 && (problem.driftedColumns ?? []).length === 0 && problem.missingProgressColumn === undefined) {
104
+ return queueTableMessage(problem.table, database)
105
+ }
106
+ const parts: string[] = []
107
+ if (problem.missingColumns.length > 0) {
108
+ parts.push(queueColumnsMessage(problem.table, database, problem.missingColumns))
109
+ }
110
+ if (problem.driftedColumns !== undefined && problem.driftedColumns.length > 0) {
111
+ parts.push(queueColumnDriftMessage(problem.table, database, problem.driftedColumns))
112
+ }
113
+ if (problem.missingProgressColumn !== undefined) {
114
+ parts.push(queueProgressColumnMessage(problem.table, database, problem.missingProgressColumn))
115
+ }
116
+ return parts.join(' ')
117
+ }
118
+
119
+ interface RequiredColumn {
120
+ readonly name: string
121
+ /** The definition's own spelling, e.g. "timestamp with time zone"; resolved to an OID by Postgres. */
122
+ readonly sqlType: string
123
+ readonly notNull: boolean
124
+ }
125
+
126
+ /** Columns the definition requires — read off the table it builds, never a second list. */
127
+ function requiredColumns(): Map<string, readonly RequiredColumn[]> {
128
+ const required = new Map<string, readonly RequiredColumn[]>()
129
+ for (const [table, definition] of queueDefinitions()) {
130
+ const columns = Object.values(getTableColumns(definition.table())).map(c => ({
131
+ name: c.name,
132
+ sqlType: c.getSQLType(),
133
+ notNull: c.notNull,
134
+ }))
135
+ required.set(table, columns)
136
+ }
137
+ return required
138
+ }
139
+
140
+ interface ActualColumn {
141
+ readonly typeOid: number
142
+ /** format_type without a typmod: the found type spelled as the definition spells it. */
143
+ readonly typeName: string
144
+ readonly notNull: boolean
145
+ }
146
+
147
+ /**
148
+ * What counts as compatible, and why:
149
+ *
150
+ * - Type is compared as canonical type identity, not spelling: the
151
+ * definition's declared type is resolved through Postgres's own
152
+ * `to_regtype` and the resulting OID compared against the column's
153
+ * `atttypid`. `timestamptz` and `timestamp with time zone` are then the
154
+ * same thing without a hand-maintained alias table that can rot. A type
155
+ * Postgres cannot resolve (to_regtype returns NULL — an enum or domain the
156
+ * database has not got) is not reported as drift: the missing type would
157
+ * have shown up as a missing table or column, and guessing here would
158
+ * report false drift. Note the arm swallows a pseudo-type too: `serial` is
159
+ * not a type Postgres resolves, so a column ever declared that way would be
160
+ * skipped silently, with no missing column to reveal it.
161
+ * - Type modifiers (length, precision) are ignored. varchar(64) vs
162
+ * varchar(128) is not reported: comparing typmod invites false drift on
163
+ * healthy tables, and a check that cries wolf gets switched off, which
164
+ * leaves things worse than not checking at all.
165
+ * - Nullability is reported in one direction only. A column the definition
166
+ * requires NOT NULL that is nullable in the database breaks a promise the
167
+ * definition makes, so it is reported. Stricter in the database than the
168
+ * definition asks cannot violate anything the definition promises, and a
169
+ * consumer may have tightened it deliberately, so it is not.
170
+ * - Defaults are not compared at all (out of scope for 0.28.0).
171
+ */
172
+ function columnDrift(
173
+ required: RequiredColumn,
174
+ actual: ActualColumn,
175
+ expectedOid: number | undefined,
176
+ ): QueueColumnDrift | undefined {
177
+ const typeDiffers = expectedOid !== undefined && expectedOid !== actual.typeOid
178
+ const nullableButRequired = required.notNull && !actual.notNull
179
+ if (!typeDiffers && !nullableButRequired) return undefined
180
+ return {
181
+ column: required.name,
182
+ ...(typeDiffers ? { requiredType: required.sqlType, foundType: actual.typeName } : {}),
183
+ ...(nullableButRequired ? { nullableButRequired: true } : {}),
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Registered queue tables that are absent from the database at `url`, or
189
+ * present without every column their definition requires, or present with a
190
+ * required column whose type or nullability contradicts the definition.
191
+ * Opens a short-lived probe connection, mirroring validateConnectivity. Two
192
+ * queries total, whatever the number of queues. `timeoutMs: null` leaves the
193
+ * connect timeout to the driver (PGCONNECT_TIMEOUT, else 30 s).
194
+ */
195
+ export async function queueTableProblems(url: string, timeoutMs: number | null = 5000): Promise<QueueTableProblem[]> {
196
+ const required = requiredColumns()
197
+ const tables = [...required.keys()]
198
+ if (tables.length === 0) return []
199
+
200
+ const { default: postgres } = await import('postgres')
201
+ const sql = postgres(url, {
202
+ max: 1,
203
+ idle_timeout: 1,
204
+ ...probeConnectTimeout(timeoutMs),
205
+ onnotice: () => {},
206
+ })
207
+
208
+ try {
209
+ const presence = await sql<{ name: string; present: boolean }[]>`
210
+ SELECT t.name, to_regclass('public.' || quote_ident(t.name)) IS NOT NULL AS present
211
+ FROM unnest(${tables}::text[]) AS t(name)
212
+ `
213
+ const present = new Set(presence.filter(r => r.present).map(r => r.name))
214
+
215
+ const actual = new Map<string, Map<string, ActualColumn>>()
216
+ const expectedOids = new Map<string, number>()
217
+ const typeNames = [...new Set(
218
+ [...present].flatMap(t => (required.get(t) ?? []).map(c => c.sqlType)),
219
+ )]
220
+ if (present.size > 0) {
221
+ // pg_attribute, not information_schema.columns: that view is filtered to
222
+ // columns the current role holds a privilege on, so a role with CONNECT
223
+ // and USAGE but no table grants would read zero columns and be told to
224
+ // migrate when the cure is a GRANT. attnum > 0 AND NOT attisdropped
225
+ // excludes system and dropped columns. The definition's declared types
226
+ // are canonicalised by the same round trip (to_regtype), so this stays
227
+ // two queries whatever the number of queues; format_type without a
228
+ // typmod prints the found type the way the definition spells it.
229
+ const [row] = await sql<{
230
+ columns: { table_name: string; column_name: string; type_oid: number; not_null: boolean; type_name: string }[]
231
+ types: { name: string; oid: number | null }[]
232
+ }[]>`
233
+ SELECT
234
+ (
235
+ SELECT coalesce(json_agg(json_build_object(
236
+ 'table_name', c.relname, 'column_name', a.attname,
237
+ 'type_oid', a.atttypid::int, 'not_null', a.attnotnull,
238
+ 'type_name', format_type(a.atttypid, NULL)
239
+ )), '[]'::json)
240
+ FROM pg_attribute a
241
+ JOIN pg_class c ON c.oid = a.attrelid
242
+ JOIN pg_namespace n ON n.oid = c.relnamespace
243
+ WHERE n.nspname = 'public' AND c.relname = ANY(${[...present]}::text[])
244
+ AND a.attnum > 0 AND NOT a.attisdropped
245
+ ) AS columns,
246
+ (
247
+ SELECT coalesce(json_agg(json_build_object('name', t, 'oid', to_regtype(t)::oid::int)), '[]'::json)
248
+ FROM unnest(${typeNames}::text[]) AS t
249
+ ) AS types
250
+ `
251
+ for (const column of row?.columns ?? []) {
252
+ let columns = actual.get(column.table_name)
253
+ if (columns === undefined) { columns = new Map(); actual.set(column.table_name, columns) }
254
+ columns.set(column.column_name, {
255
+ typeOid: column.type_oid,
256
+ typeName: column.type_name,
257
+ notNull: column.not_null,
258
+ })
259
+ }
260
+ for (const type of row?.types ?? []) {
261
+ if (type.oid !== null && type.oid !== 0) expectedOids.set(type.name, type.oid)
262
+ }
263
+ }
264
+
265
+ const problems: QueueTableProblem[] = []
266
+ for (const table of tables) {
267
+ if (!present.has(table)) {
268
+ problems.push({ table, missingColumns: [] })
269
+ continue
270
+ }
271
+ const have = actual.get(table) ?? new Map<string, ActualColumn>()
272
+ const missing: string[] = []
273
+ const drifted: QueueColumnDrift[] = []
274
+ for (const column of required.get(table) ?? []) {
275
+ const found = have.get(column.name)
276
+ if (found === undefined) { missing.push(column.name); continue }
277
+ // The unprivileged-role case: pg_attribute is readable by anyone, so a
278
+ // present column always carries its real type here.
279
+ const drift = columnDrift(column, found, expectedOids.get(column.sqlType))
280
+ if (drift !== undefined) drifted.push(drift)
281
+ }
282
+ const progress = queueProgressColumns().get(table)
283
+ const missingProgress = progress !== undefined && !have.has(progress) ? { missingProgressColumn: progress } : {}
284
+ if (missing.length > 0 || drifted.length > 0 || progress !== undefined && !have.has(progress)) {
285
+ problems.push({
286
+ table, missingColumns: missing, ...(drifted.length > 0 ? { driftedColumns: drifted } : {}), ...missingProgress,
287
+ })
288
+ }
289
+ }
290
+ return problems
291
+ } finally {
292
+ await sql.end({ timeout: 1 })
293
+ }
294
+ }
295
+
296
+ /** Startup-validation leg: every registered queue's table must exist, in shape, in the default database (where queue migrations are emitted). */
297
+ export async function validateQueueTables(
298
+ defaultDb: ResolvedDatabase,
299
+ timeoutMs = 5000,
300
+ ): Promise<ValidationIssue[]> {
301
+ try {
302
+ const problems = await queueTableProblems(defaultDb.url, timeoutMs)
303
+ return problems.map(problem => ({
304
+ phase: 'database' as const,
305
+ message: queueTableProblemMessage(problem, defaultDb.name),
306
+ }))
307
+ } catch {
308
+ // An unreachable database is validateConnectivity's finding — reporting it
309
+ // here too would double up the same root cause.
310
+ return []
311
+ }
312
+ }
@@ -0,0 +1,138 @@
1
+ import type { ValidationIssue } from './types.js'
2
+ import type { ResolvedDatabase } from '../config/load.js'
3
+ import { probeConnectTimeout } from './connectivity.js'
4
+ import type { IsolationStrategy } from '../config/schema.js'
5
+ import { appPoolCreated } from '../config/app-pool.js'
6
+
7
+ // D4b (subseam audit 2026-07-16, finding F3): a database can carry a full RLS
8
+ // policy set while its runtime role is superuser or BYPASSRLS — every policy
9
+ // inert, with zero signal from the framework. Probed at startup and at test
10
+ // bootstrap; refusal is an error, not a warning.
11
+
12
+ export interface RlsRoleProbe {
13
+ readonly role: string
14
+ readonly superuser: boolean
15
+ readonly bypassRls: boolean
16
+ }
17
+
18
+ export function rlsRoleMessage(database: string, probe: RlsRoleProbe): string {
19
+ const flag = probe.superuser ? 'SUPERUSER' : 'BYPASSRLS'
20
+ return (
21
+ `database "${database}" has rls configured, but its runtime role "${probe.role}" is ${flag} — ` +
22
+ `row-level security policies never apply to it, so tenant isolation is inert. ` +
23
+ `Provision a non-BYPASSRLS, non-superuser application role and point the runtime connection at it ` +
24
+ `(appUrl/appTestUrl, or the url/testUrl var this connection resolved from).`
25
+ )
26
+ }
27
+
28
+ /** The connection whose role actually executes queries under the harness: transaction isolation collapses every role onto the resolved `url` connection; database/container isolation resolves roles normally (`appUrl ?? url`). */
29
+ export function rlsProbeTarget(db: ResolvedDatabase, strategy: IsolationStrategy): string {
30
+ return appPoolCreated(strategy) ? (db.appUrl ?? db.url) : db.url
31
+ }
32
+
33
+ /**
34
+ * Role flags for `current_user` at `url`. Short-lived probe connection, mirroring
35
+ * validateConnectivity. `timeoutMs: null` leaves the connect timeout to the
36
+ * driver (PGCONNECT_TIMEOUT, else 30 s).
37
+ */
38
+ export async function probeRlsRole(url: string, timeoutMs: number | null = 5000): Promise<RlsRoleProbe> {
39
+ const { default: postgres } = await import('postgres')
40
+ const sql = postgres(url, {
41
+ max: 1,
42
+ idle_timeout: 1,
43
+ ...probeConnectTimeout(timeoutMs),
44
+ onnotice: () => {},
45
+ })
46
+
47
+ try {
48
+ const rows = await sql`SELECT current_user AS role, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`
49
+ const row = rows[0] as { role: string; rolsuper: boolean; rolbypassrls: boolean } | undefined
50
+ if (row === undefined) {
51
+ throw new Error('current_user not found in pg_roles')
52
+ }
53
+ return { role: row.role, superuser: row.rolsuper, bypassRls: row.rolbypassrls }
54
+ } finally {
55
+ await sql.end({ timeout: 1 })
56
+ }
57
+ }
58
+
59
+ /** Startup-validation leg: for each database with `rls` configured, the runtime connection's role (`appUrl ?? url`) must not bypass RLS. */
60
+ export async function validateRlsRoles(
61
+ databases: ReadonlyMap<string, ResolvedDatabase>,
62
+ timeoutMs = 5000,
63
+ ): Promise<ValidationIssue[]> {
64
+ const issues: ValidationIssue[] = []
65
+
66
+ const checks = [...databases.entries()]
67
+ .filter(([, db]) => db.rls !== undefined)
68
+ .map(async ([name, db]) => {
69
+ let probe: RlsRoleProbe
70
+ try {
71
+ probe = await probeRlsRole(db.appUrl ?? db.url, timeoutMs)
72
+ } catch {
73
+ // An unreachable database is validateConnectivity's finding.
74
+ return
75
+ }
76
+ if (probe.superuser || probe.bypassRls) {
77
+ issues.push({ phase: 'database', message: rlsRoleMessage(name, probe) })
78
+ }
79
+ })
80
+
81
+ await Promise.all(checks)
82
+ return issues
83
+ }
84
+
85
+ export function systemRoleMessage(database: string, platformRole: string, probe: RlsRoleProbe): string[] {
86
+ const out: string[] = []
87
+ if (probe.superuser || probe.bypassRls) {
88
+ const flag = probe.superuser ? 'SUPERUSER' : 'BYPASSRLS'
89
+ out.push(
90
+ `database "${database}": the systemUrl role "${probe.role}" is ${flag} — row-level security never applies to it, ` +
91
+ `so the system channel reaches every tenant row and no door means anything. ` +
92
+ `Point systemUrl/systemTestUrl at a role with neither flag, named rls.platformRole.`,
93
+ )
94
+ }
95
+ if (probe.role !== platformRole) {
96
+ out.push(
97
+ `database "${database}": the systemUrl connection authenticates as "${probe.role}", but rls.platformRole is "${platformRole}" — ` +
98
+ `door policies are granted to "${platformRole}" only. Point systemUrl/systemTestUrl at a connection as "${platformRole}", ` +
99
+ `or change rls.platformRole.`,
100
+ )
101
+ }
102
+ return out
103
+ }
104
+
105
+ /** Startup-validation leg: for each database in platform mode, the `systemUrl` role must be the named platform role and must not bypass RLS. */
106
+ export async function validateSystemRoles(
107
+ databases: ReadonlyMap<string, ResolvedDatabase>,
108
+ timeoutMs = 5000,
109
+ ): Promise<ValidationIssue[]> {
110
+ const issues: ValidationIssue[] = []
111
+
112
+ const checks = [...databases.entries()]
113
+ .filter(([, db]) => db.rls?.platformRole !== undefined && db.systemUrl !== undefined)
114
+ .map(async ([name, db]) => {
115
+ let probe: RlsRoleProbe
116
+ try {
117
+ probe = await probeRlsRole(db.systemUrl!, timeoutMs)
118
+ } catch {
119
+ // An unreachable database is validateConnectivity's finding.
120
+ return
121
+ }
122
+ for (const message of systemRoleMessage(name, db.rls!.platformRole!, probe)) {
123
+ issues.push({ phase: 'database', message })
124
+ }
125
+ })
126
+
127
+ await Promise.all(checks)
128
+ return issues
129
+ }
130
+
131
+ /** Databases with `rls` and no platform role: their system channel runs on the owner role, which bypasses RLS. */
132
+ export function ownerSystemChannelWarnings(databases: ReadonlyMap<string, ResolvedDatabase>): string[] {
133
+ return [...databases.entries()]
134
+ .filter(([, db]) => db.rls !== undefined && db.rls.platformRole === undefined)
135
+ .map(([name]) =>
136
+ `database "${name}" has rls configured and no platform role: pipe.system() runs on the owner role, which bypasses RLS. ` +
137
+ `Set systemUrl and rls.platformRole to run it on a role with no BYPASSRLS and read tenant tables through declared doors.`)
138
+ }
@@ -0,0 +1,51 @@
1
+ import type { ValidationIssue } from './types.js'
2
+ import { StartupValidationError } from './types.js'
3
+ import { validateDomainIntegrity } from './domain.js'
4
+ import { collectSurfaceIssues } from './surfaces.js'
5
+ import { validateConnectivity } from './connectivity.js'
6
+ import { validateQueueTables } from './queue-tables.js'
7
+ import { validateRlsRoles, validateSystemRoles } from './rls-role.js'
8
+ import { definitions } from '../../data/domain/define.js'
9
+ import type { SurfaceMap } from '../surface/types.js'
10
+ import type { ResolvedDatabase } from '../config/load.js'
11
+
12
+ export interface ValidationContext {
13
+ readonly configuredDatabases: readonly string[]
14
+ readonly environment: string
15
+ readonly surfaces?: SurfaceMap | undefined
16
+ readonly defaultDbAvailable?: boolean | undefined
17
+ readonly databases?: ReadonlyMap<string, ResolvedDatabase> | undefined
18
+ /** The database queue migrations are emitted into — queue-table existence is checked there. Undefined skips the check (genuinely ambiguous multi-DB configs fail earlier, at generate). */
19
+ readonly defaultDatabase?: ResolvedDatabase | undefined
20
+ readonly connectivityCheck?: boolean | undefined
21
+ }
22
+
23
+ export async function validateStartup(ctx: ValidationContext): Promise<void> {
24
+ const issues: ValidationIssue[] = []
25
+
26
+ issues.push(...validateDomainIntegrity(definitions(), ctx.configuredDatabases))
27
+
28
+ if (ctx.surfaces !== undefined) {
29
+ issues.push(
30
+ ...collectSurfaceIssues(
31
+ ctx.surfaces,
32
+ ctx.configuredDatabases,
33
+ ctx.defaultDbAvailable ?? ctx.configuredDatabases.length === 1,
34
+ ),
35
+ )
36
+ }
37
+
38
+ if (ctx.connectivityCheck !== false && ctx.databases !== undefined) {
39
+ issues.push(...await validateConnectivity(ctx.databases))
40
+ issues.push(...await validateRlsRoles(ctx.databases))
41
+ issues.push(...await validateSystemRoles(ctx.databases))
42
+ }
43
+
44
+ if (ctx.connectivityCheck !== false && ctx.defaultDatabase !== undefined) {
45
+ issues.push(...await validateQueueTables(ctx.defaultDatabase))
46
+ }
47
+
48
+ if (issues.length > 0) {
49
+ throw new StartupValidationError(issues)
50
+ }
51
+ }
@@ -0,0 +1,65 @@
1
+ import type { SurfaceMap } from '../surface/types.js'
2
+ import { DEFAULT_DB } from '../../request/di/types.js'
3
+ import type { ValidationIssue } from './types.js'
4
+
5
+ export function collectSurfaceIssues(
6
+ surfaces: SurfaceMap,
7
+ configuredDatabases: readonly string[],
8
+ defaultDbAvailable: boolean,
9
+ ): ValidationIssue[] {
10
+ const issues: ValidationIssue[] = []
11
+ const dbSet = new Set(configuredDatabases)
12
+
13
+ for (const [name, surface] of Object.entries(surfaces)) {
14
+ for (const db of surface.manifest.databases) {
15
+ if (db === DEFAULT_DB) {
16
+ if (!defaultDbAvailable) {
17
+ issues.push({
18
+ phase: 'surface',
19
+ message: `"${name}" uses the default database, but multiple databases are configured: ${configuredDatabases.join(', ')}`,
20
+ })
21
+ }
22
+ } else if (!dbSet.has(db)) {
23
+ issues.push({
24
+ phase: 'surface',
25
+ message: `"${name}" requires database "${db}" which is not configured (available: ${configuredDatabases.join(', ')})`,
26
+ })
27
+ }
28
+ }
29
+
30
+ if (surface.kind === 'worker' && surface.manifest.jobTypes.length === 0) {
31
+ issues.push({
32
+ phase: 'surface',
33
+ message: `"${name}" is a worker with no job handlers`,
34
+ })
35
+ }
36
+
37
+ const seen = new Set<string>()
38
+ for (const jobType of surface.manifest.jobTypes) {
39
+ if (seen.has(jobType)) {
40
+ issues.push({
41
+ phase: 'surface',
42
+ message: `"${name}" has duplicate job type "${jobType}"`,
43
+ })
44
+ }
45
+ seen.add(jobType)
46
+ }
47
+ }
48
+
49
+ const jobOwners = new Map<string, string>()
50
+ for (const [name, surface] of Object.entries(surfaces)) {
51
+ for (const jobType of surface.manifest.jobTypes) {
52
+ const existing = jobOwners.get(jobType)
53
+ if (existing !== undefined) {
54
+ issues.push({
55
+ phase: 'surface',
56
+ message: `job type "${jobType}" handled by both "${existing}" and "${name}"`,
57
+ })
58
+ } else {
59
+ jobOwners.set(jobType, name)
60
+ }
61
+ }
62
+ }
63
+
64
+ return issues
65
+ }