@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 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
- - **[Entity Map](https://schemas.mat3ra.com/map/)** — an interactive map of all schemas and the references between them, 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.
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 Entity 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.
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)} — ESSE</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">ESSE</span>
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">Map</a>
252
+ <a href="../map/index.html">Ontology</a>
229
253
  </nav>
254
+ <span id="titlebar-tagline">ESSE &middot; 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 Entity Map — each of which links into the others. Nothing else checks that
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.8.27-0",
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",