@segmentstream-cloud/ontology 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/aggregate-routes.d.ts +46 -9
- package/dist/aggregate-routes.js +186 -20
- package/dist/analyze.d.ts +12 -1
- package/dist/analyze.js +3 -1
- package/dist/diagnostics.d.ts +3 -3
- package/dist/diagnostics.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/manifest.d.ts +108 -2
- package/dist/manifest.js +31 -2
- package/dist/measure.d.ts +57 -6
- package/dist/measure.js +7 -2
- package/dist/migrate/apply.js +24 -13
- package/dist/migrate/errors.d.ts +1 -1
- package/dist/migrate/model.d.ts +23 -3
- package/dist/migrate/model.js +1 -0
- package/dist/model-measures.d.ts +22 -0
- package/dist/model-measures.js +543 -0
- package/dist/ontology.d.ts +5 -4
- package/dist/render.d.ts +15 -3
- package/dist/render.js +56 -3
- package/dist/replication/baseline.d.ts +10 -2
- package/dist/replication/baseline.js +23 -14
- package/dist/replication/migrate.d.ts +6 -0
- package/dist/replication/migrate.js +64 -0
- package/dist/replication/model.d.ts +1 -1
- package/dist/replication/store.d.ts +2 -2
- package/dist/replication/store.js +8 -3
- package/dist/service/catalog.d.ts +2 -1
- package/dist/service/catalog.js +7 -3
- package/dist/service/mcp/server.js +5 -2
- package/dist/service/mcp/tools.js +25 -10
- package/dist/service/schemas.d.ts +21 -0
- package/dist/service/schemas.js +11 -2
- package/dist/warehouse-backed.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ action and is kept in history.
|
|
|
11
11
|
TypeScript definitions, handler records and SDK calls use camelCase: `jobTitle`,
|
|
12
12
|
`assignedOwnerId`, `createdAt`, and `changeStatus`. The SDK converts ontology names,
|
|
13
13
|
select identifiers and declared action input fields to snake_case for the generated
|
|
14
|
-
REST API, version
|
|
14
|
+
REST API, version 8 manifest and storage. TypeScript helpers and options stay
|
|
15
15
|
camelCase. JSON property contents, provider payloads, opaque action results and
|
|
16
16
|
historical change names remain data and are preserved. `formatManifest` serializes
|
|
17
17
|
the canonical wire contract shared by REST, storage and future transports.
|
|
@@ -28,6 +28,31 @@ one Effect version.
|
|
|
28
28
|
`ontology/` is a package of its own. The SDK is an ES module, so `ontology/package.json`
|
|
29
29
|
must set `"type": "module"`.
|
|
30
30
|
|
|
31
|
+
SDK 0.23.0 writes manifest version 8. `defineOntology({ measures })` declares measures of the
|
|
32
|
+
model: arithmetic over measures of entities, `m.ref("acmeSales.ordered")`, and over other
|
|
33
|
+
model measures, `m.ref("planAttainment")`. `align` names the properties that stand for one another
|
|
34
|
+
across the entities a measure reads, or `null` for an entity without a counterpart, such as a
|
|
35
|
+
current snapshot without a date:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
defineOntology({
|
|
39
|
+
entities: [AcmeSales, AcmePlans],
|
|
40
|
+
measures: {
|
|
41
|
+
planAttainment: m.divide(m.ref("acmeSales.ordered"), m.ref("acmePlans.plannedUnits"), {
|
|
42
|
+
align: [{ acmeSales: "orderDate", acmePlans: "month" }],
|
|
43
|
+
unit: "ratio",
|
|
44
|
+
description: "Ordered units over planned units for the same months.",
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Aggregate requests name a measure of the model by its bare name, `plan_attainment`, beside
|
|
51
|
+
`entity.measure`; a filter must reach every entity it reads unless its alignment says the entity has
|
|
52
|
+
no counterpart, and reading it requires reading each of those entities. `check` refuses aggregates
|
|
53
|
+
inside a measure of the model, unknown references, cycles and alignments that do not name every
|
|
54
|
+
entity once with properties of one type. Entity measures take no `align`.
|
|
55
|
+
|
|
31
56
|
SDK 0.17.0 writes manifest version 7. It adds `m.avg`, `m.min` and `m.max`; a `where` option on
|
|
32
57
|
every aggregate, so that a measure reads only the rows its conditions accept (`m.sum("amount",
|
|
33
58
|
{ where: [{ property: "status", operator: "equals", value: "won" }], description })`); and
|
|
@@ -72,7 +97,7 @@ SDK 0.13.0 also serves aggregates. Upgrade notes:
|
|
|
72
97
|
- Under `signIn: { policy: "open" }`, anyone who signs in holds `member`, so `check` refuses a
|
|
73
98
|
`member` whose wildcard (`*:read`, the default, or `*:*`) would let strangers read
|
|
74
99
|
warehouse-backed entities through aggregates. Grant those to `member` by name
|
|
75
|
-
(`
|
|
100
|
+
(`acme_sales:read`), or keep the `invited` policy.
|
|
76
101
|
|
|
77
102
|
See [ontology aggregates](https://github.com/segmentstream/segmentstream/blob/main/docs/ontology-aggregates.md)
|
|
78
103
|
and [ontology analytics](https://github.com/segmentstream/segmentstream/blob/main/docs/ontology-aggregates-spec.md).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { OntologyManifest } from "./manifest.js";
|
|
1
|
+
import type { ModelMeasureManifest, OntologyManifest } from "./manifest.js";
|
|
2
2
|
/**
|
|
3
3
|
* The names of a model-level aggregate request, as `/api/ontology/aggregate` and the platform's
|
|
4
4
|
* `POST …/aggregate` receive them: wire snake_case, `entity.member`.
|
|
5
5
|
*/
|
|
6
6
|
export interface AggregateNames {
|
|
7
|
-
/** `entity.measure
|
|
7
|
+
/** `entity.measure`, or the bare name of a measure of the model. */
|
|
8
8
|
readonly measures: readonly string[];
|
|
9
9
|
/** An identity, `entity.property`, or a route, `rootEntity.relationship[.relationship].property`. */
|
|
10
10
|
readonly dimensions?: readonly string[];
|
|
@@ -16,29 +16,56 @@ export interface AggregateNames {
|
|
|
16
16
|
/** How one measure entity reads every requested name. Paths are relative to the entity. */
|
|
17
17
|
export interface EntityRoutes {
|
|
18
18
|
readonly entity: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The measures the entity computes, without the entity prefix, each once: its requested measures
|
|
21
|
+
* in request order, then the hidden inputs of the requested model measures that read it, in their
|
|
22
|
+
* declaration order.
|
|
23
|
+
*/
|
|
20
24
|
readonly measures: readonly string[];
|
|
21
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Every requested dimension, in request order: `brand`, `product.brand`, `product.region.name`.
|
|
27
|
+
* A dimension that a requested model measure aligns reads the entity's own member of the group;
|
|
28
|
+
* `null` when the member is `null`, so the entity is broadcast across the dimension's values.
|
|
29
|
+
*/
|
|
22
30
|
readonly dimensions: readonly {
|
|
23
31
|
readonly identity: string;
|
|
24
|
-
readonly path: string;
|
|
32
|
+
readonly path: string | null;
|
|
25
33
|
}[];
|
|
26
34
|
/** The filters that apply to the entity, by their requested dimension name. */
|
|
27
35
|
readonly filters: readonly {
|
|
28
36
|
readonly name: string;
|
|
29
37
|
readonly path: string;
|
|
30
38
|
}[];
|
|
31
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The requested filter names that do not apply to the entity: it cannot reach them, or an
|
|
41
|
+
* alignment says it has no counterpart.
|
|
42
|
+
*/
|
|
32
43
|
readonly unappliedFilters: readonly string[];
|
|
33
|
-
/** The path of the bucketed dimension, when the request names one. */
|
|
44
|
+
/** The path of the bucketed dimension, when the request names one and the entity groups by it. */
|
|
34
45
|
readonly granularityOf?: string;
|
|
35
46
|
}
|
|
47
|
+
/** A requested measure of the model: the entity measures it reads, and their entities. */
|
|
48
|
+
export interface ModelMeasureRoute {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
/** `entity.measure`, in declaration order. */
|
|
51
|
+
readonly inputs: readonly string[];
|
|
52
|
+
/** The entities of the inputs, each once, in the order of the inputs. */
|
|
53
|
+
readonly entities: readonly string[];
|
|
54
|
+
}
|
|
36
55
|
export type AggregateRoutes = {
|
|
37
56
|
readonly ok: true;
|
|
38
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The measure entities, in the order their first measure was requested: an entity measure's
|
|
59
|
+
* entity, or the entities of a model measure's inputs.
|
|
60
|
+
*/
|
|
39
61
|
readonly entities: readonly EntityRoutes[];
|
|
40
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Every entity the request reads, the entities of model measures' inputs included, sorted:
|
|
64
|
+
* each needs `<entity>:read`.
|
|
65
|
+
*/
|
|
41
66
|
readonly touched: readonly string[];
|
|
67
|
+
/** The requested model measures, in request order. */
|
|
68
|
+
readonly model: readonly ModelMeasureRoute[];
|
|
42
69
|
} | {
|
|
43
70
|
readonly ok: false;
|
|
44
71
|
readonly reason: string;
|
|
@@ -72,6 +99,14 @@ export declare function unknownPropertyMessage(input: {
|
|
|
72
99
|
readonly entity: string;
|
|
73
100
|
readonly property: string;
|
|
74
101
|
}): string;
|
|
102
|
+
/** A bare measure name that is not a measure of the model. */
|
|
103
|
+
export declare function unknownModelMeasureMessage(name: string): string;
|
|
104
|
+
/** A filter that applies to some entities a model measure reads and cannot reach another. */
|
|
105
|
+
export declare function partialFilterMessage(input: {
|
|
106
|
+
readonly filter: string;
|
|
107
|
+
readonly entity: string;
|
|
108
|
+
readonly measure: string;
|
|
109
|
+
}): string;
|
|
75
110
|
/**
|
|
76
111
|
* Every entity one entity reaches, with the shortest routes there, in the order they are first
|
|
77
112
|
* reached. Routes follow one to {@link MAXIMUM_HOPS} declared `rel.one`s, never inverses, never
|
|
@@ -84,6 +119,8 @@ export declare function shortestRoutes(manifest: OntologyManifest, input: {
|
|
|
84
119
|
readonly from: string;
|
|
85
120
|
readonly to: string;
|
|
86
121
|
}): readonly AggregateRoute[];
|
|
122
|
+
/** The entities a model measure reads, each once, in the order of its inputs. */
|
|
123
|
+
export declare function inputEntities(measure: Pick<ModelMeasureManifest, "inputs">): readonly string[];
|
|
87
124
|
/**
|
|
88
125
|
* Resolves the names of a model-level aggregate against a manifest (docs/ontology-aggregates-spec.md,
|
|
89
126
|
* "Resolution"). It is the one place names and routes resolve: backend-next plans each measure
|
package/dist/aggregate-routes.js
CHANGED
|
@@ -29,6 +29,15 @@ export function toManySegmentMessage(input) {
|
|
|
29
29
|
export function unknownPropertyMessage(input) {
|
|
30
30
|
return `${input.entity} has no property ${input.property}.`;
|
|
31
31
|
}
|
|
32
|
+
/** A bare measure name that is not a measure of the model. */
|
|
33
|
+
export function unknownModelMeasureMessage(name) {
|
|
34
|
+
return `The ontology has no measure named ${name}.`;
|
|
35
|
+
}
|
|
36
|
+
/** A filter that applies to some entities a model measure reads and cannot reach another. */
|
|
37
|
+
export function partialFilterMessage(input) {
|
|
38
|
+
return (`${input.filter} does not apply to ${input.entity}, which ${input.measure} reads; align the ` +
|
|
39
|
+
"properties in the measure, or request its inputs instead.");
|
|
40
|
+
}
|
|
32
41
|
function entitiesByName(manifest) {
|
|
33
42
|
return new Map(manifest.entities.map((entity) => [entity.name, entity]));
|
|
34
43
|
}
|
|
@@ -86,16 +95,25 @@ class Refusal extends Error {
|
|
|
86
95
|
function refuse(reason) {
|
|
87
96
|
throw new Refusal(reason);
|
|
88
97
|
}
|
|
89
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* The ontology's entities and measures of the model by name, and the shortest routes from each
|
|
100
|
+
* measure entity.
|
|
101
|
+
*/
|
|
90
102
|
class RouteCatalog {
|
|
91
103
|
entities;
|
|
104
|
+
measures;
|
|
92
105
|
routes = new Map();
|
|
93
106
|
constructor(manifest) {
|
|
94
107
|
this.entities = entitiesByName(manifest);
|
|
108
|
+
this.measures = new Map((manifest.measures ?? []).map((measure) => [measure.name, measure]));
|
|
95
109
|
}
|
|
96
110
|
entity(name) {
|
|
97
111
|
return this.entities.get(name) ?? refuse(`${name} is not an entity of this ontology.`);
|
|
98
112
|
}
|
|
113
|
+
/** A measure of the model; a manifest before version 8 declares none. */
|
|
114
|
+
modelMeasure(name) {
|
|
115
|
+
return this.measures.get(name) ?? refuse(unknownModelMeasureMessage(name));
|
|
116
|
+
}
|
|
99
117
|
/** Declared properties, and the `id` of a stored entity, which its replication view carries. */
|
|
100
118
|
hasProperty(entity, property) {
|
|
101
119
|
const stored = entity.source === undefined && !entity.builtIn;
|
|
@@ -113,10 +131,23 @@ class RouteCatalog {
|
|
|
113
131
|
return reached.get(to) ?? [];
|
|
114
132
|
}
|
|
115
133
|
}
|
|
116
|
-
/**
|
|
117
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The requested measures, by entity. A bare name is a measure of the model, which reads the
|
|
136
|
+
* entities of its inputs; `entity.measure` is a measure of the entity.
|
|
137
|
+
*/
|
|
138
|
+
function requestedMeasures(input) {
|
|
118
139
|
const { catalog, measures } = input;
|
|
119
|
-
const
|
|
140
|
+
const entities = new Map();
|
|
141
|
+
const model = [];
|
|
142
|
+
const measureEntity = (name) => {
|
|
143
|
+
const existing = entities.get(name);
|
|
144
|
+
if (existing !== undefined) {
|
|
145
|
+
return existing;
|
|
146
|
+
}
|
|
147
|
+
const created = { entity: catalog.entity(name).name, requested: [], inputs: [], readers: [] };
|
|
148
|
+
entities.set(name, created);
|
|
149
|
+
return created;
|
|
150
|
+
};
|
|
120
151
|
if (measures.length === 0) {
|
|
121
152
|
refuse("Request at least one measure.");
|
|
122
153
|
}
|
|
@@ -125,6 +156,18 @@ function measureEntities(input) {
|
|
|
125
156
|
if (measures.indexOf(name) !== index) {
|
|
126
157
|
refuse(`${name} is requested twice.`);
|
|
127
158
|
}
|
|
159
|
+
if (segments.length === 1 && name.length > 0) {
|
|
160
|
+
const measure = catalog.modelMeasure(name);
|
|
161
|
+
model.push(measure);
|
|
162
|
+
for (const qualified of measure.inputs) {
|
|
163
|
+
const owner = measureEntity(qualified.slice(0, qualified.indexOf(SEPARATOR)));
|
|
164
|
+
owner.inputs.push(qualified.slice(qualified.indexOf(SEPARATOR) + 1));
|
|
165
|
+
if (!owner.readers.includes(measure)) {
|
|
166
|
+
owner.readers.push(measure);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
128
171
|
if (segments.length !== 2 || segments.some((segment) => segment.length === 0)) {
|
|
129
172
|
refuse(`${name} is not a measure name; write <entity>.<measure>.`);
|
|
130
173
|
}
|
|
@@ -133,9 +176,13 @@ function measureEntities(input) {
|
|
|
133
176
|
if (!(entity.measures ?? []).some((candidate) => candidate.name === measure)) {
|
|
134
177
|
refuse(`${entityName} has no measure ${measure}.`);
|
|
135
178
|
}
|
|
136
|
-
|
|
179
|
+
measureEntity(entityName).requested.push(measure);
|
|
137
180
|
}
|
|
138
|
-
return
|
|
181
|
+
return { entities, model };
|
|
182
|
+
}
|
|
183
|
+
/** The entities a model measure reads, each once, in the order of its inputs. */
|
|
184
|
+
export function inputEntities(measure) {
|
|
185
|
+
return [...new Set(measure.inputs.map((input) => input.slice(0, input.indexOf(SEPARATOR))))];
|
|
139
186
|
}
|
|
140
187
|
/**
|
|
141
188
|
* Parses an identity, `entity.property`, or a route form, `root.relationship[.relationship].property`,
|
|
@@ -217,6 +264,79 @@ function resolveName(input) {
|
|
|
217
264
|
}
|
|
218
265
|
return { kind: "resolved", route: routes[0] };
|
|
219
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* The member of an entity in the group of a model measure's alignment that an identity belongs to:
|
|
269
|
+
* a property, `null` when the entity has no counterpart, or `undefined` when no group has the
|
|
270
|
+
* identity.
|
|
271
|
+
*/
|
|
272
|
+
function memberOf(input) {
|
|
273
|
+
const { measure, entity, identity } = input;
|
|
274
|
+
const group = measure.align.find((candidate) => Object.entries(candidate).some(([owner, property]) => property !== null && `${owner}${SEPARATOR}${property}` === identity));
|
|
275
|
+
if (group === undefined || !Object.hasOwn(group, entity)) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
return group[entity] ?? null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* How the requested model measures that read an entity align a requested name: the entity's own
|
|
282
|
+
* member of the group the name's identity belongs to, `null` when the entity has no counterpart, or
|
|
283
|
+
* `undefined` when none of them aligns it. Two that align it differently are refused: the entity
|
|
284
|
+
* computes its inputs once.
|
|
285
|
+
*/
|
|
286
|
+
function alignedMember(input) {
|
|
287
|
+
const { entity, requested } = input;
|
|
288
|
+
let aligned;
|
|
289
|
+
for (const reader of entity.readers) {
|
|
290
|
+
const member = memberOf({
|
|
291
|
+
measure: reader,
|
|
292
|
+
entity: entity.entity,
|
|
293
|
+
identity: requested.identity,
|
|
294
|
+
});
|
|
295
|
+
if (member === undefined) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (aligned !== undefined && aligned.member !== member) {
|
|
299
|
+
refuse(`${requested.identity} is aligned differently by ${aligned.by} and ${reader.name}.`);
|
|
300
|
+
}
|
|
301
|
+
aligned ??= { by: reader.name, member };
|
|
302
|
+
}
|
|
303
|
+
return aligned?.member;
|
|
304
|
+
}
|
|
305
|
+
function resolveFor(input) {
|
|
306
|
+
const { catalog, entity, requested } = input;
|
|
307
|
+
const member = alignedMember({ entity, requested });
|
|
308
|
+
if (member !== undefined) {
|
|
309
|
+
return { kind: "aligned", path: member };
|
|
310
|
+
}
|
|
311
|
+
return resolveName({ catalog, from: entity.entity, requested });
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Two requested dimensions that are members of one alignment group of a requested model measure
|
|
315
|
+
* would group each of its entities twice by the same property.
|
|
316
|
+
*/
|
|
317
|
+
function refuseAlignedTwice(input) {
|
|
318
|
+
const { model, dimensions } = input;
|
|
319
|
+
for (const measure of model) {
|
|
320
|
+
for (const group of measure.align) {
|
|
321
|
+
const members = dimensions.filter(({ identity }) => Object.entries(group).some(([entity, property]) => property !== null && `${entity}${SEPARATOR}${property}` === identity));
|
|
322
|
+
const [first, second] = members;
|
|
323
|
+
if (first !== undefined && second !== undefined) {
|
|
324
|
+
refuse(`${second.identity} is aligned with ${first.identity}; request one of them.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* A dimension that a measure entity cannot reach, named after its first requested measure, or after
|
|
331
|
+
* the first requested model measure that reads it.
|
|
332
|
+
*/
|
|
333
|
+
function unreachableMessage(entity, identity) {
|
|
334
|
+
const [requested] = entity.requested;
|
|
335
|
+
if (requested !== undefined) {
|
|
336
|
+
return `${entity.entity}${SEPARATOR}${requested} cannot reach ${identity}.`;
|
|
337
|
+
}
|
|
338
|
+
return `${entity.readers[0]?.name} reads ${entity.entity}, which cannot reach ${identity}.`;
|
|
339
|
+
}
|
|
220
340
|
function ambiguousRoute(input) {
|
|
221
341
|
const { from, requested, routes } = input;
|
|
222
342
|
const count = COUNT_WORDS[routes.length] ?? String(routes.length);
|
|
@@ -258,7 +378,8 @@ function resolve(catalog, names) {
|
|
|
258
378
|
const measureNames = namesOf("measures", names.measures);
|
|
259
379
|
const dimensionNames = namesOf("dimensions", names.dimensions ?? []);
|
|
260
380
|
const filterNames = namesOf("filters", names.filters ?? []);
|
|
261
|
-
const
|
|
381
|
+
const requested = requestedMeasures({ catalog, measures: measureNames });
|
|
382
|
+
const groups = requested.entities;
|
|
262
383
|
const touched = new Set(groups.keys());
|
|
263
384
|
const dimensions = dimensionNames.map((name) => parseName({ catalog, roots: groups, name }));
|
|
264
385
|
for (const [index, dimension] of dimensions.entries()) {
|
|
@@ -266,6 +387,7 @@ function resolve(catalog, names) {
|
|
|
266
387
|
refuse(`${dimension.identity} is requested twice.`);
|
|
267
388
|
}
|
|
268
389
|
}
|
|
390
|
+
refuseAlignedTwice({ model: requested.model, dimensions });
|
|
269
391
|
const { granularityOf } = names;
|
|
270
392
|
const bucketed = granularityOf === undefined
|
|
271
393
|
? undefined
|
|
@@ -278,15 +400,17 @@ function resolve(catalog, names) {
|
|
|
278
400
|
touched.add(filter.target);
|
|
279
401
|
filter.written?.route.entities.forEach((entity) => touched.add(entity));
|
|
280
402
|
}
|
|
281
|
-
const routed = [...groups].map((
|
|
282
|
-
const firstMeasure = `${entity}${SEPARATOR}${measures[0]}`;
|
|
403
|
+
const routed = [...groups.values()].map((entity) => {
|
|
283
404
|
const dimensionRoutes = dimensions.map((dimension) => {
|
|
284
|
-
const resolution =
|
|
405
|
+
const resolution = resolveFor({ catalog, entity, requested: dimension });
|
|
406
|
+
if (resolution.kind === "aligned") {
|
|
407
|
+
return { identity: dimension.identity, path: resolution.path };
|
|
408
|
+
}
|
|
285
409
|
if (resolution.kind === "unreachable") {
|
|
286
|
-
refuse(
|
|
410
|
+
refuse(unreachableMessage(entity, dimension.identity));
|
|
287
411
|
}
|
|
288
412
|
if (resolution.kind === "ambiguous") {
|
|
289
|
-
refuse(ambiguousRoute({ from: entity, requested: dimension, routes: resolution.routes }));
|
|
413
|
+
refuse(ambiguousRoute({ from: entity.entity, requested: dimension, routes: resolution.routes }));
|
|
290
414
|
}
|
|
291
415
|
resolution.route.entities.forEach((visited) => touched.add(visited));
|
|
292
416
|
return {
|
|
@@ -294,39 +418,81 @@ function resolve(catalog, names) {
|
|
|
294
418
|
path: pathOf(resolution.route, dimension.property),
|
|
295
419
|
};
|
|
296
420
|
});
|
|
297
|
-
return { entity,
|
|
421
|
+
return { entity, dimensionRoutes };
|
|
298
422
|
});
|
|
299
|
-
const
|
|
423
|
+
const reach = new Map();
|
|
424
|
+
const entities = routed.map(({ entity, dimensionRoutes }) => {
|
|
300
425
|
const applied = [];
|
|
301
426
|
const unappliedFilters = [];
|
|
427
|
+
const reached = new Map();
|
|
302
428
|
for (const filter of filters) {
|
|
303
|
-
const resolution =
|
|
429
|
+
const resolution = resolveFor({ catalog, entity, requested: filter });
|
|
430
|
+
if (resolution.kind === "aligned") {
|
|
431
|
+
if (resolution.path === null) {
|
|
432
|
+
unappliedFilters.push(filter.name);
|
|
433
|
+
reached.set(filter.name, "noCounterpart");
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
applied.push({ name: filter.name, path: resolution.path });
|
|
437
|
+
reached.set(filter.name, "applied");
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
304
441
|
if (resolution.kind === "ambiguous") {
|
|
305
|
-
refuse(ambiguousRoute({ from: entity, requested: filter, routes: resolution.routes }));
|
|
442
|
+
refuse(ambiguousRoute({ from: entity.entity, requested: filter, routes: resolution.routes }));
|
|
306
443
|
}
|
|
307
444
|
if (resolution.kind === "unreachable") {
|
|
308
445
|
unappliedFilters.push(filter.name);
|
|
446
|
+
reached.set(filter.name, "unreachable");
|
|
309
447
|
continue;
|
|
310
448
|
}
|
|
311
449
|
resolution.route.entities.forEach((visited) => touched.add(visited));
|
|
312
450
|
applied.push({ name: filter.name, path: pathOf(resolution.route, filter.property) });
|
|
451
|
+
reached.set(filter.name, "applied");
|
|
313
452
|
}
|
|
453
|
+
reach.set(entity.entity, reached);
|
|
314
454
|
const bucketedPath = bucketed === undefined
|
|
315
455
|
? undefined
|
|
316
456
|
: dimensionRoutes.find((route) => route.identity === bucketed.identity)?.path;
|
|
317
457
|
return {
|
|
318
|
-
entity,
|
|
319
|
-
measures,
|
|
458
|
+
entity: entity.entity,
|
|
459
|
+
measures: [...new Set([...entity.requested, ...entity.inputs])],
|
|
320
460
|
dimensions: dimensionRoutes,
|
|
321
461
|
filters: applied,
|
|
322
462
|
unappliedFilters,
|
|
323
|
-
...(bucketedPath === undefined
|
|
463
|
+
...(bucketedPath === undefined || bucketedPath === null
|
|
464
|
+
? {}
|
|
465
|
+
: { granularityOf: bucketedPath }),
|
|
324
466
|
};
|
|
325
467
|
});
|
|
468
|
+
const model = requested.model.map((measure) => ({
|
|
469
|
+
name: measure.name,
|
|
470
|
+
inputs: measure.inputs,
|
|
471
|
+
entities: inputEntities(measure),
|
|
472
|
+
}));
|
|
326
473
|
for (const filter of filters) {
|
|
474
|
+
// A model measure's inputs are filtered alike, or the filter is refused: a filtered numerator
|
|
475
|
+
// over an unfiltered denominator would answer another question. Only the measure's own
|
|
476
|
+
// alignment may say that an entity it reads has no counterpart.
|
|
477
|
+
for (const measure of requested.model) {
|
|
478
|
+
const states = inputEntities(measure).map((entity) => {
|
|
479
|
+
const state = reach.get(entity)?.get(filter.name);
|
|
480
|
+
const exempt = state === "noCounterpart" &&
|
|
481
|
+
memberOf({ measure, entity, identity: filter.identity }) === null;
|
|
482
|
+
return { entity, state: state === "noCounterpart" && !exempt ? "unreachable" : state };
|
|
483
|
+
});
|
|
484
|
+
const missed = states.find(({ state }) => state === "unreachable");
|
|
485
|
+
if (missed !== undefined && states.some(({ state }) => state === "applied")) {
|
|
486
|
+
refuse(partialFilterMessage({
|
|
487
|
+
filter: filter.name,
|
|
488
|
+
entity: missed.entity,
|
|
489
|
+
measure: measure.name,
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
327
493
|
if (entities.every((routes) => routes.unappliedFilters.includes(filter.name))) {
|
|
328
494
|
refuse(`No measure can reach ${filter.name}.`);
|
|
329
495
|
}
|
|
330
496
|
}
|
|
331
|
-
return { entities, touched: [...touched].sort() };
|
|
497
|
+
return { entities, touched: [...touched].sort(), model };
|
|
332
498
|
}
|
package/dist/analyze.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ import { type PropertyDefinition } from "./property.js";
|
|
|
7
7
|
import { type AnyRelationship } from "./relationship.js";
|
|
8
8
|
import { type Role } from "./role.js";
|
|
9
9
|
import { type SignInOptions } from "./sign-in.js";
|
|
10
|
+
import type { ModelMeasureManifest } from "./manifest.js";
|
|
11
|
+
import type { ModelMeasures } from "./measure.js";
|
|
10
12
|
/** The input of `defineOntology` and `check`. */
|
|
11
|
-
export interface OntologyConfig<Entities extends ReadonlyArray<AnyEntity> = ReadonlyArray<AnyEntity
|
|
13
|
+
export interface OntologyConfig<Entities extends ReadonlyArray<AnyEntity> = ReadonlyArray<AnyEntity>, Measures extends ModelMeasures = ModelMeasures> {
|
|
12
14
|
/**
|
|
13
15
|
* The app's entities. The built-in `User` and `AgentChat` are always included; listing them is
|
|
14
16
|
* optional.
|
|
@@ -28,6 +30,13 @@ export interface OntologyConfig<Entities extends ReadonlyArray<AnyEntity> = Read
|
|
|
28
30
|
* handle an action lists must be here, and every handle here must be used by an action.
|
|
29
31
|
*/
|
|
30
32
|
readonly connections?: ReadonlyArray<AnyConnectionHandle>;
|
|
33
|
+
/**
|
|
34
|
+
* Measures of the model, by camelCase name: each combines measures of entities, read with
|
|
35
|
+
* `m.ref("entity.measure")`, or other model measures, read with `m.ref("name")`, through
|
|
36
|
+
* `m.add`, `m.subtract`, `m.multiply`, `m.divide` and `m.literal`. `align` names the properties
|
|
37
|
+
* that stand for one another across the entities it reads.
|
|
38
|
+
*/
|
|
39
|
+
readonly measures?: Measures;
|
|
31
40
|
}
|
|
32
41
|
/** What every active app user may do, beyond the roles they are given. */
|
|
33
42
|
export interface MemberOptions {
|
|
@@ -115,6 +124,8 @@ export interface Analysis {
|
|
|
115
124
|
readonly signIn: SignInOptions;
|
|
116
125
|
/** The declared connections, in order. */
|
|
117
126
|
readonly connections: ReadonlyArray<ResolvedConnection>;
|
|
127
|
+
/** The measures of the model that passed `check`, in their manifest form. */
|
|
128
|
+
readonly measures: ReadonlyArray<ModelMeasureManifest>;
|
|
118
129
|
readonly diagnostics: ReadonlyArray<Diagnostic>;
|
|
119
130
|
}
|
|
120
131
|
/**
|
package/dist/analyze.js
CHANGED
|
@@ -15,6 +15,7 @@ import { AgentChat } from "./agent-chat.js";
|
|
|
15
15
|
import { User } from "./user.js";
|
|
16
16
|
import { authoringToWire } from "./authoring.js";
|
|
17
17
|
import { nameToWire } from "./authoring-names.js";
|
|
18
|
+
import { resolveModelMeasures } from "./model-measures.js";
|
|
18
19
|
const GENERATED_UPSERT = new RegExp(`^${GENERATED_UPSERT_PREFIX}`);
|
|
19
20
|
function errorMessage(error) {
|
|
20
21
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -60,12 +61,13 @@ export function analyze(config, options = {}) {
|
|
|
60
61
|
checkMemberNames(resolved, report);
|
|
61
62
|
checkToolNames(resolved, report);
|
|
62
63
|
checkWarehouseSource(resolved, report);
|
|
64
|
+
const measures = resolveModelMeasures({ measures: config.measures, entities: resolved, report });
|
|
63
65
|
checkConnectionsUsed(resolved, connections, config.connections ?? [], report);
|
|
64
66
|
if (options.checkPermissions !== false) {
|
|
65
67
|
checkPermissions(resolved, roles, report);
|
|
66
68
|
checkOpenWarehouseReads({ entities: resolved, roles, signIn, report });
|
|
67
69
|
}
|
|
68
|
-
return { entities: resolved, roles, signIn, connections, diagnostics };
|
|
70
|
+
return { entities: resolved, roles, signIn, connections, measures, diagnostics };
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
73
|
* The operations of a stored entity that become MCP tools, each with the path a problem is reported
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
export declare const DIAGNOSTIC_CODES: readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name"];
|
|
2
|
+
export declare const DIAGNOSTIC_CODES: readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name", "invalid-alignment", "single-entity-model-measure"];
|
|
3
3
|
export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
|
|
4
4
|
/** A problem `check` found in an ontology definition. */
|
|
5
5
|
export declare const Diagnostic: Schema.Struct<{
|
|
6
6
|
readonly severity: Schema.Literals<readonly ["error", "warning"]>;
|
|
7
|
-
readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name"]>;
|
|
7
|
+
readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name", "invalid-alignment", "single-entity-model-measure"]>;
|
|
8
8
|
/** Where the problem is, for example `lead.properties.status.options[1]`. */
|
|
9
9
|
readonly path: Schema.String;
|
|
10
10
|
readonly message: Schema.String;
|
|
@@ -14,7 +14,7 @@ declare const InvalidOntology_base: Schema.Class<InvalidOntology, Schema.TaggedS
|
|
|
14
14
|
readonly message: Schema.String;
|
|
15
15
|
readonly diagnostics: Schema.$Array<Schema.Struct<{
|
|
16
16
|
readonly severity: Schema.Literals<readonly ["error", "warning"]>;
|
|
17
|
-
readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name"]>;
|
|
17
|
+
readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name", "invalid-alignment", "single-entity-model-measure"]>;
|
|
18
18
|
/** Where the problem is, for example `lead.properties.status.options[1]`. */
|
|
19
19
|
readonly path: Schema.String;
|
|
20
20
|
readonly message: Schema.String;
|
package/dist/diagnostics.js
CHANGED
|
@@ -50,6 +50,8 @@ export const DIAGNOSTIC_CODES = [
|
|
|
50
50
|
"unused-connection",
|
|
51
51
|
"invalid-prepare",
|
|
52
52
|
"invalid-tool-name",
|
|
53
|
+
"invalid-alignment",
|
|
54
|
+
"single-entity-model-measure",
|
|
53
55
|
];
|
|
54
56
|
/** A problem `check` found in an ontology definition. */
|
|
55
57
|
export const Diagnostic = Schema.Struct({
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { BlobValue, BlobUpload, uploads, BLOB_CONTENT_TYPES, MAX_BLOB_BYTES } fr
|
|
|
2
2
|
export { warehouse, compatibleWarehouseColumn, warehouseColumnType, validateWarehouseSource, } from "./source.js";
|
|
3
3
|
export type { WarehouseSource } from "./source.js";
|
|
4
4
|
export { m } from "./measure.js";
|
|
5
|
-
export type { AggregateDeclarations, AggregateOptions, Measure, MeasureColumnCondition, MeasureCondition, Measures, MeasureExpression, MeasureOptions, } from "./measure.js";
|
|
5
|
+
export type { AggregateDeclarations, AggregateOptions, AlignmentGroup, Measure, MeasureColumnCondition, MeasureCondition, Measures, MeasureExpression, MeasureOptions, ModelMeasure, ModelMeasureOptions, ModelMeasures, } from "./measure.js";
|
|
6
6
|
export { MEASURE_CONDITION_OPERATORS } from "./aggregate-filters.js";
|
|
7
7
|
export type { AggregateValue, MeasureConditionOperator } from "./aggregate-filters.js";
|
|
8
8
|
export { action, isAction } from "./action.js";
|
|
@@ -16,7 +16,7 @@ export { entity, isEntity } from "./entity.js";
|
|
|
16
16
|
export type { ActionsBuilder, AnyEntity, AnyStoredEntity, CreateValues, DraftEntity, Entity, EntityDefinition, EntityRecord, IndexDefinition, IndexField, NoActions, Properties, RecordRef, Relationships, SystemFields, UpdateValues, } from "./entity.js";
|
|
17
17
|
export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
|
|
18
18
|
export type { ActionError } from "./errors.js";
|
|
19
|
-
export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
|
|
19
|
+
export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, ModelMeasureManifest, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
|
|
20
20
|
export { SYSTEM_PROPERTIES } from "./names.js";
|
|
21
21
|
export type { SystemProperty } from "./names.js";
|
|
22
22
|
export { check, defineOntology } from "./ontology.js";
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connectio
|
|
|
7
7
|
export { Diagnostic, DIAGNOSTIC_CODES, formatDiagnostics, InvalidOntology } from "./diagnostics.js";
|
|
8
8
|
export { entity, isEntity } from "./entity.js";
|
|
9
9
|
export { ActionRejected, Conflict, NotFound, PermissionDenied } from "./errors.js";
|
|
10
|
-
export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
|
|
10
|
+
export { ActionManifest, actionConnectionKeys, canonicalJson, ConnectionManifest, EntityManifest, EntityPermissionsManifest, decodeManifestDocument, encodeManifest, formatManifest, IndexManifest, isValueMatched, MANIFEST_VERSION, manifestHash, ModelMeasureManifest, OntologyManifest, OntologyManifestWire, PropertyManifest, RelationshipManifest, RoleManifest, SelectOptionManifest, SignInManifest, signInPolicyOf, } from "./manifest.js";
|
|
11
11
|
export { SYSTEM_PROPERTIES } from "./names.js";
|
|
12
12
|
export { check, defineOntology } from "./ontology.js";
|
|
13
13
|
export { DEFAULT_MEMBER_PERMISSIONS, generatedActionPermissions, grants, isAllowed, parsePermission, permissionsOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
|