@hypequery/datasets 0.13.6 → 0.15.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.
Files changed (128) hide show
  1. package/README.md +10 -0
  2. package/dist/agent-catalog.d.ts +97 -0
  3. package/dist/agent-catalog.d.ts.map +1 -0
  4. package/dist/agent-catalog.js +79 -0
  5. package/dist/api.type-test.js +19 -1
  6. package/dist/catalog.d.ts +17 -6
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/catalog.js +18 -1
  9. package/dist/contract.d.ts +11 -7
  10. package/dist/contract.d.ts.map +1 -1
  11. package/dist/contract.js +22 -1
  12. package/dist/dataset-query.js +1 -1
  13. package/dist/dataset.d.ts.map +1 -1
  14. package/dist/dataset.js +19 -2
  15. package/dist/executor.d.ts +6 -0
  16. package/dist/executor.d.ts.map +1 -1
  17. package/dist/executor.js +55 -23
  18. package/dist/field.d.ts.map +1 -1
  19. package/dist/field.js +2 -0
  20. package/dist/index.d.ts +12 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -0
  23. package/dist/measure.d.ts.map +1 -1
  24. package/dist/measure.js +4 -0
  25. package/dist/portable-execution-errors.d.ts +49 -0
  26. package/dist/portable-execution-errors.d.ts.map +1 -0
  27. package/dist/portable-execution-errors.js +52 -0
  28. package/dist/portable-executor.d.ts +61 -0
  29. package/dist/portable-executor.d.ts.map +1 -0
  30. package/dist/portable-executor.js +90 -0
  31. package/dist/protocol-adapter.d.ts.map +1 -1
  32. package/dist/protocol-adapter.js +26 -97
  33. package/dist/protocol-rehydrate.d.ts +56 -0
  34. package/dist/protocol-rehydrate.d.ts.map +1 -0
  35. package/dist/protocol-rehydrate.js +264 -0
  36. package/dist/publishing/features/dataset-publication.d.ts +10 -0
  37. package/dist/publishing/features/dataset-publication.d.ts.map +1 -0
  38. package/dist/publishing/features/dataset-publication.js +34 -0
  39. package/dist/publishing/node.d.ts +16 -0
  40. package/dist/publishing/node.d.ts.map +1 -0
  41. package/dist/publishing/node.js +35 -0
  42. package/dist/publishing/publisher.d.ts +17 -0
  43. package/dist/publishing/publisher.d.ts.map +1 -0
  44. package/dist/publishing/publisher.js +32 -0
  45. package/dist/publishing/types.d.ts +31 -0
  46. package/dist/publishing/types.d.ts.map +1 -0
  47. package/dist/publishing/types.js +1 -0
  48. package/dist/query-builder-protocol.d.ts +6 -2
  49. package/dist/query-builder-protocol.d.ts.map +1 -1
  50. package/dist/semantic-plan.d.ts +3 -1
  51. package/dist/semantic-plan.d.ts.map +1 -1
  52. package/dist/semantic-planner.d.ts.map +1 -1
  53. package/dist/semantic-planner.js +4 -0
  54. package/dist/semantic-query-schema.d.ts +60 -0
  55. package/dist/semantic-query-schema.d.ts.map +1 -0
  56. package/dist/semantic-query-schema.js +223 -0
  57. package/dist/sql-utils.d.ts +36 -0
  58. package/dist/sql-utils.d.ts.map +1 -1
  59. package/dist/sql-utils.js +76 -0
  60. package/dist/tools.d.ts +5 -0
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +25 -267
  63. package/dist/types.d.ts +98 -8
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/utils/agent-catalog-projection.d.ts +78 -0
  66. package/dist/utils/agent-catalog-projection.d.ts.map +1 -0
  67. package/dist/utils/agent-catalog-projection.js +179 -0
  68. package/dist/utils/dataset-cache-policy.d.ts +34 -0
  69. package/dist/utils/dataset-cache-policy.d.ts.map +1 -0
  70. package/dist/utils/dataset-cache-policy.js +50 -0
  71. package/dist/utils/dataset-contract.d.ts +2 -2
  72. package/dist/utils/dataset-contract.d.ts.map +1 -1
  73. package/dist/utils/dataset-contract.js +3 -1
  74. package/dist/utils/dataset-definition-validation.d.ts +35 -0
  75. package/dist/utils/dataset-definition-validation.d.ts.map +1 -0
  76. package/dist/utils/dataset-definition-validation.js +215 -0
  77. package/dist/utils/dataset-metric-ref.d.ts +2 -2
  78. package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
  79. package/dist/utils/dataset-metric-ref.js +5 -3
  80. package/dist/utils/metric-alias.d.ts +4 -0
  81. package/dist/utils/metric-alias.d.ts.map +1 -0
  82. package/dist/utils/metric-alias.js +28 -0
  83. package/dist/utils/portable-execution-deadline.d.ts +4 -0
  84. package/dist/utils/portable-execution-deadline.d.ts.map +1 -0
  85. package/dist/utils/portable-execution-deadline.js +27 -0
  86. package/dist/utils/portable-result-budget.d.ts +12 -0
  87. package/dist/utils/portable-result-budget.d.ts.map +1 -0
  88. package/dist/utils/portable-result-budget.js +21 -0
  89. package/dist/utils/portable-semantic-query.d.ts +5 -0
  90. package/dist/utils/portable-semantic-query.d.ts.map +1 -0
  91. package/dist/utils/portable-semantic-query.js +36 -0
  92. package/dist/utils/protocol-metric-capabilities.d.ts +7 -0
  93. package/dist/utils/protocol-metric-capabilities.d.ts.map +1 -0
  94. package/dist/utils/protocol-metric-capabilities.js +57 -0
  95. package/dist/utils/protocol-metric-expressions.d.ts +17 -0
  96. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -0
  97. package/dist/utils/protocol-metric-expressions.js +118 -0
  98. package/dist/utils/protocol-rehydrate-derivation.d.ts +27 -0
  99. package/dist/utils/protocol-rehydrate-derivation.d.ts.map +1 -0
  100. package/dist/utils/protocol-rehydrate-derivation.js +104 -0
  101. package/dist/utils/protocol-rehydrate-filters.d.ts +10 -0
  102. package/dist/utils/protocol-rehydrate-filters.d.ts.map +1 -0
  103. package/dist/utils/protocol-rehydrate-filters.js +36 -0
  104. package/dist/utils/protocol-semantic-metadata.d.ts +5 -0
  105. package/dist/utils/protocol-semantic-metadata.d.ts.map +1 -0
  106. package/dist/utils/protocol-semantic-metadata.js +5 -0
  107. package/dist/utils/published-name.d.ts +3 -0
  108. package/dist/utils/published-name.d.ts.map +1 -0
  109. package/dist/utils/published-name.js +7 -0
  110. package/dist/utils/published-relationships.d.ts +34 -0
  111. package/dist/utils/published-relationships.d.ts.map +1 -0
  112. package/dist/utils/published-relationships.js +35 -0
  113. package/dist/utils/relationship-fields.d.ts +7 -0
  114. package/dist/utils/relationship-fields.d.ts.map +1 -1
  115. package/dist/utils/relationship-fields.js +13 -1
  116. package/dist/utils/result-limits.d.ts +34 -0
  117. package/dist/utils/result-limits.d.ts.map +1 -0
  118. package/dist/utils/result-limits.js +40 -0
  119. package/dist/utils/semantic-metadata-validation.d.ts +4 -0
  120. package/dist/utils/semantic-metadata-validation.d.ts.map +1 -0
  121. package/dist/utils/semantic-metadata-validation.js +103 -0
  122. package/dist/utils/semantic-metadata.d.ts +4 -0
  123. package/dist/utils/semantic-metadata.d.ts.map +1 -0
  124. package/dist/utils/semantic-metadata.js +13 -0
  125. package/dist/utils/semantic-tool.d.ts +5 -0
  126. package/dist/utils/semantic-tool.d.ts.map +1 -0
  127. package/dist/utils/semantic-tool.js +29 -0
  128. package/package.json +5 -4
@@ -1,5 +1,7 @@
1
- import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, validateCanonicalValue, validateProtocolDatasetContract, } from '@hypequery/protocol';
1
+ import { filterExpression, metricExpression, metricDerivation } from './utils/protocol-metric-expressions.js';
2
+ import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, validateProtocolDatasetContract, } from '@hypequery/protocol';
2
3
  import { SEMANTIC_FILTER_OPERATORS } from './constants.js';
4
+ import { toProtocolSemanticMetadata } from './utils/protocol-semantic-metadata.js';
3
5
  function byName(left, right) {
4
6
  return left.name.localeCompare(right.name);
5
7
  }
@@ -16,107 +18,11 @@ function fieldSchema(type) {
16
18
  return { kind: 'any' };
17
19
  }
18
20
  }
19
- function canonicalValue(input) {
20
- if (Array.isArray(input)) {
21
- return validateCanonicalValue({
22
- $hypequery: {
23
- type: 'array',
24
- version: 1,
25
- values: input.map(canonicalValue),
26
- },
27
- });
28
- }
29
- if (typeof input === 'object' && input !== null) {
30
- if ('$hypequery' in input)
31
- return validateCanonicalValue(input);
32
- const prototype = Object.getPrototypeOf(input);
33
- if (prototype !== Object.prototype && prototype !== null) {
34
- throw new TypeError('Dataset protocol adapter only accepts plain filter values.');
35
- }
36
- return validateCanonicalValue({
37
- $hypequery: {
38
- type: 'map',
39
- version: 1,
40
- entries: Object.entries(input)
41
- .sort(([left], [right]) => left.localeCompare(right))
42
- .map(([key, value]) => [key, canonicalValue(value)]),
43
- },
44
- });
45
- }
46
- return validateCanonicalValue(input);
47
- }
48
- function filterExpression(filter) {
49
- const left = {
50
- kind: 'reference',
51
- name: parseProtocolQualifiedIdentifier(filter.field),
52
- };
53
- const right = {
54
- kind: 'literal',
55
- value: canonicalValue(filter.value),
56
- };
57
- const result = {
58
- kind: 'comparison',
59
- operator: filter.operator,
60
- left,
61
- right,
62
- };
63
- return result;
64
- }
65
- function aggregationExpression(spec) {
66
- const result = {
67
- kind: 'aggregate',
68
- aggregation: spec.aggregation,
69
- field: parseProtocolQualifiedIdentifier(spec.field),
70
- ...(spec.argField !== undefined
71
- ? { argField: parseProtocolQualifiedIdentifier(spec.argField) }
72
- : {}),
73
- ...(spec.level !== undefined ? { level: spec.level } : {}),
74
- ...(spec.filters?.length
75
- ? { filters: spec.filters.map(filterExpression) }
76
- : {}),
77
- };
78
- return result;
79
- }
80
- function semanticExpression(expression, references = {}) {
81
- switch (expression.kind) {
82
- case 'ref':
83
- return references[expression.name]
84
- ?? {
85
- kind: 'reference',
86
- name: parseProtocolQualifiedIdentifier(expression.name),
87
- };
88
- case 'literal':
89
- return { kind: 'literal', value: canonicalValue(expression.value) };
90
- case 'binary':
91
- return {
92
- kind: 'binary',
93
- operator: expression.operator,
94
- left: semanticExpression(expression.left, references),
95
- right: semanticExpression(expression.right, references),
96
- };
97
- case 'function':
98
- return {
99
- kind: 'call',
100
- function: expression.name,
101
- args: expression.args.map(argument => semanticExpression(argument, references)),
102
- };
103
- }
104
- }
105
21
  function unwrapMetric(metric) {
106
22
  return metric.__type === 'grained_metric_ref'
107
23
  ? { ref: metric.metric, grain: metric.grain }
108
24
  : { ref: metric };
109
25
  }
110
- function metricExpression(spec) {
111
- if (spec.__type === 'aggregation_spec')
112
- return aggregationExpression(spec);
113
- const aliases = Object.fromEntries(Object.keys(spec.uses).map(alias => [alias, alias]));
114
- const references = Object.fromEntries(Object.entries(spec.uses).map(([alias, metric]) => [
115
- alias,
116
- aggregationExpression(metric.spec),
117
- ]));
118
- return semanticExpression(spec.formula(aliases).expression, references);
119
- }
120
26
  function metricContract(exposedName, metric, endpoint) {
121
27
  const { ref, grain } = unwrapMetric(metric);
122
28
  const contract = metric.contract();
@@ -128,12 +34,16 @@ function metricContract(exposedName, metric, endpoint) {
128
34
  ? 'derived-metric'
129
35
  : 'metric',
130
36
  expression: metricExpression(ref.spec),
37
+ ...(ref.spec.__type === 'derived_metric_spec'
38
+ ? { derivation: metricDerivation(ref.spec) }
39
+ : {}),
131
40
  dimensions: [...contract.dimensions].sort().map(parseProtocolQualifiedIdentifier),
132
41
  filters: [...contract.filters].sort().map(parseProtocolIdentifier),
133
42
  grains: [...contract.grains].sort(),
134
43
  ...(grain !== undefined ? { grain } : {}),
135
44
  ...(ref.label !== undefined ? { label: ref.label } : {}),
136
45
  ...(ref.description !== undefined ? { description: ref.description } : {}),
46
+ ...toProtocolSemanticMetadata(ref),
137
47
  endpoint,
138
48
  };
139
49
  return result;
@@ -170,7 +80,23 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
170
80
  .sort(byName);
171
81
  const contract = {
172
82
  name: dataset.name,
83
+ ...(dataset.description !== undefined ? { description: dataset.description } : {}),
84
+ ...toProtocolSemanticMetadata(dataset),
173
85
  source: dataset.source,
86
+ ...(dataset.freshness !== undefined ? { freshness: { ...dataset.freshness } } : {}),
87
+ ...(dataset.owner !== undefined ? { owner: dataset.owner } : {}),
88
+ ...(dataset.defaults !== undefined
89
+ ? {
90
+ defaults: {
91
+ ...(dataset.defaults.dimensions !== undefined
92
+ ? { dimensions: [...dataset.defaults.dimensions].map(parseProtocolIdentifier) }
93
+ : {}),
94
+ ...(dataset.defaults.timeGrain !== undefined
95
+ ? { timeGrain: dataset.defaults.timeGrain }
96
+ : {}),
97
+ },
98
+ }
99
+ : {}),
174
100
  tenant: dataset.tenantKey
175
101
  ? { kind: 'required', field: dataset.tenantKey }
176
102
  : { kind: 'not-required' },
@@ -185,6 +111,7 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
185
111
  groupable: dimension.groupable !== false,
186
112
  ...(dimension.label !== undefined ? { label: dimension.label } : {}),
187
113
  ...(dimension.description !== undefined ? { description: dimension.description } : {}),
114
+ ...toProtocolSemanticMetadata(dimension),
188
115
  })).sort(byName),
189
116
  measures: Object.entries(dataset.measures).map(([name, measure]) => ({
190
117
  name,
@@ -200,6 +127,7 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
200
127
  filters: (measure.filters ?? []).map(filterExpression),
201
128
  ...(measure.label !== undefined ? { label: measure.label } : {}),
202
129
  ...(measure.description !== undefined ? { description: measure.description } : {}),
130
+ ...toProtocolSemanticMetadata(measure),
203
131
  })).sort(byName),
204
132
  filters: Object.entries(dataset.filters).map(([name, filter]) => ({
205
133
  name,
@@ -207,6 +135,7 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
207
135
  operators: [...(filter.operators ?? SEMANTIC_FILTER_OPERATORS)],
208
136
  ...(filter.label !== undefined ? { label: filter.label } : {}),
209
137
  ...(filter.description !== undefined ? { description: filter.description } : {}),
138
+ ...toProtocolSemanticMetadata(filter),
210
139
  })).sort(byName),
211
140
  metrics,
212
141
  relationships: Object.entries(dataset.relationships).map(([name, relationship]) => ({
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Rebuilds executable datasets from a portable deployment contract.
3
+ *
4
+ * The inverse of `protocol-adapter.ts`. Decision 0005 chose portable native
5
+ * execution, which means a runtime resolves a dataset from the validated active
6
+ * contract and plans the query with the existing semantic planner — no customer
7
+ * module is loaded. This is the function that makes the contract executable.
8
+ *
9
+ * Rehydration deliberately routes through the public `dataset()` factory rather
10
+ * than assembling an instance by hand, so a rebuilt dataset is constructed by
11
+ * exactly the same code path as an authored one. That is what makes the
12
+ * byte-identical SQL claim mitigated by construction rather than by review.
13
+ */
14
+ import type { ProtocolDatasetContract } from '@hypequery/protocol';
15
+ import type { AnyDatasetInstance, DatasetCachePolicy, MetricHandle } from './types.js';
16
+ /** A rebuilt dataset in the registry shape Serve, MCP, and the planner accept. */
17
+ export type RehydratedDataset = AnyDatasetInstance & {
18
+ readonly metrics: Readonly<Record<string, MetricHandle>>;
19
+ };
20
+ export interface RehydrateProtocolDatasetsOptions {
21
+ /**
22
+ * Cache policy for the rebuilt datasets. Not carried by the contract — it is
23
+ * an operational concern of whoever runs the query, not of the deployment.
24
+ */
25
+ readonly cache?: DatasetCachePolicy;
26
+ /**
27
+ * What to do with a metric portable execution cannot rebuild.
28
+ *
29
+ * `throw` (the default) refuses the whole contract, which is right when a
30
+ * caller is checking whether a deployment is fully portable. `skip` omits
31
+ * just that metric, which is what a runtime wants: one derived metric must
32
+ * not make every other dataset in the deployment unexecutable. A caller that
33
+ * skips is responsible for refusing the skipped target at the point of use.
34
+ */
35
+ readonly onUnsupportedMetric?: 'throw' | 'skip';
36
+ }
37
+ /**
38
+ * Thrown when a contract describes something portable execution cannot rebuild.
39
+ *
40
+ * Callers map this onto the `unsupported-capability` invocation failure rather
41
+ * than executing an approximation, as decision 0005 requires.
42
+ */
43
+ export declare class UnsupportedContractFeatureError extends Error {
44
+ readonly dataset: string;
45
+ readonly feature: string;
46
+ constructor(datasetName: string, feature: string, detail: string);
47
+ }
48
+ /**
49
+ * Rebuild every dataset in a validated deployment contract.
50
+ *
51
+ * Datasets are returned keyed by contract name with their named metrics
52
+ * attached, which is the registry shape `getDatasetCatalog`,
53
+ * `projectAgentSafeCatalog`, and `DatasetClient` already accept.
54
+ */
55
+ export declare function rehydrateProtocolDatasets(contracts: readonly ProtocolDatasetContract[], options?: RehydrateProtocolDatasetsOptions): Readonly<Record<string, RehydratedDataset>>;
56
+ //# sourceMappingURL=protocol-rehydrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol-rehydrate.d.ts","sourceRoot":"","sources":["../src/protocol-rehydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,uBAAuB,EAKxB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAKlB,YAAY,EAIb,MAAM,YAAY,CAAC;AAMpB,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG;IACnD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D,CAAC;AAEF,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACjD;AAED;;;;;GAKG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAMjE;AAwOD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,uBAAuB,EAAE,EAC7C,OAAO,GAAE,gCAAqC,GAC7C,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAqE7C"}
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Rebuilds executable datasets from a portable deployment contract.
3
+ *
4
+ * The inverse of `protocol-adapter.ts`. Decision 0005 chose portable native
5
+ * execution, which means a runtime resolves a dataset from the validated active
6
+ * contract and plans the query with the existing semantic planner — no customer
7
+ * module is loaded. This is the function that makes the contract executable.
8
+ *
9
+ * Rehydration deliberately routes through the public `dataset()` factory rather
10
+ * than assembling an instance by hand, so a rebuilt dataset is constructed by
11
+ * exactly the same code path as an authored one. That is what makes the
12
+ * byte-identical SQL claim mitigated by construction rather than by review.
13
+ */
14
+ import { dataset } from './dataset.js';
15
+ import { snapshotSemanticMetadata } from './utils/semantic-metadata.js';
16
+ import { withContractCapabilities } from './utils/protocol-metric-capabilities.js';
17
+ import { rehydrateDerivedFormula } from './utils/protocol-rehydrate-derivation.js';
18
+ import { rehydrateMeasureFilter } from './utils/protocol-rehydrate-filters.js';
19
+ /**
20
+ * Thrown when a contract describes something portable execution cannot rebuild.
21
+ *
22
+ * Callers map this onto the `unsupported-capability` invocation failure rather
23
+ * than executing an approximation, as decision 0005 requires.
24
+ */
25
+ export class UnsupportedContractFeatureError extends Error {
26
+ dataset;
27
+ feature;
28
+ constructor(datasetName, feature, detail) {
29
+ super(`Cannot rebuild ${feature} on dataset "${datasetName}": ${detail}`);
30
+ this.name = 'UnsupportedContractFeatureError';
31
+ this.dataset = datasetName;
32
+ this.feature = feature;
33
+ }
34
+ }
35
+ function rehydrateDimensions(contract) {
36
+ const dimensions = {};
37
+ for (const dimension of contract.dimensions) {
38
+ const name = String(dimension.name);
39
+ const source = dimension.source;
40
+ dimensions[name] = {
41
+ __type: 'field_definition',
42
+ ...snapshotSemanticMetadata(dimension),
43
+ fieldType: dimension.type,
44
+ label: dimension.label,
45
+ description: dimension.description,
46
+ // A column source always carries its column, including when it matches
47
+ // the dimension name — the forward adapter defaults it, so dropping it
48
+ // here would still round-trip but would lose an authored mapping.
49
+ column: source.kind === 'column' ? source.column : undefined,
50
+ sql: source.kind === 'column' ? undefined : source.sql,
51
+ dependencies: source.kind === 'column' ? undefined : [...source.dependencies],
52
+ filterable: dimension.filterable,
53
+ groupable: dimension.groupable,
54
+ };
55
+ }
56
+ return dimensions;
57
+ }
58
+ function rehydrateMeasure(datasetName, measure) {
59
+ const filters = measure.filters.map((expression, index) => rehydrateMeasureFilter(expression, () => new UnsupportedContractFeatureError(datasetName, `measure "${String(measure.name)}"`, `fixed filter ${index} is not a field/operator/value comparison`)));
60
+ return {
61
+ __type: 'measure_definition',
62
+ ...snapshotSemanticMetadata(measure),
63
+ aggregation: measure.aggregation,
64
+ field: String(measure.field),
65
+ ...(measure.argField !== undefined ? { argField: String(measure.argField) } : {}),
66
+ ...(measure.level !== undefined ? { level: measure.level } : {}),
67
+ ...(measure.sql !== undefined
68
+ ? { sql: measure.sql.sql, dependencies: [...measure.sql.dependencies] }
69
+ : {}),
70
+ ...(measure.label !== undefined ? { label: measure.label } : {}),
71
+ ...(measure.description !== undefined ? { description: measure.description } : {}),
72
+ ...(filters.length > 0 ? { filters: filters } : {}),
73
+ };
74
+ }
75
+ function rehydrateFilters(contract) {
76
+ const filters = {};
77
+ for (const filter of contract.filters) {
78
+ filters[String(filter.name)] = {
79
+ __type: 'filter_definition',
80
+ ...snapshotSemanticMetadata(filter),
81
+ field: String(filter.field),
82
+ operators: [...filter.operators],
83
+ ...(filter.label !== undefined ? { label: filter.label } : {}),
84
+ ...(filter.description !== undefined ? { description: filter.description } : {}),
85
+ };
86
+ }
87
+ return filters;
88
+ }
89
+ function rehydrateRelationships(contract, resolve) {
90
+ const relationships = {};
91
+ for (const relationship of contract.relationships) {
92
+ const target = String(relationship.target);
93
+ relationships[String(relationship.name)] = {
94
+ __type: 'relationship',
95
+ kind: relationship.kind,
96
+ // Resolved on call: datasets in one contract may reference each other, so
97
+ // the target may not be built yet when this relationship is created.
98
+ target: () => resolve(target),
99
+ from: String(relationship.from),
100
+ to: String(relationship.to),
101
+ };
102
+ }
103
+ return relationships;
104
+ }
105
+ /**
106
+ * Identity of an aggregation, used to bind a metric back to the measure it was
107
+ * built from.
108
+ *
109
+ * Aggregation and field alone are not enough: two measures may share both and
110
+ * differ only by their fixed filters — `sum(amount)` and `sum(amount) where
111
+ * status = 'paid'`. Matching on the looser key binds the metric to whichever
112
+ * measure happens to sort first and silently changes the SQL it emits.
113
+ */
114
+ function aggregationKey(value) {
115
+ return JSON.stringify([
116
+ value.aggregation,
117
+ String(value.field),
118
+ value.argField === undefined ? null : String(value.argField),
119
+ value.level ?? null,
120
+ value.filters ?? [],
121
+ ]);
122
+ }
123
+ /**
124
+ * The declared measure an aggregate expression was built from.
125
+ *
126
+ * A metric's expression names an aggregation and a field, not the measure that
127
+ * produced it, so the measure is recovered by matching aggregation identity.
128
+ */
129
+ function measureForAggregate(instance, contract, subject, expression) {
130
+ if (expression.kind !== 'aggregate') {
131
+ throw new UnsupportedContractFeatureError(instance.name, subject, `expected an aggregate expression, received "${expression.kind}"`);
132
+ }
133
+ const field = String(expression.field);
134
+ const candidates = contract.measures.filter(measure => (aggregationKey(measure) === aggregationKey(expression)));
135
+ if (candidates.length === 0) {
136
+ throw new UnsupportedContractFeatureError(instance.name, subject, `no declared measure matches ${expression.aggregation}(${field})`);
137
+ }
138
+ // A metric expression carries no raw SQL, so two measures that share an
139
+ // aggregation identity but override SQL differently are indistinguishable in
140
+ // contract v1. Binding to either would be a guess at which SQL to emit.
141
+ const distinctSql = new Set(candidates.map(measure => measure.sql?.sql ?? null));
142
+ if (distinctSql.size > 1) {
143
+ throw new UnsupportedContractFeatureError(instance.name, subject, `${candidates.map(measure => `"${String(measure.name)}"`).join(' and ')} share `
144
+ + `${expression.aggregation}(${field}) but emit different SQL, so the contract cannot `
145
+ + 'say which one this metric was built from');
146
+ }
147
+ return String(candidates[0].name);
148
+ }
149
+ /**
150
+ * Rebuilds the `uses` map and formula of a derived metric.
151
+ *
152
+ * Each input becomes a base metric named by its alias, because that alias is
153
+ * what both plan builders emit as the column of the intermediate aggregate.
154
+ */
155
+ function derivedMetricConfig(instance, contract, subject, derivation) {
156
+ const unsupported = (reason) => new UnsupportedContractFeatureError(instance.name, subject, reason);
157
+ const uses = Object.fromEntries(derivation.inputs.map(input => {
158
+ const alias = String(input.alias);
159
+ return [
160
+ alias,
161
+ instance.metric(alias, {
162
+ measure: measureForAggregate(instance, contract, `${subject} input "${alias}"`, input.expression),
163
+ }),
164
+ ];
165
+ }));
166
+ return { uses, formula: rehydrateDerivedFormula(derivation, unsupported) };
167
+ }
168
+ function rehydrateMetric(instance, contract, metric) {
169
+ const name = String(metric.name);
170
+ const subject = `metric "${name}"`;
171
+ const derived = metric.expression.kind !== 'aggregate';
172
+ if (derived && metric.derivation === undefined) {
173
+ // The inlined expression states what a derived metric means but drops the
174
+ // aliases its SQL is written in terms of. Without them a rebuild computes
175
+ // the same number through different SQL, which decision 0005 excludes.
176
+ throw new UnsupportedContractFeatureError(instance.name, subject, 'a derived metric requires its authored formula, which this contract does not carry');
177
+ }
178
+ const metadata = {
179
+ ...snapshotSemanticMetadata(metric),
180
+ ...(metric.label !== undefined ? { label: metric.label } : {}),
181
+ ...(metric.description !== undefined ? { description: metric.description } : {}),
182
+ };
183
+ const base = (metric.derivation === undefined
184
+ ? instance.metric(name, {
185
+ measure: measureForAggregate(instance, contract, subject, metric.expression),
186
+ ...metadata,
187
+ })
188
+ : instance.metric(name, {
189
+ ...derivedMetricConfig(instance, contract, subject, metric.derivation),
190
+ ...metadata,
191
+ }));
192
+ const handle = metric.grain === undefined
193
+ ? base
194
+ : base.by(metric.grain);
195
+ return withContractCapabilities(handle, metric);
196
+ }
197
+ /**
198
+ * Rebuild every dataset in a validated deployment contract.
199
+ *
200
+ * Datasets are returned keyed by contract name with their named metrics
201
+ * attached, which is the registry shape `getDatasetCatalog`,
202
+ * `projectAgentSafeCatalog`, and `DatasetClient` already accept.
203
+ */
204
+ export function rehydrateProtocolDatasets(contracts, options = {}) {
205
+ const instances = new Map();
206
+ const registry = {};
207
+ // Relationship targets resolve to the published entry, not the bare instance
208
+ // built below, so a caller that follows a relationship lands on the same
209
+ // object the registry exposes. Safe because `target()` is only called after
210
+ // the registry is complete.
211
+ const resolve = (target) => {
212
+ const instance = registry[target] ?? instances.get(target);
213
+ if (!instance) {
214
+ // The deployment validator already rejects a dangling relationship, so
215
+ // reaching this means the caller passed a partial contract.
216
+ throw new UnsupportedContractFeatureError(target, 'relationship target', `dataset "${target}" is not part of the supplied contract`);
217
+ }
218
+ return instance;
219
+ };
220
+ for (const contract of contracts) {
221
+ const name = String(contract.name);
222
+ const instance = dataset(name, {
223
+ source: contract.source,
224
+ ...snapshotSemanticMetadata(contract),
225
+ ...(contract.description !== undefined ? { description: contract.description } : {}),
226
+ ...(contract.owner !== undefined ? { owner: contract.owner } : {}),
227
+ ...(contract.freshness !== undefined ? { freshness: { ...contract.freshness } } : {}),
228
+ ...(contract.defaults !== undefined ? { defaults: {
229
+ ...contract.defaults,
230
+ ...(contract.defaults.dimensions !== undefined ? { dimensions: [...contract.defaults.dimensions] } : {}),
231
+ } } : {}),
232
+ ...(contract.tenant.kind === 'required' ? { tenantKey: contract.tenant.field } : {}),
233
+ ...(contract.timeField !== undefined ? { timeKey: String(contract.timeField) } : {}),
234
+ dimensions: rehydrateDimensions(contract),
235
+ measures: Object.fromEntries(contract.measures.map(measure => [
236
+ String(measure.name),
237
+ rehydrateMeasure(name, measure),
238
+ ])),
239
+ filters: rehydrateFilters(contract),
240
+ relationships: rehydrateRelationships(contract, resolve),
241
+ ...(contract.limits !== undefined ? { limits: { ...contract.limits } } : {}),
242
+ ...(options.cache !== undefined ? { cache: options.cache } : {}),
243
+ });
244
+ instances.set(name, instance);
245
+ }
246
+ for (const contract of contracts) {
247
+ const name = String(contract.name);
248
+ const instance = instances.get(name);
249
+ const metrics = {};
250
+ for (const metric of contract.metrics) {
251
+ try {
252
+ metrics[String(metric.name)] = rehydrateMetric(instance, contract, metric);
253
+ }
254
+ catch (error) {
255
+ if (options.onUnsupportedMetric !== 'skip'
256
+ || !(error instanceof UnsupportedContractFeatureError)) {
257
+ throw error;
258
+ }
259
+ }
260
+ }
261
+ registry[name] = Object.assign(Object.create(Object.getPrototypeOf(instance)), instance, { metrics });
262
+ }
263
+ return registry;
264
+ }
@@ -0,0 +1,10 @@
1
+ import type { AnyDatasetInstance, MetricHandle } from '../../types.js';
2
+ import type { DatasetPublisherNode } from '../node.js';
3
+ export declare class DatasetPublicationFeature {
4
+ private readonly publisher;
5
+ constructor(publisher: {
6
+ getPublicationNode(): DatasetPublisherNode;
7
+ });
8
+ add(dataset: AnyDatasetInstance, alias: string, metrics: Readonly<Record<string, MetricHandle>>): DatasetPublisherNode;
9
+ }
10
+ //# sourceMappingURL=dataset-publication.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-publication.d.ts","sourceRoot":"","sources":["../../../src/publishing/features/dataset-publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,qBAAa,yBAAyB;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE;QACtC,kBAAkB,IAAI,oBAAoB,CAAC;KAC5C;IAED,GAAG,CACD,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAC9C,oBAAoB;CA8BxB"}
@@ -0,0 +1,34 @@
1
+ import { aliasMetricHandle } from '../../utils/metric-alias.js';
2
+ import { assertPublishedName } from '../../utils/published-name.js';
3
+ export class DatasetPublicationFeature {
4
+ publisher;
5
+ constructor(publisher) {
6
+ this.publisher = publisher;
7
+ }
8
+ add(dataset, alias, metrics) {
9
+ const node = this.publisher.getPublicationNode();
10
+ assertPublishedName(alias, 'dataset');
11
+ if (node.entries.some(entry => entry.alias === alias)) {
12
+ throw new Error(`Dataset alias "${alias}" is already published.`);
13
+ }
14
+ if (node.entries.some(entry => entry.dataset.name === dataset.name)) {
15
+ throw new Error(`Dataset "${dataset.name}" is already published.`);
16
+ }
17
+ const publishedMetrics = {};
18
+ for (const [metricAlias, metric] of Object.entries(metrics)) {
19
+ assertPublishedName(metricAlias, 'metric');
20
+ const ref = metric.__type === 'grained_metric_ref' ? metric.metric : metric;
21
+ if (ref.datasetName !== dataset.name) {
22
+ throw new Error(`Metric "${metricAlias}" belongs to dataset "${ref.datasetName}", expected "${dataset.name}".`);
23
+ }
24
+ publishedMetrics[metricAlias] = aliasMetricHandle(metric, metricAlias, alias);
25
+ }
26
+ return {
27
+ kind: 'dataset-publisher',
28
+ entries: [
29
+ ...node.entries,
30
+ { alias, dataset, metrics: publishedMetrics },
31
+ ],
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,16 @@
1
+ import type { AnyDatasetInstance, MetricHandle } from '../types.js';
2
+ export interface DatasetPublicationEntry {
3
+ readonly alias: string;
4
+ readonly dataset: AnyDatasetInstance;
5
+ readonly metrics: Readonly<Record<string, MetricHandle>>;
6
+ }
7
+ export interface DatasetPublisherNode {
8
+ readonly kind: 'dataset-publisher';
9
+ readonly entries: readonly DatasetPublicationEntry[];
10
+ }
11
+ export declare function createDatasetPublisherNode(entries?: readonly DatasetPublicationEntry[]): DatasetPublisherNode;
12
+ export declare function cloneDatasetPublisherNode(node: DatasetPublisherNode): DatasetPublisherNode;
13
+ export declare function materializeDatasetPublisherNode(node: DatasetPublisherNode): Readonly<Record<string, AnyDatasetInstance & {
14
+ readonly metrics: Readonly<Record<string, MetricHandle>>;
15
+ }>>;
16
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/publishing/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACtD;AAED,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,SAAS,uBAAuB,EAAO,GAC/C,oBAAoB,CAStB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,oBAAoB,GAAG,oBAAoB,CAE1F;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,oBAAoB,GACzB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,GAAG;IAC9C,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D,CAAC,CAAC,CAyBF"}
@@ -0,0 +1,35 @@
1
+ import { publishedAliasesByName, rewirePublishedRelationships, } from '../utils/published-relationships.js';
2
+ export function createDatasetPublisherNode(entries = []) {
3
+ return {
4
+ kind: 'dataset-publisher',
5
+ entries: entries.map(entry => ({
6
+ alias: entry.alias,
7
+ dataset: entry.dataset,
8
+ metrics: { ...entry.metrics },
9
+ })),
10
+ };
11
+ }
12
+ export function cloneDatasetPublisherNode(node) {
13
+ return createDatasetPublisherNode(node.entries);
14
+ }
15
+ export function materializeDatasetPublisherNode(node) {
16
+ const aliasesByName = publishedAliasesByName(node.entries);
17
+ const registry = new Map();
18
+ // Relationships resolve their target through this map rather than capturing
19
+ // an instance, so datasets that reference each other are rewired without an
20
+ // ordering constraint.
21
+ const lookup = (alias) => registry.get(alias);
22
+ const entries = [...node.entries]
23
+ .sort((left, right) => left.alias < right.alias ? -1 : left.alias > right.alias ? 1 : 0)
24
+ .map(entry => {
25
+ const published = Object.freeze({
26
+ ...entry.dataset,
27
+ name: entry.alias,
28
+ relationships: rewirePublishedRelationships(entry.dataset.relationships, aliasesByName, lookup),
29
+ metrics: Object.freeze({ ...entry.metrics }),
30
+ });
31
+ registry.set(entry.alias, published);
32
+ return [entry.alias, published];
33
+ });
34
+ return Object.freeze(Object.fromEntries(entries));
35
+ }
@@ -0,0 +1,17 @@
1
+ import type { AnyDatasetInstance } from '../types.js';
2
+ import { type DatasetPublisherNode } from './node.js';
3
+ import type { AddPublishedDataset, AnyDatasetPublisherState, DatasetPublisherState, PublishedAlias, PublishedMetricMap, PublishDatasetOptions } from './types.js';
4
+ /** Immutable builder for publishing datasets and named metrics to consumers. */
5
+ export declare class DatasetPublisher<TState extends AnyDatasetPublisherState = DatasetPublisherState> {
6
+ private readonly state;
7
+ private readonly node;
8
+ private readonly publication;
9
+ constructor(state: TState, node?: DatasetPublisherNode);
10
+ getPublicationNode(): DatasetPublisherNode;
11
+ private transition;
12
+ publish<TDataset extends AnyDatasetInstance, const TAlias extends string | undefined = undefined, const TMetrics extends PublishedMetricMap<TDataset> = Record<string, never>>(dataset: TDataset, options?: PublishDatasetOptions<TDataset, TAlias, TMetrics>): DatasetPublisher<AddPublishedDataset<TState, TDataset, PublishedAlias<TDataset, TAlias>, TMetrics>>;
13
+ /** Materialize the plain registry shape accepted by existing Serve/MCP APIs. */
14
+ build(): TState['registry'];
15
+ }
16
+ export declare function publishDatasets(): DatasetPublisher<DatasetPublisherState>;
17
+ //# sourceMappingURL=publisher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../../src/publishing/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB,gFAAgF;AAChF,qBAAa,gBAAgB,CAC3B,MAAM,SAAS,wBAAwB,GAAG,qBAAqB;IAK7D,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;gBAGrC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,oBAAmD;IAK5E,kBAAkB,IAAI,oBAAoB;IAI1C,OAAO,CAAC,UAAU;IAOlB,OAAO,CACL,QAAQ,SAAS,kBAAkB,EACnC,KAAK,CAAC,MAAM,SAAS,MAAM,GAAG,SAAS,GAAG,SAAS,EACnD,KAAK,CAAC,QAAQ,SAAS,kBAAkB,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAE3E,OAAO,EAAE,QAAQ,EACjB,OAAO,GAAE,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAM,GAC9D,gBAAgB,CAAC,mBAAmB,CACrC,MAAM,EACN,QAAQ,EACR,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,EAChC,QAAQ,CACT,CAAC;IAaF,gFAAgF;IAChF,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC;CAG5B;AAED,wBAAgB,eAAe,IAAI,gBAAgB,CAAC,qBAAqB,CAAC,CAEzE"}
@@ -0,0 +1,32 @@
1
+ import { DatasetPublicationFeature } from './features/dataset-publication.js';
2
+ import { cloneDatasetPublisherNode, createDatasetPublisherNode, materializeDatasetPublisherNode, } from './node.js';
3
+ /** Immutable builder for publishing datasets and named metrics to consumers. */
4
+ export class DatasetPublisher {
5
+ state;
6
+ node;
7
+ publication;
8
+ constructor(state, node = createDatasetPublisherNode()) {
9
+ this.state = state;
10
+ this.node = node;
11
+ this.publication = new DatasetPublicationFeature(this);
12
+ }
13
+ getPublicationNode() {
14
+ return cloneDatasetPublisherNode(this.node);
15
+ }
16
+ transition(state, node) {
17
+ return new DatasetPublisher(state, node);
18
+ }
19
+ publish(dataset, options = {}) {
20
+ const alias = options.alias ?? dataset.name;
21
+ const node = this.publication.add(dataset, alias, options.metrics ?? {});
22
+ const state = { registry: materializeDatasetPublisherNode(node) };
23
+ return this.transition(state, node);
24
+ }
25
+ /** Materialize the plain registry shape accepted by existing Serve/MCP APIs. */
26
+ build() {
27
+ return this.state.registry;
28
+ }
29
+ }
30
+ export function publishDatasets() {
31
+ return new DatasetPublisher({ registry: {} });
32
+ }