@ontrails/core 1.0.0-beta.15 → 1.0.0-beta.16

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 (348) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +61 -16
  3. package/package.json +9 -8
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/context.ts +4 -0
  10. package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
  11. package/src/detours.ts +8 -0
  12. package/src/draft.ts +4 -0
  13. package/src/error-projection.ts +51 -0
  14. package/src/errors.ts +192 -39
  15. package/src/execute.ts +443 -44
  16. package/src/fire.ts +965 -91
  17. package/src/index.ts +285 -38
  18. package/src/layer-projection.ts +192 -0
  19. package/src/layer.ts +32 -1
  20. package/src/observe.ts +361 -0
  21. package/src/permits.ts +12 -0
  22. package/src/redaction/patterns.ts +6 -3
  23. package/src/resilience.ts +8 -8
  24. package/src/resource-config.ts +477 -44
  25. package/src/resource.ts +21 -1
  26. package/src/result.ts +18 -0
  27. package/src/run.ts +2 -2
  28. package/src/schedule-runtime.ts +689 -0
  29. package/src/schedule.ts +324 -0
  30. package/src/serialization.ts +90 -57
  31. package/src/signal-diagnostics.ts +633 -0
  32. package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
  33. package/src/signal.ts +37 -0
  34. package/src/store/accessor-protocol.ts +5 -5
  35. package/src/structured-examples.ts +245 -0
  36. package/src/surface-derivation.ts +91 -0
  37. package/src/surface-filter.ts +7 -13
  38. package/src/topo.ts +386 -6
  39. package/src/tracing.ts +345 -0
  40. package/src/trail.ts +168 -22
  41. package/src/trails/derive-trail.ts +1 -1
  42. package/src/trails/ingest.ts +7 -15
  43. package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
  44. package/src/transport-error-map.ts +146 -28
  45. package/src/types.ts +82 -28
  46. package/src/validate-established-topo.ts +6 -0
  47. package/src/validate-topo.ts +212 -5
  48. package/src/validation.ts +58 -1
  49. package/src/webhook.ts +276 -0
  50. package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
  51. package/.turbo/turbo-build.log +0 -1
  52. package/.turbo/turbo-lint.log +0 -3
  53. package/.turbo/turbo-typecheck.log +0 -1
  54. package/dist/blob-ref.d.ts +0 -20
  55. package/dist/blob-ref.d.ts.map +0 -1
  56. package/dist/blob-ref.js +0 -22
  57. package/dist/blob-ref.js.map +0 -1
  58. package/dist/branded.d.ts +0 -36
  59. package/dist/branded.d.ts.map +0 -1
  60. package/dist/branded.js +0 -89
  61. package/dist/branded.js.map +0 -1
  62. package/dist/collections.d.ts +0 -31
  63. package/dist/collections.d.ts.map +0 -1
  64. package/dist/collections.js +0 -60
  65. package/dist/collections.js.map +0 -1
  66. package/dist/context.d.ts +0 -22
  67. package/dist/context.d.ts.map +0 -1
  68. package/dist/context.js +0 -37
  69. package/dist/context.js.map +0 -1
  70. package/dist/contour.d.ts +0 -81
  71. package/dist/contour.d.ts.map +0 -1
  72. package/dist/contour.js +0 -176
  73. package/dist/contour.js.map +0 -1
  74. package/dist/cross-schema.d.ts +0 -18
  75. package/dist/cross-schema.d.ts.map +0 -1
  76. package/dist/cross-schema.js +0 -29
  77. package/dist/cross-schema.js.map +0 -1
  78. package/dist/derive.d.ts +0 -44
  79. package/dist/derive.d.ts.map +0 -1
  80. package/dist/derive.js +0 -149
  81. package/dist/derive.js.map +0 -1
  82. package/dist/draft.d.ts +0 -28
  83. package/dist/draft.d.ts.map +0 -1
  84. package/dist/draft.js +0 -157
  85. package/dist/draft.js.map +0 -1
  86. package/dist/errors.d.ts +0 -141
  87. package/dist/errors.d.ts.map +0 -1
  88. package/dist/errors.js +0 -185
  89. package/dist/errors.js.map +0 -1
  90. package/dist/execute.d.ts +0 -52
  91. package/dist/execute.d.ts.map +0 -1
  92. package/dist/execute.js +0 -511
  93. package/dist/execute.js.map +0 -1
  94. package/dist/fetch.d.ts +0 -15
  95. package/dist/fetch.d.ts.map +0 -1
  96. package/dist/fetch.js +0 -102
  97. package/dist/fetch.js.map +0 -1
  98. package/dist/fire.d.ts +0 -43
  99. package/dist/fire.d.ts.map +0 -1
  100. package/dist/fire.js +0 -185
  101. package/dist/fire.js.map +0 -1
  102. package/dist/guards.d.ts +0 -17
  103. package/dist/guards.d.ts.map +0 -1
  104. package/dist/guards.js +0 -25
  105. package/dist/guards.js.map +0 -1
  106. package/dist/index.d.ts +0 -59
  107. package/dist/index.d.ts.map +0 -1
  108. package/dist/index.js +0 -61
  109. package/dist/index.js.map +0 -1
  110. package/dist/internal/cross-batch.d.ts +0 -44
  111. package/dist/internal/cross-batch.d.ts.map +0 -1
  112. package/dist/internal/cross-batch.js +0 -58
  113. package/dist/internal/cross-batch.js.map +0 -1
  114. package/dist/internal/fork-ctx.d.ts +0 -41
  115. package/dist/internal/fork-ctx.d.ts.map +0 -1
  116. package/dist/internal/fork-ctx.js +0 -47
  117. package/dist/internal/fork-ctx.js.map +0 -1
  118. package/dist/internal/signal-ref.d.ts +0 -15
  119. package/dist/internal/signal-ref.d.ts.map +0 -1
  120. package/dist/internal/signal-ref.js +0 -41
  121. package/dist/internal/signal-ref.js.map +0 -1
  122. package/dist/internal/topo-saves.d.ts +0 -53
  123. package/dist/internal/topo-saves.d.ts.map +0 -1
  124. package/dist/internal/topo-saves.js +0 -449
  125. package/dist/internal/topo-saves.js.map +0 -1
  126. package/dist/internal/topo-snapshots.d.ts +0 -57
  127. package/dist/internal/topo-snapshots.d.ts.map +0 -1
  128. package/dist/internal/topo-snapshots.js +0 -372
  129. package/dist/internal/topo-snapshots.js.map +0 -1
  130. package/dist/internal/topo-store-read.d.ts +0 -70
  131. package/dist/internal/topo-store-read.d.ts.map +0 -1
  132. package/dist/internal/topo-store-read.js +0 -221
  133. package/dist/internal/topo-store-read.js.map +0 -1
  134. package/dist/internal/topo-store.d.ts +0 -26
  135. package/dist/internal/topo-store.d.ts.map +0 -1
  136. package/dist/internal/topo-store.js +0 -621
  137. package/dist/internal/topo-store.js.map +0 -1
  138. package/dist/internal/tracing.d.ts +0 -98
  139. package/dist/internal/tracing.d.ts.map +0 -1
  140. package/dist/internal/tracing.js +0 -102
  141. package/dist/internal/tracing.js.map +0 -1
  142. package/dist/internal/trails-db.d.ts +0 -16
  143. package/dist/internal/trails-db.d.ts.map +0 -1
  144. package/dist/internal/trails-db.js +0 -118
  145. package/dist/internal/trails-db.js.map +0 -1
  146. package/dist/internal/zod-wrappers.d.ts +0 -40
  147. package/dist/internal/zod-wrappers.d.ts.map +0 -1
  148. package/dist/internal/zod-wrappers.js +0 -59
  149. package/dist/internal/zod-wrappers.js.map +0 -1
  150. package/dist/layer.d.ts +0 -23
  151. package/dist/layer.d.ts.map +0 -1
  152. package/dist/layer.js +0 -21
  153. package/dist/layer.js.map +0 -1
  154. package/dist/path-security.d.ts +0 -28
  155. package/dist/path-security.d.ts.map +0 -1
  156. package/dist/path-security.js +0 -63
  157. package/dist/path-security.js.map +0 -1
  158. package/dist/patterns/bulk.d.ts +0 -15
  159. package/dist/patterns/bulk.d.ts.map +0 -1
  160. package/dist/patterns/bulk.js +0 -14
  161. package/dist/patterns/bulk.js.map +0 -1
  162. package/dist/patterns/change.d.ts +0 -10
  163. package/dist/patterns/change.d.ts.map +0 -1
  164. package/dist/patterns/change.js +0 -10
  165. package/dist/patterns/change.js.map +0 -1
  166. package/dist/patterns/date-range.d.ts +0 -10
  167. package/dist/patterns/date-range.d.ts.map +0 -1
  168. package/dist/patterns/date-range.js +0 -10
  169. package/dist/patterns/date-range.js.map +0 -1
  170. package/dist/patterns/index.d.ts +0 -9
  171. package/dist/patterns/index.d.ts.map +0 -1
  172. package/dist/patterns/index.js +0 -9
  173. package/dist/patterns/index.js.map +0 -1
  174. package/dist/patterns/pagination.d.ts +0 -18
  175. package/dist/patterns/pagination.d.ts.map +0 -1
  176. package/dist/patterns/pagination.js +0 -18
  177. package/dist/patterns/pagination.js.map +0 -1
  178. package/dist/patterns/progress.d.ts +0 -11
  179. package/dist/patterns/progress.d.ts.map +0 -1
  180. package/dist/patterns/progress.js +0 -11
  181. package/dist/patterns/progress.js.map +0 -1
  182. package/dist/patterns/sorting.d.ts +0 -13
  183. package/dist/patterns/sorting.d.ts.map +0 -1
  184. package/dist/patterns/sorting.js +0 -10
  185. package/dist/patterns/sorting.js.map +0 -1
  186. package/dist/patterns/status.d.ts +0 -15
  187. package/dist/patterns/status.d.ts.map +0 -1
  188. package/dist/patterns/status.js +0 -9
  189. package/dist/patterns/status.js.map +0 -1
  190. package/dist/patterns/timestamps.d.ts +0 -10
  191. package/dist/patterns/timestamps.d.ts.map +0 -1
  192. package/dist/patterns/timestamps.js +0 -10
  193. package/dist/patterns/timestamps.js.map +0 -1
  194. package/dist/redaction/index.d.ts +0 -4
  195. package/dist/redaction/index.d.ts.map +0 -1
  196. package/dist/redaction/index.js +0 -3
  197. package/dist/redaction/index.js.map +0 -1
  198. package/dist/redaction/patterns.d.ts +0 -9
  199. package/dist/redaction/patterns.d.ts.map +0 -1
  200. package/dist/redaction/patterns.js +0 -39
  201. package/dist/redaction/patterns.js.map +0 -1
  202. package/dist/redaction/redactor.d.ts +0 -27
  203. package/dist/redaction/redactor.d.ts.map +0 -1
  204. package/dist/redaction/redactor.js +0 -89
  205. package/dist/redaction/redactor.js.map +0 -1
  206. package/dist/resilience.d.ts +0 -34
  207. package/dist/resilience.d.ts.map +0 -1
  208. package/dist/resilience.js +0 -164
  209. package/dist/resilience.js.map +0 -1
  210. package/dist/resource-config.d.ts +0 -22
  211. package/dist/resource-config.d.ts.map +0 -1
  212. package/dist/resource-config.js +0 -210
  213. package/dist/resource-config.js.map +0 -1
  214. package/dist/resource.d.ts +0 -74
  215. package/dist/resource.d.ts.map +0 -1
  216. package/dist/resource.js +0 -61
  217. package/dist/resource.js.map +0 -1
  218. package/dist/result.d.ts +0 -57
  219. package/dist/result.d.ts.map +0 -1
  220. package/dist/result.js +0 -156
  221. package/dist/result.js.map +0 -1
  222. package/dist/run.d.ts +0 -28
  223. package/dist/run.d.ts.map +0 -1
  224. package/dist/run.js +0 -35
  225. package/dist/run.js.map +0 -1
  226. package/dist/serialization.d.ts +0 -27
  227. package/dist/serialization.d.ts.map +0 -1
  228. package/dist/serialization.js +0 -154
  229. package/dist/serialization.js.map +0 -1
  230. package/dist/signal.d.ts +0 -33
  231. package/dist/signal.d.ts.map +0 -1
  232. package/dist/signal.js +0 -17
  233. package/dist/signal.js.map +0 -1
  234. package/dist/store/accessor-protocol.d.ts +0 -51
  235. package/dist/store/accessor-protocol.d.ts.map +0 -1
  236. package/dist/store/accessor-protocol.js +0 -2
  237. package/dist/store/accessor-protocol.js.map +0 -1
  238. package/dist/store/index.d.ts +0 -2
  239. package/dist/store/index.d.ts.map +0 -1
  240. package/dist/store/index.js +0 -2
  241. package/dist/store/index.js.map +0 -1
  242. package/dist/surface-filter.d.ts +0 -13
  243. package/dist/surface-filter.d.ts.map +0 -1
  244. package/dist/surface-filter.js +0 -76
  245. package/dist/surface-filter.js.map +0 -1
  246. package/dist/topo-store.d.ts +0 -62
  247. package/dist/topo-store.d.ts.map +0 -1
  248. package/dist/topo-store.js +0 -319
  249. package/dist/topo-store.js.map +0 -1
  250. package/dist/topo.d.ts +0 -39
  251. package/dist/topo.d.ts.map +0 -1
  252. package/dist/topo.js +0 -241
  253. package/dist/topo.js.map +0 -1
  254. package/dist/trail.d.ts +0 -163
  255. package/dist/trail.d.ts.map +0 -1
  256. package/dist/trail.js +0 -75
  257. package/dist/trail.js.map +0 -1
  258. package/dist/trails/derive-trail.d.ts +0 -66
  259. package/dist/trails/derive-trail.d.ts.map +0 -1
  260. package/dist/trails/derive-trail.js +0 -378
  261. package/dist/trails/derive-trail.js.map +0 -1
  262. package/dist/trails/index.d.ts +0 -5
  263. package/dist/trails/index.d.ts.map +0 -1
  264. package/dist/trails/index.js +0 -3
  265. package/dist/trails/index.js.map +0 -1
  266. package/dist/trails/ingest.d.ts +0 -24
  267. package/dist/trails/ingest.d.ts.map +0 -1
  268. package/dist/trails/ingest.js +0 -63
  269. package/dist/trails/ingest.js.map +0 -1
  270. package/dist/transport-error-map.d.ts +0 -106
  271. package/dist/transport-error-map.d.ts.map +0 -1
  272. package/dist/transport-error-map.js +0 -24
  273. package/dist/transport-error-map.js.map +0 -1
  274. package/dist/type-checks.test-d.d.ts +0 -71
  275. package/dist/type-checks.test-d.d.ts.map +0 -1
  276. package/dist/type-checks.test-d.js +0 -12
  277. package/dist/type-checks.test-d.js.map +0 -1
  278. package/dist/type-utils.d.ts +0 -33
  279. package/dist/type-utils.d.ts.map +0 -1
  280. package/dist/type-utils.js +0 -12
  281. package/dist/type-utils.js.map +0 -1
  282. package/dist/types.d.ts +0 -177
  283. package/dist/types.d.ts.map +0 -1
  284. package/dist/types.js +0 -3
  285. package/dist/types.js.map +0 -1
  286. package/dist/validate-established-topo.d.ts +0 -76
  287. package/dist/validate-established-topo.d.ts.map +0 -1
  288. package/dist/validate-established-topo.js +0 -43
  289. package/dist/validate-established-topo.js.map +0 -1
  290. package/dist/validate-topo.d.ts +0 -24
  291. package/dist/validate-topo.d.ts.map +0 -1
  292. package/dist/validate-topo.js +0 -193
  293. package/dist/validate-topo.js.map +0 -1
  294. package/dist/validation.d.ts +0 -27
  295. package/dist/validation.d.ts.map +0 -1
  296. package/dist/validation.js +0 -165
  297. package/dist/validation.js.map +0 -1
  298. package/dist/workspace.d.ts +0 -25
  299. package/dist/workspace.d.ts.map +0 -1
  300. package/dist/workspace.js +0 -57
  301. package/dist/workspace.js.map +0 -1
  302. package/src/__tests__/blob-ref.test.ts +0 -103
  303. package/src/__tests__/branded.test.ts +0 -148
  304. package/src/__tests__/collections.test.ts +0 -126
  305. package/src/__tests__/context.test.ts +0 -77
  306. package/src/__tests__/contour.test.ts +0 -263
  307. package/src/__tests__/derive-trail.test.ts +0 -668
  308. package/src/__tests__/derive.test.ts +0 -260
  309. package/src/__tests__/errors.test.ts +0 -427
  310. package/src/__tests__/execute.test.ts +0 -1563
  311. package/src/__tests__/fetch.test.ts +0 -217
  312. package/src/__tests__/fire.test.ts +0 -1056
  313. package/src/__tests__/fork-ctx.test.ts +0 -83
  314. package/src/__tests__/guards.test.ts +0 -102
  315. package/src/__tests__/ingest.test.ts +0 -191
  316. package/src/__tests__/layer.test.ts +0 -125
  317. package/src/__tests__/path-security.test.ts +0 -114
  318. package/src/__tests__/patterns.test.ts +0 -273
  319. package/src/__tests__/redaction.test.ts +0 -244
  320. package/src/__tests__/resilience.test.ts +0 -246
  321. package/src/__tests__/resource.test.ts +0 -204
  322. package/src/__tests__/result.test.ts +0 -155
  323. package/src/__tests__/run.test.ts +0 -181
  324. package/src/__tests__/serialization.test.ts +0 -401
  325. package/src/__tests__/service-config.test.ts +0 -224
  326. package/src/__tests__/signal.test.ts +0 -82
  327. package/src/__tests__/surface-filter.test.ts +0 -196
  328. package/src/__tests__/topo-store-read.test.ts +0 -256
  329. package/src/__tests__/topo-store.test.ts +0 -869
  330. package/src/__tests__/topo.test.ts +0 -573
  331. package/src/__tests__/trail-permit.test.ts +0 -60
  332. package/src/__tests__/trail.test.ts +0 -426
  333. package/src/__tests__/trails-db.test.ts +0 -198
  334. package/src/__tests__/transport-error-map.test.ts +0 -82
  335. package/src/__tests__/type-utils.test.ts +0 -118
  336. package/src/__tests__/validate-topo.test.ts +0 -611
  337. package/src/__tests__/validation.test.ts +0 -336
  338. package/src/__tests__/workspace.test.ts +0 -183
  339. package/src/__tests__/zod-wrappers.test.ts +0 -62
  340. package/src/internal/topo-snapshots.ts +0 -517
  341. package/src/internal/topo-store-read.ts +0 -481
  342. package/src/internal/topo-store.ts +0 -1192
  343. package/src/internal/tracing.ts +0 -187
  344. package/src/topo-store.ts +0 -510
  345. package/src/type-checks.test-d.ts +0 -106
  346. package/tsconfig.json +0 -9
  347. package/tsconfig.tests.json +0 -10
  348. package/tsconfig.tsbuildinfo +0 -1
@@ -1,611 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { z } from 'zod';
4
-
5
- import { deriveDraftReport, isDraftId } from '../draft.js';
6
- import { contour } from '../contour.js';
7
- import { validateEstablishedTopo } from '../validate-established-topo.js';
8
- import { resource } from '../resource.js';
9
- import { Result } from '../result.js';
10
- import { topo } from '../topo.js';
11
- import type { TopoIssue } from '../validate-topo.js';
12
- import { validateTopo } from '../validate-topo.js';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Helpers
16
- // ---------------------------------------------------------------------------
17
-
18
- // oxlint-disable-next-line require-await -- satisfies async interface
19
- const noop = async () => Result.ok();
20
-
21
- const mockTrail = (
22
- id: string,
23
- overrides?: {
24
- crosses?: readonly string[];
25
- contours?: readonly ReturnType<typeof contour>[];
26
- examples?: readonly {
27
- name: string;
28
- input: unknown;
29
- expected?: unknown;
30
- error?: string;
31
- }[];
32
- output?: z.ZodType;
33
- resources?: readonly ReturnType<typeof resource>[];
34
- }
35
- ) => ({
36
- blaze: noop,
37
- contours: Object.freeze([...(overrides?.contours ?? [])]),
38
- crosses: Object.freeze([...(overrides?.crosses ?? [])]),
39
- id,
40
- input: z.object({ name: z.string() }),
41
- kind: 'trail' as const,
42
- resources: Object.freeze([...(overrides?.resources ?? [])]),
43
- ...overrides,
44
- });
45
-
46
- const mockResource = (id: string) =>
47
- resource(id, {
48
- create: () => Result.ok({ id }),
49
- });
50
-
51
- const mockEvent = (id: string, from?: readonly string[]) => ({
52
- from,
53
- id,
54
- kind: 'signal' as const,
55
- payload: z.object({ data: z.string() }),
56
- });
57
-
58
- /** Extract issues from a failed validateTopo result. */
59
- const extractIssues = (result: Result<void, Error>): TopoIssue[] => {
60
- if (result.isOk()) {
61
- return [];
62
- }
63
- const ctx = (result.error as { context?: { issues?: TopoIssue[] } }).context;
64
- return (ctx?.issues ?? []) as TopoIssue[];
65
- };
66
-
67
- // ---------------------------------------------------------------------------
68
- // Tests
69
- // ---------------------------------------------------------------------------
70
-
71
- describe('validateTopo', () => {
72
- test('valid topo passes', () => {
73
- const app = topo('app', {
74
- add: mockTrail('entity.add'),
75
- onboard: mockTrail('entity.onboard', {
76
- crosses: ['entity.add'],
77
- }),
78
- updated: mockEvent('entity.updated', ['entity.add']),
79
- });
80
-
81
- const result = validateTopo(app);
82
- expect(result.isOk()).toBe(true);
83
- });
84
-
85
- describe('trail crossing', () => {
86
- test('draft crossings are allowed in the authored graph', () => {
87
- const app = topo('app', {
88
- exportTrail: mockTrail('entity.export', {
89
- crosses: ['_draft.entity.prepare'],
90
- }),
91
- });
92
-
93
- const result = validateTopo(app);
94
- expect(result.isOk()).toBe(true);
95
- });
96
-
97
- test('trail crossing a non-existent trail fails', () => {
98
- const app = topo('app', {
99
- onboard: mockTrail('entity.onboard', {
100
- crosses: ['entity.missing'],
101
- }),
102
- });
103
-
104
- const result = validateTopo(app);
105
- expect(result.isErr()).toBe(true);
106
-
107
- const issues = extractIssues(result);
108
- expect(issues).toHaveLength(1);
109
- expect(issues[0]?.rule).toBe('cross-exists');
110
- expect(issues[0]?.message).toContain('entity.missing');
111
- });
112
-
113
- test('trail crossing itself fails', () => {
114
- const app = topo('app', {
115
- loop: mockTrail('entity.loop', { crosses: ['entity.loop'] }),
116
- });
117
-
118
- const result = validateTopo(app);
119
- expect(result.isErr()).toBe(true);
120
-
121
- const issues = extractIssues(result);
122
- expect(issues.some((i) => i.rule === 'no-self-cross')).toBe(true);
123
- });
124
-
125
- test('two-node cycle (a→b→a) is detected', () => {
126
- const app = topo('app', {
127
- a: mockTrail('a', { crosses: ['b'] }),
128
- b: mockTrail('b', { crosses: ['a'] }),
129
- });
130
-
131
- const result = validateTopo(app);
132
- expect(result.isErr()).toBe(true);
133
-
134
- const issues = extractIssues(result);
135
- const cycleIssues = issues.filter((i) => i.rule === 'cross-cycle');
136
- expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
137
- expect(cycleIssues[0]?.message).toContain('Cycle detected');
138
- });
139
-
140
- test('three-node cycle (a→b→c→a) is detected', () => {
141
- const app = topo('app', {
142
- a: mockTrail('a', { crosses: ['b'] }),
143
- b: mockTrail('b', { crosses: ['c'] }),
144
- c: mockTrail('c', { crosses: ['a'] }),
145
- });
146
-
147
- const result = validateTopo(app);
148
- expect(result.isErr()).toBe(true);
149
-
150
- const issues = extractIssues(result);
151
- const cycleIssues = issues.filter((i) => i.rule === 'cross-cycle');
152
- expect(cycleIssues.length).toBeGreaterThanOrEqual(1);
153
- expect(cycleIssues[0]?.message).toContain('Cycle detected');
154
- });
155
-
156
- test('valid DAG with shared targets is not flagged', () => {
157
- const app = topo('app', {
158
- a: mockTrail('a', { crosses: ['c'] }),
159
- b: mockTrail('b', { crosses: ['c'] }),
160
- c: mockTrail('c'),
161
- });
162
-
163
- const result = validateTopo(app);
164
- expect(result.isOk()).toBe(true);
165
- });
166
- });
167
-
168
- describe('resource declarations', () => {
169
- test('draft resource references are allowed in the authored graph', () => {
170
- const db = mockResource('_draft.db.main');
171
- const app = topo('app', {
172
- show: mockTrail('entity.show', {
173
- resources: [db],
174
- }),
175
- });
176
-
177
- const result = validateTopo(app);
178
- expect(result.isOk()).toBe(true);
179
- });
180
-
181
- test('trail declaring a registered resource passes', () => {
182
- const db = mockResource('db.main');
183
- const app = topo('app', {
184
- db,
185
- show: mockTrail('entity.show', {
186
- resources: [db],
187
- }),
188
- });
189
-
190
- const result = validateTopo(app);
191
- expect(result.isOk()).toBe(true);
192
- });
193
-
194
- test('trail declaring a missing resource fails', () => {
195
- const db = mockResource('db.main');
196
- const app = topo('app', {
197
- show: mockTrail('entity.show', {
198
- resources: [db],
199
- }),
200
- });
201
-
202
- const result = validateTopo(app);
203
- expect(result.isErr()).toBe(true);
204
-
205
- const issues = extractIssues(result);
206
- expect(issues).toHaveLength(1);
207
- expect(issues[0]?.rule).toBe('resource-exists');
208
- expect(issues[0]?.message).toContain('db.main');
209
- });
210
- });
211
-
212
- describe('example validation', () => {
213
- test('example with invalid input fails', () => {
214
- const app = topo('app', {
215
- show: mockTrail('entity.show', {
216
- examples: [{ input: { name: 123 }, name: 'Bad input' }],
217
- }),
218
- });
219
-
220
- const result = validateTopo(app);
221
- expect(result.isErr()).toBe(true);
222
-
223
- const issues = extractIssues(result);
224
- expect(issues).toHaveLength(1);
225
- expect(issues[0]?.rule).toBe('example-input-valid');
226
- expect(issues[0]?.message).toContain('Bad input');
227
- });
228
-
229
- test('example with expected output but no output schema warns', () => {
230
- const app = topo('app', {
231
- show: mockTrail('entity.show', {
232
- examples: [
233
- {
234
- expected: { result: 'ok' },
235
- input: { name: 'test' },
236
- name: 'Has expected',
237
- },
238
- ],
239
- }),
240
- });
241
-
242
- const result = validateTopo(app);
243
- expect(result.isErr()).toBe(true);
244
-
245
- const issues = extractIssues(result);
246
- expect(issues).toHaveLength(1);
247
- expect(issues[0]?.rule).toBe('output-schema-present');
248
- });
249
-
250
- test('ValidationError example with invalid input is allowed', () => {
251
- const app = topo('app', {
252
- show: mockTrail('entity.show', {
253
- examples: [
254
- {
255
- error: 'ValidationError',
256
- input: { name: 123 },
257
- name: 'Error case',
258
- },
259
- ],
260
- }),
261
- });
262
-
263
- const result = validateTopo(app);
264
- expect(result.isOk()).toBe(true);
265
- });
266
-
267
- test('NotFoundError example with invalid input fails', () => {
268
- const app = topo('app', {
269
- show: mockTrail('entity.show', {
270
- examples: [
271
- {
272
- error: 'NotFoundError',
273
- input: { name: 123 },
274
- name: 'Not found case',
275
- },
276
- ],
277
- }),
278
- });
279
-
280
- const result = validateTopo(app);
281
- expect(result.isErr()).toBe(true);
282
-
283
- const issues = extractIssues(result);
284
- expect(issues).toHaveLength(1);
285
- expect(issues[0]?.rule).toBe('example-input-valid');
286
- });
287
-
288
- test('NotFoundError example with valid input passes', () => {
289
- const app = topo('app', {
290
- show: mockTrail('entity.show', {
291
- examples: [
292
- {
293
- error: 'NotFoundError',
294
- input: { name: 'test' },
295
- name: 'Not found case',
296
- },
297
- ],
298
- }),
299
- });
300
-
301
- const result = validateTopo(app);
302
- expect(result.isOk()).toBe(true);
303
- });
304
- });
305
-
306
- describe('contour references', () => {
307
- test('contour referencing a registered contour passes', () => {
308
- const user = contour(
309
- 'user',
310
- { id: z.string().uuid(), name: z.string() },
311
- { identity: 'id' }
312
- );
313
- const post = contour(
314
- 'post',
315
- { authorId: user.id(), id: z.string().uuid() },
316
- { identity: 'id' }
317
- );
318
-
319
- const app = topo('app', { post, user });
320
-
321
- const result = validateTopo(app);
322
- expect(result.isOk()).toBe(true);
323
- });
324
-
325
- test('contour referencing a missing contour fails', () => {
326
- const user = contour(
327
- 'user',
328
- { id: z.string().uuid(), name: z.string() },
329
- { identity: 'id' }
330
- );
331
- const post = contour(
332
- 'post',
333
- { authorId: user.id(), id: z.string().uuid() },
334
- { identity: 'id' }
335
- );
336
-
337
- // Only register post, not user — the reference is dangling
338
- const app = topo('app', { post });
339
-
340
- const result = validateTopo(app);
341
- expect(result.isErr()).toBe(true);
342
-
343
- const issues = extractIssues(result);
344
- expect(issues).toHaveLength(1);
345
- expect(issues[0]?.rule).toBe('contour-reference-exists');
346
- expect(issues[0]?.message).toContain('user');
347
- });
348
-
349
- test('draft contour references are allowed', () => {
350
- const draftUser = contour(
351
- '_draft.user',
352
- { id: z.string().uuid() },
353
- { identity: 'id' }
354
- );
355
- const post = contour(
356
- 'post',
357
- { authorId: draftUser.id(), id: z.string().uuid() },
358
- { identity: 'id' }
359
- );
360
-
361
- const app = topo('app', { post });
362
-
363
- const result = validateTopo(app);
364
- expect(result.isOk()).toBe(true);
365
- });
366
- });
367
-
368
- describe('event origins', () => {
369
- test('event with non-existent origin fails', () => {
370
- const app = topo('app', {
371
- updated: mockEvent('entity.updated', ['entity.ghost']),
372
- });
373
-
374
- const result = validateTopo(app);
375
- expect(result.isErr()).toBe(true);
376
-
377
- const issues = extractIssues(result);
378
- expect(issues).toHaveLength(1);
379
- expect(issues[0]?.rule).toBe('signal-origin-exists');
380
- expect(issues[0]?.message).toContain('entity.ghost');
381
- });
382
-
383
- test('event without origins is accepted', () => {
384
- const app = topo('app', {
385
- updated: mockEvent('entity.updated'),
386
- });
387
-
388
- const result = validateTopo(app);
389
- expect(result.isOk()).toBe(true);
390
- });
391
- });
392
-
393
- test('collects multiple issues', () => {
394
- const db = mockResource('db.main');
395
- const app = topo('app', {
396
- broken: mockTrail('entity.broken', { crosses: ['entity.missing'] }),
397
- missingResource: mockTrail('entity.missing-resource', {
398
- resources: [db],
399
- }),
400
- show: mockTrail('entity.show', {
401
- examples: [{ input: { name: 123 }, name: 'Bad' }],
402
- }),
403
- updated: mockEvent('entity.updated', ['entity.ghost']),
404
- });
405
-
406
- const result = validateTopo(app);
407
- expect(result.isErr()).toBe(true);
408
-
409
- const issues = extractIssues(result);
410
- expect(issues).toHaveLength(4);
411
- });
412
-
413
- describe('signal origins', () => {
414
- test('draft signal origins are allowed in the authored graph', () => {
415
- const app = topo('app', {
416
- updated: mockEvent('entity.updated', ['_draft.entity.prepare']),
417
- });
418
-
419
- const result = validateTopo(app);
420
- expect(result.isOk()).toBe(true);
421
- });
422
- });
423
- });
424
-
425
- describe('draft state analysis', () => {
426
- test('detects declared draft ids', () => {
427
- const app = topo('app', {
428
- draftTrail: mockTrail('_draft.entity.prepare'),
429
- });
430
-
431
- const analysis = deriveDraftReport(app);
432
-
433
- expect(analysis.declaredDraftIds.has('_draft.entity.prepare')).toBe(true);
434
- expect(analysis.contaminatedIds.has('_draft.entity.prepare')).toBe(true);
435
- expect(analysis.findings.map((finding) => finding.rule)).toContain(
436
- 'draft-id'
437
- );
438
- });
439
-
440
- test('propagates contamination through dependencies', () => {
441
- const app = topo('app', {
442
- exportTrail: mockTrail('entity.export', {
443
- crosses: ['entity.prepare'],
444
- }),
445
- prepareTrail: mockTrail('entity.prepare', {
446
- crosses: ['_draft.entity.store'],
447
- }),
448
- });
449
-
450
- const analysis = deriveDraftReport(app);
451
- const exportFinding = analysis.findings.find(
452
- (finding) => finding.id === 'entity.export'
453
- );
454
-
455
- expect(analysis.contaminatedIds.has('entity.prepare')).toBe(true);
456
- expect(analysis.contaminatedIds.has('entity.export')).toBe(true);
457
- expect(exportFinding).toBeDefined();
458
- expect(exportFinding?.rule).toBe('draft-contamination');
459
- });
460
-
461
- test('detects draft contour declarations and schema-reference contamination', () => {
462
- const draftUser = contour(
463
- '_draft.user',
464
- {
465
- id: z.string().uuid(),
466
- },
467
- { identity: 'id' }
468
- );
469
- const gist = contour(
470
- 'gist',
471
- {
472
- id: z.string().uuid(),
473
- ownerId: draftUser.id(),
474
- },
475
- { identity: 'id' }
476
- );
477
-
478
- const app = topo('app', {
479
- draftUser,
480
- gist,
481
- });
482
-
483
- const analysis = deriveDraftReport(app);
484
- expect(analysis.declaredDraftIds.has('_draft.user')).toBe(true);
485
- expect(analysis.contaminatedIds.has('gist')).toBe(true);
486
- expect(analysis.dependencies).toContainEqual({
487
- fromId: 'gist',
488
- kind: 'schema-reference',
489
- toId: '_draft.user',
490
- });
491
- });
492
-
493
- test('propagates contamination through trail contour dependencies', () => {
494
- const draftUser = contour(
495
- '_draft.user',
496
- {
497
- id: z.string().uuid(),
498
- },
499
- { identity: 'id' }
500
- );
501
-
502
- const app = topo('app', {
503
- createGist: mockTrail('gist.create', {
504
- contours: [draftUser],
505
- }),
506
- draftUser,
507
- });
508
-
509
- const analysis = deriveDraftReport(app);
510
- const finding = analysis.findings.find(
511
- (entry) => entry.id === 'gist.create'
512
- );
513
-
514
- expect(analysis.contaminatedIds.has('gist.create')).toBe(true);
515
- expect(finding?.via).toBe('contour');
516
- });
517
- });
518
-
519
- describe('validateEstablishedTopo', () => {
520
- test('passes for an established topo', () => {
521
- const app = topo('app', {
522
- show: mockTrail('entity.show'),
523
- });
524
-
525
- const result = validateEstablishedTopo(app);
526
- expect(result.isOk()).toBe(true);
527
- });
528
-
529
- test('fails when draft declarations remain', () => {
530
- const app = topo('app', {
531
- draftTrail: mockTrail('_draft.entity.prepare'),
532
- });
533
-
534
- const result = validateEstablishedTopo(app);
535
- expect(result.isErr()).toBe(true);
536
-
537
- const issues = extractIssues(result);
538
- expect(issues).toHaveLength(1);
539
- expect(issues[0]?.rule).toBe('draft-id');
540
- });
541
-
542
- test('fails when established trails depend on draft state', () => {
543
- const app = topo('app', {
544
- exportTrail: mockTrail('entity.export', {
545
- crosses: ['_draft.entity.prepare'],
546
- }),
547
- });
548
-
549
- const result = validateEstablishedTopo(app);
550
- expect(result.isErr()).toBe(true);
551
-
552
- const issues = extractIssues(result);
553
- expect(issues).toHaveLength(1);
554
- expect(issues[0]?.rule).toBe('draft-contamination');
555
- expect(issues[0]?.message).toContain('entity.export');
556
- });
557
-
558
- test('fails when authored structural validation still fails', () => {
559
- const app = topo('app', {
560
- exportTrail: mockTrail('entity.export', {
561
- crosses: ['entity.missing'],
562
- }),
563
- });
564
-
565
- const result = validateEstablishedTopo(app);
566
- expect(result.isErr()).toBe(true);
567
-
568
- const issues = extractIssues(result);
569
- expect(issues).toHaveLength(1);
570
- expect(issues[0]?.rule).toBe('cross-exists');
571
- });
572
-
573
- test('fails when resource declarations are not established in the topo', () => {
574
- const app = topo('app', {
575
- show: mockTrail('entity.show', {
576
- resources: [mockResource('db.main')],
577
- }),
578
- });
579
-
580
- const result = validateEstablishedTopo(app);
581
- expect(result.isErr()).toBe(true);
582
-
583
- const issues = extractIssues(result);
584
- expect(issues).toHaveLength(1);
585
- expect(issues[0]?.rule).toBe('resource-exists');
586
- });
587
-
588
- test('allows authoring-only example issues outside projection checks', () => {
589
- const app = topo('app', {
590
- show: mockTrail('entity.show', {
591
- examples: [
592
- {
593
- expected: { ok: true },
594
- input: { name: 'test' },
595
- name: 'Has expected',
596
- },
597
- ],
598
- }),
599
- });
600
-
601
- const result = validateEstablishedTopo(app);
602
- expect(result.isOk()).toBe(true);
603
- });
604
- });
605
-
606
- describe('isDraftId', () => {
607
- test('recognizes the reserved draft prefix', () => {
608
- expect(isDraftId('_draft.entity.show')).toBe(true);
609
- expect(isDraftId('entity.show')).toBe(false);
610
- });
611
- });