@mat3ra/esse 2026.8.31-0 → 2026.9.6-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/dist/js/scripts/buildDocsPages.js +12 -9
- package/dist/js/scripts/buildEntityGraph.d.ts +36 -0
- package/dist/js/scripts/buildEntityGraph.js +206 -1
- package/dist/js/scripts/buildExplorerViews.d.ts +28 -0
- package/dist/js/scripts/buildExplorerViews.js +410 -0
- package/dist/js/scripts/entity_graph.schema.json +7 -0
- package/package.json +2 -1
|
@@ -139,7 +139,7 @@ function categorizationSchemesFragment(graph) {
|
|
|
139
139
|
const rest = graph.nodes.filter((node) => node.id.startsWith(prefix) && !reachesCategories(node.id));
|
|
140
140
|
const tiered = total - rest.length;
|
|
141
141
|
if (!tiered)
|
|
142
|
-
return [`\`${domain}\``, String(total), "
|
|
142
|
+
return [`\`${domain}\``, String(total), "M-CODE — compositional"];
|
|
143
143
|
// Today the untiered remainder is entirely enum holders; say so only while true.
|
|
144
144
|
const allEnums = rest.every((node) => /\/enum-options$|\/enums$/.test(node.id));
|
|
145
145
|
const remainder = rest.length
|
|
@@ -245,13 +245,15 @@ function renderPage(page, pages, html) {
|
|
|
245
245
|
</head>
|
|
246
246
|
<body>
|
|
247
247
|
<div id="titlebar">
|
|
248
|
-
<span class="app-name">AI4Materials
|
|
248
|
+
<span class="app-name">AI4Materials</span>
|
|
249
249
|
<nav id="surfaces">
|
|
250
250
|
<a href="index.html" class="current">Docs</a>
|
|
251
251
|
<a href="../index.html">Explorer</a>
|
|
252
|
+
<a href="../index.html#/categories">Categories</a>
|
|
253
|
+
<a href="../index.html#/directories">Directories</a>
|
|
252
254
|
<a href="../map/index.html">Ontology</a>
|
|
253
255
|
</nav>
|
|
254
|
-
<span id="titlebar-tagline">ESSE · materials science ontology</span>
|
|
256
|
+
<span id="titlebar-tagline">ESSE · materials science data standards and ontology</span>
|
|
255
257
|
</div>
|
|
256
258
|
<div id="workspace">
|
|
257
259
|
<aside id="docs-nav">
|
|
@@ -308,15 +310,16 @@ body {
|
|
|
308
310
|
z-index: 5;
|
|
309
311
|
}
|
|
310
312
|
#titlebar .app-name { color: var(--text-primary); font-weight: 600; }
|
|
311
|
-
/*
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
#surfaces { display:
|
|
313
|
+
/* Five items do not fit a phone: the nav shrinks and scrolls inside the bar rather than
|
|
314
|
+
being clipped or pushing the page sideways. */
|
|
315
|
+
#surfaces { display: flex; gap: 14px; flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
|
316
|
+
#surfaces::-webkit-scrollbar { display: none; }
|
|
317
|
+
@media (max-width: 700px) { #surfaces { gap: 10px; } }
|
|
315
318
|
#surfaces a { color: var(--text-muted); text-decoration: none; }
|
|
316
319
|
#surfaces a:hover { color: var(--text-primary); }
|
|
317
320
|
#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:
|
|
321
|
+
#titlebar-tagline { margin-left: auto; color: var(--text-muted); white-space: nowrap; }
|
|
322
|
+
@media (max-width: 900px) { #titlebar-tagline { display: none; } }
|
|
320
323
|
#workspace { display: flex; flex: 1; align-items: flex-start; }
|
|
321
324
|
#docs-nav {
|
|
322
325
|
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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { EntityGraph } from "./buildEntityGraph";
|
|
2
|
+
/**
|
|
3
|
+
* One openable file in a view.
|
|
4
|
+
*
|
|
5
|
+
* `segments` is the folder chain with the leaf label last. An array rather than a joined
|
|
6
|
+
* string because leaf labels themselves contain slashes — a catalogue entry filed under a
|
|
7
|
+
* vocabulary node reads `models_directory/gga.json` — and joining would invent folders.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExplorerViewEntry {
|
|
10
|
+
segments: string[];
|
|
11
|
+
/** Site-relative path of the file to open, e.g. "schema/models_directory/gga.json". */
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ExplorerViews {
|
|
15
|
+
categories: ExplorerViewEntry[];
|
|
16
|
+
directories: ExplorerViewEntry[];
|
|
17
|
+
}
|
|
18
|
+
export declare function buildExplorerViews(graph: EntityGraph): ExplorerViews;
|
|
19
|
+
/**
|
|
20
|
+
* L12 — the views stay in step with the corpus.
|
|
21
|
+
*
|
|
22
|
+
* A view that silently drops a schema is worse than no view: it tells the reader the corpus
|
|
23
|
+
* does not contain something it does contain. Every browsable schema must appear, and every
|
|
24
|
+
* row must open a file that exists.
|
|
25
|
+
*/
|
|
26
|
+
export declare function lintExplorerViews(graph: EntityGraph, views: ExplorerViews): string[];
|
|
27
|
+
/** Writes `views.json` into the given directory, creating it when missing. */
|
|
28
|
+
export declare function writeExplorerViews(views: ExplorerViews, outputDir: string): string;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildExplorerViews = buildExplorerViews;
|
|
7
|
+
exports.lintExplorerViews = lintExplorerViews;
|
|
8
|
+
exports.writeExplorerViews = writeExplorerViews;
|
|
9
|
+
/**
|
|
10
|
+
* Builds the Explorer's Categories and Directories trees from the entity graph.
|
|
11
|
+
*
|
|
12
|
+
* The Explorer browses by file path, which is the one arrangement that cannot show what the
|
|
13
|
+
* categorization layer is *for*: a CateCom tier ladder is a chain of `allOf`s, and M-CODE's
|
|
14
|
+
* three axes are three different ways through the same 17 recipes. Neither is a directory
|
|
15
|
+
* listing. These views are virtual trees over the same files — same widget, same editor, same
|
|
16
|
+
* tabs — so a coordinate becomes something you can walk.
|
|
17
|
+
*
|
|
18
|
+
* Built here rather than in the browser so the shape is testable and lint-able, and so the
|
|
19
|
+
* client stays a renderer. The output is `views.json`, written beside `graph.json`.
|
|
20
|
+
*/
|
|
21
|
+
const fs_1 = __importDefault(require("fs"));
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const MODELS_ROOT = "Models · CateCom";
|
|
24
|
+
const METHODS_ROOT = "Methods · CateCom";
|
|
25
|
+
const MATERIALS_ROOT = "Materials · M-CODE";
|
|
26
|
+
/** Display names for the catalogues; the entry count is appended at build time. */
|
|
27
|
+
const CATALOGUE_LABELS = {
|
|
28
|
+
properties: "Properties",
|
|
29
|
+
models: "Models",
|
|
30
|
+
methods: "Methods",
|
|
31
|
+
software: "Software",
|
|
32
|
+
"context-providers": "Context providers",
|
|
33
|
+
};
|
|
34
|
+
/** Titles are written for a schema reader; a tree row wants the noun, not the file kind. */
|
|
35
|
+
function shortTitle(node) {
|
|
36
|
+
return node.title
|
|
37
|
+
.replace(/\s+(category|configuration)?\s*schema$/i, "")
|
|
38
|
+
.replace(/\s+category$/i, "")
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The published file name, not the source one. They differ: a source basename may contain a
|
|
43
|
+
* dash, which round-trips to an underscore through `$id` (`p-norm.json` is published as
|
|
44
|
+
* `p_norm.json`). Labelling a row with the source name names a file the site does not serve
|
|
45
|
+
* and that searching for the published name cannot find.
|
|
46
|
+
*/
|
|
47
|
+
function fileName(node) {
|
|
48
|
+
return node.publishedPath.split("/").pop();
|
|
49
|
+
}
|
|
50
|
+
function slugOf(node) {
|
|
51
|
+
return node.id.split("/").pop();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `pb · physics-based model`. The title is dropped when it adds nothing — either because it
|
|
55
|
+
* is the slug, or because it merely ends with it, which would otherwise print the slug twice
|
|
56
|
+
* (`nstruct · Unstructured meshing category nstruct`).
|
|
57
|
+
*/
|
|
58
|
+
function vocabularyLabel(node) {
|
|
59
|
+
const slug = slugOf(node);
|
|
60
|
+
const title = shortTitle(node)
|
|
61
|
+
.replace(new RegExp(`\\s*\\b${slug}$`, "i"), "")
|
|
62
|
+
.trim();
|
|
63
|
+
return title && title.toLowerCase() !== slug.toLowerCase() ? `${slug} · ${title}` : slug;
|
|
64
|
+
}
|
|
65
|
+
function examplePath(node) {
|
|
66
|
+
return node.hasExample ? node.publishedPath.replace(/^schema\//, "example/") : undefined;
|
|
67
|
+
}
|
|
68
|
+
function buildContext(graph) {
|
|
69
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
70
|
+
const ladderParents = new Map();
|
|
71
|
+
const filedUnder = new Map();
|
|
72
|
+
const filedEntries = new Map();
|
|
73
|
+
graph.edges.forEach((edge) => {
|
|
74
|
+
var _a, _b;
|
|
75
|
+
const source = byId.get(edge.source);
|
|
76
|
+
const target = byId.get(edge.target);
|
|
77
|
+
if (!source || !target)
|
|
78
|
+
return;
|
|
79
|
+
if (edge.kind === "extends" && source.layer === "category" && target.layer === "category") {
|
|
80
|
+
const parents = (_a = ladderParents.get(edge.source)) !== null && _a !== void 0 ? _a : [];
|
|
81
|
+
parents.push(edge.target);
|
|
82
|
+
ladderParents.set(edge.source, parents);
|
|
83
|
+
}
|
|
84
|
+
if (edge.kind === "contains" &&
|
|
85
|
+
edge.label === "categories" &&
|
|
86
|
+
source.layer === "directory" &&
|
|
87
|
+
target.layer === "category") {
|
|
88
|
+
filedUnder.set(edge.source, edge.target);
|
|
89
|
+
const entries = (_b = filedEntries.get(edge.target)) !== null && _b !== void 0 ? _b : [];
|
|
90
|
+
entries.push(edge.source);
|
|
91
|
+
filedEntries.set(edge.target, entries);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
ladderParents.forEach((parents) => parents.sort());
|
|
95
|
+
filedEntries.forEach((entries) => entries.sort());
|
|
96
|
+
return { byId, ladderParents, filedUnder, filedEntries };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The chain of vocabulary schemas from the top of the ladder down to `nodeId`.
|
|
100
|
+
*
|
|
101
|
+
* This is the `allOf` chain, not the directory path: it is how the corpus itself defines the
|
|
102
|
+
* ladder, and it is right where the path is not. `methods_category/mathematical/regression`
|
|
103
|
+
* narrows a lower field than its depth suggests, so reading position off the path — or off
|
|
104
|
+
* which facet fields are present — misplaces it.
|
|
105
|
+
*/
|
|
106
|
+
function ladderChain(nodeId, context) {
|
|
107
|
+
var _a;
|
|
108
|
+
const chain = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
let current = nodeId;
|
|
111
|
+
while (current && !seen.has(current)) {
|
|
112
|
+
seen.add(current);
|
|
113
|
+
const node = context.byId.get(current);
|
|
114
|
+
if (!node)
|
|
115
|
+
break;
|
|
116
|
+
chain.push(node);
|
|
117
|
+
[current] = (_a = context.ladderParents.get(current)) !== null && _a !== void 0 ? _a : [];
|
|
118
|
+
}
|
|
119
|
+
return chain.reverse();
|
|
120
|
+
}
|
|
121
|
+
function isEnumHolder(node) {
|
|
122
|
+
var _a;
|
|
123
|
+
return ((_a = node.facets) === null || _a === void 0 ? void 0 : _a.role) === "enum-options";
|
|
124
|
+
}
|
|
125
|
+
// ── Categories ────────────────────────────────────────────────────────────────
|
|
126
|
+
function categoriesView(graph, context) {
|
|
127
|
+
const entries = [];
|
|
128
|
+
const push = (segments, node) => {
|
|
129
|
+
entries.push({ segments: [...segments, fileName(node)], path: node.publishedPath });
|
|
130
|
+
const example = examplePath(node);
|
|
131
|
+
if (example) {
|
|
132
|
+
entries.push({ segments: [...segments, `${fileName(node)} · example`], path: example });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
graph.nodes
|
|
136
|
+
.filter((node) => { var _a; return ((_a = node.facets) === null || _a === void 0 ? void 0 : _a.role) === "vocabulary"; })
|
|
137
|
+
.forEach((node) => {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
const root = node.domain === "models_category" ? MODELS_ROOT : METHODS_ROOT;
|
|
140
|
+
// The branch is the first path segment but not part of the `allOf` chain, so it
|
|
141
|
+
// has to be prepended for a methods coordinate to be complete.
|
|
142
|
+
const branch = (_a = node.facets) === null || _a === void 0 ? void 0 : _a.branch;
|
|
143
|
+
const folders = [
|
|
144
|
+
root,
|
|
145
|
+
...(branch ? [branch] : []),
|
|
146
|
+
...ladderChain(node.id, context).map(vocabularyLabel),
|
|
147
|
+
];
|
|
148
|
+
push(folders, node);
|
|
149
|
+
((_b = context.filedEntries.get(node.id)) !== null && _b !== void 0 ? _b : []).forEach((entryId) => {
|
|
150
|
+
const entry = context.byId.get(entryId);
|
|
151
|
+
if (!entry)
|
|
152
|
+
return;
|
|
153
|
+
// Named with its domain: inside `gga · DFT GGA functional` this sits beside
|
|
154
|
+
// the vocabulary schema's own `gga.json`, and the two must not read alike.
|
|
155
|
+
const label = `${entry.domain}/${fileName(entry)}`;
|
|
156
|
+
entries.push({ segments: [...folders, label], path: entry.publishedPath });
|
|
157
|
+
const example = examplePath(entry);
|
|
158
|
+
if (example) {
|
|
159
|
+
entries.push({
|
|
160
|
+
segments: [...folders, `${label} · example`],
|
|
161
|
+
path: example,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
// M-CODE's axes are orthogonal, so a recipe is filed under each of them rather than
|
|
167
|
+
// once: reaching a slab by dimensionality and by structural class are both legitimate.
|
|
168
|
+
graph.nodes
|
|
169
|
+
.filter((node) => { var _a; return ((_a = node.facets) === null || _a === void 0 ? void 0 : _a.role) === "recipe"; })
|
|
170
|
+
.forEach((node) => {
|
|
171
|
+
var _a;
|
|
172
|
+
const { structuralClass, dimensionality, operation } = (_a = node.facets) !== null && _a !== void 0 ? _a : {};
|
|
173
|
+
if (structuralClass && dimensionality) {
|
|
174
|
+
push([MATERIALS_ROOT, "by structural class", structuralClass, dimensionality], node);
|
|
175
|
+
push([MATERIALS_ROOT, "by dimensionality", dimensionality, structuralClass], node);
|
|
176
|
+
}
|
|
177
|
+
if (operation)
|
|
178
|
+
push([MATERIALS_ROOT, "by operation", operation], node);
|
|
179
|
+
});
|
|
180
|
+
graph.nodes
|
|
181
|
+
.filter((node) => { var _a; return ((_a = node.facets) === null || _a === void 0 ? void 0 : _a.role) === "entity"; })
|
|
182
|
+
.forEach((node) => {
|
|
183
|
+
var _a;
|
|
184
|
+
const { entityRole, dimensionality } = (_a = node.facets) !== null && _a !== void 0 ? _a : {};
|
|
185
|
+
push([
|
|
186
|
+
MATERIALS_ROOT,
|
|
187
|
+
"components",
|
|
188
|
+
"entities",
|
|
189
|
+
...(entityRole ? [entityRole] : []),
|
|
190
|
+
...(dimensionality ? [dimensionality] : []),
|
|
191
|
+
], node);
|
|
192
|
+
});
|
|
193
|
+
graph.nodes
|
|
194
|
+
.filter((node) => { var _a; return ((_a = node.facets) === null || _a === void 0 ? void 0 : _a.role) === "operation"; })
|
|
195
|
+
.forEach((node) => {
|
|
196
|
+
var _a;
|
|
197
|
+
const { operationKind } = (_a = node.facets) !== null && _a !== void 0 ? _a : {};
|
|
198
|
+
push([
|
|
199
|
+
MATERIALS_ROOT,
|
|
200
|
+
"components",
|
|
201
|
+
"operations",
|
|
202
|
+
...(operationKind ? [operationKind] : []),
|
|
203
|
+
], node);
|
|
204
|
+
});
|
|
205
|
+
return entries;
|
|
206
|
+
}
|
|
207
|
+
// ── Directories ───────────────────────────────────────────────────────────────
|
|
208
|
+
/** Where a catalogue entry sits below its catalogue root. */
|
|
209
|
+
function directoryGroup(node, context) {
|
|
210
|
+
var _a, _b, _c;
|
|
211
|
+
const facets = (_a = node.facets) !== null && _a !== void 0 ? _a : {};
|
|
212
|
+
if (facets.legacy === "true")
|
|
213
|
+
return ["legacy"];
|
|
214
|
+
switch (facets.catalogue) {
|
|
215
|
+
case "properties":
|
|
216
|
+
return facets.valueShape ? [facets.valueShape] : ["uncategorized"];
|
|
217
|
+
case "software":
|
|
218
|
+
return facets.softwareKind ? [facets.softwareKind] : ["uncategorized"];
|
|
219
|
+
case "context-providers":
|
|
220
|
+
return [
|
|
221
|
+
...(facets.scope ? [facets.scope] : ["uncategorized"]),
|
|
222
|
+
...(facets.application ? [facets.application] : []),
|
|
223
|
+
];
|
|
224
|
+
case "models":
|
|
225
|
+
case "methods": {
|
|
226
|
+
// The coordinate, slugs only — a position on the ladder, not a label for one.
|
|
227
|
+
const target = context.filedUnder.get(node.id);
|
|
228
|
+
if (!target)
|
|
229
|
+
return ["uncategorized"];
|
|
230
|
+
const branch = (_c = (_b = context.byId.get(target)) === null || _b === void 0 ? void 0 : _b.facets) === null || _c === void 0 ? void 0 : _c.branch;
|
|
231
|
+
return [...(branch ? [branch] : []), ...ladderChain(target, context).map(slugOf)];
|
|
232
|
+
}
|
|
233
|
+
default:
|
|
234
|
+
return ["uncategorized"];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function directoriesView(graph, context) {
|
|
238
|
+
const directories = graph.nodes.filter((node) => node.layer === "directory" && !isEnumHolder(node));
|
|
239
|
+
const counts = new Map();
|
|
240
|
+
directories.forEach((node) => {
|
|
241
|
+
var _a, _b, _c;
|
|
242
|
+
const catalogue = (_b = (_a = node.facets) === null || _a === void 0 ? void 0 : _a.catalogue) !== null && _b !== void 0 ? _b : "other";
|
|
243
|
+
counts.set(catalogue, ((_c = counts.get(catalogue)) !== null && _c !== void 0 ? _c : 0) + 1);
|
|
244
|
+
});
|
|
245
|
+
const entries = [];
|
|
246
|
+
directories.forEach((node) => {
|
|
247
|
+
var _a, _b, _c, _d;
|
|
248
|
+
const catalogue = (_b = (_a = node.facets) === null || _a === void 0 ? void 0 : _a.catalogue) !== null && _b !== void 0 ? _b : "other";
|
|
249
|
+
const label = (_c = CATALOGUE_LABELS[catalogue]) !== null && _c !== void 0 ? _c : catalogue;
|
|
250
|
+
const root = `${label} (${(_d = counts.get(catalogue)) !== null && _d !== void 0 ? _d : 0})`;
|
|
251
|
+
const folders = [root, ...directoryGroup(node, context)];
|
|
252
|
+
entries.push({ segments: [...folders, fileName(node)], path: node.publishedPath });
|
|
253
|
+
const example = examplePath(node);
|
|
254
|
+
if (example) {
|
|
255
|
+
entries.push({ segments: [...folders, `${fileName(node)} · example`], path: example });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return entries;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Makes every leaf label unique within its folder.
|
|
262
|
+
*
|
|
263
|
+
* File names in this corpus are often generic: all 17 M-CODE recipes are `configuration.json`
|
|
264
|
+
* and the recipe name is the folder above them, so grouping by facet lands four indentical
|
|
265
|
+
* rows side by side. Where that happens, as much of the source path is prefixed as it takes
|
|
266
|
+
* to tell them apart — `adatom/configuration.json`, `island/configuration.json`.
|
|
267
|
+
*/
|
|
268
|
+
function disambiguateLeaves(entries) {
|
|
269
|
+
const groups = new Map();
|
|
270
|
+
entries.forEach((entry) => {
|
|
271
|
+
const key = entry.segments.join("\u0000");
|
|
272
|
+
const group = groups.get(key);
|
|
273
|
+
if (group)
|
|
274
|
+
group.push(entry);
|
|
275
|
+
else
|
|
276
|
+
groups.set(key, [entry]);
|
|
277
|
+
});
|
|
278
|
+
groups.forEach((group) => {
|
|
279
|
+
if (group.length < 2)
|
|
280
|
+
return;
|
|
281
|
+
const directoriesOf = (entry) => entry.path.split("/").slice(0, -1);
|
|
282
|
+
const deepest = Math.max(...group.map((entry) => directoriesOf(entry).length));
|
|
283
|
+
for (let depth = 1; depth <= deepest; depth += 1) {
|
|
284
|
+
const labelled = group.map((entry) => {
|
|
285
|
+
const leaf = entry.segments[entry.segments.length - 1];
|
|
286
|
+
const prefix = directoriesOf(entry).slice(-depth).join("/");
|
|
287
|
+
return prefix ? `${prefix}/${leaf}` : leaf;
|
|
288
|
+
});
|
|
289
|
+
if (new Set(labelled).size === group.length) {
|
|
290
|
+
group.forEach((entry, index) => {
|
|
291
|
+
entry.segments = [...entry.segments.slice(0, -1), labelled[index]];
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return entries;
|
|
298
|
+
}
|
|
299
|
+
function compareEntries(a, b) {
|
|
300
|
+
const depth = Math.min(a.segments.length, b.segments.length);
|
|
301
|
+
for (let i = 0; i < depth; i += 1) {
|
|
302
|
+
const order = a.segments[i].localeCompare(b.segments[i]);
|
|
303
|
+
if (order !== 0)
|
|
304
|
+
return order;
|
|
305
|
+
}
|
|
306
|
+
return a.segments.length - b.segments.length || a.path.localeCompare(b.path);
|
|
307
|
+
}
|
|
308
|
+
function buildExplorerViews(graph) {
|
|
309
|
+
const context = buildContext(graph);
|
|
310
|
+
return {
|
|
311
|
+
categories: disambiguateLeaves(categoriesView(graph, context)).sort(compareEntries),
|
|
312
|
+
directories: disambiguateLeaves(directoriesView(graph, context)).sort(compareEntries),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* L12 — the views stay in step with the corpus.
|
|
317
|
+
*
|
|
318
|
+
* A view that silently drops a schema is worse than no view: it tells the reader the corpus
|
|
319
|
+
* does not contain something it does contain. Every browsable schema must appear, and every
|
|
320
|
+
* row must open a file that exists.
|
|
321
|
+
*/
|
|
322
|
+
function lintExplorerViews(graph, views) {
|
|
323
|
+
const failures = [];
|
|
324
|
+
const files = new Set();
|
|
325
|
+
graph.nodes.forEach((node) => {
|
|
326
|
+
files.add(node.publishedPath);
|
|
327
|
+
const example = examplePath(node);
|
|
328
|
+
if (example)
|
|
329
|
+
files.add(example);
|
|
330
|
+
});
|
|
331
|
+
const check = (name, entries) => {
|
|
332
|
+
const rows = new Map();
|
|
333
|
+
entries.forEach((entry) => {
|
|
334
|
+
if (!files.has(entry.path)) {
|
|
335
|
+
failures.push(`L12 views.json ${name}: "${entry.path}" is not a published file`);
|
|
336
|
+
}
|
|
337
|
+
if (entry.segments.length < 2) {
|
|
338
|
+
failures.push(`L12 views.json ${name}: "${entry.path}" has no folder above its leaf`);
|
|
339
|
+
}
|
|
340
|
+
// The label must name the file the row opens. Checked because it is the one
|
|
341
|
+
// thing here NOT derivable from the inputs: everything else in this function
|
|
342
|
+
// rebuilds `entry.path` from the same node fields that produced it, and so
|
|
343
|
+
// holds by construction. Reading the source path instead of the published one
|
|
344
|
+
// shipped four rows labelled `p-norm.json` that open `p_norm.json`.
|
|
345
|
+
const leaf = entry.segments[entry.segments.length - 1]
|
|
346
|
+
.replace(/ · example$/, "")
|
|
347
|
+
.split("/")
|
|
348
|
+
.pop();
|
|
349
|
+
if (leaf !== entry.path.split("/").pop()) {
|
|
350
|
+
failures.push(`L12 views.json ${name}: row "${entry.segments.join(" / ")}" is labelled ` +
|
|
351
|
+
`"${leaf}" but opens ${entry.path}`);
|
|
352
|
+
}
|
|
353
|
+
// Two rows reading the same in the same folder are indistinguishable to a
|
|
354
|
+
// reader, whichever files they point at.
|
|
355
|
+
const row = entry.segments.join(" / ");
|
|
356
|
+
const claimed = rows.get(row);
|
|
357
|
+
if (claimed && claimed !== entry.path) {
|
|
358
|
+
failures.push(`L12 views.json ${name}: "${row}" is used by both ${claimed} and ${entry.path}`);
|
|
359
|
+
}
|
|
360
|
+
rows.set(row, entry.path);
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
check("categories", views.categories);
|
|
364
|
+
check("directories", views.directories);
|
|
365
|
+
const countBy = (entries) => {
|
|
366
|
+
const counts = new Map();
|
|
367
|
+
entries.forEach((entry) => { var _a; return counts.set(entry.path, ((_a = counts.get(entry.path)) !== null && _a !== void 0 ? _a : 0) + 1); });
|
|
368
|
+
return counts;
|
|
369
|
+
};
|
|
370
|
+
// Every browsable category schema, not only the vocabulary pass: the recipe, entity and
|
|
371
|
+
// operation passes produce 145 of the 250 rows and were policed by nothing, so a schema
|
|
372
|
+
// one of them silently skipped would vanish from the tree with a green build. "At least
|
|
373
|
+
// once" rather than "exactly once" because M-CODE files a recipe under each axis it has.
|
|
374
|
+
const categoryCounts = countBy(views.categories);
|
|
375
|
+
graph.nodes
|
|
376
|
+
.filter((node) => node.layer === "category" && !isEnumHolder(node))
|
|
377
|
+
.forEach((node) => {
|
|
378
|
+
var _a, _b, _c;
|
|
379
|
+
const seen = (_a = categoryCounts.get(node.publishedPath)) !== null && _a !== void 0 ? _a : 0;
|
|
380
|
+
if (seen < 1) {
|
|
381
|
+
failures.push(`L12 views.json categories: ${node.id} (role ` +
|
|
382
|
+
`${(_c = (_b = node.facets) === null || _b === void 0 ? void 0 : _b.role) !== null && _c !== void 0 ? _c : "none"}) appears nowhere in the tree`);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
const directoryCounts = countBy(views.directories);
|
|
386
|
+
graph.nodes
|
|
387
|
+
.filter((node) => node.layer === "directory" && !isEnumHolder(node))
|
|
388
|
+
.forEach((node) => {
|
|
389
|
+
var _a;
|
|
390
|
+
const seen = (_a = directoryCounts.get(node.publishedPath)) !== null && _a !== void 0 ? _a : 0;
|
|
391
|
+
if (seen !== 1) {
|
|
392
|
+
failures.push(`L12 views.json directories: catalogue entry ${node.id} appears ${seen} ` +
|
|
393
|
+
`time(s), expected exactly 1`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
const holders = graph.nodes.filter(isEnumHolder).map((node) => node.publishedPath);
|
|
397
|
+
holders.forEach((held) => {
|
|
398
|
+
if (categoryCounts.has(held) || directoryCounts.has(held)) {
|
|
399
|
+
failures.push(`L12 views.json: enum holder ${held} should not be browsable`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
return failures;
|
|
403
|
+
}
|
|
404
|
+
/** Writes `views.json` into the given directory, creating it when missing. */
|
|
405
|
+
function writeExplorerViews(views, outputDir) {
|
|
406
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
407
|
+
const outputPath = path_1.default.join(outputDir, "views.json");
|
|
408
|
+
fs_1.default.writeFileSync(outputPath, `${JSON.stringify(views, null, 4)}\n`, "utf8");
|
|
409
|
+
return outputPath;
|
|
410
|
+
}
|
|
@@ -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.6-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",
|