laymos 0.0.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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +740 -0
  3. package/dist/cli/cli.d.ts +3 -0
  4. package/dist/cli/cli.d.ts.map +1 -0
  5. package/dist/cli/cli.js +6 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +1 -0
  9. package/dist/cli/run.d.ts +11 -0
  10. package/dist/cli/run.d.ts.map +1 -0
  11. package/dist/cli/run.js +242 -0
  12. package/dist/config/define-config.d.ts +3 -0
  13. package/dist/config/define-config.d.ts.map +1 -0
  14. package/dist/config/define-config.js +228 -0
  15. package/dist/config/index.d.ts +5 -0
  16. package/dist/config/index.d.ts.map +1 -0
  17. package/dist/config/index.js +4 -0
  18. package/dist/config/layer-graph.d.ts +7 -0
  19. package/dist/config/layer-graph.d.ts.map +1 -0
  20. package/dist/config/layer-graph.js +53 -0
  21. package/dist/config/layer.d.ts +5 -0
  22. package/dist/config/layer.d.ts.map +1 -0
  23. package/dist/config/layer.js +21 -0
  24. package/dist/config/module.d.ts +9 -0
  25. package/dist/config/module.d.ts.map +1 -0
  26. package/dist/config/module.js +29 -0
  27. package/dist/config/path.d.ts +4 -0
  28. package/dist/config/path.d.ts.map +1 -0
  29. package/dist/config/path.js +32 -0
  30. package/dist/config/types.d.ts +36 -0
  31. package/dist/config/types.d.ts.map +1 -0
  32. package/dist/config/types.js +1 -0
  33. package/dist/engine/1-extract/extract.d.ts +12 -0
  34. package/dist/engine/1-extract/extract.d.ts.map +1 -0
  35. package/dist/engine/1-extract/extract.js +160 -0
  36. package/dist/engine/1-extract/index.d.ts +3 -0
  37. package/dist/engine/1-extract/index.d.ts.map +1 -0
  38. package/dist/engine/1-extract/index.js +1 -0
  39. package/dist/engine/2-resolve/index.d.ts +3 -0
  40. package/dist/engine/2-resolve/index.d.ts.map +1 -0
  41. package/dist/engine/2-resolve/index.js +1 -0
  42. package/dist/engine/2-resolve/resolve.d.ts +24 -0
  43. package/dist/engine/2-resolve/resolve.d.ts.map +1 -0
  44. package/dist/engine/2-resolve/resolve.js +74 -0
  45. package/dist/engine/3-evaluate/evaluate.d.ts +10 -0
  46. package/dist/engine/3-evaluate/evaluate.d.ts.map +1 -0
  47. package/dist/engine/3-evaluate/evaluate.js +80 -0
  48. package/dist/engine/3-evaluate/index.d.ts +3 -0
  49. package/dist/engine/3-evaluate/index.d.ts.map +1 -0
  50. package/dist/engine/3-evaluate/index.js +1 -0
  51. package/dist/engine/4-emit/emit.d.ts +7 -0
  52. package/dist/engine/4-emit/emit.d.ts.map +1 -0
  53. package/dist/engine/4-emit/emit.js +77 -0
  54. package/dist/engine/4-emit/index.d.ts +2 -0
  55. package/dist/engine/4-emit/index.d.ts.map +1 -0
  56. package/dist/engine/4-emit/index.js +1 -0
  57. package/dist/engine/errors.d.ts +19 -0
  58. package/dist/engine/errors.d.ts.map +1 -0
  59. package/dist/engine/errors.js +5 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +1 -0
  63. package/dist/node.d.ts +24 -0
  64. package/dist/node.d.ts.map +1 -0
  65. package/dist/node.js +139 -0
  66. package/dist/report/architecture.d.ts +104 -0
  67. package/dist/report/architecture.d.ts.map +1 -0
  68. package/dist/report/architecture.js +1 -0
  69. package/dist/report/index.d.ts +3 -0
  70. package/dist/report/index.d.ts.map +1 -0
  71. package/dist/report/index.js +2 -0
  72. package/dist/report/stories.d.ts +22 -0
  73. package/dist/report/stories.d.ts.map +1 -0
  74. package/dist/report/stories.js +1 -0
  75. package/dist/story/artifact/artifact.d.ts +57 -0
  76. package/dist/story/artifact/artifact.d.ts.map +1 -0
  77. package/dist/story/artifact/artifact.js +234 -0
  78. package/dist/story/artifact/index.d.ts +2 -0
  79. package/dist/story/artifact/index.d.ts.map +1 -0
  80. package/dist/story/artifact/index.js +1 -0
  81. package/dist/story/artifact/storage.d.ts +10 -0
  82. package/dist/story/artifact/storage.d.ts.map +1 -0
  83. package/dist/story/artifact/storage.js +162 -0
  84. package/dist/story/artifact/types.d.ts +74 -0
  85. package/dist/story/artifact/types.d.ts.map +1 -0
  86. package/dist/story/artifact/types.js +1 -0
  87. package/dist/story/core/blocks.d.ts +20 -0
  88. package/dist/story/core/blocks.d.ts.map +1 -0
  89. package/dist/story/core/blocks.js +104 -0
  90. package/dist/story/core/declare.d.ts +63 -0
  91. package/dist/story/core/declare.d.ts.map +1 -0
  92. package/dist/story/core/declare.js +62 -0
  93. package/dist/story/core/index.d.ts +4 -0
  94. package/dist/story/core/index.d.ts.map +1 -0
  95. package/dist/story/core/index.js +1 -0
  96. package/dist/story/core/recorder.d.ts +38 -0
  97. package/dist/story/core/recorder.d.ts.map +1 -0
  98. package/dist/story/core/recorder.js +28 -0
  99. package/dist/story/core/runtime-context.d.ts +14 -0
  100. package/dist/story/core/runtime-context.d.ts.map +1 -0
  101. package/dist/story/core/runtime-context.js +19 -0
  102. package/dist/story/core/types.d.ts +21 -0
  103. package/dist/story/core/types.d.ts.map +1 -0
  104. package/dist/story/core/types.js +1 -0
  105. package/dist/story/effect/effect.d.ts +52 -0
  106. package/dist/story/effect/effect.d.ts.map +1 -0
  107. package/dist/story/effect/effect.js +99 -0
  108. package/dist/story/effect/index.d.ts +3 -0
  109. package/dist/story/effect/index.d.ts.map +1 -0
  110. package/dist/story/effect/index.js +1 -0
  111. package/dist/story/runner/index.d.ts +3 -0
  112. package/dist/story/runner/index.d.ts.map +1 -0
  113. package/dist/story/runner/index.js +1 -0
  114. package/dist/story/runner/runner.d.ts +44 -0
  115. package/dist/story/runner/runner.d.ts.map +1 -0
  116. package/dist/story/runner/runner.js +476 -0
  117. package/dist/story/story-runtime/index.d.ts +2 -0
  118. package/dist/story/story-runtime/index.d.ts.map +1 -0
  119. package/dist/story/story-runtime/index.js +1 -0
  120. package/dist/story/story-runtime/story-runtime.d.ts +52 -0
  121. package/dist/story/story-runtime/story-runtime.d.ts.map +1 -0
  122. package/dist/story/story-runtime/story-runtime.js +194 -0
  123. package/dist/story/vanilla/index.d.ts +29 -0
  124. package/dist/story/vanilla/index.d.ts.map +1 -0
  125. package/dist/story/vanilla/index.js +78 -0
  126. package/package.json +68 -0
@@ -0,0 +1,476 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { basename, join, relative, resolve, sep } from 'node:path';
4
+ import { Cause, Context, Data, Duration, Effect, Exit, Layer, Option, Ref, } from 'effect';
5
+ import { createJiti } from 'jiti';
6
+ import { ScenarioRecorder, StoryBlockRegistry, roundMillis, } from '../artifact/index.js';
7
+ import { collectDeclaredStories } from '../core/declare.js';
8
+ import { CurrentRecorder } from '../core/recorder.js';
9
+ export class StoryRunnerError extends Data.TaggedError('StoryRunnerError') {
10
+ }
11
+ export class StoryDiscoveryError extends Data.TaggedError('StoryDiscoveryError') {
12
+ }
13
+ const storyFilePattern = /\.story\.[cm]?[jt]sx?$/;
14
+ const validStoryFile = /^[a-z0-9]+(?:-[a-z0-9]+)*\.story\.(?:[cm]?[jt]sx?)$/;
15
+ const skippedSegments = new Set(['node_modules', 'dist']);
16
+ const defaultTimeout = '60 seconds';
17
+ const storyRuntimePath = resolve(import.meta.dirname, '../story-runtime/index.js');
18
+ export function executeStories(baseDir, filters, options) {
19
+ return Effect.suspend(() => {
20
+ enableSourceMaps();
21
+ return runStoriesGeneration(baseDir, filters, options);
22
+ }).pipe(Effect.mapError((cause) => storyRunnerError('execute', cause)));
23
+ }
24
+ function runStoriesGeneration(baseDir, filters, options) {
25
+ return Effect.gen(function* () {
26
+ const files = filters.length > 0
27
+ ? [...filters].sort()
28
+ : yield* discoverStories(baseDir).pipe(Effect.map(({ stories }) => stories.map(({ storyId }) => storyId)));
29
+ yield* attempt(() => {
30
+ for (const storyId of files) {
31
+ validateStoryFile(storyId);
32
+ if (!existsSync(resolve(baseDir, storyId))) {
33
+ throw new Error(`Story file not found: ${storyId}`);
34
+ }
35
+ }
36
+ });
37
+ const failures = [];
38
+ const stories = {};
39
+ for (const storyId of files) {
40
+ const declarations = yield* fromPromise(() => collectDeclaredStories(() => loadStoryModule(baseDir, storyId)));
41
+ if (declarations.length !== 1) {
42
+ return yield* Effect.fail(new Error(`Story file "${storyId}" must declare exactly one Story`));
43
+ }
44
+ stories[storyId] = yield* runDeclaredStory(baseDir, storyId, declarations[0], options, failures);
45
+ }
46
+ return {
47
+ status: failures.length === 0 ? 'passed' : 'failed',
48
+ report: { stories },
49
+ failures,
50
+ };
51
+ });
52
+ }
53
+ function runDeclaredStory(baseDir, storyId, declaration, options, failures) {
54
+ return Effect.scoped(Effect.gen(function* () {
55
+ if (declaration.execution === undefined) {
56
+ return yield* Effect.fail(new Error(`Story "${declaration.name}" must declare one execution`));
57
+ }
58
+ const environment = declaration.execution.layer === undefined
59
+ ? Context.empty()
60
+ : yield* Layer.build(declaration.execution.layer);
61
+ const blocks = new StoryBlockRegistry(baseDir);
62
+ const scenarios = [];
63
+ for (const declared of declaration.scenarios) {
64
+ scenarios.push(yield* runScenario(storyId, declaration, declared, blocks, environment, options, failures));
65
+ }
66
+ return {
67
+ schemaVersion: 3,
68
+ generatedAt: Date.now(),
69
+ name: declaration.name,
70
+ description: declaration.description,
71
+ blocks: blocks.toRecord(),
72
+ scenarios,
73
+ };
74
+ }));
75
+ }
76
+ function runScenario(storyId, story, declared, blocks, environment, options, failures) {
77
+ const base = {
78
+ name: declared.name,
79
+ description: declared.description,
80
+ location: {
81
+ file: storyId,
82
+ line: declared.location.line,
83
+ column: declared.location.column,
84
+ },
85
+ };
86
+ if (declared.mode === 'skip') {
87
+ return Effect.succeed({
88
+ ...base,
89
+ outcome: 'skipped',
90
+ execution: [],
91
+ failures: [],
92
+ });
93
+ }
94
+ const recorder = new ScenarioRecorder(blocks);
95
+ const startedAt = Date.now();
96
+ const startMonotonic = performance.now();
97
+ const scenarioEnvironment = Context.add(environment, CurrentRecorder, recorder);
98
+ return runScenarioLifecycle(story, declared, recorder, scenarioEnvironment, options).pipe(Effect.ensuring(Effect.sync(() => recorder.close())), Effect.map((result) => {
99
+ const durationMillis = roundMillis(performance.now() - startMonotonic);
100
+ for (const failure of result.failures) {
101
+ failures.push({
102
+ storyId,
103
+ scenario: declared.name,
104
+ phase: failure.phase,
105
+ message: failure.message,
106
+ });
107
+ }
108
+ return {
109
+ ...base,
110
+ outcome: result.outcome,
111
+ startedAt,
112
+ durationMillis,
113
+ execution: recorder.execution(),
114
+ failures: result.failures,
115
+ };
116
+ }));
117
+ }
118
+ function runScenarioLifecycle(story, scenario, recorder, environment, options) {
119
+ const execution = story.execution;
120
+ if (execution === undefined) {
121
+ return Effect.fail(new Error(`Story "${story.name}" has no execution`));
122
+ }
123
+ const timeout = (scenario.run.timeout ??
124
+ options?.timeout ??
125
+ defaultTimeout);
126
+ return Effect.gen(function* () {
127
+ const progress = yield* Ref.make({
128
+ phase: 'preparation',
129
+ failures: [],
130
+ interrupted: false,
131
+ });
132
+ const lifecycleExit = yield* runEffectLifecycle(execution, scenario.run, recorder, progress).pipe(Effect.provide(environment), Effect.timeout(timeout), Effect.exit);
133
+ if (Exit.isSuccess(lifecycleExit))
134
+ return lifecycleExit.value;
135
+ if (!isTimeoutCause(lifecycleExit.cause)) {
136
+ return yield* Effect.failCause(lifecycleExit.cause);
137
+ }
138
+ const current = yield* Ref.get(progress);
139
+ return {
140
+ outcome: 'interrupted',
141
+ failures: [
142
+ ...current.failures,
143
+ { phase: current.phase, message: timeoutMessage(timeout) },
144
+ ],
145
+ };
146
+ });
147
+ }
148
+ function runEffectLifecycle(execution, run, recorder, progress) {
149
+ return Effect.gen(function* () {
150
+ const prepareExit = yield* runEffectPhase(run.prepare).pipe(Effect.exit);
151
+ if (Exit.isFailure(prepareExit)) {
152
+ yield* recordPhaseFailure(progress, 'preparation', prepareExit.cause);
153
+ return yield* scenarioResult(progress);
154
+ }
155
+ const prepared = prepareExit.value;
156
+ const body = Effect.gen(function* () {
157
+ yield* setPhase(progress, 'execution');
158
+ const executionExit = yield* Effect.acquireUseRelease(Effect.sync(() => recorder.activate()), () => runEffectPhase(() => execution.execute(prepared)).pipe(Effect.exit), () => Effect.sync(() => recorder.deactivate()));
159
+ if (Exit.isFailure(executionExit) &&
160
+ Cause.hasInterruptsOnly(executionExit.cause)) {
161
+ yield* recordPhaseFailure(progress, 'execution', executionExit.cause, 'Story execution was interrupted');
162
+ return;
163
+ }
164
+ const verification = expectedVerification(run.expectation, executionExit, prepared);
165
+ if (verification.kind === 'mismatch') {
166
+ yield* addFailure(progress, {
167
+ phase: 'execution',
168
+ message: verification.message,
169
+ });
170
+ return;
171
+ }
172
+ yield* setPhase(progress, 'verification');
173
+ const verificationExit = yield* runEffectPhase(verification.verify).pipe(Effect.exit);
174
+ if (Exit.isFailure(verificationExit)) {
175
+ yield* recordPhaseFailure(progress, 'verification', verificationExit.cause);
176
+ }
177
+ });
178
+ yield* Effect.onExitPrimitive(body, (bodyExit) => runCleanup(run, prepared, progress, bodyExit), true);
179
+ return yield* scenarioResult(progress);
180
+ });
181
+ }
182
+ function runEffectPhase(phase) {
183
+ return Effect.suspend(phase);
184
+ }
185
+ function expectedVerification(expectation, executionExit, prepared) {
186
+ if (expectation.kind === 'success') {
187
+ return Exit.isSuccess(executionExit)
188
+ ? {
189
+ kind: 'verify',
190
+ verify: () => expectation.verify(executionExit.value, prepared),
191
+ }
192
+ : {
193
+ kind: 'mismatch',
194
+ message: `Expected a success value but execution failed: ${describeFailure(Cause.squash(executionExit.cause))}`,
195
+ };
196
+ }
197
+ if (Exit.isSuccess(executionExit)) {
198
+ return {
199
+ kind: 'mismatch',
200
+ message: 'Expected a typed error but execution succeeded',
201
+ };
202
+ }
203
+ if (Cause.hasDies(executionExit.cause) ||
204
+ Cause.hasInterrupts(executionExit.cause)) {
205
+ return {
206
+ kind: 'mismatch',
207
+ message: `Expected a typed error but execution terminated unexpectedly: ${describeFailure(Cause.squash(executionExit.cause))}`,
208
+ };
209
+ }
210
+ const error = Cause.findErrorOption(executionExit.cause);
211
+ return Option.isSome(error)
212
+ ? {
213
+ kind: 'verify',
214
+ verify: () => expectation.verify(error.value, prepared),
215
+ }
216
+ : {
217
+ kind: 'mismatch',
218
+ message: 'Expected a typed error but execution produced none',
219
+ };
220
+ }
221
+ function runCleanup(run, prepared, progress, bodyExit) {
222
+ if (run.cleanup === undefined)
223
+ return Effect.void;
224
+ return Effect.gen(function* () {
225
+ if (Exit.isSuccess(bodyExit) || !Cause.hasInterruptsOnly(bodyExit.cause)) {
226
+ yield* setPhase(progress, 'cleanup');
227
+ }
228
+ const cleanupExit = yield* runEffectPhase(() => run.cleanup(prepared)).pipe(Effect.exit);
229
+ if (Exit.isFailure(cleanupExit)) {
230
+ yield* recordPhaseFailure(progress, 'cleanup', cleanupExit.cause);
231
+ }
232
+ });
233
+ }
234
+ function setPhase(progress, phase) {
235
+ return Ref.update(progress, (current) => ({ ...current, phase }));
236
+ }
237
+ function addFailure(progress, failure, interrupted = false) {
238
+ return Ref.update(progress, (current) => ({
239
+ ...current,
240
+ failures: [...current.failures, failure],
241
+ interrupted: current.interrupted || interrupted,
242
+ }));
243
+ }
244
+ function recordPhaseFailure(progress, phase, cause, interruptionMessage) {
245
+ const interrupted = Cause.hasInterruptsOnly(cause);
246
+ return addFailure(progress, {
247
+ phase,
248
+ message: interrupted && interruptionMessage !== undefined
249
+ ? interruptionMessage
250
+ : describeFailure(Cause.squash(cause)),
251
+ }, interrupted);
252
+ }
253
+ function scenarioResult(progress) {
254
+ return Ref.get(progress).pipe(Effect.map((current) => ({
255
+ outcome: current.interrupted
256
+ ? 'interrupted'
257
+ : current.failures.length === 0
258
+ ? 'succeeded'
259
+ : 'failed',
260
+ failures: current.failures,
261
+ })));
262
+ }
263
+ function isTimeoutCause(cause) {
264
+ const error = Cause.findErrorOption(cause);
265
+ return Option.isSome(error) && Cause.isTimeoutError(error.value);
266
+ }
267
+ async function loadStoryModule(baseDir, storyId) {
268
+ const path = resolve(baseDir, storyId);
269
+ const jiti = createJiti(import.meta.url, {
270
+ alias: { 'laymos/story': storyRuntimePath },
271
+ interopDefault: true,
272
+ moduleCache: false,
273
+ });
274
+ await jiti.import(path);
275
+ }
276
+ export function discoverStories(baseDir) {
277
+ return Effect.tryPromise({
278
+ try: () => buildStoryCatalog(baseDir),
279
+ catch: (cause) => cause instanceof StoryDiscoveryError
280
+ ? cause
281
+ : discoveryError([{ message: errorMessage(cause) }]),
282
+ });
283
+ }
284
+ async function buildStoryCatalog(baseDir) {
285
+ const { storyIds, issues } = await discoverStoryFileIds(baseDir);
286
+ const declarations = [];
287
+ for (const storyId of storyIds) {
288
+ try {
289
+ const found = await collectDeclaredStories(() => loadStoryModule(baseDir, storyId));
290
+ if (found.length !== 1) {
291
+ issues.push({
292
+ storyId,
293
+ message: `must declare exactly one Story; found ${found.length}`,
294
+ });
295
+ continue;
296
+ }
297
+ declarations.push({ storyId, declaration: found[0] });
298
+ }
299
+ catch (cause) {
300
+ issues.push({ storyId, message: errorMessage(cause) });
301
+ }
302
+ }
303
+ const catalog = validateStoryCatalog(declarations, issues);
304
+ if (issues.length > 0)
305
+ throw discoveryError(issues);
306
+ return catalog;
307
+ }
308
+ async function discoverStoryFileIds(baseDir) {
309
+ const storyIds = [];
310
+ const issues = [];
311
+ const directories = [baseDir];
312
+ while (directories.length > 0) {
313
+ const directory = directories.pop();
314
+ const entries = await readdir(directory, { withFileTypes: true });
315
+ for (const entry of entries) {
316
+ if (entry.isDirectory()) {
317
+ if (skippedSegments.has(entry.name) || entry.name.startsWith('.')) {
318
+ continue;
319
+ }
320
+ directories.push(join(directory, entry.name));
321
+ continue;
322
+ }
323
+ if (!entry.isFile() || !storyFilePattern.test(entry.name))
324
+ continue;
325
+ const storyId = relative(baseDir, join(directory, entry.name))
326
+ .split(sep)
327
+ .join('/');
328
+ storyIds.push(storyId);
329
+ try {
330
+ validateStoryFile(storyId);
331
+ }
332
+ catch (cause) {
333
+ issues.push({ storyId, message: errorMessage(cause) });
334
+ }
335
+ }
336
+ }
337
+ storyIds.sort();
338
+ return { storyIds, issues };
339
+ }
340
+ function validateStoryCatalog(declarations, issues) {
341
+ const groups = new Map();
342
+ const stories = [];
343
+ const destinations = new Map();
344
+ const registerDestination = (parentPath, name, kind, storyId) => {
345
+ const siblings = destinations.get(pathKey(parentPath)) ?? new Map();
346
+ destinations.set(pathKey(parentPath), siblings);
347
+ const existing = siblings.get(name);
348
+ if (existing === undefined) {
349
+ siblings.set(name, { kind, storyId });
350
+ return;
351
+ }
352
+ if (kind === 'Group' && existing.kind === 'Group')
353
+ return;
354
+ issues.push({
355
+ storyId,
356
+ message: `${kind} "${displayPath([...parentPath, name])}" conflicts with ${existing.kind} declared by "${existing.storyId}"`,
357
+ });
358
+ };
359
+ for (const { storyId, declaration } of declarations) {
360
+ const lineage = storyGroupLineage(declaration.group, storyId, issues);
361
+ const groupPath = [];
362
+ for (const group of lineage) {
363
+ const parentPath = [...groupPath];
364
+ groupPath.push(group.name);
365
+ const key = pathKey(groupPath);
366
+ const existing = groups.get(key);
367
+ if (existing === undefined) {
368
+ groups.set(key, {
369
+ path: [...groupPath],
370
+ name: group.name,
371
+ description: group.description,
372
+ storyId,
373
+ });
374
+ }
375
+ else if (existing.description !== group.description) {
376
+ issues.push({
377
+ storyId,
378
+ message: `Story Group "${displayPath(groupPath)}" conflicts with metadata declared by "${existing.storyId}"`,
379
+ });
380
+ }
381
+ registerDestination(parentPath, group.name, 'Group', storyId);
382
+ }
383
+ registerDestination(groupPath, declaration.name, 'Story', storyId);
384
+ stories.push({
385
+ storyId,
386
+ name: declaration.name,
387
+ description: declaration.description,
388
+ groupPath,
389
+ });
390
+ }
391
+ return {
392
+ groups: [...groups.values()]
393
+ .map(({ storyId: _storyId, ...group }) => group)
394
+ .sort((left, right) => comparePaths(left.path, right.path)),
395
+ stories: stories.sort((left, right) => comparePaths(left.groupPath, right.groupPath) ||
396
+ left.name.localeCompare(right.name) ||
397
+ left.storyId.localeCompare(right.storyId)),
398
+ };
399
+ }
400
+ function storyGroupLineage(leaf, storyId, issues) {
401
+ const reversed = [];
402
+ const seen = new Set();
403
+ let current = leaf;
404
+ while (current !== undefined) {
405
+ if (seen.has(current)) {
406
+ issues.push({
407
+ storyId,
408
+ message: 'Story Group ancestry contains a cycle',
409
+ });
410
+ return [];
411
+ }
412
+ seen.add(current);
413
+ reversed.push(current);
414
+ current = current.parent;
415
+ }
416
+ return reversed.reverse();
417
+ }
418
+ const pathKey = (path) => JSON.stringify(path);
419
+ const displayPath = (path) => path.join(' / ');
420
+ function comparePaths(left, right) {
421
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
422
+ const compared = left[index].localeCompare(right[index]);
423
+ if (compared !== 0)
424
+ return compared;
425
+ }
426
+ return left.length - right.length;
427
+ }
428
+ function discoveryError(issues) {
429
+ const details = issues.map(({ storyId, message }) => `- ${storyId === undefined ? message : `${storyId}: ${message}`}`);
430
+ return new StoryDiscoveryError({
431
+ message: `Story catalog is invalid:\n${details.join('\n')}`,
432
+ issues,
433
+ });
434
+ }
435
+ function validateStoryFile(storyId) {
436
+ if (!validStoryFile.test(basename(storyId))) {
437
+ throw new Error(`Story file "${storyId}" must use the kebab-case <story-name>.story.ts convention`);
438
+ }
439
+ }
440
+ function timeoutMessage(timeout) {
441
+ const millis = typeof timeout === 'number' ? timeout : Duration.toMillis(timeout);
442
+ return `Scenario timed out after ${millis}ms`;
443
+ }
444
+ function describeFailure(failure) {
445
+ if (failure instanceof Error)
446
+ return failure.message;
447
+ if (typeof failure === 'string')
448
+ return failure;
449
+ return String(failure);
450
+ }
451
+ function errorMessage(cause) {
452
+ return cause instanceof Error ? cause.message : String(cause);
453
+ }
454
+ function enableSourceMaps() {
455
+ try {
456
+ process.setSourceMapsEnabled(true);
457
+ }
458
+ catch {
459
+ // best-effort; some runtimes do not implement it
460
+ }
461
+ }
462
+ function fromPromise(body) {
463
+ return Effect.tryPromise({ try: body, catch: (cause) => cause });
464
+ }
465
+ function attempt(body) {
466
+ return Effect.try({ try: body, catch: (cause) => cause });
467
+ }
468
+ function storyRunnerError(operation, cause) {
469
+ if (cause instanceof StoryRunnerError)
470
+ return cause;
471
+ return new StoryRunnerError({
472
+ operation,
473
+ message: describeFailure(cause),
474
+ cause,
475
+ });
476
+ }
@@ -0,0 +1,2 @@
1
+ export { decision, functionBlock, step, story, storyGroup, } from './story-runtime.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/story/story-runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,aAAa,EACb,IAAI,EACJ,KAAK,EACL,UAAU,GACX,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1 @@
1
+ export { decision, functionBlock, step, story, storyGroup, } from './story-runtime.js';
@@ -0,0 +1,52 @@
1
+ import { Effect } from 'effect';
2
+ import type { Duration, Layer } from 'effect';
3
+ import type { ArmMeta, BlockMeta, DecisionValue, StoryGroupMeta, StoryMeta } from '../core/types.js';
4
+ export interface ScenarioMeta {
5
+ readonly description: string;
6
+ readonly timeout?: Duration.Input;
7
+ }
8
+ export interface EffectVerifiedScenario<Prepared, R> {
9
+ cleanup<E>(cleanup: (prepared: Prepared) => Effect.Effect<unknown, E, R>): EffectVerifiedScenario<Prepared, R>;
10
+ }
11
+ export interface EffectPreparedScenario<Prepared, Output, Error, R> {
12
+ cleanup<CleanupError>(cleanup: (prepared: Prepared) => Effect.Effect<unknown, CleanupError, R>): EffectPreparedScenario<Prepared, Output, Error, R>;
13
+ verify<VerificationError>(verify: (output: Output, prepared: Prepared) => Effect.Effect<unknown, VerificationError, R>): EffectVerifiedScenario<Prepared, R>;
14
+ verifyError<VerificationError>(verify: (error: Error, prepared: Prepared) => Effect.Effect<unknown, VerificationError, R>): EffectVerifiedScenario<Prepared, R>;
15
+ }
16
+ export interface EffectScenario<Prepared, Output, Error, R> {
17
+ prepare<PreparationError>(prepare: () => Effect.Effect<Prepared, PreparationError, R>): EffectPreparedScenario<Prepared, Output, Error, R>;
18
+ }
19
+ export interface EffectStory<Prepared, Output, Error, R> {
20
+ scenario(name: string, meta: ScenarioMeta, define: (scenario: EffectScenario<Prepared, Output, Error, R>) => EffectVerifiedScenario<Prepared, R>): EffectStory<Prepared, Output, Error, R>;
21
+ skip(name: string, meta: ScenarioMeta): EffectStory<Prepared, Output, Error, R>;
22
+ }
23
+ export interface EffectStoryBuilder<R> {
24
+ provide<Services, LayerError>(layer: Layer.Layer<Services, LayerError, never>): EffectStoryBuilder<Services>;
25
+ execute<Prepared, Output, Error>(execute: (prepared: Prepared) => Effect.Effect<Output, Error, R>): EffectStory<Prepared, Output, Error, R>;
26
+ }
27
+ export interface EffectStoryGroup {
28
+ group(name: string, meta: StoryGroupMeta): EffectStoryGroup;
29
+ story(name: string, meta: StoryMeta): EffectStoryBuilder<never>;
30
+ }
31
+ /** Declares the single Story owned by a Story file. */
32
+ export declare function story(name: string, meta: StoryMeta): EffectStoryBuilder<never>;
33
+ /** Declares a reusable Story Group. */
34
+ export declare function storyGroup(name: string, meta: StoryGroupMeta): EffectStoryGroup;
35
+ type AnyEffect = Effect.Effect<any, any, any>;
36
+ type Success<T> = T extends Effect.Effect<infer A, any, any> ? A : never;
37
+ type Error<T> = T extends Effect.Effect<any, infer E, any> ? E : never;
38
+ type Services<T> = T extends Effect.Effect<any, any, infer R> ? R : never;
39
+ export interface DecisionBuilder<Remaining extends DecisionValue, A, E, R> {
40
+ when<Key extends Remaining, Next extends AnyEffect>(value: Key, meta: ArmMeta, body: (value: Key) => Next): DecisionBuilder<Exclude<Remaining, Key>, A | Success<Next>, E | Error<Next>, R | Services<Next>>;
41
+ otherwise<Next extends AnyEffect>(meta: ArmMeta, body: (value: Remaining) => Next): Effect.Effect<A | Success<Next>, E | Error<Next>, R | Services<Next>>;
42
+ readonly exhaustive: [Remaining] extends [never] ? () => Effect.Effect<A, E, R> : never;
43
+ [Symbol.iterator](): Effect.EffectIterator<Effect.Effect<A, E, R>>;
44
+ }
45
+ /** Marks a reusable Effect-returning function boundary as a Story Block. */
46
+ export declare function functionBlock<Args extends readonly unknown[], A, E, R>(name: string, meta: BlockMeta<Args>, fn: (...args: Args) => Effect.Effect<A, E, R>): (...args: Args) => Effect.Effect<A, E, R>;
47
+ /** Wraps an Effect in an inline Story Block. */
48
+ export declare function step<A, E, R>(name: string, meta: BlockMeta, effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
49
+ /** Builds an eager, literal-keyed Effect Decision. */
50
+ export declare function decision<const Input extends DecisionValue>(name: string, meta: BlockMeta<[Input]>, input: Input): DecisionBuilder<Input, never, never, never>;
51
+ export {};
52
+ //# sourceMappingURL=story-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"story-runtime.d.ts","sourceRoot":"","sources":["../../../src/story/story-runtime/story-runtime.ts"],"names":[],"mappings":"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"}