@mat3ra/esse 2026.8.27-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.
- package/README.md +2 -2
- package/dist/js/scripts/buildDocsPages.js +33 -3
- package/dist/js/scripts/buildEntityGraph.d.ts +36 -0
- package/dist/js/scripts/buildEntityGraph.js +206 -1
- package/dist/js/scripts/checkSiteLinks.js +1 -1
- package/dist/js/scripts/entity_graph.schema.json +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The latest variants of schemas and examples are available at [schemas.mat3ra.com
|
|
|
12
12
|
|
|
13
13
|
- **[Documentation](https://schemas.mat3ra.com/docs/)** — how ESSE is put together and why: the schema layering, entity anatomy, categorization, conventions and the build pipeline. Start here if you are new.
|
|
14
14
|
- **[Explorer](https://schemas.mat3ra.com/)** — a file browser over every resolved schema and example.
|
|
15
|
-
- **[
|
|
15
|
+
- **[Ontology](https://schemas.mat3ra.com/map/)** — an interactive map of the ontology: every entity type and every relationship the schemas declare between them (`extends`, `contains`, `variant`), laid out by architectural layer — primitives at the centre, root entities around them, catalogues on the rim. Search for a schema, fly to it, and follow what it extends, contains and is used by.
|
|
16
16
|
|
|
17
17
|
The map is built from `graph.json`, an extracted reference graph that is also published at [schemas.mat3ra.com/graph.json](https://schemas.mat3ra.com/graph.json) and doubles as a lint over the corpus.
|
|
18
18
|
|
|
@@ -160,7 +160,7 @@ The following outlines the development process workflow:
|
|
|
160
160
|
2. Edit code and commit changes.
|
|
161
161
|
3. Pre commit is used to regenerate the modules.
|
|
162
162
|
4. Push the changes to GitHub.
|
|
163
|
-
5. GH workflow is used to generate the fully resolved file (without "$ref"s and "$allOf" etc.) and examples, render the documentation and the
|
|
163
|
+
5. GH workflow is used to generate the fully resolved file (without "$ref"s and "$allOf" etc.) and examples, render the documentation and the ontology map, and publish them to [schemas.mat3ra.com](http://schemas.mat3ra.com/). The site is assembled in a gitignored `site/` staging directory; internal links are checked before it deploys.
|
|
164
164
|
6. Publish the new version of the package to PyPI and npm.
|
|
165
165
|
|
|
166
166
|
The [pre-commit](.husky/pre-commit) is using both JS and PY runtime(s) to regenerate the schemas and examples.
|
|
@@ -157,6 +157,28 @@ function categorizationSchemesFragment(graph) {
|
|
|
157
157
|
|
|
158
158
|
Entities carrying a tiered \`categories\` field: ${carrierList || "_none_"}.`;
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* The three relation kinds, with their ontological reading alongside the JSON Schema
|
|
162
|
+
* keyword that expresses each. Counts come from the graph, so the prose calling these
|
|
163
|
+
* "the familiar ontological relations" cannot drift from what the corpus declares.
|
|
164
|
+
*/
|
|
165
|
+
function ontologyRelationsFragment(graph) {
|
|
166
|
+
const kinds = [
|
|
167
|
+
["`extends`", "`allOf`", "**is a** (subsumption)", "extends"],
|
|
168
|
+
["`contains`", "`properties` / `items`", "**has a** (composition)", "contains"],
|
|
169
|
+
["`variant`", "`oneOf` / `anyOf`", "**is one of** (disjunction)", "variant"],
|
|
170
|
+
];
|
|
171
|
+
const counts = graph.meta.edgeCountsByKind;
|
|
172
|
+
const rows = kinds.map(([relation, keyword, reading, key]) => [
|
|
173
|
+
relation,
|
|
174
|
+
keyword,
|
|
175
|
+
reading,
|
|
176
|
+
String(counts[key]),
|
|
177
|
+
]);
|
|
178
|
+
return `${table(["Relation", "Declared by", "Reads as", "Edges"], rows)}
|
|
179
|
+
|
|
180
|
+
Across ${graph.meta.nodeCount} entity types, ${graph.meta.edgeCount} declared relationships.`;
|
|
181
|
+
}
|
|
160
182
|
function layerInventoryFragment(graph) {
|
|
161
183
|
const rows = Object.entries(graph.meta.layerCounts)
|
|
162
184
|
.sort((a, b) => b[1] - a[1])
|
|
@@ -186,6 +208,8 @@ function expandFragments(body, graph) {
|
|
|
186
208
|
]);
|
|
187
209
|
case "layer-inventory":
|
|
188
210
|
return layerInventoryFragment(graph);
|
|
211
|
+
case "ontology-relations":
|
|
212
|
+
return ontologyRelationsFragment(graph);
|
|
189
213
|
case "categorization-schemes":
|
|
190
214
|
return categorizationSchemesFragment(graph);
|
|
191
215
|
case "hub-table":
|
|
@@ -215,18 +239,19 @@ function renderPage(page, pages, html) {
|
|
|
215
239
|
<head>
|
|
216
240
|
<meta charset="UTF-8">
|
|
217
241
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
218
|
-
<title>${escapeHtml(page.title)} —
|
|
242
|
+
<title>${escapeHtml(page.title)} — AI4Materials</title>
|
|
219
243
|
<meta name="description" content="${escapeHtml(page.summary)}">
|
|
220
244
|
<link rel="stylesheet" href="docs.css">
|
|
221
245
|
</head>
|
|
222
246
|
<body>
|
|
223
247
|
<div id="titlebar">
|
|
224
|
-
<span class="app-name">
|
|
248
|
+
<span class="app-name">AI4Materials<span class="app-name-suffix">: data standards</span></span>
|
|
225
249
|
<nav id="surfaces">
|
|
226
250
|
<a href="index.html" class="current">Docs</a>
|
|
227
251
|
<a href="../index.html">Explorer</a>
|
|
228
|
-
<a href="../map/index.html">
|
|
252
|
+
<a href="../map/index.html">Ontology</a>
|
|
229
253
|
</nav>
|
|
254
|
+
<span id="titlebar-tagline">ESSE · materials science ontology</span>
|
|
230
255
|
</div>
|
|
231
256
|
<div id="workspace">
|
|
232
257
|
<aside id="docs-nav">
|
|
@@ -283,10 +308,15 @@ body {
|
|
|
283
308
|
z-index: 5;
|
|
284
309
|
}
|
|
285
310
|
#titlebar .app-name { color: var(--text-primary); font-weight: 600; }
|
|
311
|
+
/* Completes the wordmark so the name never reads as a dangling fragment. */
|
|
312
|
+
#titlebar .app-name-suffix { color: var(--text-muted); font-weight: 400; }
|
|
313
|
+
@media (max-width: 700px) { #titlebar .app-name-suffix { display: none; } }
|
|
286
314
|
#surfaces { display: flex; gap: 14px; }
|
|
287
315
|
#surfaces a { color: var(--text-muted); text-decoration: none; }
|
|
288
316
|
#surfaces a:hover { color: var(--text-primary); }
|
|
289
317
|
#surfaces a.current { color: var(--text-primary); border-bottom: 2px solid var(--accent); }
|
|
318
|
+
#titlebar-tagline { margin-left: auto; color: var(--text-muted); }
|
|
319
|
+
@media (max-width: 640px) { #titlebar-tagline { display: none; } }
|
|
290
320
|
#workspace { display: flex; flex: 1; align-items: flex-start; }
|
|
291
321
|
#docs-nav {
|
|
292
322
|
width: 250px;
|
|
@@ -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;
|
|
@@ -8,7 +8,7 @@ exports.checkSiteLinks = checkSiteLinks;
|
|
|
8
8
|
* Verifies that every internal link in the assembled site resolves.
|
|
9
9
|
*
|
|
10
10
|
* The site is stitched together from four sources — resolved schemas, the concept docs, the
|
|
11
|
-
* explorer and the
|
|
11
|
+
* explorer and the ontology map — each of which links into the others. Nothing else checks that
|
|
12
12
|
* those links land anywhere, and a stale href is invisible until someone clicks it.
|
|
13
13
|
*
|
|
14
14
|
* External URLs are not fetched; this is a build step, not a crawler.
|
|
@@ -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",
|