@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +19 -14
- package/dist/branded.d.ts +1 -1
- package/dist/branded.d.ts.map +1 -1
- package/dist/branded.js +1 -1
- package/dist/branded.js.map +1 -1
- package/dist/context.d.ts +13 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/contour.d.ts +81 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +176 -0
- package/dist/contour.js.map +1 -0
- package/dist/cross-schema.d.ts +18 -0
- package/dist/cross-schema.d.ts.map +1 -0
- package/dist/cross-schema.js +29 -0
- package/dist/cross-schema.js.map +1 -0
- package/dist/derive.d.ts +6 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +29 -5
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +157 -0
- package/dist/draft.js.map +1 -0
- package/dist/errors.d.ts +62 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +26 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +441 -20
- package/dist/execute.js.map +1 -1
- package/dist/fire.d.ts +43 -0
- package/dist/fire.d.ts.map +1 -0
- package/dist/fire.js +185 -0
- package/dist/fire.js.map +1 -0
- package/dist/index.d.ts +33 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cross-batch.d.ts +44 -0
- package/dist/internal/cross-batch.d.ts.map +1 -0
- package/dist/internal/cross-batch.js +58 -0
- package/dist/internal/cross-batch.js.map +1 -0
- package/dist/internal/fork-ctx.d.ts +41 -0
- package/dist/internal/fork-ctx.d.ts.map +1 -0
- package/dist/internal/fork-ctx.js +47 -0
- package/dist/internal/fork-ctx.js.map +1 -0
- package/dist/internal/signal-ref.d.ts +15 -0
- package/dist/internal/signal-ref.d.ts.map +1 -0
- package/dist/internal/signal-ref.js +41 -0
- package/dist/internal/signal-ref.js.map +1 -0
- package/dist/internal/topo-saves.d.ts +53 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +449 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-snapshots.d.ts +57 -0
- package/dist/internal/topo-snapshots.d.ts.map +1 -0
- package/dist/internal/topo-snapshots.js +372 -0
- package/dist/internal/topo-snapshots.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +70 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +221 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +26 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +621 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/tracing.d.ts +98 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +102 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/internal/zod-wrappers.d.ts +40 -0
- package/dist/internal/zod-wrappers.d.ts.map +1 -0
- package/dist/internal/zod-wrappers.js +59 -0
- package/dist/internal/zod-wrappers.js.map +1 -0
- package/dist/layer.d.ts +22 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +20 -3
- package/dist/layer.js.map +1 -1
- package/dist/path-security.d.ts +1 -1
- package/dist/path-security.d.ts.map +1 -1
- package/dist/path-security.js +1 -1
- package/dist/path-security.js.map +1 -1
- package/dist/resilience.d.ts +1 -1
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +3 -3
- package/dist/resilience.js.map +1 -1
- package/dist/resource-config.d.ts +22 -0
- package/dist/resource-config.d.ts.map +1 -0
- package/dist/resource-config.js +210 -0
- package/dist/resource-config.js.map +1 -0
- package/dist/resource.d.ts +74 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +61 -0
- package/dist/resource.js.map +1 -0
- package/dist/run.d.ts +4 -3
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +5 -4
- package/dist/run.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/store/accessor-protocol.d.ts +51 -0
- package/dist/store/accessor-protocol.d.ts.map +1 -0
- package/dist/store/accessor-protocol.js +2 -0
- package/dist/store/accessor-protocol.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/surface-filter.d.ts +13 -0
- package/dist/surface-filter.d.ts.map +1 -0
- package/dist/surface-filter.js +76 -0
- package/dist/surface-filter.js.map +1 -0
- package/dist/topo-store.d.ts +62 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +319 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/topo.d.ts +23 -9
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +175 -46
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +86 -19
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +59 -4
- package/dist/trail.js.map +1 -1
- package/dist/trails/derive-trail.d.ts +66 -0
- package/dist/trails/derive-trail.d.ts.map +1 -0
- package/dist/trails/derive-trail.js +378 -0
- package/dist/trails/derive-trail.js.map +1 -0
- package/dist/trails/index.d.ts +5 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +3 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/ingest.d.ts +24 -0
- package/dist/trails/ingest.d.ts.map +1 -0
- package/dist/trails/ingest.js +63 -0
- package/dist/trails/ingest.js.map +1 -0
- package/dist/transport-error-map.d.ts +106 -0
- package/dist/transport-error-map.d.ts.map +1 -0
- package/dist/transport-error-map.js +24 -0
- package/dist/transport-error-map.js.map +1 -0
- package/dist/type-checks.test-d.d.ts +71 -0
- package/dist/type-checks.test-d.d.ts.map +1 -0
- package/dist/type-checks.test-d.js +12 -0
- package/dist/type-checks.test-d.js.map +1 -0
- package/dist/type-utils.d.ts +11 -2
- package/dist/type-utils.d.ts.map +1 -1
- package/dist/type-utils.js.map +1 -1
- package/dist/types.d.ts +113 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +27 -8
- package/dist/validate-topo.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +10 -1
- package/src/__tests__/branded.test.ts +5 -5
- package/src/__tests__/context.test.ts +3 -3
- package/src/__tests__/contour.test.ts +263 -0
- package/src/__tests__/derive-trail.test.ts +668 -0
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/errors.test.ts +118 -0
- package/src/__tests__/execute.test.ts +1179 -141
- package/src/__tests__/fire.test.ts +1056 -0
- package/src/__tests__/fork-ctx.test.ts +83 -0
- package/src/__tests__/ingest.test.ts +191 -0
- package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
- package/src/__tests__/path-security.test.ts +7 -7
- package/src/__tests__/resilience.test.ts +8 -8
- package/src/__tests__/resource.test.ts +204 -0
- package/src/__tests__/run.test.ts +13 -13
- package/src/__tests__/service-config.test.ts +28 -32
- package/src/__tests__/surface-filter.test.ts +196 -0
- package/src/__tests__/topo-store-read.test.ts +256 -0
- package/src/__tests__/topo-store.test.ts +869 -0
- package/src/__tests__/topo.test.ts +350 -54
- package/src/__tests__/trail.test.ts +189 -24
- package/src/__tests__/trails-db.test.ts +198 -0
- package/src/__tests__/transport-error-map.test.ts +82 -0
- package/src/__tests__/type-utils.test.ts +29 -1
- package/src/__tests__/validate-topo.test.ts +305 -16
- package/src/__tests__/workspace.test.ts +6 -6
- package/src/__tests__/zod-wrappers.test.ts +62 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +24 -4
- package/src/contour.ts +344 -0
- package/src/cross-schema.ts +36 -0
- package/src/derive.ts +39 -8
- package/src/draft.ts +338 -0
- package/src/errors.ts +62 -18
- package/src/execute.ts +840 -29
- package/src/fire.ts +295 -0
- package/src/index.ts +142 -26
- package/src/internal/cross-batch.ts +77 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/internal/signal-ref.ts +87 -0
- package/src/internal/topo-snapshots.ts +517 -0
- package/src/internal/topo-store-read.ts +481 -0
- package/src/internal/topo-store.ts +1192 -0
- package/src/internal/tracing.ts +187 -0
- package/src/internal/trails-db.ts +187 -0
- package/src/internal/zod-wrappers.ts +78 -0
- package/src/layer.ts +50 -0
- package/src/path-security.ts +1 -1
- package/src/resilience.ts +3 -3
- package/src/{provision-config.ts → resource-config.ts} +109 -123
- package/src/resource.ts +155 -0
- package/src/run.ts +5 -4
- package/src/serialization.ts +2 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/surface-filter.ts +182 -0
- package/src/topo-store.ts +510 -0
- package/src/topo.ts +328 -57
- package/src/trail.ts +199 -31
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +146 -0
- package/src/transport-error-map.ts +56 -0
- package/src/type-checks.test-d.ts +106 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +148 -11
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +37 -9
- package/src/workspace.ts +1 -1
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/src/__tests__/service.test.ts +0 -197
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/provision.ts +0 -148
|
@@ -2,7 +2,10 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
import {
|
|
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';
|
|
6
9
|
import { Result } from '../result.js';
|
|
7
10
|
import { topo } from '../topo.js';
|
|
8
11
|
import type { TopoIssue } from '../validate-topo.js';
|
|
@@ -19,6 +22,7 @@ const mockTrail = (
|
|
|
19
22
|
id: string,
|
|
20
23
|
overrides?: {
|
|
21
24
|
crosses?: readonly string[];
|
|
25
|
+
contours?: readonly ReturnType<typeof contour>[];
|
|
22
26
|
examples?: readonly {
|
|
23
27
|
name: string;
|
|
24
28
|
input: unknown;
|
|
@@ -26,20 +30,21 @@ const mockTrail = (
|
|
|
26
30
|
error?: string;
|
|
27
31
|
}[];
|
|
28
32
|
output?: z.ZodType;
|
|
29
|
-
|
|
33
|
+
resources?: readonly ReturnType<typeof resource>[];
|
|
30
34
|
}
|
|
31
35
|
) => ({
|
|
32
36
|
blaze: noop,
|
|
37
|
+
contours: Object.freeze([...(overrides?.contours ?? [])]),
|
|
33
38
|
crosses: Object.freeze([...(overrides?.crosses ?? [])]),
|
|
34
39
|
id,
|
|
35
40
|
input: z.object({ name: z.string() }),
|
|
36
41
|
kind: 'trail' as const,
|
|
37
|
-
|
|
42
|
+
resources: Object.freeze([...(overrides?.resources ?? [])]),
|
|
38
43
|
...overrides,
|
|
39
44
|
});
|
|
40
45
|
|
|
41
|
-
const
|
|
42
|
-
|
|
46
|
+
const mockResource = (id: string) =>
|
|
47
|
+
resource(id, {
|
|
43
48
|
create: () => Result.ok({ id }),
|
|
44
49
|
});
|
|
45
50
|
|
|
@@ -78,6 +83,17 @@ describe('validateTopo', () => {
|
|
|
78
83
|
});
|
|
79
84
|
|
|
80
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
|
+
|
|
81
97
|
test('trail crossing a non-existent trail fails', () => {
|
|
82
98
|
const app = topo('app', {
|
|
83
99
|
onboard: mockTrail('entity.onboard', {
|
|
@@ -149,13 +165,25 @@ describe('validateTopo', () => {
|
|
|
149
165
|
});
|
|
150
166
|
});
|
|
151
167
|
|
|
152
|
-
describe('
|
|
153
|
-
test('
|
|
154
|
-
const db =
|
|
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');
|
|
155
183
|
const app = topo('app', {
|
|
156
184
|
db,
|
|
157
185
|
show: mockTrail('entity.show', {
|
|
158
|
-
|
|
186
|
+
resources: [db],
|
|
159
187
|
}),
|
|
160
188
|
});
|
|
161
189
|
|
|
@@ -163,11 +191,11 @@ describe('validateTopo', () => {
|
|
|
163
191
|
expect(result.isOk()).toBe(true);
|
|
164
192
|
});
|
|
165
193
|
|
|
166
|
-
test('trail declaring a missing
|
|
167
|
-
const db =
|
|
194
|
+
test('trail declaring a missing resource fails', () => {
|
|
195
|
+
const db = mockResource('db.main');
|
|
168
196
|
const app = topo('app', {
|
|
169
197
|
show: mockTrail('entity.show', {
|
|
170
|
-
|
|
198
|
+
resources: [db],
|
|
171
199
|
}),
|
|
172
200
|
});
|
|
173
201
|
|
|
@@ -176,7 +204,7 @@ describe('validateTopo', () => {
|
|
|
176
204
|
|
|
177
205
|
const issues = extractIssues(result);
|
|
178
206
|
expect(issues).toHaveLength(1);
|
|
179
|
-
expect(issues[0]?.rule).toBe('
|
|
207
|
+
expect(issues[0]?.rule).toBe('resource-exists');
|
|
180
208
|
expect(issues[0]?.message).toContain('db.main');
|
|
181
209
|
});
|
|
182
210
|
});
|
|
@@ -275,6 +303,68 @@ describe('validateTopo', () => {
|
|
|
275
303
|
});
|
|
276
304
|
});
|
|
277
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
|
+
|
|
278
368
|
describe('event origins', () => {
|
|
279
369
|
test('event with non-existent origin fails', () => {
|
|
280
370
|
const app = topo('app', {
|
|
@@ -301,11 +391,11 @@ describe('validateTopo', () => {
|
|
|
301
391
|
});
|
|
302
392
|
|
|
303
393
|
test('collects multiple issues', () => {
|
|
304
|
-
const db =
|
|
394
|
+
const db = mockResource('db.main');
|
|
305
395
|
const app = topo('app', {
|
|
306
396
|
broken: mockTrail('entity.broken', { crosses: ['entity.missing'] }),
|
|
307
|
-
|
|
308
|
-
|
|
397
|
+
missingResource: mockTrail('entity.missing-resource', {
|
|
398
|
+
resources: [db],
|
|
309
399
|
}),
|
|
310
400
|
show: mockTrail('entity.show', {
|
|
311
401
|
examples: [{ input: { name: 123 }, name: 'Bad' }],
|
|
@@ -319,4 +409,203 @@ describe('validateTopo', () => {
|
|
|
319
409
|
const issues = extractIssues(result);
|
|
320
410
|
expect(issues).toHaveLength(4);
|
|
321
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
|
+
});
|
|
322
611
|
});
|
|
@@ -7,7 +7,7 @@ import { NotFoundError } from '../errors.js';
|
|
|
7
7
|
import {
|
|
8
8
|
findWorkspaceRoot,
|
|
9
9
|
isInsideWorkspace,
|
|
10
|
-
|
|
10
|
+
deriveRelativePath,
|
|
11
11
|
} from '../workspace.js';
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
@@ -162,22 +162,22 @@ describe('isInsideWorkspace', () => {
|
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
// ---------------------------------------------------------------------------
|
|
165
|
-
//
|
|
165
|
+
// deriveRelativePath
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
167
|
|
|
168
|
-
describe('
|
|
168
|
+
describe('deriveRelativePath', () => {
|
|
169
169
|
test('returns relative path from workspace root', () => {
|
|
170
170
|
expect(
|
|
171
|
-
|
|
171
|
+
deriveRelativePath('/project/packages/core/src/index.ts', '/project')
|
|
172
172
|
).toBe('packages/core/src/index.ts');
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
test('returns .. segments for paths outside workspace', () => {
|
|
176
|
-
const rel =
|
|
176
|
+
const rel = deriveRelativePath('/other/file.ts', '/project');
|
|
177
177
|
expect(rel.startsWith('..')).toBe(true);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
test('returns empty string for the root itself', () => {
|
|
181
|
-
expect(
|
|
181
|
+
expect(deriveRelativePath('/project', '/project')).toBe('');
|
|
182
182
|
});
|
|
183
183
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
stripDefaultsFromShape,
|
|
6
|
+
stripDefaultWrappers,
|
|
7
|
+
} from '../internal/zod-wrappers.js';
|
|
8
|
+
|
|
9
|
+
describe('stripDefaultWrappers', () => {
|
|
10
|
+
test('drops plain default wrappers', () => {
|
|
11
|
+
const withDefault = z.string().default('x');
|
|
12
|
+
const stripped = stripDefaultWrappers(withDefault);
|
|
13
|
+
|
|
14
|
+
expect(stripped.def.type).toBe('string');
|
|
15
|
+
// Feeding an undefined through the stripped schema no longer
|
|
16
|
+
// materializes the default.
|
|
17
|
+
expect(stripped.safeParse().success).toBe(false);
|
|
18
|
+
expect(stripped.safeParse('value').success).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('drops optional + default layers together', () => {
|
|
22
|
+
const schema = z.string().optional().default('x');
|
|
23
|
+
const stripped = stripDefaultWrappers(schema);
|
|
24
|
+
|
|
25
|
+
// Every wrapper around the underlying string is gone — optional is
|
|
26
|
+
// dropped on purpose because downstream `.partial()` calls re-add it.
|
|
27
|
+
expect(stripped.def.type).toBe('string');
|
|
28
|
+
expect(stripped.safeParse().success).toBe(false);
|
|
29
|
+
expect(stripped.safeParse('value').success).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('preserves nullable constraints through the strip', () => {
|
|
33
|
+
const schema = z.string().nullable().default('x');
|
|
34
|
+
const stripped = stripDefaultWrappers(schema);
|
|
35
|
+
|
|
36
|
+
// Default is gone; null is still a legal input.
|
|
37
|
+
expect(stripped.def.type).toBe('nullable');
|
|
38
|
+
expect(stripped.safeParse(null).success).toBe(true);
|
|
39
|
+
expect(stripped.safeParse('value').success).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('is load-bearing for .partial() on both call sites', () => {
|
|
43
|
+
const shape = {
|
|
44
|
+
author: z.string().optional().default('anon'),
|
|
45
|
+
title: z.string().default('untitled'),
|
|
46
|
+
};
|
|
47
|
+
const schema = z.object(shape);
|
|
48
|
+
|
|
49
|
+
// Both the store `deriveUpdateSchema` helper and the core derive-trail
|
|
50
|
+
// `toPartialSchema` helper compose the same pattern: extend the schema
|
|
51
|
+
// with the stripped shape, then call `.partial()`. Both call sites
|
|
52
|
+
// must produce a schema that accepts an empty object without
|
|
53
|
+
// re-materializing defaults.
|
|
54
|
+
const stripped = stripDefaultsFromShape(schema);
|
|
55
|
+
const partial = schema.extend(stripped).partial();
|
|
56
|
+
|
|
57
|
+
const parsed = partial.parse({});
|
|
58
|
+
expect(parsed).toEqual({});
|
|
59
|
+
expect('author' in parsed).toBe(false);
|
|
60
|
+
expect('title' in parsed).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/branded.ts
CHANGED
|
@@ -120,7 +120,7 @@ export const shortId = (length = 8): string => {
|
|
|
120
120
|
* Produce a deterministic hex hash from an input string.
|
|
121
121
|
* Uses a simple FNV-1a 32-bit hash — good enough for non-cryptographic IDs.
|
|
122
122
|
*/
|
|
123
|
-
export const
|
|
123
|
+
export const deriveIdHash = (input: string): string => {
|
|
124
124
|
// FNV offset basis
|
|
125
125
|
let hash = 2_166_136_261;
|
|
126
126
|
for (let i = 0; i < input.length; i += 1) {
|
package/src/context.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { TrailContext, TrailContextInit } from './types.js';
|
|
1
|
+
import { createResourceLookup } from './resource.js';
|
|
2
|
+
import type { TrailContext, TrailContextInit, TraceFn } from './types.js';
|
|
3
3
|
|
|
4
4
|
type MutableTrailContext = {
|
|
5
5
|
-readonly [K in keyof TrailContext]: TrailContext[K];
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Default passthrough `trace` used when a context is built outside
|
|
10
|
+
* `executeTrail`. `executeTrail` replaces this with a real sink-writing
|
|
11
|
+
* implementation. The passthrough runs `fn` without recording anything so
|
|
12
|
+
* direct `createTrailContext()` callers (tests, ad-hoc compositions) don't
|
|
13
|
+
* crash when invoking `ctx.trace(...)`.
|
|
14
|
+
*
|
|
15
|
+
* Declared `async` so both synchronous throws and async rejections from `fn`
|
|
16
|
+
* propagate as a rejected promise to the caller — matching the real
|
|
17
|
+
* sink-writing implementation's error semantics.
|
|
18
|
+
*/
|
|
19
|
+
export const passthroughTrace: TraceFn = async <T>(
|
|
20
|
+
_label: string,
|
|
21
|
+
fn: () => T | Promise<T>
|
|
22
|
+
): Promise<T> => await fn();
|
|
23
|
+
|
|
8
24
|
/**
|
|
9
25
|
* Create a TrailContext with sensible defaults.
|
|
10
26
|
*
|
|
@@ -20,9 +36,13 @@ export const createTrailContext = (
|
|
|
20
36
|
cwd: process.cwd(),
|
|
21
37
|
env: process.env as Record<string, string | undefined>,
|
|
22
38
|
requestId: Bun.randomUUIDv7(),
|
|
39
|
+
trace: passthroughTrace,
|
|
23
40
|
...overrides,
|
|
24
41
|
} as MutableTrailContext;
|
|
25
|
-
const lookup = overrides?.
|
|
26
|
-
ctx.
|
|
42
|
+
const lookup = overrides?.resource ?? createResourceLookup(() => ctx);
|
|
43
|
+
ctx.resource = lookup;
|
|
44
|
+
if (ctx.trace === undefined) {
|
|
45
|
+
ctx.trace = passthroughTrace;
|
|
46
|
+
}
|
|
27
47
|
return ctx;
|
|
28
48
|
};
|