@hypequery/deployment 0.7.5 → 0.8.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.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * What one principal is allowed to see of a deployment contract.
3
+ *
4
+ * Discovery and execution must agree about who may reach what. The data plane
5
+ * already decides that per call, from the endpoint policy on the target; a
6
+ * gateway listing tools has to make the same decision over the whole contract,
7
+ * before any call exists. Restating the rule there would put an authorization
8
+ * predicate in two packages, and the copy that drifts is the one that leaks.
9
+ *
10
+ * The projection narrows, never widens: it can remove a target a principal may
11
+ * not reach, and can never add one or loosen a policy. Feeding the result to
12
+ * catalog projection or schema compilation therefore cannot advertise something
13
+ * execution would refuse.
14
+ */
15
+ import type { ProtocolAccessPolicy, ProtocolDeploymentContract, ProtocolEndpointPolicy } from '@hypequery/protocol';
16
+ import type { DeploymentDataPlanePrincipal } from './data-plane.js';
17
+ /**
18
+ * Whether a principal satisfies an access policy.
19
+ *
20
+ * Shares `missing` with the semantic data plane rather than restating it, which
21
+ * is the whole point of this module existing in Core: one predicate, so
22
+ * discovery cannot come to answer differently than execution.
23
+ */
24
+ export declare function satisfiesDeploymentAccess(access: ProtocolAccessPolicy, principal: DeploymentDataPlanePrincipal | null): boolean;
25
+ /** Whether a principal may reach a target published under this endpoint policy. */
26
+ export declare function isDeploymentEndpointAuthorized(endpoint: ProtocolEndpointPolicy | undefined, principal: DeploymentDataPlanePrincipal | null): boolean;
27
+ export interface AuthorizedDeploymentProjection {
28
+ /**
29
+ * A valid contract narrowed to what the principal may see, including any
30
+ * dataset retained only to support a join or a named query.
31
+ */
32
+ readonly contract: ProtocolDeploymentContract;
33
+ /**
34
+ * Datasets to advertise at all, in contract order.
35
+ *
36
+ * Wider than `queryable`: a dataset the principal cannot address may still
37
+ * carry a metric it can, and hiding the dataset would hide that metric.
38
+ * Narrower than `contract.datasets`, which also holds datasets retained only
39
+ * to support a join.
40
+ *
41
+ * Always read this before advertising anything. Catalog projection and
42
+ * rehydration enumerate every dataset they are given and neither consults an
43
+ * endpoint, so handing them the whole contract would advertise a supporting
44
+ * dataset and disclose its dimensions and measures to a principal with no
45
+ * access to it.
46
+ */
47
+ readonly advertised: readonly string[];
48
+ /**
49
+ * Datasets addressable as a `query_dataset` target. A subset of `advertised`.
50
+ *
51
+ * The two differ because a deployment authorizes a dataset and each of its
52
+ * metrics through separate endpoint policies. Collapsing them would either
53
+ * offer a target execution refuses or withhold a metric it would run.
54
+ *
55
+ * The intended composition rehydrates the whole contract, so relationship
56
+ * targets still resolve, and advertises the two sets separately:
57
+ *
58
+ * ```ts
59
+ * const { contract, advertised, queryable } =
60
+ * projectAuthorizedDeploymentContract(active, principal);
61
+ * const registry = rehydrateProtocolDatasets(contract.datasets, { onUnsupportedMetric: 'skip' });
62
+ * createMCPDiscoveryExecutor({
63
+ * datasets: Object.fromEntries(advertised.map(name => [name, registry[name]])),
64
+ * queryableDatasets: queryable,
65
+ * });
66
+ * ```
67
+ *
68
+ * A joined dimension such as `orders.employee.id` still resolves through the
69
+ * full registry, which is correct: that join is governed by the endpoint of
70
+ * the dataset being queried, not by the target's.
71
+ */
72
+ readonly queryable: readonly string[];
73
+ }
74
+ /**
75
+ * Projects the contract down to what `principal` may see.
76
+ *
77
+ * A dataset is *published* when the principal may address it directly, and
78
+ * *supporting* when something published still needs it to stay coherent — the
79
+ * target of a relationship, or the dataset a named query plans over. A
80
+ * supporting dataset keeps its shape and loses its endpoint and metrics, so a
81
+ * relationship can still be traversed exactly as execution would traverse it
82
+ * while the dataset itself is not offered as a target.
83
+ *
84
+ * Dropping a relationship target instead would be the tempting alternative and
85
+ * is wrong twice: it would invalidate any metric declaring a dimension across
86
+ * that relationship, and it would advertise less than the data plane permits,
87
+ * since a one-hop join is governed by the endpoint of the dataset being
88
+ * queried, not by the target's.
89
+ *
90
+ * The two are returned separately rather than as one contract because nothing
91
+ * downstream reads an endpoint. `projectAgentSafeCatalog` and
92
+ * `rehydrateProtocolDatasets` enumerate whatever they are handed, so an
93
+ * unpublished dataset left in the contract they see is an advertised one.
94
+ *
95
+ * The contract is revalidated. Callers hand it to catalog projection, schema
96
+ * compilation, and rehydration, all of which assume a valid contract; a
97
+ * projection that could emit an invalid one would move that failure to whatever
98
+ * read it next.
99
+ */
100
+ export declare function projectAuthorizedDeploymentContract(contract: ProtocolDeploymentContract, principal: DeploymentDataPlanePrincipal | null): AuthorizedDeploymentProjection;
101
+ //# sourceMappingURL=authorized-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorized-contract.d.ts","sourceRoot":"","sources":["../src/authorized-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EAEpB,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGpE;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,oBAAoB,EAC5B,SAAS,EAAE,4BAA4B,GAAG,IAAI,GAC7C,OAAO,CAIT;AAED,mFAAmF;AACnF,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,SAAS,EAAE,4BAA4B,GAAG,IAAI,GAC7C,OAAO,CAKT;AA4GD,MAAM,WAAW,8BAA8B;IAC7C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,4BAA4B,GAAG,IAAI,GAC7C,8BAA8B,CA8FhC"}
Binary file
package/dist/index.d.ts CHANGED
@@ -34,4 +34,9 @@ export { createDeploymentHost, DeploymentHostError, } from './host.js';
34
34
  export { createFileSystemDeploymentHost } from './filesystem-host.js';
35
35
  export type { FileSystemDeploymentHost, FileSystemDeploymentHostOptions, } from './filesystem-host.js';
36
36
  export type { DeploymentHost, DeploymentHostDataPlaneConfiguration, DeploymentHostDataPlaneInput, DeploymentHostErrorCode, DeploymentHostOptions, } from './host.js';
37
+ export { createDeploymentSemanticDataPlane, DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure, } from './semantic-data-plane.js';
38
+ export type { DeploymentSemanticAuthenticationInput, DeploymentSemanticBudget, DeploymentSemanticDataPlane, DeploymentSemanticDataPlaneOptions, DeploymentSemanticExecutionInput, DeploymentSemanticInvocationRequest, DeploymentSemanticTenantInput, } from './semantic-data-plane.js';
39
+ export { validateSemanticOperation } from './semantic-operation-validation.js';
40
+ export type { SemanticOperationLimits, SemanticOperationViolation, } from './semantic-operation-validation.js';
41
+ export { isDeploymentEndpointAuthorized, projectAuthorizedDeploymentContract, satisfiesDeploymentAccess, } from './authorized-contract.js';
37
42
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -16,3 +16,9 @@ export { createDeploymentRuntimeSupervisorExecutor } from './data-plane-runtime.
16
16
  export { createDeploymentDataPlaneFetchHandler, createDeploymentDataPlaneNodeHandler, } from './data-plane-adapters.js';
17
17
  export { createDeploymentHost, DeploymentHostError, } from './host.js';
18
18
  export { createFileSystemDeploymentHost } from './filesystem-host.js';
19
+ // Semantic invocation beside named-query execution (decision 0002).
20
+ export { createDeploymentSemanticDataPlane, DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure, } from './semantic-data-plane.js';
21
+ export { validateSemanticOperation } from './semantic-operation-validation.js';
22
+ // What one principal may see of a contract, for a gateway listing targets
23
+ // before any call exists (decision 0002, CLOUD-03).
24
+ export { isDeploymentEndpointAuthorized, projectAuthorizedDeploymentContract, satisfiesDeploymentAccess, } from './authorized-contract.js';
@@ -0,0 +1,74 @@
1
+ export { DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure } from './semantic-invocation-errors.js';
2
+ /**
3
+ * Semantic invocation beside named-query execution.
4
+ *
5
+ * Decision 0002 requires a dataset or metric call to run the same enforcement
6
+ * sequence a named query does — select the active generation, resolve the
7
+ * target from its validated contract, authenticate, enforce roles and scopes,
8
+ * resolve tenant, apply the most restrictive budget, validate input, execute,
9
+ * then validate and bound the output.
10
+ *
11
+ * Execution itself is injected. This module decides whether a call is allowed
12
+ * and what it is allowed to ask for; `CORE-12` supplies the executor that
13
+ * answers it.
14
+ */
15
+ import type { ProtocolDatasetContract, ProtocolDatasetMetric, ProtocolDeploymentContract, ProtocolSemanticInvocation, ProtocolSemanticInvocationResult, ProtocolSemanticQuery } from '@hypequery/protocol';
16
+ import type { DeploymentDataPlanePrincipal } from './data-plane.js';
17
+ import { type SemanticOperationLimits } from './semantic-operation-validation.js';
18
+ /** The ceilings that survived after every source was applied. */
19
+ export interface DeploymentSemanticBudget {
20
+ readonly maxRows: number;
21
+ readonly deadlineMs?: number;
22
+ readonly maxResponseBytes?: number;
23
+ }
24
+ export interface DeploymentSemanticAuthenticationInput {
25
+ readonly credentials: unknown;
26
+ readonly invocation: ProtocolSemanticInvocation;
27
+ readonly dataset: ProtocolDatasetContract;
28
+ readonly metric?: ProtocolDatasetMetric;
29
+ }
30
+ export interface DeploymentSemanticTenantInput {
31
+ readonly principal: DeploymentDataPlanePrincipal | null;
32
+ readonly invocation: ProtocolSemanticInvocation;
33
+ readonly dataset: ProtocolDatasetContract;
34
+ readonly metric?: ProtocolDatasetMetric;
35
+ }
36
+ export interface DeploymentSemanticExecutionInput {
37
+ readonly deployment: ProtocolDeploymentContract;
38
+ readonly dataset: ProtocolDatasetContract;
39
+ readonly metric?: ProtocolDatasetMetric;
40
+ readonly operation: ProtocolSemanticQuery;
41
+ readonly principal: DeploymentDataPlanePrincipal | null;
42
+ /**
43
+ * Resolved by the provider callback. Never a caller-supplied value — the
44
+ * invocation record has no field that could carry one.
45
+ */
46
+ readonly tenant: unknown;
47
+ readonly budget: DeploymentSemanticBudget;
48
+ readonly activationRevision: string;
49
+ readonly signal?: AbortSignal;
50
+ }
51
+ export interface DeploymentSemanticInvocationRequest {
52
+ /** An unvalidated invocation record; validated before anything else runs. */
53
+ readonly invocation: unknown;
54
+ readonly credentials?: unknown;
55
+ readonly signal?: AbortSignal;
56
+ }
57
+ export interface DeploymentSemanticDataPlaneOptions {
58
+ readonly deployment: ProtocolDeploymentContract;
59
+ /** The immutable generation this data plane serves. */
60
+ readonly activationRevision: string;
61
+ readonly authenticate?: (input: DeploymentSemanticAuthenticationInput) => Promise<DeploymentDataPlanePrincipal | null>;
62
+ readonly resolveTenant?: (input: DeploymentSemanticTenantInput) => Promise<unknown>;
63
+ readonly execute: (input: DeploymentSemanticExecutionInput) => Promise<unknown>;
64
+ /** Server-side ceilings, applied on top of contract and caller limits. */
65
+ readonly limits?: Partial<SemanticOperationLimits> & {
66
+ readonly deadlineMs?: number;
67
+ readonly maxResponseBytes?: number;
68
+ };
69
+ }
70
+ export interface DeploymentSemanticDataPlane {
71
+ invoke(request: DeploymentSemanticInvocationRequest): Promise<ProtocolSemanticInvocationResult>;
72
+ }
73
+ export declare function createDeploymentSemanticDataPlane(options: DeploymentSemanticDataPlaneOptions): DeploymentSemanticDataPlane;
74
+ //# sourceMappingURL=semantic-data-plane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-data-plane.d.ts","sourceRoot":"","sources":["../src/semantic-data-plane.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,224 @@
1
+ import { claimedFailure } from './utils/semantic-executor-failure.js';
2
+ import { DeploymentSemanticInvocationError, fail, throwIfAborted } from './semantic-invocation-errors.js';
3
+ export { DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure } from './semantic-invocation-errors.js';
4
+ import { definedLimits, lowest, tighten } from './utils/semantic-budget-limits.js';
5
+ import { missing } from './utils/required-access.js';
6
+ import { ProtocolSemanticInvocationError, validateProtocolDeploymentContract, validateProtocolSemanticInvocation, validateProtocolSemanticInvocationResult, } from '@hypequery/protocol';
7
+ import { validateSemanticOperation, } from './semantic-operation-validation.js';
8
+ const DEFAULT_LIMITS = Object.freeze({
9
+ maxRows: 10_000,
10
+ maxOffset: 10_000,
11
+ maxDimensions: 50,
12
+ maxMeasures: 50,
13
+ maxFilters: 100,
14
+ });
15
+ const REVISION_PATTERN = /^[0-9a-f]{64}$/;
16
+ export function createDeploymentSemanticDataPlane(options) {
17
+ let deployment;
18
+ try {
19
+ deployment = validateProtocolDeploymentContract(options.deployment);
20
+ }
21
+ catch (error) {
22
+ throw new DeploymentSemanticInvocationError('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'The semantic data plane requires a valid deployment contract.', { cause: error });
23
+ }
24
+ if (!REVISION_PATTERN.test(options.activationRevision)) {
25
+ throw new RangeError('activationRevision must be a lowercase SHA-256 identity');
26
+ }
27
+ const datasets = new Map(deployment.datasets.map(entry => [String(entry.name), entry]));
28
+ // Spreading `options.limits` directly would let an explicitly `undefined`
29
+ // property erase a default ceiling, and an undefined bound compares false
30
+ // against everything. A caller that omits a limit gets the default.
31
+ const configured = { ...DEFAULT_LIMITS, ...definedLimits(options.limits) };
32
+ function resolveTarget(operation) {
33
+ const dataset = datasets.get(String(operation.dataset));
34
+ if (dataset === undefined) {
35
+ fail('not-found', 'HQ_SEMANTIC_DATASET_NOT_FOUND', 'The dataset was not found.', {
36
+ path: '$.operation.dataset',
37
+ relist: true,
38
+ });
39
+ }
40
+ const metric = operation.kind === 'metric'
41
+ ? dataset.metrics.find(entry => String(entry.name) === String(operation.metric))
42
+ : undefined;
43
+ if (operation.kind === 'metric' && metric === undefined) {
44
+ fail('not-found', 'HQ_SEMANTIC_METRIC_NOT_FOUND', 'The metric was not found.', {
45
+ path: '$.operation.metric',
46
+ relist: true,
47
+ });
48
+ }
49
+ // An endpoint policy is what publishes a target. Without one the contract
50
+ // describes the dataset but never exposed it, so it is not addressable.
51
+ const endpoint = metric?.endpoint ?? dataset.endpoint;
52
+ if (endpoint === undefined) {
53
+ fail('not-found', 'HQ_SEMANTIC_NOT_PUBLISHED', 'The target is not published.', {
54
+ path: '$.operation',
55
+ relist: true,
56
+ });
57
+ }
58
+ return { dataset, ...(metric === undefined ? {} : { metric }), endpoint };
59
+ }
60
+ async function invoke(request) {
61
+ throwIfAborted(request.signal);
62
+ let invocation;
63
+ try {
64
+ invocation = validateProtocolSemanticInvocation(request.invocation);
65
+ }
66
+ catch (error) {
67
+ fail('input-invalid', 'HQ_SEMANTIC_INVOCATION_INVALID', 'The invocation record is invalid.', {
68
+ path: error instanceof ProtocolSemanticInvocationError ? error.path : undefined,
69
+ cause: error,
70
+ });
71
+ }
72
+ // Pinning is checked before anything else observable, so a stale caller
73
+ // cannot learn whether a target exists in a generation it may not use.
74
+ if (invocation.activationRevision !== undefined
75
+ && invocation.activationRevision !== options.activationRevision) {
76
+ fail('stale-activation', 'HQ_SEMANTIC_STALE_ACTIVATION', 'The pinned activation is no longer active.', { retryable: true, relist: true });
77
+ }
78
+ const operation = invocation.operation;
79
+ const { dataset, metric, endpoint } = resolveTarget(operation);
80
+ let principal = null;
81
+ if (endpoint.access.kind === 'authenticated' && !options.authenticate) {
82
+ fail('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'An authenticator is required for this target.');
83
+ }
84
+ if (options.authenticate
85
+ && (endpoint.access.kind === 'authenticated' || request.credentials !== undefined)) {
86
+ try {
87
+ principal = await options.authenticate({
88
+ credentials: request.credentials, invocation, dataset, ...(metric ? { metric } : {}),
89
+ });
90
+ }
91
+ catch (error) {
92
+ fail('unauthenticated', 'HQ_SEMANTIC_UNAUTHENTICATED', 'Authentication failed.', { cause: error });
93
+ }
94
+ }
95
+ throwIfAborted(request.signal);
96
+ if (endpoint.access.kind === 'authenticated') {
97
+ if (!principal) {
98
+ fail('unauthenticated', 'HQ_SEMANTIC_UNAUTHENTICATED', 'Authentication is required.');
99
+ }
100
+ if (missing(endpoint.access.roles, principal.roles)
101
+ || missing(endpoint.access.scopes, principal.scopes)) {
102
+ fail('forbidden', 'HQ_SEMANTIC_FORBIDDEN', 'The principal lacks required access.');
103
+ }
104
+ }
105
+ let tenant;
106
+ if (endpoint.tenant.kind !== 'not-required') {
107
+ if (!options.resolveTenant) {
108
+ if (endpoint.tenant.kind === 'required') {
109
+ fail('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'A tenant resolver is required for this target.');
110
+ }
111
+ }
112
+ else {
113
+ try {
114
+ tenant = await options.resolveTenant({
115
+ principal, invocation, dataset, ...(metric ? { metric } : {}),
116
+ });
117
+ }
118
+ catch (error) {
119
+ fail('forbidden', 'HQ_SEMANTIC_FORBIDDEN', 'Tenant resolution failed.', { cause: error });
120
+ }
121
+ }
122
+ if (endpoint.tenant.kind === 'required' && (tenant === undefined || tenant === null)) {
123
+ fail('tenant-required', 'HQ_SEMANTIC_TENANT_REQUIRED', 'Tenant context is required.');
124
+ }
125
+ }
126
+ throwIfAborted(request.signal);
127
+ const budget = Object.freeze({
128
+ maxRows: lowest(invocation.budget?.maxRows, endpoint.maxLimit, dataset.limits?.maxResultSize, configured.maxRows) ?? configured.maxRows,
129
+ ...(lowest(invocation.budget?.deadlineMs, options.limits?.deadlineMs) === undefined
130
+ ? {}
131
+ : { deadlineMs: lowest(invocation.budget?.deadlineMs, options.limits?.deadlineMs) }),
132
+ ...(lowest(invocation.budget?.maxResponseBytes, options.limits?.maxResponseBytes) === undefined
133
+ ? {}
134
+ : {
135
+ maxResponseBytes: lowest(invocation.budget?.maxResponseBytes, options.limits?.maxResponseBytes),
136
+ }),
137
+ });
138
+ // Every ceiling the dataset declared binds here, not just the one that
139
+ // reaches the row budget. `maxDimensions`, `maxMeasures`, and `maxFilters`
140
+ // are published in the contract precisely so a gateway can apply them
141
+ // without loading the authoring package, and a set of ceilings where only
142
+ // one of the four binds is not a set of ceilings.
143
+ const violations = validateSemanticOperation(operation, dataset, datasets, {
144
+ maxRows: budget.maxRows,
145
+ maxOffset: configured.maxOffset,
146
+ maxDimensions: tighten(dataset.limits?.maxDimensions, configured.maxDimensions),
147
+ maxMeasures: tighten(dataset.limits?.maxMeasures, configured.maxMeasures),
148
+ maxFilters: tighten(dataset.limits?.maxFilters, configured.maxFilters),
149
+ });
150
+ if (violations.length > 0) {
151
+ const [first] = violations;
152
+ fail('input-invalid', 'HQ_SEMANTIC_INPUT_INVALID', first.message, { path: first.path });
153
+ }
154
+ throwIfAborted(request.signal);
155
+ let output;
156
+ try {
157
+ output = await options.execute({
158
+ deployment,
159
+ dataset,
160
+ ...(metric === undefined ? {} : { metric }),
161
+ operation,
162
+ principal,
163
+ tenant,
164
+ budget,
165
+ activationRevision: options.activationRevision,
166
+ ...(request.signal === undefined ? {} : { signal: request.signal }),
167
+ });
168
+ }
169
+ catch (error) {
170
+ if (error instanceof DeploymentSemanticInvocationError)
171
+ throw error;
172
+ if (request.signal?.aborted) {
173
+ fail('cancelled', 'HQ_SEMANTIC_CANCELLED', 'The invocation was cancelled.', { cause: error });
174
+ }
175
+ const claimed = claimedFailure(error);
176
+ if (claimed !== undefined) {
177
+ fail(claimed.category, claimed.code, claimed.message, {
178
+ cause: error,
179
+ retryable: claimed.retryable,
180
+ relist: claimed.relist,
181
+ });
182
+ }
183
+ fail('executor-failed', 'HQ_SEMANTIC_EXECUTION_FAILED', 'Semantic execution failed.', {
184
+ cause: error,
185
+ });
186
+ }
187
+ // An executor may ignore the signal and resolve anyway. Every other await
188
+ // in this sequence is followed by this check; so is the last one.
189
+ throwIfAborted(request.signal);
190
+ let result;
191
+ try {
192
+ result = validateProtocolSemanticInvocationResult(output);
193
+ }
194
+ catch (error) {
195
+ // An executor that returned something unexpected is an internal fault,
196
+ // never a correctable caller error.
197
+ fail('output-invalid', 'HQ_SEMANTIC_OUTPUT_INVALID', 'The executor returned an invalid result.', {
198
+ cause: error,
199
+ });
200
+ }
201
+ // Protocol validation proves the record is well-formed, not that it honored
202
+ // this invocation. The bounds this layer computed are enforced here, so an
203
+ // executor cannot widen them by returning more than it was allowed to.
204
+ if (result.activationRevision !== options.activationRevision) {
205
+ fail('output-invalid', 'HQ_SEMANTIC_OUTPUT_INVALID', 'The executor served a different activation than the one selected.');
206
+ }
207
+ // The caller's own `limit` is part of what it was allowed to ask for, and
208
+ // validation already proved it is no larger than the budget. An executor
209
+ // that returns more rows than were requested widened the request just as
210
+ // surely as one that exceeded the budget.
211
+ const maxRows = Math.min(budget.maxRows, operation.limit ?? budget.maxRows);
212
+ if (result.data.length > maxRows) {
213
+ fail('budget-exceeded', 'HQ_SEMANTIC_BUDGET_EXCEEDED', `The result has ${result.data.length} rows; the effective limit is ${maxRows}.`);
214
+ }
215
+ if (budget.maxResponseBytes !== undefined) {
216
+ const bytes = new TextEncoder().encode(JSON.stringify(result)).byteLength;
217
+ if (bytes > budget.maxResponseBytes) {
218
+ fail('budget-exceeded', 'HQ_SEMANTIC_BUDGET_EXCEEDED', `The result is ${bytes} bytes; the effective limit is ${budget.maxResponseBytes}.`);
219
+ }
220
+ }
221
+ return result;
222
+ }
223
+ return Object.freeze({ invoke });
224
+ }
@@ -0,0 +1,30 @@
1
+ import type { ProtocolSemanticInvocationFailure, ProtocolSemanticInvocationFailureCategory } from '@hypequery/protocol';
2
+ /** A failure that already carries the public category a caller should see. */
3
+ export declare class DeploymentSemanticInvocationError extends Error {
4
+ readonly category: ProtocolSemanticInvocationFailureCategory;
5
+ readonly code: string;
6
+ readonly path?: string;
7
+ readonly retryable: boolean;
8
+ readonly relist: boolean;
9
+ constructor(category: ProtocolSemanticInvocationFailureCategory, code: string, message: string, options?: {
10
+ readonly path?: string;
11
+ readonly cause?: unknown;
12
+ readonly retryable?: boolean;
13
+ readonly relist?: boolean;
14
+ });
15
+ }
16
+ export declare function fail(category: ProtocolSemanticInvocationFailureCategory, code: string, message: string, options?: {
17
+ readonly path?: string;
18
+ readonly cause?: unknown;
19
+ readonly retryable?: boolean;
20
+ readonly relist?: boolean;
21
+ }): never;
22
+ /**
23
+ * Projects a failure onto the portable record.
24
+ *
25
+ * The message is deliberately the one this module produced. A cause is never
26
+ * unwrapped into it, so a provider exception cannot reach a caller.
27
+ */
28
+ export declare function toProtocolSemanticInvocationFailure(error: unknown, activationRevision?: string): ProtocolSemanticInvocationFailure;
29
+ export declare function throwIfAborted(signal: AbortSignal | undefined): void;
30
+ //# sourceMappingURL=semantic-invocation-errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-invocation-errors.d.ts","sourceRoot":"","sources":["../src/semantic-invocation-errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iCAAiC,EAAE,yCAAyC,EAAE,MAAM,qBAAqB,CAAC;AAExH,8EAA8E;AAC9E,qBAAa,iCAAkC,SAAQ,KAAK;IAC1D,QAAQ,CAAC,QAAQ,EAAE,yCAAyC,CAAC;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAGvB,QAAQ,EAAE,yCAAyC,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QACP,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;KACtB;CAUT;AAED,wBAAgB,IAAI,CAClB,QAAQ,EAAE,yCAAyC,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,KAAK,CAEP;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,OAAO,EACd,kBAAkB,CAAC,EAAE,MAAM,GAC1B,iCAAiC,CAanC;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAIpE"}
@@ -0,0 +1,45 @@
1
+ /** A failure that already carries the public category a caller should see. */
2
+ export class DeploymentSemanticInvocationError extends Error {
3
+ category;
4
+ code;
5
+ path;
6
+ retryable;
7
+ relist;
8
+ constructor(category, code, message, options = {}) {
9
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
10
+ this.name = 'DeploymentSemanticInvocationError';
11
+ this.category = category;
12
+ this.code = code;
13
+ this.path = options.path;
14
+ this.retryable = options.retryable ?? false;
15
+ this.relist = options.relist ?? false;
16
+ }
17
+ }
18
+ export function fail(category, code, message, options = {}) {
19
+ throw new DeploymentSemanticInvocationError(category, code, message, options);
20
+ }
21
+ /**
22
+ * Projects a failure onto the portable record.
23
+ *
24
+ * The message is deliberately the one this module produced. A cause is never
25
+ * unwrapped into it, so a provider exception cannot reach a caller.
26
+ */
27
+ export function toProtocolSemanticInvocationFailure(error, activationRevision) {
28
+ const known = error instanceof DeploymentSemanticInvocationError;
29
+ return Object.freeze({
30
+ kind: 'hypequery-semantic-invocation-failure',
31
+ version: 1,
32
+ category: known ? error.category : 'executor-failed',
33
+ code: known ? error.code : 'HQ_SEMANTIC_EXECUTION_FAILED',
34
+ message: known ? error.message : 'Semantic invocation failed.',
35
+ ...(known && error.path !== undefined ? { path: error.path } : {}),
36
+ retryable: known ? error.retryable : false,
37
+ relist: known ? error.relist : false,
38
+ ...(activationRevision === undefined ? {} : { activationRevision }),
39
+ });
40
+ }
41
+ export function throwIfAborted(signal) {
42
+ if (signal?.aborted) {
43
+ fail('cancelled', 'HQ_SEMANTIC_CANCELLED', 'The invocation was cancelled.');
44
+ }
45
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Contract-driven validation of a semantic operation.
3
+ *
4
+ * The deployment package deliberately depends only on `@hypequery/protocol`, so
5
+ * every check here is made against the validated contract rather than by
6
+ * loading the authoring package. A gateway must be able to reject a bad
7
+ * invocation without any of the deploying application's code.
8
+ */
9
+ import type { ProtocolDatasetContract, ProtocolSemanticQuery } from '@hypequery/protocol';
10
+ export interface SemanticOperationViolation {
11
+ readonly message: string;
12
+ readonly path: string;
13
+ }
14
+ export interface SemanticOperationLimits {
15
+ /** Largest row count the caller may ask for after every ceiling is applied. */
16
+ readonly maxRows: number;
17
+ readonly maxOffset: number;
18
+ readonly maxDimensions: number;
19
+ readonly maxMeasures: number;
20
+ readonly maxFilters: number;
21
+ }
22
+ /**
23
+ * Validates one semantic operation against the contract that will serve it.
24
+ *
25
+ * Returns every violation rather than the first, so a caller correcting an
26
+ * agent-authored query sees the whole problem in one round trip.
27
+ */
28
+ export declare function validateSemanticOperation(operation: ProtocolSemanticQuery, dataset: ProtocolDatasetContract, datasets: ReadonlyMap<string, ProtocolDatasetContract>, limits: SemanticOperationLimits): readonly SemanticOperationViolation[];
29
+ //# sourceMappingURL=semantic-operation-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-operation-validation.d.ts","sourceRoot":"","sources":["../src/semantic-operation-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,uBAAuB,EAGvB,qBAAqB,EAEtB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAiGD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,uBAAuB,CAAC,EACtD,MAAM,EAAE,uBAAuB,GAC9B,SAAS,0BAA0B,EAAE,CAgGvC"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Contract-driven validation of a semantic operation.
3
+ *
4
+ * The deployment package deliberately depends only on `@hypequery/protocol`, so
5
+ * every check here is made against the validated contract rather than by
6
+ * loading the authoring package. A gateway must be able to reject a bad
7
+ * invocation without any of the deploying application's code.
8
+ */
9
+ const ALL_GRAINS = ['day', 'week', 'month', 'quarter', 'year'];
10
+ /**
11
+ * What a dataset exposes, before any metric narrows it.
12
+ *
13
+ * A relationship contributes `<name>.<dimension>` for its target's groupable
14
+ * dimensions, matching the one-hop rule the authoring layer enforces. Filters
15
+ * over a relationship are accepted on the same qualified names, because the
16
+ * contract has no separate filter declaration for a joined field.
17
+ */
18
+ function resolveDataset(dataset, datasets) {
19
+ const groupable = new Set();
20
+ const filters = new Map();
21
+ for (const dimension of dataset.dimensions) {
22
+ if (dimension.groupable)
23
+ groupable.add(String(dimension.name));
24
+ }
25
+ for (const filter of dataset.filters) {
26
+ filters.set(String(filter.name), new Set(filter.operators));
27
+ }
28
+ for (const relationship of dataset.relationships) {
29
+ if (!relationship.queryable)
30
+ continue;
31
+ const target = datasets.get(String(relationship.target));
32
+ for (const dimension of target?.dimensions ?? []) {
33
+ const qualified = `${String(relationship.name)}.${String(dimension.name)}`;
34
+ // The two capabilities are declared separately and travel separately
35
+ // across a join. Coupling them would both hide a filterable field that
36
+ // cannot be grouped and expose one the target declared unfilterable.
37
+ if (dimension.groupable)
38
+ groupable.add(qualified);
39
+ if (dimension.filterable) {
40
+ // A joined field has no named filter in the contract, so it accepts the
41
+ // full operator set the protocol allows rather than a narrowed list.
42
+ filters.set(qualified, new Set([
43
+ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'between', 'like',
44
+ ]));
45
+ }
46
+ }
47
+ }
48
+ return {
49
+ groupable,
50
+ filters,
51
+ measures: new Set(dataset.measures.map(measure => String(measure.name))),
52
+ grains: new Set(dataset.timeField === undefined ? [] : ALL_GRAINS),
53
+ };
54
+ }
55
+ /** A metric may only narrow what its dataset exposes, never widen it. */
56
+ function narrowToMetric(base, metric) {
57
+ const declaredDimensions = new Set(metric.dimensions.map(String));
58
+ const declaredFilters = new Set(metric.filters.map(String));
59
+ return {
60
+ groupable: new Set([...base.groupable].filter(name => declaredDimensions.has(name))),
61
+ filters: new Map([...base.filters].filter(([name]) => declaredFilters.has(name))),
62
+ // A metric selects itself; a caller cannot add measures to it.
63
+ measures: new Set(),
64
+ grains: new Set(metric.grain === undefined
65
+ ? metric.grains.filter(grain => base.grains.has(grain))
66
+ : [metric.grain]),
67
+ };
68
+ }
69
+ /**
70
+ * The field a filter expression addresses, or null when it is not a plain
71
+ * comparison.
72
+ *
73
+ * Both operands are checked. A right-hand side that is anything but a literal —
74
+ * another reference, an aggregate, a nested comparison — addresses something
75
+ * this validator never matched against the contract's allowlist, so accepting
76
+ * it would leave a published-surface check to whichever executor happens to be
77
+ * injected. It is also a caller mistake rather than a deployment capability
78
+ * gap, and only a violation raised here reports it as one.
79
+ */
80
+ function comparisonField(expression) {
81
+ if (expression.kind !== 'comparison')
82
+ return null;
83
+ if (expression.left.kind !== 'reference' || expression.right.kind !== 'literal')
84
+ return null;
85
+ return { field: String(expression.left.name), operator: expression.operator };
86
+ }
87
+ /**
88
+ * Validates one semantic operation against the contract that will serve it.
89
+ *
90
+ * Returns every violation rather than the first, so a caller correcting an
91
+ * agent-authored query sees the whole problem in one round trip.
92
+ */
93
+ export function validateSemanticOperation(operation, dataset, datasets, limits) {
94
+ const violations = [];
95
+ const fail = (message, path) => violations.push({ message, path });
96
+ let metric;
97
+ if (operation.kind === 'metric') {
98
+ metric = dataset.metrics.find(entry => String(entry.name) === String(operation.metric));
99
+ if (metric === undefined) {
100
+ fail(`Unknown metric "${String(operation.metric)}".`, '$.operation.metric');
101
+ return violations;
102
+ }
103
+ }
104
+ const base = resolveDataset(dataset, datasets);
105
+ const allowed = metric === undefined ? base : narrowToMetric(base, metric);
106
+ const dimensions = operation.dimensions ?? [];
107
+ if (dimensions.length > limits.maxDimensions) {
108
+ fail(`At most ${limits.maxDimensions} dimensions may be selected.`, '$.operation.dimensions');
109
+ }
110
+ dimensions.forEach((name, index) => {
111
+ if (!allowed.groupable.has(String(name))) {
112
+ fail(`Unknown or non-groupable dimension "${String(name)}".`, `$.operation.dimensions[${index}]`);
113
+ }
114
+ });
115
+ if (new Set(dimensions.map(String)).size !== dimensions.length) {
116
+ fail('Dimensions must be unique.', '$.operation.dimensions');
117
+ }
118
+ if (operation.kind === 'dataset') {
119
+ const measures = operation.measures ?? [];
120
+ if (measures.length > limits.maxMeasures) {
121
+ fail(`At most ${limits.maxMeasures} measures may be selected.`, '$.operation.measures');
122
+ }
123
+ measures.forEach((name, index) => {
124
+ if (!allowed.measures.has(String(name))) {
125
+ fail(`Unknown measure "${String(name)}".`, `$.operation.measures[${index}]`);
126
+ }
127
+ });
128
+ if (dimensions.length === 0 && measures.length === 0) {
129
+ fail('At least one dimension or measure must be selected.', '$.operation');
130
+ }
131
+ }
132
+ const filters = operation.filters ?? [];
133
+ if (filters.length > limits.maxFilters) {
134
+ fail(`At most ${limits.maxFilters} filters may be applied.`, '$.operation.filters');
135
+ }
136
+ filters.forEach((expression, index) => {
137
+ const path = `$.operation.filters[${index}]`;
138
+ const comparison = comparisonField(expression);
139
+ if (comparison === null) {
140
+ // Only `field <operator> value` is expressible against a contract; a
141
+ // richer expression could reference something the contract never
142
+ // published.
143
+ fail('Only a field/operator/value comparison may be filtered on.', path);
144
+ return;
145
+ }
146
+ const operators = allowed.filters.get(comparison.field);
147
+ if (operators === undefined) {
148
+ fail(`Unknown or unfilterable field "${comparison.field}".`, path);
149
+ return;
150
+ }
151
+ if (!operators.has(comparison.operator)) {
152
+ fail(`Operator "${comparison.operator}" is not allowed on "${comparison.field}".`, `${path}.operator`);
153
+ }
154
+ });
155
+ const orderBy = operation.orderBy ?? [];
156
+ const orderable = new Set([
157
+ ...allowed.groupable,
158
+ ...allowed.measures,
159
+ ...(metric === undefined ? [] : [String(metric.name)]),
160
+ ...(operation.by === undefined ? [] : ['period']),
161
+ ]);
162
+ orderBy.forEach((entry, index) => {
163
+ if (!orderable.has(String(entry.field))) {
164
+ fail(`Cannot order by "${String(entry.field)}".`, `$.operation.orderBy[${index}].field`);
165
+ }
166
+ });
167
+ if (operation.by !== undefined && !allowed.grains.has(operation.by)) {
168
+ fail(`Time grain "${operation.by}" is not supported here.`, '$.operation.by');
169
+ }
170
+ if (operation.limit !== undefined && operation.limit > limits.maxRows) {
171
+ fail(`The row limit may not exceed ${limits.maxRows}.`, '$.operation.limit');
172
+ }
173
+ if (operation.offset !== undefined && operation.offset > limits.maxOffset) {
174
+ fail(`The offset may not exceed ${limits.maxOffset}.`, '$.operation.offset');
175
+ }
176
+ return violations;
177
+ }
@@ -0,0 +1,2 @@
1
+ export declare function missing(required: readonly string[], held: readonly string[] | undefined): boolean;
2
+ //# sourceMappingURL=required-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"required-access.d.ts","sourceRoot":"","sources":["../../src/utils/required-access.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAGjG"}
@@ -0,0 +1,4 @@
1
+ export function missing(required, held) {
2
+ const available = new Set(held ?? []);
3
+ return required.some(value => !available.has(value));
4
+ }
@@ -0,0 +1,7 @@
1
+ /** Drops explicitly-undefined properties, so a spread cannot erase a default. */
2
+ export declare function definedLimits<T extends object>(limits: T | undefined): Partial<T>;
3
+ /** The lowest of every ceiling that applies. A caller can tighten, never widen. */
4
+ export declare function lowest(...values: readonly (number | undefined)[]): number | undefined;
5
+ /** A declared ceiling under a server one; declaring nothing leaves the server's. */
6
+ export declare function tighten(declared: number | undefined, ceiling: number): number;
7
+ //# sourceMappingURL=semantic-budget-limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-budget-limits.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-budget-limits.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAKjF;AAED,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,GAAG,SAAS,CAGrF;AAED,oFAAoF;AACpF,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E"}
@@ -0,0 +1,15 @@
1
+ /** Drops explicitly-undefined properties, so a spread cannot erase a default. */
2
+ export function definedLimits(limits) {
3
+ if (limits === undefined)
4
+ return {};
5
+ return Object.fromEntries(Object.entries(limits).filter(([, value]) => value !== undefined));
6
+ }
7
+ /** The lowest of every ceiling that applies. A caller can tighten, never widen. */
8
+ export function lowest(...values) {
9
+ const finite = values.filter((value) => value !== undefined);
10
+ return finite.length === 0 ? undefined : Math.min(...finite);
11
+ }
12
+ /** A declared ceiling under a server one; declaring nothing leaves the server's. */
13
+ export function tighten(declared, ceiling) {
14
+ return declared === undefined ? ceiling : Math.min(declared, ceiling);
15
+ }
@@ -0,0 +1,21 @@
1
+ import type { ProtocolSemanticInvocationFailureCategory } from '@hypequery/protocol';
2
+ /**
3
+ * The category an executor claimed, when it deliberately claimed one.
4
+ *
5
+ * The `hypequerySemanticFailure` marker, not the shape of the error, is what
6
+ * grants the claim. Allow-listing a `category` string alone would let a
7
+ * provider or library exception that happens to carry a generic one — a
8
+ * `not-found` from an HTTP client, say — put its own message in front of a
9
+ * caller and decide whether the call is retried. This module refuses to unwrap
10
+ * a cause for exactly that reason, and duck-typing would have reopened the door
11
+ * beside it. An error without the marker stays `executor-failed` with the
12
+ * generic message.
13
+ */
14
+ export declare function claimedFailure(error: unknown): {
15
+ category: ProtocolSemanticInvocationFailureCategory;
16
+ code: string;
17
+ message: string;
18
+ retryable: boolean;
19
+ relist: boolean;
20
+ } | undefined;
21
+ //# sourceMappingURL=semantic-executor-failure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-executor-failure.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-executor-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yCAAyC,EAAE,MAAM,qBAAqB,CAAC;AAiCrF;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG;IAC9C,QAAQ,EAAE,yCAAyC,CAAC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,SAAS,CAgBZ"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Categories an injected executor may claim for itself.
3
+ *
4
+ * Execution is injected, so the executor is the only component that knows the
5
+ * difference between "this query broke", "this deployment cannot express that",
6
+ * and "this deployment is incoherent". Every category here is one only the
7
+ * executor can determine; the ones it must never claim — `unauthenticated`,
8
+ * `forbidden`, `cancelled`, `stale-activation` — are decided by this module and
9
+ * are deliberately absent.
10
+ */
11
+ const EXECUTOR_CATEGORIES = new Set([
12
+ 'unsupported-capability',
13
+ 'budget-exceeded',
14
+ 'tenant-required',
15
+ 'configuration-invalid',
16
+ 'not-found',
17
+ 'input-invalid',
18
+ 'output-invalid',
19
+ 'executor-unavailable',
20
+ 'executor-failed',
21
+ ]);
22
+ /**
23
+ * The category an executor claimed, when it deliberately claimed one.
24
+ *
25
+ * The `hypequerySemanticFailure` marker, not the shape of the error, is what
26
+ * grants the claim. Allow-listing a `category` string alone would let a
27
+ * provider or library exception that happens to carry a generic one — a
28
+ * `not-found` from an HTTP client, say — put its own message in front of a
29
+ * caller and decide whether the call is retried. This module refuses to unwrap
30
+ * a cause for exactly that reason, and duck-typing would have reopened the door
31
+ * beside it. An error without the marker stays `executor-failed` with the
32
+ * generic message.
33
+ */
34
+ export function claimedFailure(error) {
35
+ if (typeof error !== 'object' || error === null)
36
+ return undefined;
37
+ const shape = error;
38
+ if (shape.hypequerySemanticFailure !== true)
39
+ return undefined;
40
+ if (typeof shape.category !== 'string' || !EXECUTOR_CATEGORIES.has(shape.category)) {
41
+ return undefined;
42
+ }
43
+ return {
44
+ category: shape.category,
45
+ code: typeof shape.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(shape.code)
46
+ ? shape.code
47
+ : 'HQ_SEMANTIC_EXECUTION_FAILED',
48
+ message: typeof shape.message === 'string' ? shape.message : 'Semantic execution failed.',
49
+ retryable: shape.retryable === true,
50
+ relist: shape.relist === true,
51
+ };
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/deployment",
3
- "version": "0.7.5",
3
+ "version": "0.8.0",
4
4
  "description": "Verified deployment intake, activation, and runtime hosting for Hypequery analytics",
5
5
  "keywords": [
6
6
  "hypequery",
@@ -26,12 +26,13 @@
26
26
  "README.md"
27
27
  ],
28
28
  "dependencies": {
29
- "@hypequery/protocol": "0.12.0"
29
+ "@hypequery/protocol": "0.13.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^22.5.0",
33
33
  "typescript": "^5.7.3",
34
- "vitest": "^3.2.6"
34
+ "vitest": "^3.2.6",
35
+ "@hypequery/datasets": "0.15.0"
35
36
  },
36
37
  "engines": {
37
38
  "node": ">=20"