@x12i/memorix-metadata 3.2.3 → 3.3.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/dist/validate.js CHANGED
@@ -142,6 +142,26 @@ function validateObjectType(raw, path, issues) {
142
142
  requireNonEmptyString(o.contentTypes[i]?.id, `${path}.contentTypes[${i}].id`, issues);
143
143
  }
144
144
  }
145
+ if (o.fields !== undefined) {
146
+ if (!Array.isArray(o.fields)) {
147
+ issues.push(issue(`${path}.fields`, "META-TYPE", `${path}.fields must be an array`));
148
+ }
149
+ else {
150
+ for (let i = 0; i < o.fields.length; i++) {
151
+ const f = o.fields[i];
152
+ const fp = `${path}.fields[${i}]`;
153
+ if (!f || typeof f !== "object") {
154
+ issues.push(issue(fp, "META-TYPE", `${fp} must be an object`));
155
+ continue;
156
+ }
157
+ requireNonEmptyString(f.path, `${fp}.path`, issues);
158
+ requireNonEmptyString(f.valueType, `${fp}.valueType`, issues);
159
+ if (f.enum !== undefined) {
160
+ validateEnumList(f.enum, `${fp}.enum`, issues);
161
+ }
162
+ }
163
+ }
164
+ }
145
165
  }
146
166
  function validateAbstractType(raw, path, issues) {
147
167
  if (!raw || typeof raw !== "object") {
@@ -167,6 +187,9 @@ function validateAbstractType(raw, path, issues) {
167
187
  const p = a.properties[i];
168
188
  requireNonEmptyString(p?.id, `${path}.properties[${i}].id`, issues);
169
189
  requireNonEmptyString(p?.valueType, `${path}.properties[${i}].valueType`, issues);
190
+ if (p?.enum !== undefined) {
191
+ validateEnumList(p.enum, `${path}.properties[${i}].enum`, issues);
192
+ }
170
193
  }
171
194
  }
172
195
  }
@@ -331,6 +354,294 @@ function validateWrite(raw, path, issues) {
331
354
  requireNonEmptyString(w.objectType, `${path}.objectType`, issues);
332
355
  requireNonEmptyString(w.contentType, `${path}.contentType`, issues);
333
356
  }
357
+ function validateEnumList(value, path, issues) {
358
+ if (!Array.isArray(value) || value.length === 0) {
359
+ issues.push(issue(path, "META-ENUM", `${path} must be a non-empty string array`));
360
+ return;
361
+ }
362
+ const seen = new Set();
363
+ for (let i = 0; i < value.length; i++) {
364
+ const v = value[i];
365
+ if (typeof v !== "string" || !v.trim()) {
366
+ issues.push(issue(`${path}[${i}]`, "META-ENUM", `${path}[${i}] must be a non-empty string (empty string is never a vocabulary member)`));
367
+ continue;
368
+ }
369
+ if (seen.has(v)) {
370
+ issues.push(issue(`${path}[${i}]`, "META-DUP", `${path} duplicate value "${v}"`));
371
+ }
372
+ seen.add(v);
373
+ }
374
+ }
375
+ function requirePredicate(raw, path, issues) {
376
+ if (!raw || typeof raw !== "object") {
377
+ issues.push(issue(path, "META-TYPE", `${path} must be an object`));
378
+ return;
379
+ }
380
+ const p = raw;
381
+ requireNonEmptyString(p.property, `${path}.property`, issues);
382
+ if (!("equals" in p)) {
383
+ issues.push(issue(`${path}.equals`, "META-REQUIRED", `${path}.equals is required`));
384
+ }
385
+ }
386
+ function validateAnalytics(raw, path, issues) {
387
+ if (!raw || typeof raw !== "object") {
388
+ issues.push(issue(path, "META-TYPE", `${path} must be an object`));
389
+ return;
390
+ }
391
+ const a = raw;
392
+ requireNonEmptyString(a.id, `${path}.id`, issues);
393
+ requireNonEmptyString(a.name, `${path}.name`, issues);
394
+ if (!Array.isArray(a.scope) || a.scope.length !== 2) {
395
+ issues.push(issue(`${path}.scope`, "META-ANALYTICS-SCOPE", `${path}.scope must be an ordered pair of two properties`));
396
+ }
397
+ else {
398
+ for (let i = 0; i < 2; i++) {
399
+ const s = a.scope[i];
400
+ const sp = `${path}.scope[${i}]`;
401
+ if (!s || typeof s !== "object") {
402
+ issues.push(issue(sp, "META-TYPE", `${sp} must be an object`));
403
+ continue;
404
+ }
405
+ requireNonEmptyString(s.key, `${sp}.key`, issues);
406
+ requireNonEmptyString(s.abstractType, `${sp}.abstractType`, issues);
407
+ requireNonEmptyString(s.property, `${sp}.property`, issues);
408
+ }
409
+ }
410
+ if (a.clock !== undefined) {
411
+ if (!a.clock || typeof a.clock !== "object") {
412
+ issues.push(issue(`${path}.clock`, "META-TYPE", `${path}.clock must be an object`));
413
+ }
414
+ else {
415
+ requireNonEmptyString(a.clock.abstractType, `${path}.clock.abstractType`, issues);
416
+ requireNonEmptyString(a.clock.property, `${path}.clock.property`, issues);
417
+ }
418
+ }
419
+ if (!a.target || typeof a.target !== "object") {
420
+ issues.push(issue(`${path}.target`, "META-REQUIRED", `${path}.target is required`));
421
+ }
422
+ else {
423
+ requireNonEmptyString(a.target.objectType, `${path}.target.objectType`, issues);
424
+ requireNonEmptyString(a.target.contentType, `${path}.target.contentType`, issues);
425
+ requireNonEmptyString(a.target.recordId, `${path}.target.recordId`, issues);
426
+ }
427
+ if (a.history !== undefined) {
428
+ if (!a.history || typeof a.history !== "object") {
429
+ issues.push(issue(`${path}.history`, "META-TYPE", `${path}.history must be an object`));
430
+ }
431
+ else {
432
+ requireNonEmptyString(a.history.recordId, `${path}.history.recordId`, issues);
433
+ if (a.history.limit !== undefined) {
434
+ if (typeof a.history.limit !== "number" || !Number.isInteger(a.history.limit) || a.history.limit < 1) {
435
+ issues.push(issue(`${path}.history.limit`, "META-TYPE", `${path}.history.limit must be a positive integer`));
436
+ }
437
+ }
438
+ }
439
+ }
440
+ if (a.measures !== undefined) {
441
+ if (!Array.isArray(a.measures)) {
442
+ issues.push(issue(`${path}.measures`, "META-TYPE", `${path}.measures must be an array`));
443
+ }
444
+ else {
445
+ requireUniqueIds(a.measures.map((m) => ({ id: m.key })), `${path}.measures`, issues);
446
+ for (let i = 0; i < a.measures.length; i++) {
447
+ const m = a.measures[i];
448
+ const mp = `${path}.measures[${i}]`;
449
+ requireNonEmptyString(m?.key, `${mp}.key`, issues);
450
+ requireNonEmptyString(m?.abstractType, `${mp}.abstractType`, issues);
451
+ if (!m?.kind ||
452
+ !["count", "distinct", "sum", "mean", "max"].includes(m.kind)) {
453
+ issues.push(issue(`${mp}.kind`, "META-ANALYTICS", `${mp}.kind must be count|distinct|sum|mean|max`));
454
+ }
455
+ if (m?.kind && m.kind !== "count" && !(typeof m.property === "string" && m.property.trim())) {
456
+ issues.push(issue(`${mp}.property`, "META-REQUIRED", `${mp}.property is required for kind ${m.kind}`));
457
+ }
458
+ if (m?.where !== undefined)
459
+ requirePredicate(m.where, `${mp}.where`, issues);
460
+ }
461
+ }
462
+ }
463
+ if (a.groups !== undefined) {
464
+ if (!Array.isArray(a.groups)) {
465
+ issues.push(issue(`${path}.groups`, "META-TYPE", `${path}.groups must be an array`));
466
+ }
467
+ else {
468
+ for (let i = 0; i < a.groups.length; i++) {
469
+ const g = a.groups[i];
470
+ const gp = `${path}.groups[${i}]`;
471
+ requireNonEmptyString(g?.key, `${gp}.key`, issues);
472
+ requireNonEmptyString(g?.abstractType, `${gp}.abstractType`, issues);
473
+ requireNonEmptyString(g?.property, `${gp}.property`, issues);
474
+ if (!Array.isArray(g?.buckets) || g.buckets.length === 0) {
475
+ issues.push(issue(`${gp}.buckets`, "META-REQUIRED", `${gp}.buckets must be a non-empty array`));
476
+ }
477
+ else {
478
+ validateEnumList(g.buckets, `${gp}.buckets`, issues);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ if (a.ratios !== undefined) {
484
+ if (!Array.isArray(a.ratios)) {
485
+ issues.push(issue(`${path}.ratios`, "META-TYPE", `${path}.ratios must be an array`));
486
+ }
487
+ else {
488
+ for (let i = 0; i < a.ratios.length; i++) {
489
+ const r = a.ratios[i];
490
+ const rp = `${path}.ratios[${i}]`;
491
+ requireNonEmptyString(r?.key, `${rp}.key`, issues);
492
+ requireNonEmptyString(r?.numerator, `${rp}.numerator`, issues);
493
+ requireNonEmptyString(r?.denominator, `${rp}.denominator`, issues);
494
+ }
495
+ }
496
+ }
497
+ if (a.splits !== undefined) {
498
+ if (!Array.isArray(a.splits)) {
499
+ issues.push(issue(`${path}.splits`, "META-TYPE", `${path}.splits must be an array`));
500
+ }
501
+ else {
502
+ for (let i = 0; i < a.splits.length; i++) {
503
+ const s = a.splits[i];
504
+ const sp = `${path}.splits[${i}]`;
505
+ requireNonEmptyString(s?.key, `${sp}.key`, issues);
506
+ requireNonEmptyString(s?.abstractType, `${sp}.abstractType`, issues);
507
+ requireNonEmptyString(s?.property, `${sp}.property`, issues);
508
+ requireNonEmptyString(s?.totalKey, `${sp}.totalKey`, issues);
509
+ requireNonEmptyString(s?.subsetKey, `${sp}.subsetKey`, issues);
510
+ if (!Array.isArray(s?.buckets) || s.buckets.length === 0) {
511
+ issues.push(issue(`${sp}.buckets`, "META-REQUIRED", `${sp}.buckets must be a non-empty array`));
512
+ }
513
+ else {
514
+ validateEnumList(s.buckets, `${sp}.buckets`, issues);
515
+ }
516
+ if (s?.subset !== undefined)
517
+ requirePredicate(s.subset, `${sp}.subset`, issues);
518
+ else
519
+ issues.push(issue(`${sp}.subset`, "META-REQUIRED", `${sp}.subset is required`));
520
+ }
521
+ }
522
+ }
523
+ if (a.unwinds !== undefined) {
524
+ if (!Array.isArray(a.unwinds)) {
525
+ issues.push(issue(`${path}.unwinds`, "META-TYPE", `${path}.unwinds must be an array`));
526
+ }
527
+ else {
528
+ for (let i = 0; i < a.unwinds.length; i++) {
529
+ const u = a.unwinds[i];
530
+ const up = `${path}.unwinds[${i}]`;
531
+ requireNonEmptyString(u?.key, `${up}.key`, issues);
532
+ requireNonEmptyString(u?.abstractType, `${up}.abstractType`, issues);
533
+ requireNonEmptyString(u?.arrayProperty, `${up}.arrayProperty`, issues);
534
+ if (!Array.isArray(u?.columns) || u.columns.length === 0) {
535
+ issues.push(issue(`${up}.columns`, "META-REQUIRED", `${up}.columns must be a non-empty array`));
536
+ }
537
+ else {
538
+ for (let j = 0; j < u.columns.length; j++) {
539
+ requireNonEmptyString(u.columns[j]?.key, `${up}.columns[${j}].key`, issues);
540
+ requireNonEmptyString(u.columns[j]?.path, `${up}.columns[${j}].path`, issues);
541
+ }
542
+ }
543
+ }
544
+ }
545
+ }
546
+ if (a.leaders !== undefined) {
547
+ if (!Array.isArray(a.leaders)) {
548
+ issues.push(issue(`${path}.leaders`, "META-TYPE", `${path}.leaders must be an array`));
549
+ }
550
+ else {
551
+ for (let i = 0; i < a.leaders.length; i++) {
552
+ const l = a.leaders[i];
553
+ const lp = `${path}.leaders[${i}]`;
554
+ requireNonEmptyString(l?.key, `${lp}.key`, issues);
555
+ requireNonEmptyString(l?.abstractType, `${lp}.abstractType`, issues);
556
+ requireNonEmptyString(l?.idProperty, `${lp}.idProperty`, issues);
557
+ requireNonEmptyString(l?.labelProperty, `${lp}.labelProperty`, issues);
558
+ if (!Array.isArray(l?.sort) || l.sort.length === 0) {
559
+ issues.push(issue(`${lp}.sort`, "META-REQUIRED", `${lp}.sort must be a non-empty array`));
560
+ }
561
+ else {
562
+ for (let j = 0; j < l.sort.length; j++) {
563
+ requireNonEmptyString(l.sort[j]?.property, `${lp}.sort[${j}].property`, issues);
564
+ }
565
+ }
566
+ }
567
+ }
568
+ }
569
+ if (a.coverage !== undefined) {
570
+ if (!Array.isArray(a.coverage)) {
571
+ issues.push(issue(`${path}.coverage`, "META-TYPE", `${path}.coverage must be an array`));
572
+ }
573
+ else {
574
+ for (let i = 0; i < a.coverage.length; i++) {
575
+ const c = a.coverage[i];
576
+ const cp = `${path}.coverage[${i}]`;
577
+ requireNonEmptyString(c?.key, `${cp}.key`, issues);
578
+ requireNonEmptyString(c?.abstractType, `${cp}.abstractType`, issues);
579
+ if (typeof c?.expected === "number") {
580
+ if (!Number.isFinite(c.expected) || c.expected < 0) {
581
+ issues.push(issue(`${cp}.expected`, "META-TYPE", `${cp}.expected must be a non-negative number`));
582
+ }
583
+ }
584
+ else if (c?.expected && typeof c.expected === "object" && "measure" in c.expected) {
585
+ requireNonEmptyString(c.expected.measure, `${cp}.expected.measure`, issues);
586
+ }
587
+ else {
588
+ issues.push(issue(`${cp}.expected`, "META-REQUIRED", `${cp}.expected must be a number or { measure }`));
589
+ }
590
+ if (c?.where !== undefined)
591
+ requirePredicate(c.where, `${cp}.where`, issues);
592
+ }
593
+ }
594
+ }
595
+ if (a.deltas !== undefined) {
596
+ if (!Array.isArray(a.deltas)) {
597
+ issues.push(issue(`${path}.deltas`, "META-TYPE", `${path}.deltas must be an array`));
598
+ }
599
+ else {
600
+ for (let i = 0; i < a.deltas.length; i++) {
601
+ requireNonEmptyString(a.deltas[i]?.key, `${path}.deltas[${i}].key`, issues);
602
+ requireNonEmptyString(a.deltas[i]?.measure, `${path}.deltas[${i}].measure`, issues);
603
+ }
604
+ }
605
+ }
606
+ if (a.transitions !== undefined) {
607
+ if (!Array.isArray(a.transitions)) {
608
+ issues.push(issue(`${path}.transitions`, "META-TYPE", `${path}.transitions must be an array`));
609
+ }
610
+ else {
611
+ for (let i = 0; i < a.transitions.length; i++) {
612
+ const t = a.transitions[i];
613
+ const tp = `${path}.transitions[${i}]`;
614
+ requireNonEmptyString(t?.key, `${tp}.key`, issues);
615
+ requireNonEmptyString(t?.abstractType, `${tp}.abstractType`, issues);
616
+ requireNonEmptyString(t?.property, `${tp}.property`, issues);
617
+ }
618
+ }
619
+ }
620
+ if (a.trends !== undefined) {
621
+ if (!Array.isArray(a.trends)) {
622
+ issues.push(issue(`${path}.trends`, "META-TYPE", `${path}.trends must be an array`));
623
+ }
624
+ else {
625
+ for (let i = 0; i < a.trends.length; i++) {
626
+ const t = a.trends[i];
627
+ const tp = `${path}.trends[${i}]`;
628
+ requireNonEmptyString(t?.key, `${tp}.key`, issues);
629
+ requireNonEmptyString(t?.label, `${tp}.label`, issues);
630
+ requireNonEmptyString(t?.measure, `${tp}.measure`, issues);
631
+ if (!Array.isArray(t?.windows) || t.windows.length === 0) {
632
+ issues.push(issue(`${tp}.windows`, "META-REQUIRED", `${tp}.windows must be a non-empty array`));
633
+ }
634
+ else {
635
+ for (let j = 0; j < t.windows.length; j++) {
636
+ if (!["snapshots", "7d", "30d"].includes(t.windows[j])) {
637
+ issues.push(issue(`${tp}.windows[${j}]`, "META-ANALYTICS", `${tp}.windows[${j}] must be snapshots|7d|30d`));
638
+ }
639
+ }
640
+ }
641
+ }
642
+ }
643
+ }
644
+ }
334
645
  function validateSource(raw, path, issues) {
335
646
  if (!raw || typeof raw !== "object") {
336
647
  issues.push(issue(path, "META-TYPE", `${path} must be an object`));
@@ -708,6 +1019,119 @@ function validateReferenceIntegrity(pack, issues) {
708
1019
  issues.push(issue(`writes/${w.id}.objectType`, "META-REF-OBJECT", `write references unknown object type "${w.objectType}"`));
709
1020
  }
710
1021
  }
1022
+ const absProps = new Map();
1023
+ for (const abs of pack.abstractTypes) {
1024
+ const props = new Map();
1025
+ for (const p of abs.properties ?? []) {
1026
+ if (p?.id)
1027
+ props.set(p.id, { enum: p.enum });
1028
+ }
1029
+ absProps.set(abs.id, props);
1030
+ }
1031
+ const measureKeysByAnalytics = new Map();
1032
+ for (const an of pack.analytics ?? []) {
1033
+ const measureKeys = new Set((an.measures ?? []).map((m) => m.key).filter(Boolean));
1034
+ measureKeysByAnalytics.set(an.id, measureKeys);
1035
+ const checkAbsProp = (abstractType, property, path) => {
1036
+ if (!abstractIds.has(abstractType)) {
1037
+ issues.push(issue(path, "META-REF-ABSTRACT", `analytics references unknown abstract "${abstractType}"`));
1038
+ return null;
1039
+ }
1040
+ const props = absProps.get(abstractType);
1041
+ if (props && props.size > 0 && !props.has(property)) {
1042
+ issues.push(issue(path, "META-REF-PROP", `analytics references unknown property "${property}" on "${abstractType}"`));
1043
+ return null;
1044
+ }
1045
+ return props?.get(property) ?? null;
1046
+ };
1047
+ for (const s of an.scope ?? []) {
1048
+ if (s?.abstractType && s?.property) {
1049
+ checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.scope`);
1050
+ }
1051
+ }
1052
+ if (an.clock?.abstractType && an.clock.property) {
1053
+ checkAbsProp(an.clock.abstractType, an.clock.property, `analytics/${an.id}.clock`);
1054
+ }
1055
+ if (an.target?.objectType && !objectTypeIds.has(an.target.objectType)) {
1056
+ issues.push(issue(`analytics/${an.id}.target.objectType`, "META-REF-OBJECT", `analytics target references unknown object type "${an.target.objectType}"`));
1057
+ }
1058
+ for (const m of an.measures ?? []) {
1059
+ if (m.abstractType && !abstractIds.has(m.abstractType)) {
1060
+ issues.push(issue(`analytics/${an.id}.measures/${m.key}`, "META-REF-ABSTRACT", `analytics references unknown abstract "${m.abstractType}"`));
1061
+ }
1062
+ if (m.property && m.abstractType) {
1063
+ checkAbsProp(m.abstractType, m.property, `analytics/${an.id}.measures/${m.key}.property`);
1064
+ }
1065
+ if (m.where?.property && m.abstractType) {
1066
+ checkAbsProp(m.abstractType, m.where.property, `analytics/${an.id}.measures/${m.key}.where`);
1067
+ }
1068
+ }
1069
+ for (const g of an.groups ?? []) {
1070
+ const prop = g.abstractType && g.property
1071
+ ? checkAbsProp(g.abstractType, g.property, `analytics/${an.id}.groups/${g.key}`)
1072
+ : null;
1073
+ if (prop?.enum && Array.isArray(g.buckets)) {
1074
+ for (const b of g.buckets) {
1075
+ if (!prop.enum.includes(b)) {
1076
+ issues.push(issue(`analytics/${an.id}.groups/${g.key}.buckets`, "META-ENUM-BUCKET", `bucket "${b}" is not in enum for ${g.abstractType}.${g.property}`));
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ for (const s of an.splits ?? []) {
1082
+ const prop = s.abstractType && s.property
1083
+ ? checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.splits/${s.key}`)
1084
+ : null;
1085
+ if (prop?.enum && Array.isArray(s.buckets)) {
1086
+ for (const b of s.buckets) {
1087
+ if (!prop.enum.includes(b)) {
1088
+ issues.push(issue(`analytics/${an.id}.splits/${s.key}.buckets`, "META-ENUM-BUCKET", `bucket "${b}" is not in enum for ${s.abstractType}.${s.property}`));
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ for (const u of an.unwinds ?? []) {
1094
+ if (u.abstractType && u.arrayProperty) {
1095
+ checkAbsProp(u.abstractType, u.arrayProperty, `analytics/${an.id}.unwinds/${u.key}`);
1096
+ }
1097
+ }
1098
+ for (const l of an.leaders ?? []) {
1099
+ if (l.abstractType) {
1100
+ checkAbsProp(l.abstractType, l.idProperty, `analytics/${an.id}.leaders/${l.key}.idProperty`);
1101
+ checkAbsProp(l.abstractType, l.labelProperty, `analytics/${an.id}.leaders/${l.key}.labelProperty`);
1102
+ }
1103
+ }
1104
+ for (const c of an.coverage ?? []) {
1105
+ if (c.abstractType && !abstractIds.has(c.abstractType)) {
1106
+ issues.push(issue(`analytics/${an.id}.coverage/${c.key}`, "META-REF-ABSTRACT", `analytics references unknown abstract "${c.abstractType}"`));
1107
+ }
1108
+ if (c.where?.property && c.abstractType) {
1109
+ checkAbsProp(c.abstractType, c.where.property, `analytics/${an.id}.coverage/${c.key}.where`);
1110
+ }
1111
+ if (typeof c.expected === "object" && c.expected?.measure && !measureKeys.has(c.expected.measure)) {
1112
+ issues.push(issue(`analytics/${an.id}.coverage/${c.key}.expected`, "META-REF-MEASURE", `coverage expected measure "${c.expected.measure}" unknown`));
1113
+ }
1114
+ }
1115
+ for (const d of an.deltas ?? []) {
1116
+ if (d.measure && !measureKeys.has(d.measure)) {
1117
+ issues.push(issue(`analytics/${an.id}.deltas/${d.key}`, "META-REF-MEASURE", `delta references unknown measure "${d.measure}"`));
1118
+ }
1119
+ }
1120
+ for (const t of an.trends ?? []) {
1121
+ if (t.measure && !measureKeys.has(t.measure)) {
1122
+ issues.push(issue(`analytics/${an.id}.trends/${t.key}`, "META-REF-MEASURE", `trend references unknown measure "${t.measure}"`));
1123
+ }
1124
+ }
1125
+ for (const r of an.ratios ?? []) {
1126
+ if (r.numerator && !measureKeys.has(r.numerator)) {
1127
+ issues.push(issue(`analytics/${an.id}.ratios/${r.key}`, "META-REF-MEASURE", `ratio numerator "${r.numerator}" unknown`));
1128
+ }
1129
+ if (r.denominator && !measureKeys.has(r.denominator)) {
1130
+ issues.push(issue(`analytics/${an.id}.ratios/${r.key}`, "META-REF-MEASURE", `ratio denominator "${r.denominator}" unknown`));
1131
+ }
1132
+ }
1133
+ }
1134
+ void measureKeysByAnalytics;
711
1135
  for (const p of pack.pipelines) {
712
1136
  for (const step of p.steps) {
713
1137
  if (step.sourceId && !sourceIds.has(step.sourceId)) {
@@ -823,6 +1247,7 @@ export function validateMetadataPack(input, opts) {
823
1247
  ["services", pack.services, validateService],
824
1248
  ["pipelines", pack.pipelines, validatePipeline],
825
1249
  ["schedules", pack.schedules, validateSchedule],
1250
+ ["analytics", pack.analytics ?? [], validateAnalytics],
826
1251
  ["kindDeclarations", pack.kindDeclarations ?? [], validateKindDeclaration],
827
1252
  ];
828
1253
  for (const [name, value, validator] of arrays) {