@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.
- package/dist/agent-run-engine.d.ts +13 -0
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +65 -2
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/analytical-compatibility.d.ts +61 -0
- package/dist/analytical-compatibility.d.ts.map +1 -0
- package/dist/analytical-compatibility.js +515 -0
- package/dist/analytical-compatibility.js.map +1 -0
- package/dist/analytical-execution-graph.d.ts +174 -0
- package/dist/analytical-execution-graph.d.ts.map +1 -0
- package/dist/analytical-execution-graph.js +599 -0
- package/dist/analytical-execution-graph.js.map +1 -0
- package/dist/analytical-failure-repair.d.ts +91 -0
- package/dist/analytical-failure-repair.d.ts.map +1 -0
- package/dist/analytical-failure-repair.js +325 -0
- package/dist/analytical-failure-repair.js.map +1 -0
- package/dist/analytical-frame.d.ts +19 -0
- package/dist/analytical-frame.d.ts.map +1 -0
- package/dist/analytical-frame.js +299 -0
- package/dist/analytical-frame.js.map +1 -0
- package/dist/analytical-period-resolution.d.ts +53 -0
- package/dist/analytical-period-resolution.d.ts.map +1 -0
- package/dist/analytical-period-resolution.js +218 -0
- package/dist/analytical-period-resolution.js.map +1 -0
- package/dist/analytical-readiness.d.ts +17 -0
- package/dist/analytical-readiness.d.ts.map +1 -0
- package/dist/analytical-readiness.js +95 -0
- package/dist/analytical-readiness.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +79 -0
- package/dist/analytical-result-facts.d.ts.map +1 -0
- package/dist/analytical-result-facts.js +301 -0
- package/dist/analytical-result-facts.js.map +1 -0
- package/dist/answer-loop.d.ts +82 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1129 -38
- package/dist/answer-loop.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -1
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +189 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -0
- package/dist/governed-relational-compiler.js +458 -0
- package/dist/governed-relational-compiler.js.map +1 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +5 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.d.ts +2 -2
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +329 -9
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +17 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +83 -4
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +37 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +86 -3
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +607 -36
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +18 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +182 -7
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +131 -0
- package/dist/plan-execution-adapter.d.ts.map +1 -0
- package/dist/plan-execution-adapter.js +447 -0
- package/dist/plan-execution-adapter.js.map +1 -0
- package/dist/research-governance.d.ts +56 -0
- package/dist/research-governance.d.ts.map +1 -0
- package/dist/research-governance.js +68 -0
- package/dist/research-governance.js.map +1 -0
- package/dist/research-loop.d.ts +14 -0
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +77 -23
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +123 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -0
- package/dist/resolved-analytical-plan.js +400 -0
- package/dist/resolved-analytical-plan.js.map +1 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +195 -25
- package/dist/router.js.map +1 -1
- package/dist/skills/loader.d.ts +17 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +62 -4
- package/dist/skills/loader.js.map +1 -1
- 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 {
|
|
12
|
-
import {
|
|
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
|
-
]
|
|
689
|
-
|
|
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;
|