@duckcodeailabs/dql-agent 1.9.5 → 1.10.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 (94) hide show
  1. package/dist/agent-run-engine.d.ts +13 -0
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +65 -2
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/analytical-compatibility.d.ts +61 -0
  6. package/dist/analytical-compatibility.d.ts.map +1 -0
  7. package/dist/analytical-compatibility.js +515 -0
  8. package/dist/analytical-compatibility.js.map +1 -0
  9. package/dist/analytical-execution-graph.d.ts +174 -0
  10. package/dist/analytical-execution-graph.d.ts.map +1 -0
  11. package/dist/analytical-execution-graph.js +599 -0
  12. package/dist/analytical-execution-graph.js.map +1 -0
  13. package/dist/analytical-failure-repair.d.ts +91 -0
  14. package/dist/analytical-failure-repair.d.ts.map +1 -0
  15. package/dist/analytical-failure-repair.js +325 -0
  16. package/dist/analytical-failure-repair.js.map +1 -0
  17. package/dist/analytical-frame.d.ts +19 -0
  18. package/dist/analytical-frame.d.ts.map +1 -0
  19. package/dist/analytical-frame.js +299 -0
  20. package/dist/analytical-frame.js.map +1 -0
  21. package/dist/analytical-period-resolution.d.ts +53 -0
  22. package/dist/analytical-period-resolution.d.ts.map +1 -0
  23. package/dist/analytical-period-resolution.js +218 -0
  24. package/dist/analytical-period-resolution.js.map +1 -0
  25. package/dist/analytical-readiness.d.ts +17 -0
  26. package/dist/analytical-readiness.d.ts.map +1 -0
  27. package/dist/analytical-readiness.js +95 -0
  28. package/dist/analytical-readiness.js.map +1 -0
  29. package/dist/analytical-result-facts.d.ts +79 -0
  30. package/dist/analytical-result-facts.d.ts.map +1 -0
  31. package/dist/analytical-result-facts.js +301 -0
  32. package/dist/analytical-result-facts.js.map +1 -0
  33. package/dist/answer-loop.d.ts +82 -2
  34. package/dist/answer-loop.d.ts.map +1 -1
  35. package/dist/answer-loop.js +1129 -38
  36. package/dist/answer-loop.js.map +1 -1
  37. package/dist/embeddings/provider.d.ts +1 -1
  38. package/dist/embeddings/provider.d.ts.map +1 -1
  39. package/dist/embeddings/provider.js.map +1 -1
  40. package/dist/governed-relational-compiler.d.ts +189 -0
  41. package/dist/governed-relational-compiler.d.ts.map +1 -0
  42. package/dist/governed-relational-compiler.js +458 -0
  43. package/dist/governed-relational-compiler.js.map +1 -0
  44. package/dist/index.d.ts +25 -5
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +15 -4
  47. package/dist/index.js.map +1 -1
  48. package/dist/intent-controller.d.ts +5 -0
  49. package/dist/intent-controller.d.ts.map +1 -1
  50. package/dist/intent-controller.js.map +1 -1
  51. package/dist/kg/build.d.ts +2 -2
  52. package/dist/kg/build.d.ts.map +1 -1
  53. package/dist/kg/build.js +329 -9
  54. package/dist/kg/build.js.map +1 -1
  55. package/dist/meaning-resolution.d.ts +17 -0
  56. package/dist/meaning-resolution.d.ts.map +1 -1
  57. package/dist/meaning-resolution.js +83 -4
  58. package/dist/meaning-resolution.js.map +1 -1
  59. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  60. package/dist/metadata/analysis-planner.js +37 -7
  61. package/dist/metadata/analysis-planner.js.map +1 -1
  62. package/dist/metadata/catalog.d.ts +86 -3
  63. package/dist/metadata/catalog.d.ts.map +1 -1
  64. package/dist/metadata/catalog.js +607 -36
  65. package/dist/metadata/catalog.js.map +1 -1
  66. package/dist/metadata/meaning-evidence.d.ts +18 -2
  67. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  68. package/dist/metadata/meaning-evidence.js +182 -7
  69. package/dist/metadata/meaning-evidence.js.map +1 -1
  70. package/dist/plan-execution-adapter.d.ts +131 -0
  71. package/dist/plan-execution-adapter.d.ts.map +1 -0
  72. package/dist/plan-execution-adapter.js +447 -0
  73. package/dist/plan-execution-adapter.js.map +1 -0
  74. package/dist/research-governance.d.ts +56 -0
  75. package/dist/research-governance.d.ts.map +1 -0
  76. package/dist/research-governance.js +68 -0
  77. package/dist/research-governance.js.map +1 -0
  78. package/dist/research-loop.d.ts +14 -0
  79. package/dist/research-loop.d.ts.map +1 -1
  80. package/dist/research-loop.js +77 -23
  81. package/dist/research-loop.js.map +1 -1
  82. package/dist/resolved-analytical-plan.d.ts +123 -0
  83. package/dist/resolved-analytical-plan.d.ts.map +1 -0
  84. package/dist/resolved-analytical-plan.js +400 -0
  85. package/dist/resolved-analytical-plan.js.map +1 -0
  86. package/dist/router.d.ts +3 -0
  87. package/dist/router.d.ts.map +1 -1
  88. package/dist/router.js +195 -25
  89. package/dist/router.js.map +1 -1
  90. package/dist/skills/loader.d.ts +17 -0
  91. package/dist/skills/loader.d.ts.map +1 -1
  92. package/dist/skills/loader.js +62 -4
  93. package/dist/skills/loader.js.map +1 -1
  94. package/package.json +4 -4
package/dist/kg/build.js CHANGED
@@ -8,8 +8,9 @@
8
8
  *
9
9
  * The output is intentionally flat — caller passes it to `KGStore.rebuild()`.
10
10
  */
11
- import { trustLabelIdForStatus } from '@duckcodeailabs/dql-core';
12
- import { buildBlockBusinessFingerprint, buildBlockSqlFingerprints } from '../metadata/block-fingerprints.js';
11
+ import { createHash } from "node:crypto";
12
+ import { trustLabelIdForStatus } from "@duckcodeailabs/dql-core";
13
+ import { buildBlockBusinessFingerprint, buildBlockSqlFingerprints, } from "../metadata/block-fingerprints.js";
13
14
  export function buildKGFromManifest(manifest) {
14
15
  const nodes = [];
15
16
  const edges = [];
@@ -86,6 +87,19 @@ export function buildKGFromManifest(manifest) {
86
87
  sources: [...(block.tableDependencies ?? []), ...(block.rawTableRefs ?? [])],
87
88
  sourceSystems: block.sourceSystems,
88
89
  }),
90
+ payload: {
91
+ qualifiedId: `${block.domain ?? 'global'}::block::${block.name}`,
92
+ localId: block.name,
93
+ aliases: [block.name],
94
+ metricRefs: [
95
+ ...new Set([
96
+ ...(block.metricRef ? [block.metricRef] : []),
97
+ ...(block.metricsRef ?? []),
98
+ ...(block.metricRefs ?? []),
99
+ ]),
100
+ ],
101
+ dimensionsRef: block.dimensionsRef ?? [],
102
+ },
89
103
  datalexContract: block.datalexContract,
90
104
  businessRules: block.businessRules ?? block.invariants,
91
105
  caveats: block.caveats,
@@ -595,6 +609,8 @@ export function buildKGFromSemanticLayer(layer) {
595
609
  if (!layer)
596
610
  return { nodes, edges };
597
611
  const semanticMeasures = layer.listMeasures();
612
+ const semanticDimensions = layer.listDimensions();
613
+ const semanticModels = layer.listSemanticModels();
598
614
  const measuresByName = new Map();
599
615
  for (const measure of semanticMeasures) {
600
616
  for (const key of [measure.name, qualifiedSemanticName(measure.cube, measure.name)]) {
@@ -605,6 +621,27 @@ export function buildKGFromSemanticLayer(layer) {
605
621
  }
606
622
  }
607
623
  for (const metric of layer.listMetrics()) {
624
+ const model = semanticModels.find((candidate) => candidate.name === metric.cube);
625
+ const availableDimensions = semanticDimensions.filter((dimension) => !metric.cube || dimension.cube === metric.cube);
626
+ // MetricFlow may deduplicate same-named domain dimensions in its flattened
627
+ // registry. The semantic model retains the authoritative per-model shape,
628
+ // so use it to recover dimensions/time even when listDimensions() only
629
+ // exposes another model's copy of the shared concept.
630
+ const modelDimensionNames = Array.from(new Set([
631
+ ...(model?.dimensions ?? []),
632
+ ...(model?.timeDimensions ?? []),
633
+ ...availableDimensions.map((dimension) => dimension.name),
634
+ ]));
635
+ const qualifiedDimensions = modelDimensionNames.map((name) => {
636
+ const dimension = availableDimensions.find((candidate) => candidate.name === name);
637
+ return stringValue(semanticIdentityPayload('dimension', dimension ?? {
638
+ name,
639
+ domain: metric.domain ?? model?.domain,
640
+ cube: metric.cube,
641
+ }).qualifiedId) ?? qualifiedSemanticName(metric.cube, name);
642
+ });
643
+ const hasTimeDimension = (model?.timeDimensions.length ?? 0) > 0
644
+ || availableDimensions.some((dimension) => dimension.isTimeDimension || dimension.type === 'date');
608
645
  const backingCandidates = semanticMetricBackingMeasureNames(metric)
609
646
  .flatMap((name) => measuresByName.get(name) ?? [])
610
647
  .filter((measure, index, values) => values.indexOf(measure) === index);
@@ -623,6 +660,12 @@ export function buildKGFromSemanticLayer(layer) {
623
660
  const nonAdditiveDimensions = backingMeasures
624
661
  .map((measure) => measure.nonAdditiveDimension)
625
662
  .filter((value) => Boolean(value));
663
+ const analyticalCapability = buildSemanticMetricCapability({
664
+ layer,
665
+ metric,
666
+ model,
667
+ backingMeasures,
668
+ });
626
669
  const nodeId = `metric:${qualifiedSemanticName(metric.cube, metric.name)}`;
627
670
  nodes.push({
628
671
  nodeId,
@@ -643,6 +686,7 @@ export function buildKGFromSemanticLayer(layer) {
643
686
  nonAdditiveDimensions.length > 0 ? 'non-additive: semantic runtime required' : '',
644
687
  ].filter(Boolean).join('\n') || undefined,
645
688
  payload: {
689
+ ...semanticIdentityPayload('metric', metric),
646
690
  metricType: metric.metricType,
647
691
  aggregation: metric.aggregation,
648
692
  table: metric.table,
@@ -651,6 +695,12 @@ export function buildKGFromSemanticLayer(layer) {
651
695
  backingMeasureNames: backingMeasures.map((measure) => measure.name),
652
696
  nonAdditiveMeasures,
653
697
  nonAdditiveDimensions,
698
+ dimensions: qualifiedDimensions,
699
+ entities: model?.entities ?? [],
700
+ timeGrains: hasTimeDimension
701
+ ? ["day", "week", "month", "quarter", "year"]
702
+ : [],
703
+ analyticalCapability,
654
704
  },
655
705
  sourceTier: 'semantic_layer',
656
706
  certification: certificationFromStatus(metric.status),
@@ -662,7 +712,6 @@ export function buildKGFromSemanticLayer(layer) {
662
712
  if (metric.cube)
663
713
  edges.push({ src: nodeId, dst: `semantic_model:${metric.cube}`, kind: 'depends_on' });
664
714
  }
665
- const semanticDimensions = layer.listDimensions();
666
715
  // PERF-001: dbt fallback semantics may expose every physical column as a
667
716
  // synthetic dimension. At enterprise scale the model nodes already retain
668
717
  // their bounded column lists; duplicating 300k dimensions into KG + FTS is
@@ -681,12 +730,30 @@ export function buildKGFromSemanticLayer(layer) {
681
730
  tags: dimension.tags ?? [],
682
731
  ...businessMetadataFromRaw(dimension.source?.extra?.raw),
683
732
  llmContext: [
684
- dimension.label ? `label: ${dimension.label}` : '',
685
- dimension.type ? `type: ${dimension.type}` : '',
686
- dimension.table ? `table: ${dimension.table}` : '',
687
- dimension.sql ? `sql: ${dimension.sql}` : '',
688
- ].filter(Boolean).join('\n') || undefined,
689
- sourceTier: 'semantic_layer',
733
+ dimension.label ? `label: ${dimension.label}` : "",
734
+ dimension.type ? `type: ${dimension.type}` : "",
735
+ dimension.table ? `table: ${dimension.table}` : "",
736
+ dimension.sql ? `sql: ${dimension.sql}` : "",
737
+ ]
738
+ .filter(Boolean)
739
+ .join("\n") || undefined,
740
+ payload: {
741
+ ...semanticIdentityPayload("dimension", dimension),
742
+ table: dimension.table,
743
+ expression: dimension.expr ?? dimension.sql,
744
+ dimensionType: dimension.type,
745
+ entityLink: dimension.entityLink,
746
+ qualifiedName: dimension.qualifiedName,
747
+ isTimeDimension: dimension.isTimeDimension,
748
+ ...(isTimeDimension(dimension)
749
+ ? {
750
+ granularities: dimension.granularities,
751
+ primaryTime: dimension.primaryTime,
752
+ timeRole: authoredTimeRole(dimension),
753
+ }
754
+ : {}),
755
+ },
756
+ sourceTier: "semantic_layer",
690
757
  certification: certificationFromStatus(dimension.status),
691
758
  provenance: dimension.source?.provider === 'dbt'
692
759
  ? `dbt ${dimension.source.objectType}`
@@ -717,6 +784,7 @@ export function buildKGFromSemanticLayer(layer) {
717
784
  measure.nonAdditiveDimension ? `non-additive dimension: ${JSON.stringify(measure.nonAdditiveDimension)}` : '',
718
785
  ].filter(Boolean).join('\n') || undefined,
719
786
  payload: {
787
+ ...semanticIdentityPayload('measure', measure),
720
788
  aggregation: measure.agg,
721
789
  table: measure.table,
722
790
  expression: measure.expr,
@@ -751,6 +819,7 @@ export function buildKGFromSemanticLayer(layer) {
751
819
  entity.table ? `table: ${entity.table}` : '',
752
820
  entity.expr ? `expr: ${entity.expr}` : '',
753
821
  ].filter(Boolean).join('\n') || undefined,
822
+ payload: semanticIdentityPayload('entity', entity),
754
823
  sourceTier: 'semantic_layer',
755
824
  certification: certificationFromStatus(semanticObjectStatus(entity)),
756
825
  provenance: entity.source?.provider === 'dbt'
@@ -782,6 +851,7 @@ export function buildKGFromSemanticLayer(layer) {
782
851
  model.dimensions.length ? `dimensions: ${model.dimensions.join(', ')}` : '',
783
852
  model.timeDimensions.length ? `time_dimensions: ${model.timeDimensions.join(', ')}` : '',
784
853
  ].filter(Boolean).join('\n') || undefined,
854
+ payload: semanticIdentityPayload('model', model),
785
855
  sourceTier: 'semantic_layer',
786
856
  certification: certificationFromStatus(semanticObjectStatus(model)),
787
857
  provenance: model.source?.provider === 'dbt'
@@ -811,6 +881,7 @@ export function buildKGFromSemanticLayer(layer) {
811
881
  query.timeDimension ? `time_dimension: ${query.timeDimension}` : '',
812
882
  query.granularity ? `granularity: ${query.granularity}` : '',
813
883
  ].filter(Boolean).join('\n') || undefined,
884
+ payload: semanticIdentityPayload('saved_query', query),
814
885
  sourceTier: 'semantic_layer',
815
886
  certification: certificationFromStatus(semanticObjectStatus(query)),
816
887
  provenance: query.source?.provider === 'dbt'
@@ -825,6 +896,211 @@ export function buildKGFromSemanticLayer(layer) {
825
896
  }
826
897
  return { nodes, edges };
827
898
  }
899
+ /**
900
+ * Project one semantic metric into the execution-neutral capability contract.
901
+ * Every admitted fact comes from the semantic registry itself. An incomplete
902
+ * model is intentionally left without a capability instead of being completed
903
+ * from names or descriptions.
904
+ *
905
+ * Acceptance: CONTRACT-002, AGT-018.
906
+ */
907
+ function buildSemanticMetricCapability(input) {
908
+ const { layer, metric, model, backingMeasures } = input;
909
+ if (!model || !metric.cube || model.name !== metric.cube)
910
+ return undefined;
911
+ const cube = typeof layer.getCube === "function"
912
+ ? layer.getCube(metric.cube)
913
+ : undefined;
914
+ const registryDimensions = layer
915
+ .listDimensions()
916
+ .filter((dimension) => dimension.cube === metric.cube);
917
+ const cubeDimensions = cube
918
+ ? [...cube.dimensions, ...cube.timeDimensions]
919
+ : [];
920
+ const dimensionsByName = new Map();
921
+ for (const dimension of [...registryDimensions, ...cubeDimensions]) {
922
+ if (!dimensionsByName.has(dimension.name))
923
+ dimensionsByName.set(dimension.name, dimension);
924
+ }
925
+ const declaredDimensionNames = [
926
+ ...new Set([...model.dimensions, ...model.timeDimensions]),
927
+ ];
928
+ if (declaredDimensionNames.some((name) => !dimensionsByName.has(name)))
929
+ return undefined;
930
+ const metricIdentity = semanticIdentityPayload("metric", metric);
931
+ const metricId = stringValue(metricIdentity.qualifiedId);
932
+ const modelId = stringValue(semanticIdentityPayload("model", model).qualifiedId);
933
+ if (!metricId || !modelId)
934
+ return undefined;
935
+ const scopedEntities = layer
936
+ .listEntities()
937
+ .filter((entity) => entity.cube === metric.cube && model.entities.includes(entity.name));
938
+ const primaryEntities = scopedEntities.filter((entity) => entity.type === "primary");
939
+ if (primaryEntities.length > 1)
940
+ return undefined;
941
+ const primaryEntity = primaryEntities[0];
942
+ const primaryEntityId = primaryEntity
943
+ ? stringValue(semanticIdentityPayload("entity", primaryEntity).qualifiedId)
944
+ : modelId;
945
+ if (!primaryEntityId)
946
+ return undefined;
947
+ const entityIds = new Map(scopedEntities.flatMap((entity) => {
948
+ const id = stringValue(semanticIdentityPayload("entity", entity).qualifiedId);
949
+ return id ? [[entity.name, id]] : [];
950
+ }));
951
+ const timeNameSet = new Set(model.timeDimensions);
952
+ const normalDimensions = [];
953
+ const timeDimensions = [];
954
+ const explicitDefaultNames = new Set([
955
+ metric.aggTimeDimension,
956
+ cube?.defaultTimeDimension,
957
+ ...backingMeasures.map((measure) => measure.aggTimeDimension),
958
+ ...[...dimensionsByName.values()]
959
+ .filter((dimension) => isTimeDimension(dimension) && dimension.primaryTime)
960
+ .map((dimension) => dimension.name),
961
+ ].filter((value) => Boolean(value?.trim())));
962
+ for (const name of declaredDimensionNames) {
963
+ const dimension = dimensionsByName.get(name);
964
+ const dimensionId = stringValue(semanticIdentityPayload("dimension", dimension).qualifiedId);
965
+ if (!dimensionId)
966
+ return undefined;
967
+ const entityId = dimension.entityLink
968
+ ? entityIds.get(dimension.entityLink)
969
+ : primaryEntityId;
970
+ if (!entityId)
971
+ return undefined;
972
+ const timeDimension = timeNameSet.has(name) || isTimeDimension(dimension);
973
+ if (timeDimension) {
974
+ const supportedGrains = isTimeDimension(dimension)
975
+ ? [...new Set(dimension.granularities ?? [])]
976
+ : [];
977
+ if (supportedGrains.length === 0)
978
+ return undefined;
979
+ const defaultFor = explicitDefaultNames.has(name) ||
980
+ (explicitDefaultNames.size === 0 && model.timeDimensions.length === 1)
981
+ ? ["scalar", "trend", "comparison"]
982
+ : undefined;
983
+ timeDimensions.push({
984
+ dimensionId,
985
+ role: authoredTimeRole(dimension) ?? dimensionId,
986
+ supportedGrains,
987
+ ...(defaultFor ? { defaultFor: [...defaultFor] } : {}),
988
+ });
989
+ continue;
990
+ }
991
+ // Dimensions declared on the metric's own semantic model remain at the
992
+ // model's primary grain. Cross-model dimensions require a separately
993
+ // normalized, governed relationship path and are not admitted here.
994
+ if (entityId !== primaryEntityId)
995
+ return undefined;
996
+ normalDimensions.push({
997
+ dimensionId,
998
+ entityId,
999
+ supportedRoles: ["group_by", "filter", "display", "rank_entity"],
1000
+ });
1001
+ }
1002
+ const nonAdditiveDimensionIds = backingMeasures.flatMap((measure) => {
1003
+ const raw = measure.nonAdditiveDimension?.name;
1004
+ if (typeof raw !== "string")
1005
+ return [];
1006
+ const dimension = dimensionsByName.get(raw);
1007
+ const id = dimension &&
1008
+ stringValue(semanticIdentityPayload("dimension", dimension).qualifiedId);
1009
+ return id ? [id] : [];
1010
+ });
1011
+ const aggregation = metric.aggregation ?? metric.type ?? metric.metricType;
1012
+ if (!aggregation)
1013
+ return undefined;
1014
+ const completenessPolicy = authoredCompletenessPolicy(metric);
1015
+ const measureIds = backingMeasures.flatMap((measure) => {
1016
+ const id = stringValue(semanticIdentityPayload("measure", measure).qualifiedId);
1017
+ return id ? [id] : [];
1018
+ });
1019
+ const additive = ["sum", "count"].includes(aggregation.toLowerCase());
1020
+ const capabilityWithoutFingerprint = {
1021
+ metricId,
1022
+ semanticModelId: modelId,
1023
+ measureIds: measureIds.length > 0 ? [...new Set(measureIds)] : [metricId],
1024
+ primaryEntityId,
1025
+ defaultResultGrainId: primaryEntityId,
1026
+ resultGrainIds: [
1027
+ ...new Set([
1028
+ primaryEntityId,
1029
+ ...normalDimensions.map((dimension) => dimension.entityId),
1030
+ ]),
1031
+ ],
1032
+ aggregation,
1033
+ additivity: {
1034
+ entities: additive ? "additive" : "non_additive",
1035
+ time: nonAdditiveDimensionIds.length > 0
1036
+ ? "semi_additive"
1037
+ : additive
1038
+ ? "additive"
1039
+ : "non_additive",
1040
+ ...(nonAdditiveDimensionIds.length > 0
1041
+ ? { nonAdditiveDimensionIds: [...new Set(nonAdditiveDimensionIds)] }
1042
+ : {}),
1043
+ },
1044
+ dimensions: normalDimensions,
1045
+ timeDimensions,
1046
+ ...(completenessPolicy
1047
+ ? { freshness: { defaultCompletenessPolicy: completenessPolicy } }
1048
+ : {}),
1049
+ // The current native semantic adapter supports filtering, grouping,
1050
+ // time-series grouping, ordering, and limiting. Multi-period arithmetic is
1051
+ // withheld until the AC3 executable graph ships.
1052
+ operations: ["filter", "group", "trend", "rank"],
1053
+ supportedOutputKinds: ["dimension", "metric_value", "rank"],
1054
+ executionCapabilities: [
1055
+ {
1056
+ route: "semantic",
1057
+ adapterId: metric.source?.provider === "dbt" ? "metricflow" : "semantic-native",
1058
+ },
1059
+ ],
1060
+ };
1061
+ return {
1062
+ ...capabilityWithoutFingerprint,
1063
+ sourceFingerprint: `sha256:${createHash("sha256").update(stableJson(capabilityWithoutFingerprint)).digest("hex")}`,
1064
+ };
1065
+ }
1066
+ function isTimeDimension(dimension) {
1067
+ return Boolean(dimension.isTimeDimension ||
1068
+ dimension.source?.objectType === "time_dimension");
1069
+ }
1070
+ function authoredTimeRole(dimension) {
1071
+ const raw = recordValue(dimension.source?.extra?.raw);
1072
+ const meta = recordValue(raw.meta);
1073
+ const dql = recordValue(meta.dql);
1074
+ return (stringValue(dql.time_role) ??
1075
+ stringValue(dql.timeRole) ??
1076
+ stringValue(meta.time_role) ??
1077
+ stringValue(meta.timeRole));
1078
+ }
1079
+ function authoredCompletenessPolicy(metric) {
1080
+ const raw = recordValue(metric.source?.extra?.raw);
1081
+ const meta = recordValue(raw.meta);
1082
+ const dql = recordValue(meta.dql);
1083
+ const value = stringValue(dql.completeness_policy) ??
1084
+ stringValue(dql.completenessPolicy) ??
1085
+ stringValue(meta.completeness_policy) ??
1086
+ stringValue(meta.completenessPolicy);
1087
+ return value === "partial_current" ||
1088
+ value === "latest_complete" ||
1089
+ value === "closed_period"
1090
+ ? value
1091
+ : undefined;
1092
+ }
1093
+ function stableJson(value) {
1094
+ if (value === null || typeof value !== "object")
1095
+ return JSON.stringify(value);
1096
+ if (Array.isArray(value))
1097
+ return `[${value.map(stableJson).join(",")}]`;
1098
+ const record = value;
1099
+ return `{${Object.keys(record)
1100
+ .sort()
1101
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
1102
+ .join(",")}}`;
1103
+ }
828
1104
  function semanticMetricBackingMeasureNames(metric) {
829
1105
  const names = new Set([metric.name, qualifiedSemanticName(metric.cube, metric.name)]);
830
1106
  const add = (value) => {
@@ -846,6 +1122,50 @@ function semanticMetricBackingMeasureNames(metric) {
846
1122
  function qualifiedSemanticName(cube, name) {
847
1123
  return cube ? `${cube}.${name}` : name;
848
1124
  }
1125
+ /**
1126
+ * Canonical semantic registry identity carried into the immutable search
1127
+ * snapshot. A source-authored `meta.concept_id` wins; otherwise DQL derives a
1128
+ * deterministic domain-qualified ID while preserving native dbt IDs as exact
1129
+ * aliases. This is additive: legacy object keys remain stable during rollout.
1130
+ *
1131
+ * Acceptance: ID-001, CTX-005, AGT-013.
1132
+ */
1133
+ function semanticIdentityPayload(kind, item) {
1134
+ const raw = recordValue(item.source?.extra?.raw);
1135
+ const meta = recordValue(raw.meta);
1136
+ const dqlMeta = recordValue(meta.dql);
1137
+ const authoredConceptId = stringValue(meta.concept_id)
1138
+ ?? stringValue(meta.conceptId)
1139
+ ?? stringValue(dqlMeta.concept_id)
1140
+ ?? stringValue(dqlMeta.conceptId);
1141
+ const domain = normalizeSemanticIdentityPart(item.domain ?? item.cube ?? 'uncategorized');
1142
+ const localName = normalizeSemanticIdentityPart(item.name);
1143
+ const qualifiedId = authoredConceptId
1144
+ ?? (kind === 'metric'
1145
+ ? `semantic:${domain}:${localName}`
1146
+ : `semantic:${domain}:${kind}:${localName}`);
1147
+ const sourceNativeId = stringValue(item.source?.objectId);
1148
+ const aliases = Array.from(new Set([
1149
+ item.name,
1150
+ item.label,
1151
+ item.cube ? qualifiedSemanticName(item.cube, item.name) : undefined,
1152
+ sourceNativeId,
1153
+ ].filter((value) => Boolean(value?.trim()))));
1154
+ return {
1155
+ qualifiedId,
1156
+ localId: item.name,
1157
+ sourceNativeId,
1158
+ aliases,
1159
+ };
1160
+ }
1161
+ function recordValue(value) {
1162
+ return value && typeof value === 'object' && !Array.isArray(value)
1163
+ ? value
1164
+ : {};
1165
+ }
1166
+ function normalizeSemanticIdentityPart(value) {
1167
+ return value.trim().toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').replace(/^_+|_+$/g, '') || 'unknown';
1168
+ }
849
1169
  function semanticSourcePath(extra) {
850
1170
  const path = extra?.path ?? (extra?.raw && typeof extra.raw === 'object' ? extra.raw.original_file_path : undefined);
851
1171
  return typeof path === 'string' ? path : undefined;