@mat3ra/esse 2026.8.31-0 → 2026.9.5-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.
|
@@ -7,6 +7,17 @@ export interface EntityGraphNodeManifest {
|
|
|
7
7
|
isMonitor?: boolean;
|
|
8
8
|
defaultUnits?: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Navigation facets derived at build time. Keys are sorted; a value is the slug as the
|
|
12
|
+
* corpus spells it — the enum a CateCom vocabulary schema narrows, or a path segment for
|
|
13
|
+
* M-CODE and the catalogues. A narrowing to several values is joined with "|" in enum
|
|
14
|
+
* order, because such a schema is the branch rather than a point on the ladder.
|
|
15
|
+
*
|
|
16
|
+
* Present on every `category` and `directory` node, absent on every other layer.
|
|
17
|
+
*/
|
|
18
|
+
export type EntityGraphFacets = Record<string, string>;
|
|
19
|
+
/** The five coordinate fields of `core/reusable/categories`, in ladder order. */
|
|
20
|
+
export declare const CATECOM_FIELDS: readonly ["tier1", "tier2", "tier3", "type", "subtype"];
|
|
10
21
|
export interface EntityGraphNode {
|
|
11
22
|
/** Schema `$id`, dash-separated, e.g. "in-memory-entity/named-defaultable". */
|
|
12
23
|
id: string;
|
|
@@ -31,6 +42,8 @@ export interface EntityGraphNode {
|
|
|
31
42
|
propertyCount: number;
|
|
32
43
|
hasExample: boolean;
|
|
33
44
|
manifest?: EntityGraphNodeManifest;
|
|
45
|
+
/** Navigation facets — see EntityGraphFacets. Category and directory nodes only. */
|
|
46
|
+
facets?: EntityGraphFacets;
|
|
34
47
|
/** Map coordinates, baked at build time so the client renders without laying out. */
|
|
35
48
|
x?: number;
|
|
36
49
|
y?: number;
|
|
@@ -110,6 +123,29 @@ interface RawReference {
|
|
|
110
123
|
* under `properties.data.oneOf` is recorded as a variant *of `data`*.
|
|
111
124
|
*/
|
|
112
125
|
export declare function collectReferences(schema: JSONSchema): RawReference[];
|
|
126
|
+
interface LoadedSchema {
|
|
127
|
+
sourcePath: string;
|
|
128
|
+
relativePath: string;
|
|
129
|
+
schema: JSONSchema;
|
|
130
|
+
}
|
|
131
|
+
interface FacetContext {
|
|
132
|
+
nodes: Map<string, EntityGraphNode>;
|
|
133
|
+
schemaById: Map<string, LoadedSchema>;
|
|
134
|
+
schemasBySourcePath: Map<string, LoadedSchema>;
|
|
135
|
+
/** `extends` targets per source id, sorted, so the upward walk is deterministic. */
|
|
136
|
+
extendsTargets: Map<string, string[]>;
|
|
137
|
+
/** Target of the `categories` property edge, per directory node id. */
|
|
138
|
+
categoriesTarget: Map<string, string>;
|
|
139
|
+
coordinateMemo: Map<string, EntityGraphFacets>;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Facets for one node, or undefined when no rule covers it.
|
|
143
|
+
*
|
|
144
|
+
* Category nodes carry their scheme's coordinate (CateCom's ladder or M-CODE's axes);
|
|
145
|
+
* directory nodes carry the catalogue they belong to, that catalogue's own axis, and — when
|
|
146
|
+
* the entry declares a `categories` property — the coordinate it is filed at.
|
|
147
|
+
*/
|
|
148
|
+
export declare function deriveFacets(node: EntityGraphNode, context: FacetContext): EntityGraphFacets | undefined;
|
|
113
149
|
export interface BuildEntityGraphResult {
|
|
114
150
|
graph: EntityGraph;
|
|
115
151
|
lint: EntityGraphLintResult;
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ISOLATED_NODE_BASELINE = void 0;
|
|
6
|
+
exports.ISOLATED_NODE_BASELINE = exports.CATECOM_FIELDS = void 0;
|
|
7
7
|
exports.schemaIdToPublishedPath = schemaIdToPublishedPath;
|
|
8
8
|
exports.publishedPathToSchemaId = publishedPathToSchemaId;
|
|
9
9
|
exports.buildPublishedPathIndex = buildPublishedPathIndex;
|
|
@@ -11,6 +11,7 @@ exports.sourcePathToSchemaId = sourcePathToSchemaId;
|
|
|
11
11
|
exports.classifyLayer = classifyLayer;
|
|
12
12
|
exports.resolveJsonPointer = resolveJsonPointer;
|
|
13
13
|
exports.collectReferences = collectReferences;
|
|
14
|
+
exports.deriveFacets = deriveFacets;
|
|
14
15
|
exports.buildEntityGraph = buildEntityGraph;
|
|
15
16
|
exports.validateEntityGraph = validateEntityGraph;
|
|
16
17
|
exports.writeEntityGraph = writeEntityGraph;
|
|
@@ -34,6 +35,8 @@ const ajv_1 = require("../utils/ajv");
|
|
|
34
35
|
const filesystem_1 = require("../utils/filesystem");
|
|
35
36
|
const entity_graph_schema_json_1 = __importDefault(require("./entity_graph.schema.json"));
|
|
36
37
|
const entityGraphLayout_1 = require("./entityGraphLayout");
|
|
38
|
+
/** The five coordinate fields of `core/reusable/categories`, in ladder order. */
|
|
39
|
+
exports.CATECOM_FIELDS = ["tier1", "tier2", "tier3", "type", "subtype"];
|
|
37
40
|
/**
|
|
38
41
|
* Isolated schemas at the time the lint baseline was taken; growth is reported, not failed.
|
|
39
42
|
* These are leaf definitions and externally-consumed formats that nothing else references.
|
|
@@ -210,6 +213,164 @@ function readPropertiesManifest() {
|
|
|
210
213
|
return {};
|
|
211
214
|
return ((_a = js_yaml_1.default.load(fs_1.default.readFileSync(settings_1.PROPERTIES_MANIFEST_PATH, "utf-8"))) !== null && _a !== void 0 ? _a : {});
|
|
212
215
|
}
|
|
216
|
+
// ── Facets ────────────────────────────────────────────────────────────────────
|
|
217
|
+
const ENUM_HOLDER = /^(enum_options|enum|enums)$/;
|
|
218
|
+
const DIMENSIONALITY = /^(zero|one|two|three)-dimensional$/;
|
|
219
|
+
const OPERATIONS_PREFIX = "materials-category-components/operations/";
|
|
220
|
+
const VALUE_SHAPES = ["scalar", "non-scalar", "structural", "elemental", "workflow"];
|
|
221
|
+
const METHOD_BRANCHES = ["mathematical", "physical"];
|
|
222
|
+
/** Source path segments below `schema/`, so `non-scalar` keeps the dash its `$id` loses. */
|
|
223
|
+
function sourceSegments(node) {
|
|
224
|
+
return node.path
|
|
225
|
+
.replace(/^schema\//, "")
|
|
226
|
+
.replace(/\.json$/, "")
|
|
227
|
+
.split("/");
|
|
228
|
+
}
|
|
229
|
+
function sortedFacets(facets) {
|
|
230
|
+
return Object.fromEntries(Object.entries(facets).sort(([a], [b]) => a.localeCompare(b)));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Which of the five CateCom fields this vocabulary schema narrows, each resolved to its enum.
|
|
234
|
+
*
|
|
235
|
+
* The enum is the coordinate, not the path segment: `fapprx/basisexp.json` narrows `tier2`
|
|
236
|
+
* to `basisExp`, and for methods the first segment (`mathematical`, `physical`) is not a tier
|
|
237
|
+
* at all — so counting path depth gets the ladder wrong.
|
|
238
|
+
*/
|
|
239
|
+
function ownNarrowings(id, context) {
|
|
240
|
+
var _a;
|
|
241
|
+
const loaded = context.schemaById.get(id);
|
|
242
|
+
if (!loaded)
|
|
243
|
+
return {};
|
|
244
|
+
const properties = ((_a = loaded.schema.properties) !== null && _a !== void 0 ? _a : {});
|
|
245
|
+
const narrowed = {};
|
|
246
|
+
exports.CATECOM_FIELDS.forEach((field) => {
|
|
247
|
+
var _a;
|
|
248
|
+
const property = properties[field];
|
|
249
|
+
if (!property)
|
|
250
|
+
return;
|
|
251
|
+
let values = Array.isArray(property.enum) ? property.enum : undefined;
|
|
252
|
+
if (!values && typeof property.$ref === "string") {
|
|
253
|
+
const [filePart, pointerPart] = property.$ref.split("#");
|
|
254
|
+
const target = (_a = context.schemasBySourcePath.get(path_1.default.resolve(path_1.default.dirname(loaded.sourcePath), filePart))) === null || _a === void 0 ? void 0 : _a.schema;
|
|
255
|
+
const fragment = resolveJsonPointer(target, `/${(pointerPart !== null && pointerPart !== void 0 ? pointerPart : "").replace(/^\//, "")}`);
|
|
256
|
+
values = fragment === null || fragment === void 0 ? void 0 : fragment.enum;
|
|
257
|
+
}
|
|
258
|
+
if (Array.isArray(values) && values.length > 0) {
|
|
259
|
+
narrowed[field] = values.map(String).join("|");
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
return narrowed;
|
|
263
|
+
}
|
|
264
|
+
/** Ancestors' narrowings (walking `extends` within the category layer), then this node's; own wins. */
|
|
265
|
+
function catecomCoordinate(id, context) {
|
|
266
|
+
var _a;
|
|
267
|
+
const memoised = context.coordinateMemo.get(id);
|
|
268
|
+
if (memoised)
|
|
269
|
+
return memoised;
|
|
270
|
+
context.coordinateMemo.set(id, {});
|
|
271
|
+
const coordinate = {};
|
|
272
|
+
((_a = context.extendsTargets.get(id)) !== null && _a !== void 0 ? _a : [])
|
|
273
|
+
.filter((parent) => { var _a; return ((_a = context.nodes.get(parent)) === null || _a === void 0 ? void 0 : _a.layer) === "category"; })
|
|
274
|
+
.forEach((parent) => Object.assign(coordinate, catecomCoordinate(parent, context)));
|
|
275
|
+
Object.assign(coordinate, ownNarrowings(id, context));
|
|
276
|
+
context.coordinateMemo.set(id, coordinate);
|
|
277
|
+
return coordinate;
|
|
278
|
+
}
|
|
279
|
+
/** Last segment of the first operation reached walking `extends` upward. */
|
|
280
|
+
function mcodeOperation(id, context, seen = new Set()) {
|
|
281
|
+
var _a;
|
|
282
|
+
if (seen.has(id))
|
|
283
|
+
return undefined;
|
|
284
|
+
seen.add(id);
|
|
285
|
+
const parents = (_a = context.extendsTargets.get(id)) !== null && _a !== void 0 ? _a : [];
|
|
286
|
+
const direct = parents.find((parent) => parent.startsWith(OPERATIONS_PREFIX));
|
|
287
|
+
if (direct)
|
|
288
|
+
return direct.split("/").pop();
|
|
289
|
+
return parents.map((parent) => mcodeOperation(parent, context, seen)).find(Boolean);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Facets for one node, or undefined when no rule covers it.
|
|
293
|
+
*
|
|
294
|
+
* Category nodes carry their scheme's coordinate (CateCom's ladder or M-CODE's axes);
|
|
295
|
+
* directory nodes carry the catalogue they belong to, that catalogue's own axis, and — when
|
|
296
|
+
* the entry declares a `categories` property — the coordinate it is filed at.
|
|
297
|
+
*/
|
|
298
|
+
function deriveFacets(node, context) {
|
|
299
|
+
if (node.layer !== "category" && node.layer !== "directory")
|
|
300
|
+
return undefined;
|
|
301
|
+
const segments = sourceSegments(node);
|
|
302
|
+
const [, second, third, fourth] = segments;
|
|
303
|
+
const holder = ENUM_HOLDER.test(segments[segments.length - 1]);
|
|
304
|
+
const facets = {};
|
|
305
|
+
if (node.layer === "category") {
|
|
306
|
+
if (node.domain === "models_category" || node.domain === "methods_category") {
|
|
307
|
+
facets.scheme = "catecom";
|
|
308
|
+
if (node.domain === "methods_category")
|
|
309
|
+
facets.branch = second;
|
|
310
|
+
if (holder) {
|
|
311
|
+
facets.role = "enum-options";
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
facets.role = "vocabulary";
|
|
315
|
+
Object.assign(facets, catecomCoordinate(node.id, context));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (node.domain === "materials_category") {
|
|
319
|
+
facets.scheme = "mcode";
|
|
320
|
+
facets.role = "recipe";
|
|
321
|
+
facets.structuralClass = second.replace(/_structures$/, "").replace(/_/g, "-");
|
|
322
|
+
facets.dimensionality = third;
|
|
323
|
+
const operation = mcodeOperation(node.id, context);
|
|
324
|
+
if (operation)
|
|
325
|
+
facets.operation = operation;
|
|
326
|
+
}
|
|
327
|
+
else if (node.domain === "materials_category_components" && second === "entities") {
|
|
328
|
+
facets.scheme = "mcode";
|
|
329
|
+
facets.role = "entity";
|
|
330
|
+
facets.entityRole = third;
|
|
331
|
+
// Dimensionality sits one segment deeper here than under materials_category.
|
|
332
|
+
facets.dimensionality = fourth;
|
|
333
|
+
}
|
|
334
|
+
else if (node.domain === "materials_category_components" && second === "operations") {
|
|
335
|
+
facets.scheme = "mcode";
|
|
336
|
+
facets.role = holder ? "enum-options" : "operation";
|
|
337
|
+
facets.operationKind = fourth;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return sortedFacets(facets);
|
|
343
|
+
}
|
|
344
|
+
facets.catalogue = node.domain.replace(/_directory$/, "").replace(/_/g, "-");
|
|
345
|
+
if (holder) {
|
|
346
|
+
facets.role = "enum-options";
|
|
347
|
+
return sortedFacets(facets);
|
|
348
|
+
}
|
|
349
|
+
if (second === "legacy")
|
|
350
|
+
facets.legacy = "true";
|
|
351
|
+
if (node.domain === "properties_directory" && VALUE_SHAPES.includes(second)) {
|
|
352
|
+
facets.valueShape = second;
|
|
353
|
+
}
|
|
354
|
+
if (node.domain === "software_directory")
|
|
355
|
+
facets.softwareKind = second;
|
|
356
|
+
if (node.domain === "methods_directory" && METHOD_BRANCHES.includes(second)) {
|
|
357
|
+
facets.branch = second;
|
|
358
|
+
}
|
|
359
|
+
if (node.domain === "context_providers_directory") {
|
|
360
|
+
facets.scope = second === "by_application" ? "by-application" : "generic";
|
|
361
|
+
if (second === "by_application")
|
|
362
|
+
[facets.application] = third.split("_");
|
|
363
|
+
}
|
|
364
|
+
const coordinate = context.categoriesTarget.get(node.id);
|
|
365
|
+
if (coordinate) {
|
|
366
|
+
const inherited = catecomCoordinate(coordinate, context);
|
|
367
|
+
exports.CATECOM_FIELDS.forEach((field) => {
|
|
368
|
+
if (inherited[field])
|
|
369
|
+
facets[field] = inherited[field];
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return sortedFacets(facets);
|
|
373
|
+
}
|
|
213
374
|
function buildEntityGraph() {
|
|
214
375
|
const failures = [];
|
|
215
376
|
const warnings = [];
|
|
@@ -350,6 +511,50 @@ function buildEntityGraph() {
|
|
|
350
511
|
...(((_a = entry.defaults) === null || _a === void 0 ? void 0 : _a.units) ? { defaultUnits: String(entry.defaults.units) } : {}),
|
|
351
512
|
};
|
|
352
513
|
});
|
|
514
|
+
// L11 — facets are total on the category and directory layers, and well formed.
|
|
515
|
+
// Assigned after the edges exist (the CateCom walk and the `categories` join both need
|
|
516
|
+
// them) and before layout, so node key order stays deterministic.
|
|
517
|
+
const extendsTargets = new Map();
|
|
518
|
+
const categoriesTarget = new Map();
|
|
519
|
+
edges.forEach((edge) => {
|
|
520
|
+
var _a, _b;
|
|
521
|
+
if (edge.kind === "extends") {
|
|
522
|
+
if (!extendsTargets.has(edge.source))
|
|
523
|
+
extendsTargets.set(edge.source, []);
|
|
524
|
+
extendsTargets.get(edge.source).push(edge.target);
|
|
525
|
+
}
|
|
526
|
+
if (edge.kind === "contains" && edge.label === "categories") {
|
|
527
|
+
if (((_a = nodes.get(edge.source)) === null || _a === void 0 ? void 0 : _a.layer) === "directory" &&
|
|
528
|
+
((_b = nodes.get(edge.target)) === null || _b === void 0 ? void 0 : _b.layer) === "category") {
|
|
529
|
+
categoriesTarget.set(edge.source, edge.target);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
extendsTargets.forEach((targets) => targets.sort());
|
|
534
|
+
const facetContext = {
|
|
535
|
+
nodes,
|
|
536
|
+
schemaById: new Map(loadedSchemas
|
|
537
|
+
.map((item) => [nodeIdBySourcePath.get(item.sourcePath), item])
|
|
538
|
+
.filter(([id]) => Boolean(id))),
|
|
539
|
+
schemasBySourcePath,
|
|
540
|
+
extendsTargets,
|
|
541
|
+
categoriesTarget,
|
|
542
|
+
coordinateMemo: new Map(),
|
|
543
|
+
};
|
|
544
|
+
[...nodes.keys()].sort().forEach((id) => {
|
|
545
|
+
const node = nodes.get(id);
|
|
546
|
+
if (node.layer !== "category" && node.layer !== "directory")
|
|
547
|
+
return;
|
|
548
|
+
const facets = deriveFacets(node, facetContext);
|
|
549
|
+
if (!facets || Object.keys(facets).length === 0) {
|
|
550
|
+
failures.push(`L11 ${node.path}: no facet rule covers this ${node.layer} path — add one to deriveFacets`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (facets.dimensionality && !DIMENSIONALITY.test(facets.dimensionality)) {
|
|
554
|
+
failures.push(`L11 ${node.path}: dimensionality "${facets.dimensionality}" is not <zero|one|two|three>-dimensional`);
|
|
555
|
+
}
|
|
556
|
+
node.facets = facets;
|
|
557
|
+
});
|
|
353
558
|
const sortedNodes = (0, entityGraphLayout_1.computeEntityGraphLayout)([...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)));
|
|
354
559
|
const sortedEdges = edges.sort((a, b) => {
|
|
355
560
|
var _a, _b, _c, _d;
|
|
@@ -154,6 +154,13 @@
|
|
|
154
154
|
"required": [
|
|
155
155
|
"name"
|
|
156
156
|
]
|
|
157
|
+
},
|
|
158
|
+
"facets": {
|
|
159
|
+
"description": "Navigation facets derived at build time: the CateCom coordinate a vocabulary schema narrows to, the M-CODE axes carried in a materials path, or the catalogue and axis a directory entry belongs to. Present on every category and directory node.",
|
|
160
|
+
"type": "object",
|
|
161
|
+
"additionalProperties": {
|
|
162
|
+
"type": "string"
|
|
163
|
+
}
|
|
157
164
|
}
|
|
158
165
|
},
|
|
159
166
|
"required": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mat3ra/esse",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.9.5-0",
|
|
4
4
|
"description": "Mat3ra's Excellent Source of Schemas and Examples (ESSE) for Digital Materials R&D",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"transpile": "tsc -p tsconfig-transpile.json",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"build-entity-graph": "ts-node build_entity_graph.ts --output ./site",
|
|
9
9
|
"lint-entity-graph": "ts-node build_entity_graph.ts",
|
|
10
10
|
"build-docs-pages": "ts-node build_docs_pages.ts",
|
|
11
|
+
"build-site": "npm run transpile-and-build-assets && ts-node build_site.ts && npm run check-site-links -- --site site",
|
|
11
12
|
"check-site-links": "ts-node check_site_links.ts",
|
|
12
13
|
"compile-types": "ts-node compile_ts.ts",
|
|
13
14
|
"transpile-and-build-assets": "npm run set-schema-ids && npm run build-schemas && npm run compile-types && npm run transpile",
|