@feasibleone/blong-chain 1.9.0 โ†’ 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.11.0](https://github.com/feasibleone/blong/compare/blong-chain-v1.10.0...blong-chain-v1.11.0) (2026-09-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * progress points in test reports ([fb73036](https://github.com/feasibleone/blong/commit/fb730360877eb371ab3d1603ae7bf35976c170dc))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * misc CI failures ([60689af](https://github.com/feasibleone/blong/commit/60689af66dc246ef8f88fef8edbd2b8a16a6c4d9))
14
+
15
+ ## [1.10.0](https://github.com/feasibleone/blong/compare/blong-chain-v1.9.0...blong-chain-v1.10.0) (2026-09-24)
16
+
17
+
18
+ ### Features
19
+
20
+ * sequence diagram progress points ([dc5b153](https://github.com/feasibleone/blong/commit/dc5b15327f31698a26a75ec73bdf9a0956a43dd9))
21
+
3
22
  ## [1.9.0](https://github.com/feasibleone/blong/compare/blong-chain-v1.8.2...blong-chain-v1.9.0) (2026-09-15)
4
23
 
5
24
 
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  **Parallel test execution with automatic dependency detection through thenable proxies.**
4
4
 
5
- `blong-chain` is a TypeScript test framework that automatically detects
6
- dependencies between test steps and executes them in parallel when possible,
7
- maximizing performance while maintaining correctness.
5
+ `blong-chain` is a TypeScript test framework that automatically detects dependencies between test
6
+ steps and executes them in parallel when possible, maximizing performance while maintaining
7
+ correctness.
8
8
 
9
9
  ## Key Features
10
10
 
@@ -14,10 +14,8 @@ maximizing performance while maintaining correctness.
14
14
  - ๐ŸŽฏ **Thenable Proxies** - Natural async/await syntax for step dependencies
15
15
  - ๐Ÿ“ˆ **Performance Metrics** - Queue time, execution time, and critical path analysis
16
16
  - ๐ŸŒณ **Nested Groups** - Hierarchical test organization with proper indentation
17
- - ๐Ÿ”ง **Error Handling** - Graceful failure with continued execution of
18
- independent steps
19
- - ๐Ÿงช **Test Framework Integration** - Works seamlessly with node:test, tap,
20
- and others
17
+ - ๐Ÿ”ง **Error Handling** - Graceful failure with continued execution of independent steps
18
+ - ๐Ÿงช **Test Framework Integration** - Works seamlessly with node:test, tap, and others
21
19
 
22
20
  ## Installation
23
21
 
@@ -32,7 +30,7 @@ import {TestExecutor} from '@feasibleone/blong-chain';
32
30
  import assert from 'node:assert/strict';
33
31
  import {test} from 'node:test';
34
32
 
35
- test('parallel execution example', async (t) => {
33
+ test('parallel execution example', async t => {
36
34
  const executor = new TestExecutor({concurrency: 10});
37
35
 
38
36
  const steps = [
@@ -69,8 +67,7 @@ test('parallel execution example', async (t) => {
69
67
 
70
68
  ### Thenable Proxies
71
69
 
72
- Steps access previous results through thenable proxies that automatically track
73
- dependencies:
70
+ Steps access previous results through thenable proxies that automatically track dependencies:
74
71
 
75
72
  ```typescript
76
73
  // Pattern 1: Direct await
@@ -89,7 +86,14 @@ async function step3(assert, {previousStep}) {
89
86
  }
90
87
 
91
88
  // Pattern 4: Deep destructuring
92
- async function step4(assert, {previousStep: {user: {name}}}) {
89
+ async function step4(
90
+ assert,
91
+ {
92
+ previousStep: {
93
+ user: {name},
94
+ },
95
+ },
96
+ ) {
93
97
  const userName = await name;
94
98
  }
95
99
  ```
@@ -155,23 +159,37 @@ Organize steps into hierarchical groups for better structure and output:
155
159
 
156
160
  ```typescript
157
161
  const databaseSetup = [
158
- async function connect() { return {connected: true}; },
159
- async function createSchema() { return {created: true}; },
160
- async function seedData() { return {users: []}; },
162
+ async function connect() {
163
+ return {connected: true};
164
+ },
165
+ async function createSchema() {
166
+ return {created: true};
167
+ },
168
+ async function seedData() {
169
+ return {users: []};
170
+ },
161
171
  ] as any;
162
172
  databaseSetup.name = 'Database Setup';
163
173
 
164
174
  const apiTests = [
165
- async function testEndpoint1() { return {status: 200}; },
166
- async function testEndpoint2() { return {status: 200}; },
175
+ async function testEndpoint1() {
176
+ return {status: 200};
177
+ },
178
+ async function testEndpoint2() {
179
+ return {status: 200};
180
+ },
167
181
  ] as any;
168
182
  apiTests.name = 'API Tests';
169
183
 
170
184
  const steps = [
171
- async function initialize() { return {ready: true}; },
172
- databaseSetup, // Nested group
173
- apiTests, // Nested group
174
- async function cleanup() { return {done: true}; },
185
+ async function initialize() {
186
+ return {ready: true};
187
+ },
188
+ databaseSetup, // Nested group
189
+ apiTests, // Nested group
190
+ async function cleanup() {
191
+ return {done: true};
192
+ },
175
193
  ];
176
194
 
177
195
  await executor.execute(steps, {}, t);
@@ -194,9 +212,13 @@ await executor.execute(steps, {}, t);
194
212
  โœ” cleanup
195
213
  ```
196
214
 
197
- ### Checkpoints (Synchronization Barriers)
215
+ ### Sync Barriers
216
+
217
+ Use empty arrays `[]` as sync barriers to synchronize parallel execution. All steps before a barrier
218
+ must complete before any steps after it begin:
198
219
 
199
- Use empty arrays `[]` as checkpoints to synchronize parallel execution. All steps before a checkpoint must complete before any steps after it begin:
220
+ (A _snapshot marker_ โ€” `snapshot('name', ...steps)` โ€” is a different thing: it waits for steps
221
+ **and** snapshots their results. A sync barrier snapshots nothing.)
200
222
 
201
223
  ```typescript
202
224
  const steps = [
@@ -216,7 +238,7 @@ const steps = [
216
238
  return {loggerReady: true};
217
239
  },
218
240
 
219
- // Checkpoint: Wait for all Phase 1 steps to complete
241
+ // Sync barrier: Wait for all Phase 1 steps to complete
220
242
  [],
221
243
 
222
244
  // Phase 2: These run in parallel, but only after Phase 1 completes
@@ -230,7 +252,7 @@ const steps = [
230
252
  return await fetchProducts(config.apiUrl);
231
253
  },
232
254
 
233
- // Another checkpoint
255
+ // Another barrier
234
256
  [],
235
257
 
236
258
  // Phase 3: Runs only after Phase 2 completes
@@ -243,6 +265,7 @@ const steps = [
243
265
  ```
244
266
 
245
267
  **Use Cases:**
268
+
246
269
  - **Phased execution**: Separate initialization, data loading, processing, and cleanup phases
247
270
  - **Resource management**: Ensure all resources are ready before proceeding
248
271
  - **Testing stages**: Complete all setup before running tests, then cleanup
@@ -255,7 +278,7 @@ Monitor test execution in real-time:
255
278
  ```typescript
256
279
  const executor = new TestExecutor({concurrency: 10});
257
280
 
258
- executor.on('test:start', (progress) => {
281
+ executor.on('test:start', progress => {
259
282
  console.log('Test started:', progress.testName);
260
283
  });
261
284
 
@@ -314,9 +337,7 @@ console.log(`Critical path: ${latency.criticalPath.join(' โ†’ ')}`);
314
337
 
315
338
  // View bottlenecks
316
339
  for (const bottleneck of latency.bottlenecks) {
317
- console.log(
318
- `${bottleneck.stepName} blocked ${bottleneck.blockedSteps.length} steps`
319
- );
340
+ console.log(`${bottleneck.stepName} blocked ${bottleneck.blockedSteps.length} steps`);
320
341
  }
321
342
 
322
343
  // View individual step metrics
@@ -391,7 +412,7 @@ const executor = new TestExecutor({
391
412
  ## Real-World Example
392
413
 
393
414
  ```typescript
394
- test('e-commerce checkout flow', async (t) => {
415
+ test('e-commerce checkout flow', async t => {
395
416
  const executor = new TestExecutor({concurrency: 5});
396
417
 
397
418
  const steps = [
@@ -421,9 +442,7 @@ test('e-commerce checkout flow', async (t) => {
421
442
  return {amount: product.price * 0.08};
422
443
  },
423
444
 
424
- async function calculateTotal(assert, {
425
- loadProduct, calculateShipping, calculateTax
426
- }) {
445
+ async function calculateTotal(assert, {loadProduct, calculateShipping, calculateTax}) {
427
446
  const product = await loadProduct;
428
447
  const shipping = await calculateShipping;
429
448
  const tax = await calculateTax;
@@ -438,9 +457,7 @@ test('e-commerce checkout flow', async (t) => {
438
457
  };
439
458
  },
440
459
 
441
- async function processPayment(assert, {
442
- calculateTotal, validateInventory
443
- }) {
460
+ async function processPayment(assert, {calculateTotal, validateInventory}) {
444
461
  const total = await calculateTotal;
445
462
  const inventory = await validateInventory;
446
463
 
@@ -465,10 +482,14 @@ test('e-commerce checkout flow', async (t) => {
465
482
  },
466
483
  ];
467
484
 
468
- await executor.execute(steps, {
469
- testId: 'checkout-001',
470
- environment: 'test',
471
- }, t);
485
+ await executor.execute(
486
+ steps,
487
+ {
488
+ testId: 'checkout-001',
489
+ environment: 'test',
490
+ },
491
+ t,
492
+ );
472
493
 
473
494
  // Verify results
474
495
  const progress = executor.getProgress();
@@ -494,8 +515,7 @@ In this example:
494
515
 
495
516
  - **[SHOWCASE.md](./SHOWCASE.md)** - Comprehensive feature showcase with all patterns
496
517
  - **[TESTING.md](./TESTING.md)** - Testing guide and CI integration
497
- - **[NESTED_TEST_CONTEXT.md](./NESTED_TEST_CONTEXT.md)** - Test framework
498
- integration details
518
+ - **[NESTED_TEST_CONTEXT.md](./NESTED_TEST_CONTEXT.md)** - Test framework integration details
499
519
  - **[test-types.ts](./test-types.ts)** - Complete TypeScript API documentation
500
520
 
501
521
  ## Running Tests
@@ -514,17 +534,17 @@ npm run test:all
514
534
 
515
535
  ## How It Works
516
536
 
517
- 1. **Detection Phase**: When you destructure or access `context.stepName`, a
518
- thenable proxy is returned
537
+ 1. **Detection Phase**: When you destructure or access `context.stepName`, a thenable proxy is
538
+ returned
519
539
  2. **Dependency Tracking**: The access is recorded as a dependency
520
540
  3. **Scheduling Phase**: Steps are added to a priority queue based on dependencies
521
541
  4. **Execution Phase**: Steps run as soon as all their dependencies complete
522
542
  5. **Resolution Phase**: Step results are stored and proxies are resolved
523
- 6. **Checkpoints**: Empty arrays `[]` create synchronization barriers, waiting for
524
- all previous steps to complete before continuing
543
+ 6. **Sync Barriers**: Empty arrays `[]` create synchronization barriers, waiting for all previous
544
+ steps to complete before continuing
525
545
 
526
- This creates a dynamic dependency graph that enables maximum parallelization
527
- while ensuring correctness, with optional synchronization points for phased execution.
546
+ This creates a dynamic dependency graph that enables maximum parallelization while ensuring
547
+ correctness, with optional synchronization points for phased execution.
528
548
 
529
549
  ## Use Cases
530
550
 
package/index.ts CHANGED
@@ -13,10 +13,12 @@
13
13
  import assert from 'node:assert';
14
14
  import {EventEmitter} from 'node:events';
15
15
  import PQueue from 'p-queue';
16
+ import {progressTree, reportProgress} from './progress.ts';
16
17
  import type {
17
18
  IDependencyEdge,
18
19
  IDependencyGraph,
19
20
  IMeta,
21
+ IProgressEntry,
20
22
  IPromiseEntry,
21
23
  ISourceLocation,
22
24
  IStepError,
@@ -371,7 +373,7 @@ function captureSourceLocation(): ISourceLocation {
371
373
  const DEFAULT_MAX_RETRIES = 1;
372
374
 
373
375
  // ============================================================================
374
- // Masking helpers (also used by assert.snapshot and checkpoint snapshots)
376
+ // Masking helpers (also used by assert.snapshot and snapshot markers)
375
377
  // ============================================================================
376
378
 
377
379
  /**
@@ -542,13 +544,13 @@ export class TestExecutor extends EventEmitter {
542
544
  ): Promise<void> {
543
545
  const stepPromises: Promise<void>[] = [];
544
546
  const namedPromises = new Map<string, Promise<void>>();
545
- let checkpointIndex = 0;
547
+ let snapshotIndex = 0;
546
548
 
547
549
  for (const step of steps) {
548
550
  if (Array.isArray(step)) {
549
551
  // Distinguish by element type:
550
552
  // [] empty array โ†’ sync barrier (existing behaviour)
551
- // ['*'] / ['s1','s2'] โ†’ snapshot checkpoint (new)
553
+ // ['*'] / ['s1','s2'] โ†’ snapshot marker
552
554
  // [fn, ...] nested โ†’ nested step group (existing behaviour)
553
555
  if (step.length === 0) {
554
556
  // Sync barrier โ€” wait for all parallel steps in this batch
@@ -558,37 +560,35 @@ export class TestExecutor extends EventEmitter {
558
560
  }
559
561
 
560
562
  if (step.every(s => typeof s === 'string')) {
561
- // Snapshot checkpoint: await relevant steps, then snapshot
562
- const checkpoint = step as unknown as string[];
563
+ // Snapshot marker: await the relevant steps, then snapshot
564
+ const marker = step as unknown as string[];
563
565
 
564
- if (checkpoint.length === 1 && checkpoint[0] === '*') {
566
+ if (marker.length === 1 && marker[0] === '*') {
565
567
  // ['*'] โ€” wait for entire current batch
566
568
  await Promise.all(stepPromises);
567
569
  stepPromises.length = 0;
568
570
  } else {
569
571
  // ['step1','step2'] โ€” wait only for the named steps
570
- const namedToWait = checkpoint
572
+ const namedToWait = marker
571
573
  .map(name => namedPromises.get(name))
572
574
  .filter((p): p is Promise<void> => p !== undefined);
573
575
  await Promise.all(namedToWait);
574
576
  // stepPromises is NOT cleared โ€” other steps keep running
575
577
  }
576
578
  const cpName =
577
- (checkpoint as {name?: string}).name ??
578
- (checkpoint.length === 1 && checkpoint[0] === '*'
579
- ? `context`
580
- : checkpoint.join('-'));
579
+ (marker as {name?: string}).name ??
580
+ (marker.length === 1 && marker[0] === '*' ? `context` : marker.join('-'));
581
581
  // Disambiguate when the same name is used more than once
582
582
  const snapshotName =
583
- checkpointIndex === 0 ? cpName : `${cpName}-${checkpointIndex}`;
584
- checkpointIndex++;
583
+ snapshotIndex === 0 ? cpName : `${cpName}-${snapshotIndex}`;
584
+ snapshotIndex++;
585
585
 
586
586
  const stepsToSnapshot =
587
- checkpoint.length === 1 && checkpoint[0] === '*'
587
+ marker.length === 1 && marker[0] === '*'
588
588
  ? [...this.progress.steps.entries()]
589
589
  .filter(([, s]) => s.status === 'completed')
590
590
  .map(([name]) => name)
591
- : checkpoint.filter(name =>
591
+ : marker.filter(name =>
592
592
  Object.prototype.hasOwnProperty.call(this.realContext, name),
593
593
  );
594
594
 
@@ -653,6 +653,43 @@ export class TestExecutor extends EventEmitter {
653
653
  await Promise.all(stepPromises);
654
654
  }
655
655
 
656
+ /**
657
+ * The invocation's progress list as it stands, for a read at a step boundary (PRD R26/R27).
658
+ *
659
+ * The `$meta` object is the one every step of this run shares โ€” the same object the steps
660
+ * destructure, and the one the handlers they call are handed.
661
+ */
662
+ private _progressList(): IProgressEntry[] | undefined {
663
+ const progress = (this.realContext.$meta as IMeta | undefined)?.progress;
664
+ return Array.isArray(progress) ? progress : undefined;
665
+ }
666
+
667
+ /**
668
+ * What was announced since `seen`, or `undefined` when nothing was (PRD R26/R27).
669
+ *
670
+ * Two shapes, and both are ordinary. A list that still holds what it held is *appended* to,
671
+ * and the difference between the two boundary reads is the step's own progress. A list that
672
+ * no longer begins with those entries was **replaced**: a step that resets it to scope its
673
+ * own assertions (`$meta.progress = []`, which is how a scenario keeps one step's
674
+ * checkpoints out of the next one's) then owns everything in the new list, including when it
675
+ * happens to end up as long as it started.
676
+ *
677
+ * Best-effort attribution, and deliberately so: steps that run in parallel share the one
678
+ * list, so a point announced while two of them were running is reported by whichever
679
+ * boundary read it. It is never lost, and a scenario whose steps are ordered by their
680
+ * dependencies โ€” which is what a test that asserts on progress is โ€” has one at a time.
681
+ */
682
+ private _announcedSince(seen: IProgressEntry[] | undefined): IProgressEntry[] | undefined {
683
+ const progress = this._progressList();
684
+ if (progress === undefined || progress.length === 0) {
685
+ return undefined;
686
+ }
687
+ if (!keeps(progress, seen)) {
688
+ return progress;
689
+ }
690
+ return progress.length > seen.length ? progress.slice(seen.length) : undefined;
691
+ }
692
+
656
693
  /**
657
694
  * Executes a single step function
658
695
  */
@@ -703,8 +740,6 @@ export class TestExecutor extends EventEmitter {
703
740
  // Wrap execution function for potential test context wrapping
704
741
  // When a TAP sub-test context is supplied, assert is augmented with:
705
742
  // assert.snapshot(value, 'name', opts?) โ€” explicit snapshot
706
- // assert.snapshot({mask?: []}) โ€” deferred: snapshot the
707
- // step's return value
708
743
  // assert.snapshot() โ€” deferred, no extra mask
709
744
  // Deferred snapshots are taken after fn() returns, under the step name.
710
745
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -762,6 +797,7 @@ export class TestExecutor extends EventEmitter {
762
797
  this.graph.nodes.get(stepName)!.status = 'running';
763
798
  this.graph.nodes.get(stepName)!.startTime = latency.startedAt;
764
799
 
800
+ const progressBefore = this._progressList();
765
801
  this.emit('step:start', stepName, stepProgress);
766
802
 
767
803
  try {
@@ -823,6 +859,11 @@ export class TestExecutor extends EventEmitter {
823
859
  // Store result in real context
824
860
  this.realContext[stepName] = result;
825
861
 
862
+ // What this step announced, read at the boundary before the event so a
863
+ // listener that writes the step's result file sees it (see `step:end`).
864
+ const announced = this._announcedSince(progressBefore);
865
+ if (announced !== undefined) stepProgress.progress = announced;
866
+
826
867
  // Resolve all promises for this step
827
868
  this.promiseManager.resolveStep(stepName, result);
828
869
 
@@ -847,6 +888,24 @@ export class TestExecutor extends EventEmitter {
847
888
 
848
889
  this.progress.completedSteps++;
849
890
  this.emit('step:end', stepName, stepProgress);
891
+
892
+ // A point becomes a sub-test of the step that announced it, and a branch the
893
+ // sub-test holding the points taken inside it โ€” a checkpoint as a *step* in
894
+ // the report, which is what "checkpoints drive test reporting" claims
895
+ // (PRD R26/R27). Rendered from the same entries `blong-allure` maps into
896
+ // Allure steps, so the two reports cannot describe different shapes.
897
+ //
898
+ // The context is checked the way the snapshot handling checks it, because
899
+ // the queue hands the task its own options where there is no test context:
900
+ // only a context that can nest a test gets progress nested in it.
901
+ if (
902
+ typeof stepTestContext?.test === 'function' &&
903
+ stepProgress.progress !== undefined
904
+ ) {
905
+ for (const node of progressTree(stepProgress.progress)) {
906
+ await reportProgress(stepTestContext, node);
907
+ }
908
+ }
850
909
  } catch (error) {
851
910
  // Handle error
852
911
  latency.completedAt = Date.now();
@@ -871,6 +930,12 @@ export class TestExecutor extends EventEmitter {
871
930
  this.graph.nodes.get(stepName)!.endTime = latency.completedAt;
872
931
  this.graph.nodes.get(stepName)!.error = error as Error;
873
932
 
933
+ // Kept even though the step failed, and especially then: what a failing step
934
+ // announced is the evidence a reader wants, and Allure renders it from here.
935
+ // The tap side leaves it out โ€” sub-tests are not added to a step that threw.
936
+ const announcedOnFailure = this._announcedSince(progressBefore);
937
+ if (announcedOnFailure !== undefined) stepProgress.progress = announcedOnFailure;
938
+
874
939
  this.progress.failedSteps++;
875
940
  this.emit('step:error', stepName, error as Error, stepProgress);
876
941
  this.log?.error?.({err: error}, `step ${stepName} failed`);
@@ -940,7 +1005,7 @@ export class TestExecutor extends EventEmitter {
940
1005
 
941
1006
  for (const step of steps) {
942
1007
  if (Array.isArray(step)) {
943
- // Skip checkpoint markers (string-only arrays) โ€” they are not steps
1008
+ // Skip snapshot markers (string-only arrays) โ€” they are not steps
944
1009
  if (step.every(s => typeof s === 'string')) continue;
945
1010
  // Recursively collect from nested step groups
946
1011
  const nested = this._collectStepNames(step as StepArray);
@@ -1119,3 +1184,21 @@ export class TestExecutor extends EventEmitter {
1119
1184
 
1120
1185
  // Export all types
1121
1186
  export type * from './test-types.js';
1187
+ export {progressTree, reportProgress} from './progress.ts';
1188
+
1189
+ /**
1190
+ * Whether a progress list still holds what it held, entry for entry and by identity.
1191
+ *
1192
+ * Identity rather than equality, because the entries are objects the recorder pushed: a list
1193
+ * that kept them is one that was spread back with additions, and a list whose first entry is a
1194
+ * different object is one that was reset and announced afresh โ€” which is the difference between
1195
+ * a step *adding* to the invocation's progress and *replacing* it.
1196
+ */
1197
+ const keeps = (
1198
+ progress: IProgressEntry[],
1199
+ seen: IProgressEntry[] | undefined,
1200
+ ): seen is IProgressEntry[] =>
1201
+ progress === seen ||
1202
+ (seen !== undefined &&
1203
+ progress.length >= seen.length &&
1204
+ seen.every((entry, at) => progress[at] === entry));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feasibleone/blong-chain",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Parallel testing with automatic dependencies",
5
5
  "keywords": [
6
6
  "blong",
@@ -24,7 +24,7 @@
24
24
  "@types/node": "^24",
25
25
  "tap": "^21.6.3",
26
26
  "typescript": "^6.0.3",
27
- "@feasibleone/blong-dev": "1.4.0"
27
+ "@feasibleone/blong-dev": "1.7.0"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "heft build --clean",
@@ -33,7 +33,7 @@
33
33
  "ci-lint": "blong-dev lint",
34
34
  "ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
35
35
  "ci-test": "blong-dev test '*.test.ts';./test-examples-ci.sh",
36
- "test": "tap *.test.ts",
37
- "test:examples": "node --test dist/examples/*.test.js"
36
+ "test": "blong-dev test '*.test.ts';./test-examples-ci.sh",
37
+ "test:examples": "node --test examples/*.test.ts"
38
38
  }
39
39
  }
package/test-types.ts CHANGED
@@ -49,26 +49,28 @@ export type StepFunction = (
49
49
  ) => unknown | Promise<unknown>;
50
50
 
51
51
  /**
52
- * A snapshot checkpoint โ€” an array of step-name strings placed inside the
53
- * steps array. At runtime, when the executor encounters it the current batch
54
- * is awaited and the listed step results are snapshotted into the TAP context.
52
+ * A snapshot marker โ€” an array of step-name strings placed inside the
53
+ * steps array. At runtime, when the executor encounters it the relevant steps
54
+ * are awaited and their results snapshotted into the TAP context.
55
55
  *
56
56
  * - `['*']` โ€” snapshot ALL completed steps' results into one context object
57
57
  * - `['step1', 'step2']` โ€” snapshot only those specific steps
58
- * - `[]` โ€” sync barrier only, no snapshot (existing behaviour)
58
+ * - `[]` โ€” sync barrier only, no snapshot (the empty array is the spelling;
59
+ * there is no symbol for it, because inventing one would touch every
60
+ * call site for no gain)
59
61
  *
60
62
  * The array may carry an optional `.name` to give the snapshot a stable name:
61
63
  * ```
62
- * const cp = Object.assign(['*'], {name: 'provisioning-complete'});
64
+ * const marker = Object.assign(['*'], {name: 'provisioning-complete'});
63
65
  * ```
64
66
  */
65
- export type CheckpointMarker = string[] & {name?: string};
67
+ export type SnapshotMarker = string[] & {name?: string};
66
68
 
67
69
  /**
68
70
  * Array of test steps. May contain step functions, nested step groups, or
69
- * checkpoint markers (string arrays).
71
+ * snapshot markers (string arrays; `[]` being a sync barrier).
70
72
  */
71
- export type StepArray = (StepFunction | StepArray | CheckpointMarker)[] & {name?: string};
73
+ export type StepArray = (StepFunction | StepArray | SnapshotMarker)[] & {name?: string};
72
74
 
73
75
  /**
74
76
  * Meta information passed through test execution
@@ -76,6 +78,15 @@ export type StepArray = (StepFunction | StepArray | CheckpointMarker)[] & {name?
76
78
  export interface IMeta {
77
79
  /** Optional concurrency limit for parallel step execution */
78
80
  concurrency?: number;
81
+ /**
82
+ * What the invocation has announced so far, in the order it announced it (PRD R26/R27).
83
+ *
84
+ * The same array a framework dispatch keeps on its own `$meta`: the handlers a step calls
85
+ * are handed this object, so whatever they announce while it runs appears here โ€” which is
86
+ * the only place the executor can read a step's progress from, and why it reads it at the
87
+ * step's boundaries.
88
+ */
89
+ progress?: IProgressEntry[];
79
90
  /** Additional metadata properties */
80
91
  [key: string]: unknown;
81
92
  }
@@ -87,6 +98,14 @@ export interface IMeta {
87
98
  export interface ITestFrameworkContext {
88
99
  /** Creates a nested test scope for proper indentation */
89
100
  test: (name: string, fn: (t: unknown) => void | Promise<void>) => unknown;
101
+ /**
102
+ * Writes a comment into the run's output (`# โ€ฆ` in TAP).
103
+ *
104
+ * What a point is reported as: a moment a step passed through is not a test,
105
+ * so it does not deserve a sub-test of its own โ€” but it is what a reader
106
+ * wants to see beside the step, which is what a comment is.
107
+ */
108
+ comment?: (text: string) => void;
90
109
  /** Captures a snapshot of a value under the given name */
91
110
  matchSnapshot?: (value: unknown, name: string) => void;
92
111
  }
@@ -199,6 +218,71 @@ export interface IDependencyEdge {
199
218
  // Progress Tracking Types
200
219
  // ============================================================================
201
220
 
221
+ /**
222
+ * A branch as a progress entry names it (PRD R11/R26).
223
+ *
224
+ * Declared structurally here, matching the framework's own declaration in
225
+ * `core/blong/types.ts` the way {@link IMeta} does, because this package stands at the bottom
226
+ * of the dependency graph with `p-queue` as its only dependency. The names are what a report
227
+ * groups by; the position a log mints for itself is deliberately not here.
228
+ */
229
+ export interface IRegionMark {
230
+ /** What the branch was about, stable across runs. */
231
+ discriminator: string;
232
+ /** Every candidate considered, in evaluation order. */
233
+ candidates: string[];
234
+ /** The branch taken, or `none` when none of them matched. */
235
+ chosen: string;
236
+ }
237
+
238
+ /** A milestone an invocation announced (PRD R26). */
239
+ export interface IProgressPoint {
240
+ /** Which of the two shapes this entry is; one array holds both. */
241
+ kind: 'point';
242
+ /** Stable name of the moment, e.g. `total-calculated`. */
243
+ name: string;
244
+ /** What was true there; kept locally, exactly as a record keeps it. */
245
+ data?: unknown;
246
+ /** When it was announced, in epoch milliseconds. */
247
+ timestamp: number;
248
+ /** The branches it was announced *inside*, outermost first. */
249
+ regions?: IRegionMark[];
250
+ }
251
+
252
+ /** A branch the invocation took, in the same array as the points (PRD R11/R26). */
253
+ export interface IProgressRegion extends IRegionMark {
254
+ /** Which of the two shapes this entry is; one array holds both. */
255
+ kind: 'region';
256
+ /** The values the decision was made from. */
257
+ values: Record<string, unknown>;
258
+ /** The branches this one was itself taken inside, outermost first. */
259
+ regions?: IRegionMark[];
260
+ }
261
+
262
+ /** One entry of an invocation's `progress` list. */
263
+ export type IProgressEntry = IProgressPoint | IProgressRegion;
264
+
265
+ /**
266
+ * One node of the progress tree a report is drawn from (PRD R26/R27).
267
+ *
268
+ * `name` is the display name rather than the raw entry, so both renderers label a branch the
269
+ * same way: `<discriminator> = <chosen>` for a region, the point's own name for a point.
270
+ */
271
+ export interface IProgressNode {
272
+ /** What a report calls it: the point's name, or `<discriminator> = <chosen>`. */
273
+ name: string;
274
+ /** Which of the two shapes it is. */
275
+ kind: 'point' | 'region';
276
+ /**
277
+ * What the point announced, printed beside it when it is small enough to read.
278
+ *
279
+ * A branch carries none: what it has to say is its name.
280
+ */
281
+ data?: Record<string, unknown>;
282
+ /** The points and branches announced inside it, in the order they were announced. */
283
+ children: IProgressNode[];
284
+ }
285
+
202
286
  /**
203
287
  * Overall test execution progress
204
288
  */
@@ -257,6 +341,17 @@ export interface IStepProgress {
257
341
  result?: unknown;
258
342
  /** Error information if step failed */
259
343
  error?: IStepError;
344
+ /**
345
+ * What this step announced while it ran (PRD R26/R27), or absent when it announced
346
+ * nothing.
347
+ *
348
+ * Read from the invocation's own list at the step's boundaries rather than reported by the
349
+ * step: the handlers it called announced into the `$meta` every step shares, so the window
350
+ * is the only thing that attributes a point to the step that made it. A report draws these
351
+ * as the step's nested steps โ€” a point as a step, a branch as the group of the points
352
+ * taken inside it.
353
+ */
354
+ progress?: IProgressEntry[];
260
355
  }
261
356
 
262
357
  /**
@@ -370,7 +465,7 @@ export interface ITestExecutorConfig {
370
465
  log?: ITestLogger;
371
466
  /**
372
467
  * Chain-level mask paths. Applied to ALL snapshot operations in this chain:
373
- * `autoSnapshot`, `assert.snapshot()`, and checkpoint snapshots.
468
+ * `autoSnapshot`, `assert.snapshot()`, and snapshot markers.
374
469
  *
375
470
  * Supports:
376
471
  * - Simple name: `'id'` โ€” masks the `id` field in the snapshotted value