eyeling 1.22.0 → 1.22.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.
@@ -1,5 +1,4 @@
1
1
  {
2
- "$schema": "./delfour.instance.schema.json",
3
2
  "caseName": "Delfour",
4
3
  "retailer": "Delfour",
5
4
  "question": "Is the Delfour self-scanner allowed to use a neutral shopping insight for shopping assistance, and if so what lower-sugar alternative should it suggest?",
@@ -1,5 +1,10 @@
1
1
  package main
2
2
 
3
+ // Delfour is a reference Arcling model written as a small CLI program.
4
+ // It reads delfour.data.json, derives the neutral shopping insight,
5
+ // computes the canonical envelope/hash/HMAC values, and emits either
6
+ // ARC text or a JSON result object.
7
+
3
8
  import (
4
9
  "crypto/hmac"
5
10
  "crypto/sha256"
@@ -13,6 +18,7 @@ import (
13
18
  "time"
14
19
  )
15
20
 
21
+ // Data mirrors the input instance shape from delfour.data.json.
16
22
  type Data struct {
17
23
  CaseName string `json:"caseName"`
18
24
  Retailer string `json:"retailer"`
@@ -77,6 +83,7 @@ type Integrity struct {
77
83
  VerificationMode string `json:"verificationMode"`
78
84
  }
79
85
 
86
+ // Insight is the minimized payload shared with the retailer.
80
87
  type Insight struct {
81
88
  CreatedAt string `json:"createdAt"`
82
89
  ExpiresAt string `json:"expiresAt"`
@@ -191,6 +198,7 @@ func readJSON(path string) (Data, error) {
191
198
  return data, err
192
199
  }
193
200
 
201
+ // validate performs the structural checks that used to live in JSON Schema.
194
202
  func validate(data Data) error {
195
203
  if err := must(data.CaseName != "", "caseName is required"); err != nil {
196
204
  return err
@@ -234,6 +242,7 @@ func validate(data Data) error {
234
242
  return nil
235
243
  }
236
244
 
245
+ // parseTime accepts RFC3339Nano timestamps from the case instance.
237
246
  func parseTime(s string) time.Time {
238
247
  t, err := time.Parse(time.RFC3339Nano, s)
239
248
  if err != nil {
@@ -242,6 +251,7 @@ func parseTime(s string) time.Time {
242
251
  return t
243
252
  }
244
253
 
254
+ // findProduct resolves the scanned or recommended product by its catalog id.
245
255
  func findProduct(data Data, id string) *Product {
246
256
  for i := range data.Catalog {
247
257
  if data.Catalog[i].ID == id {
@@ -251,6 +261,7 @@ func findProduct(data Data, id string) *Product {
251
261
  return nil
252
262
  }
253
263
 
264
+ // deriveInsight strips the household condition down to the neutral shopping insight.
254
265
  func deriveInsight(data Data) Insight {
255
266
  return Insight{
256
267
  CreatedAt: data.Timestamps.CreatedAt,
@@ -266,6 +277,7 @@ func deriveInsight(data Data) Insight {
266
277
  }
267
278
  }
268
279
 
280
+ // derivePolicy builds the companion ODRL-style policy used for governance checks.
269
281
  func derivePolicy(data Data) Policy {
270
282
  return Policy{
271
283
  Duty: Duty{
@@ -299,6 +311,8 @@ func derivePolicy(data Data) Policy {
299
311
  }
300
312
  }
301
313
 
314
+ // canonicalEnvelope returns the exact byte string used for the integrity vector.
315
+ // The field order and the lexical form of threshold (10.0) are intentional.
302
316
  func canonicalEnvelope(insight Insight, policy Policy) string {
303
317
  return fmt.Sprintf(
304
318
  "{\"insight\":{\"createdAt\":\"%s\",\"expiresAt\":\"%s\",\"id\":\"%s\",\"metric\":\"%s\",\"retailer\":\"%s\",\"scopeDevice\":\"%s\",\"scopeEvent\":\"%s\",\"suggestionPolicy\":\"%s\",\"threshold\":10.0,\"type\":\"%s\"},\"policy\":{\"duty\":{\"action\":\"%s\",\"constraint\":{\"leftOperand\":\"%s\",\"operator\":\"%s\",\"rightOperand\":\"%s\"}},\"permission\":{\"action\":\"%s\",\"constraint\":{\"leftOperand\":\"%s\",\"operator\":\"%s\",\"rightOperand\":\"%s\"},\"target\":\"%s\"},\"profile\":\"%s\",\"prohibition\":{\"action\":\"%s\",\"constraint\":{\"leftOperand\":\"%s\",\"operator\":\"%s\",\"rightOperand\":\"%s\"},\"target\":\"%s\"},\"type\":\"%s\"}}",
@@ -348,6 +362,8 @@ func yesNo(v bool) string {
348
362
  return "no"
349
363
  }
350
364
 
365
+ // evaluate runs the full Arcling pipeline: derive facts, select the recommendation,
366
+ // build the envelope, verify integrity values, and render the final report.
351
367
  func evaluate(data Data) (Result, error) {
352
368
  var result Result
353
369
  if err := validate(data); err != nil {
@@ -508,6 +524,8 @@ func evaluate(data Data) (Result, error) {
508
524
  return result, nil
509
525
  }
510
526
 
527
+ // main is a tiny CLI wrapper around evaluate. It defaults to delfour.data.json,
528
+ // prints ARC text, and switches to JSON output when --json is supplied.
511
529
  func main() {
512
530
  inputPath := "delfour.data.json"
513
531
  jsonMode := false
@@ -1,5 +1,4 @@
1
1
  {
2
- "$schema": "./flandor.instance.schema.json",
3
2
  "caseName": "Flandor",
4
3
  "region": "Flanders",
5
4
  "question": "Is the Flemish Economic Resilience Board allowed to use a neutral macro-economic insight for regional stabilization, and if so which package should it activate for Flanders?",
@@ -1,5 +1,9 @@
1
1
  package main
2
2
 
3
+ // Flandor is a reference Arcling model for the regional retooling-pulse case.
4
+ // It evaluates whether a region needs intervention, selects the lowest-cost
5
+ // eligible package, and emits ARC text or a JSON report.
6
+
3
7
  import (
4
8
  "crypto/hmac"
5
9
  "crypto/sha256"
@@ -13,6 +17,7 @@ import (
13
17
  "time"
14
18
  )
15
19
 
20
+ // Data mirrors the input instance shape from flandor.data.json.
16
21
  type Data struct {
17
22
  CaseName string `json:"caseName"`
18
23
  Region string `json:"region"`
@@ -233,6 +238,8 @@ func parseTime(value string) time.Time {
233
238
  return t
234
239
  }
235
240
 
241
+ // stableStringify recursively sorts map keys so the canonical envelope is
242
+ // deterministic across runs and implementations.
236
243
  func stableStringify(value any) string {
237
244
  switch v := value.(type) {
238
245
  case nil:
@@ -260,6 +267,8 @@ func stableStringify(value any) string {
260
267
  }
261
268
  }
262
269
 
270
+ // canonicalValue converts typed structs into generic JSON-like values before
271
+ // stable stringification.
263
272
  func canonicalValue(value any) any {
264
273
  b, _ := json.Marshal(value)
265
274
  var out any
@@ -267,6 +276,7 @@ func canonicalValue(value any) any {
267
276
  return out
268
277
  }
269
278
 
279
+ // validateInstance performs the structural checks for the 4-file bundle.
270
280
  func validateInstance(data Data) error {
271
281
  if err := assertTrue(data.CaseName != "", "caseName is required"); err != nil {
272
282
  return err
@@ -283,6 +293,7 @@ func validateInstance(data Data) error {
283
293
  return nil
284
294
  }
285
295
 
296
+ // countTrue is used for the active-need threshold logic in the spec.
286
297
  func countTrue(values ...bool) int {
287
298
  total := 0
288
299
  for _, value := range values {
@@ -293,6 +304,7 @@ func countTrue(values ...bool) int {
293
304
  return total
294
305
  }
295
306
 
307
+ // The R* helpers map directly to named derivation clauses in flandor.spec.md.
296
308
  func clauseR1ExportWeakness(data Data) bool {
297
309
  for _, cluster := range data.Signals.Clusters {
298
310
  if cluster.ExportOrdersIndex < data.Thresholds.ExportOrdersIndexBelow {
@@ -318,6 +330,7 @@ func clauseR5NeedsRetoolingPulse(data Data, activeNeedCount int) bool {
318
330
  return activeNeedCount >= data.Thresholds.ActiveNeedCountAtLeast
319
331
  }
320
332
 
333
+ // deriveInsight produces the minimized regional signal shared with the recipient.
321
334
  func deriveInsight(data Data) Insight {
322
335
  return Insight{
323
336
  CreatedAt: data.Timestamps.CreatedAt,
@@ -333,6 +346,7 @@ func deriveInsight(data Data) Insight {
333
346
  }
334
347
  }
335
348
 
349
+ // derivePolicy constructs the usage restrictions paired with the insight.
336
350
  func derivePolicy(data Data) Policy {
337
351
  return Policy{
338
352
  Duty: Duty{
@@ -366,12 +380,16 @@ func derivePolicy(data Data) Policy {
366
380
  }
367
381
  }
368
382
 
383
+ // packageCoversAllActiveNeeds checks whether a candidate package addresses every
384
+ // need that is active for this specific instance.
369
385
  func packageCoversAllActiveNeeds(pkg Package, exportWeakness, skillsStrain, gridStress bool) bool {
370
386
  return (!exportWeakness || pkg.CoversExportWeakness) &&
371
387
  (!skillsStrain || pkg.CoversSkillsStrain) &&
372
388
  (!gridStress || pkg.CoversGridStress)
373
389
  }
374
390
 
391
+ // clauseS1EligiblePackages filters to packages that both fit the budget and
392
+ // cover the active needs.
375
393
  func clauseS1EligiblePackages(data Data, exportWeakness, skillsStrain, gridStress bool) []Package {
376
394
  eligible := make([]Package, 0)
377
395
  for _, pkg := range data.Packages {
@@ -383,6 +401,8 @@ func clauseS1EligiblePackages(data Data, exportWeakness, skillsStrain, gridStres
383
401
  return eligible
384
402
  }
385
403
 
404
+ // clauseS2RecommendedPackage applies the tie-breaker: choose the lowest-cost
405
+ // eligible package after sorting by cost.
386
406
  func clauseS2RecommendedPackage(data Data, exportWeakness, skillsStrain, gridStress bool) ([]Package, *Package) {
387
407
  eligible := clauseS1EligiblePackages(data, exportWeakness, skillsStrain, gridStress)
388
408
  if len(eligible) == 0 {
@@ -404,6 +424,8 @@ func clauseG3DutyTimely(data Data) bool {
404
424
  return !parseTime(data.Timestamps.DutyPerformedAt).After(parseTime(data.Timestamps.ExpiresAt))
405
425
  }
406
426
 
427
+ // clauseM1CanonicalEnvelope returns both the structured envelope and the
428
+ // deterministic string hashed/signed by the integrity clauses.
407
429
  func clauseM1CanonicalEnvelope(data Data) (Envelope, string) {
408
430
  envelope := Envelope{Insight: deriveInsight(data), Policy: derivePolicy(data)}
409
431
  return envelope, stableStringify(canonicalValue(envelope))
@@ -437,6 +459,8 @@ func yesNo(value bool) string {
437
459
  return "FAIL"
438
460
  }
439
461
 
462
+ // evaluate computes all derived facts, governance checks, integrity values,
463
+ // and presentation fields expected by flandor.expected.json.
440
464
  func evaluate(data Data) (Result, error) {
441
465
  if err := validateInstance(data); err != nil {
442
466
  return Result{}, err
@@ -593,6 +617,7 @@ func derefIntString(value *int) string {
593
617
  return fmt.Sprintf("%d", *value)
594
618
  }
595
619
 
620
+ // main is the CLI entry point used by the Arcling test runner.
596
621
  func main() {
597
622
  inputPath := "flandor.data.json"
598
623
  jsonMode := false
@@ -1,5 +1,4 @@
1
1
  {
2
- "$schema": "./medior.instance.schema.json",
3
2
  "caseName": "Medior",
4
3
  "region": "Flanders",
5
4
  "question": "Is the discharge coordination team allowed to use a minimal continuity insight after hospital discharge, and if so which package should it activate?",
@@ -1,5 +1,9 @@
1
1
  package main
2
2
 
3
+ // Medior is a reference Arcling model for the care-continuity bundle case.
4
+ // It derives active needs from coarse signals, selects the lowest-cost eligible
5
+ // package, and emits ARC text or a JSON report.
6
+
3
7
  import (
4
8
  "crypto/hmac"
5
9
  "crypto/sha256"
@@ -13,6 +17,7 @@ import (
13
17
  "time"
14
18
  )
15
19
 
20
+ // Data mirrors the input instance shape from medior.data.json.
16
21
  type Data struct {
17
22
  CaseName string `json:"caseName"`
18
23
  Region string `json:"region"`
@@ -234,6 +239,8 @@ func parseTime(value string) time.Time {
234
239
  return t
235
240
  }
236
241
 
242
+ // stableStringify recursively sorts map keys so the canonical envelope stays
243
+ // byte-stable across runs and languages.
237
244
  func stableStringify(value any) string {
238
245
  switch v := value.(type) {
239
246
  case nil:
@@ -261,6 +268,8 @@ func stableStringify(value any) string {
261
268
  }
262
269
  }
263
270
 
271
+ // canonicalValue converts typed structs into generic JSON-like values before
272
+ // stable stringification.
264
273
  func canonicalValue(value any) any {
265
274
  b, _ := json.Marshal(value)
266
275
  var out any
@@ -268,6 +277,7 @@ func canonicalValue(value any) any {
268
277
  return out
269
278
  }
270
279
 
280
+ // validateInstance performs the structural checks for the 4-file bundle.
271
281
  func validateInstance(data Data) error {
272
282
  if err := assertTrue(data.CaseName != "", "caseName is required"); err != nil {
273
283
  return err
@@ -281,6 +291,7 @@ func validateInstance(data Data) error {
281
291
  return nil
282
292
  }
283
293
 
294
+ // countTrue is used for the active-need threshold logic in the spec.
284
295
  func countTrue(values ...bool) int {
285
296
  total := 0
286
297
  for _, value := range values {
@@ -291,6 +302,7 @@ func countTrue(values ...bool) int {
291
302
  return total
292
303
  }
293
304
 
305
+ // The R* helpers map directly to named derivation clauses in medior.spec.md.
294
306
  func clauseR1RenalSafetyConcern(data Data) bool {
295
307
  return data.Signals.Lab.Egfr < data.Thresholds.EgfrBelow
296
308
  }
@@ -315,6 +327,8 @@ func clauseR6NeedsContinuityBundle(data Data, activeNeedCount int) bool {
315
327
  return activeNeedCount >= data.Thresholds.ActiveNeedCountAtLeast
316
328
  }
317
329
 
330
+ // deriveInsight produces the minimized care-coordination signal shared with
331
+ // the recipient.
318
332
  func deriveInsight(data Data) Insight {
319
333
  return Insight{
320
334
  CreatedAt: data.Timestamps.CreatedAt,
@@ -330,6 +344,7 @@ func deriveInsight(data Data) Insight {
330
344
  }
331
345
  }
332
346
 
347
+ // derivePolicy constructs the usage restrictions paired with the insight.
333
348
  func derivePolicy(data Data) Policy {
334
349
  return Policy{
335
350
  Duty: Duty{
@@ -363,6 +378,8 @@ func derivePolicy(data Data) Policy {
363
378
  }
364
379
  }
365
380
 
381
+ // packageCoversAllActiveNeeds checks whether a candidate package addresses every
382
+ // active need in this instance.
366
383
  func packageCoversAllActiveNeeds(pkg Package, renalSafetyConcern, polypharmacyRisk, readmissionHistory, recentDischargeWindow bool) bool {
367
384
  return (!renalSafetyConcern || pkg.CoversRenalSafetyConcern) &&
368
385
  (!polypharmacyRisk || pkg.CoversPolypharmacyRisk) &&
@@ -370,6 +387,8 @@ func packageCoversAllActiveNeeds(pkg Package, renalSafetyConcern, polypharmacyRi
370
387
  (!recentDischargeWindow || pkg.CoversRecentDischargeWindow)
371
388
  }
372
389
 
390
+ // clauseS1EligiblePackages filters to packages that both fit the budget and
391
+ // cover the active needs.
373
392
  func clauseS1EligiblePackages(data Data, renalSafetyConcern, polypharmacyRisk, readmissionHistory, recentDischargeWindow bool) []Package {
374
393
  eligible := make([]Package, 0)
375
394
  for _, pkg := range data.Packages {
@@ -381,6 +400,8 @@ func clauseS1EligiblePackages(data Data, renalSafetyConcern, polypharmacyRisk, r
381
400
  return eligible
382
401
  }
383
402
 
403
+ // clauseS2RecommendedPackage applies the tie-breaker: choose the lowest-cost
404
+ // eligible package after sorting by cost.
384
405
  func clauseS2RecommendedPackage(data Data, renalSafetyConcern, polypharmacyRisk, readmissionHistory, recentDischargeWindow bool) ([]Package, *Package) {
385
406
  eligible := clauseS1EligiblePackages(data, renalSafetyConcern, polypharmacyRisk, readmissionHistory, recentDischargeWindow)
386
407
  if len(eligible) == 0 {
@@ -402,6 +423,8 @@ func clauseG3DutyTimely(data Data) bool {
402
423
  return !parseTime(data.Timestamps.DutyPerformedAt).After(parseTime(data.Timestamps.ExpiresAt))
403
424
  }
404
425
 
426
+ // clauseM1CanonicalEnvelope returns both the structured envelope and the
427
+ // deterministic string hashed/signed by the integrity clauses.
405
428
  func clauseM1CanonicalEnvelope(data Data) (Envelope, string) {
406
429
  envelope := Envelope{Insight: deriveInsight(data), Policy: derivePolicy(data)}
407
430
  return envelope, stableStringify(canonicalValue(envelope))
@@ -435,6 +458,8 @@ func yesNo(value bool) string {
435
458
  return "FAIL"
436
459
  }
437
460
 
461
+ // evaluate computes all derived facts, governance checks, integrity values,
462
+ // and presentation fields expected by medior.expected.json.
438
463
  func evaluate(data Data) (Result, error) {
439
464
  if err := validateInstance(data); err != nil {
440
465
  return Result{}, err
@@ -589,6 +614,7 @@ func derefIntString(value *int) string {
589
614
  return fmt.Sprintf("%d", *value)
590
615
  }
591
616
 
617
+ // main is the CLI entry point used by the Arcling test runner.
592
618
  func main() {
593
619
  inputPath := "medior.data.json"
594
620
  jsonMode := false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.22.0",
3
+ "version": "1.22.1",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -6,22 +6,30 @@ const assert = require('node:assert/strict');
6
6
  const { execFileSync } = require('node:child_process');
7
7
 
8
8
  const TTY = process.stdout.isTTY;
9
- const C = TTY ? { g: '', r: '', y: '', dim: '', n: '' } : { g: '', r: '', y: '', dim: '', n: '' };
10
- const msTag = (ms) => `${C.dim}(${ms} ms)${C.n}`;
9
+ const C = TTY
10
+ ? { g: '\u001b[32m', r: '\u001b[31m', y: '\u001b[33m', dim: '\u001b[2m', n: '\u001b[0m' }
11
+ : { g: '', r: '', y: '', dim: '', n: '' };
12
+
13
+ const ROOT = path.resolve(__dirname, '..');
14
+ const ARCLING_DIR = path.join(ROOT, 'examples', 'arcling');
15
+ const BIN_DIR_NAME = '.arcling-bin';
16
+
17
+ function msTag(ms) {
18
+ return `${C.dim}(${ms} ms)${C.n}`;
19
+ }
11
20
 
12
21
  function ok(msg) {
13
22
  console.log(`${C.g}OK ${C.n} ${msg}`);
14
23
  }
24
+
15
25
  function info(msg) {
16
26
  console.log(`${C.y}==${C.n} ${msg}`);
17
27
  }
28
+
18
29
  function fail(msg) {
19
30
  console.error(`${C.r}FAIL${C.n} ${msg}`);
20
31
  }
21
32
 
22
- const ROOT = path.resolve(__dirname, '..');
23
- const ARCLING_DIR = path.join(ROOT, 'examples', 'arcling');
24
-
25
33
  function isDirectory(p) {
26
34
  try {
27
35
  return fs.statSync(p).isDirectory();
@@ -71,12 +79,41 @@ function findCaseFiles(caseDir) {
71
79
  return { base, modelPath, dataPath, expectedPath };
72
80
  }
73
81
 
74
- function runModelJson(modelPath, dataPath) {
82
+ function binaryExtension() {
83
+ return process.platform === 'win32' ? '.exe' : '';
84
+ }
85
+
86
+ function binaryPathFor(caseDir, base) {
87
+ return path.join(caseDir, BIN_DIR_NAME, `${base}.model${binaryExtension()}`);
88
+ }
89
+
90
+ function ensureGoBinary(modelPath, caseDir, base) {
91
+ const outDir = path.join(caseDir, BIN_DIR_NAME);
92
+ const outPath = binaryPathFor(caseDir, base);
93
+
94
+ fs.mkdirSync(outDir, { recursive: true });
95
+
96
+ const modelStat = fs.statSync(modelPath);
97
+ const needsBuild = !fs.existsSync(outPath) || fs.statSync(outPath).mtimeMs < modelStat.mtimeMs;
98
+
99
+ if (needsBuild) {
100
+ execFileSync('go', ['build', '-o', outPath, modelPath], {
101
+ cwd: caseDir,
102
+ stdio: ['ignore', 'pipe', 'pipe'],
103
+ encoding: 'utf8',
104
+ });
105
+ }
106
+
107
+ return outPath;
108
+ }
109
+
110
+ function runModelJson(modelPath, dataPath, caseDir, base) {
75
111
  const ext = path.extname(modelPath);
76
112
 
77
113
  if (ext === '.go') {
78
- const stdout = execFileSync('go', ['run', modelPath, dataPath, '--json'], {
79
- cwd: path.dirname(modelPath),
114
+ const binaryPath = ensureGoBinary(modelPath, caseDir, base);
115
+ const stdout = execFileSync(binaryPath, [dataPath, '--json'], {
116
+ cwd: caseDir,
80
117
  encoding: 'utf8',
81
118
  stdio: ['ignore', 'pipe', 'pipe'],
82
119
  });
@@ -85,7 +122,7 @@ function runModelJson(modelPath, dataPath) {
85
122
 
86
123
  if (ext === '.mjs') {
87
124
  const stdout = execFileSync(process.execPath, [modelPath, dataPath, '--json'], {
88
- cwd: path.dirname(modelPath),
125
+ cwd: caseDir,
89
126
  encoding: 'utf8',
90
127
  stdio: ['ignore', 'pipe', 'pipe'],
91
128
  });
@@ -104,15 +141,14 @@ function assertArcTextShape(arcText, label) {
104
141
 
105
142
  async function runCase(caseDir) {
106
143
  const { base, modelPath, dataPath, expectedPath } = findCaseFiles(caseDir);
107
- const data = readJson(dataPath);
108
144
  const expected = readJson(expectedPath);
109
- const actual = runModelJson(modelPath, dataPath);
145
+ const actual = runModelJson(modelPath, dataPath, caseDir, base);
110
146
 
111
147
  assert.equal(actual.allChecksPass, true, `${base}: expected allChecksPass === true`);
112
148
  assertArcTextShape(actual.arcText, base);
113
149
  assert.deepStrictEqual(actual, expected, `${base}: actual result does not match expected JSON`);
114
150
 
115
- return { base, caseName: data.caseName, modelPath };
151
+ return { base, modelPath };
116
152
  }
117
153
 
118
154
  async function main() {
@@ -130,15 +166,14 @@ async function main() {
130
166
  for (let i = 0; i < caseDirs.length; i += 1) {
131
167
  const start = Date.now();
132
168
  const caseDir = caseDirs[i];
133
- const n = i + 1;
134
169
  const label = path.basename(caseDir);
135
170
 
136
171
  try {
137
172
  await runCase(caseDir);
138
173
  passed += 1;
139
- ok(`${n}. ${label} ${msTag(Date.now() - start)}`);
174
+ ok(`${i + 1}. ${label} ${msTag(Date.now() - start)}`);
140
175
  } catch (error) {
141
- fail(`${n}. ${label} ${msTag(Date.now() - start)}`);
176
+ fail(`${i + 1}. ${label} ${msTag(Date.now() - start)}`);
142
177
  fail(error.stack || String(error));
143
178
  process.exit(2);
144
179
  }