@mat3ra/esse 2026.8.18-2 → 2026.8.25-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 +19 -3
- package/dist/js/scripts/buildDocsPages.d.ts +19 -0
- package/dist/js/scripts/buildDocsPages.js +419 -0
- package/dist/js/scripts/buildEntityGraph.d.ts +129 -0
- package/dist/js/scripts/buildEntityGraph.js +420 -0
- package/dist/js/scripts/checkSiteLinks.d.ts +6 -0
- package/dist/js/scripts/checkSiteLinks.js +73 -0
- package/dist/js/scripts/entityGraphLayout.d.ts +21 -0
- package/dist/js/scripts/entityGraphLayout.js +76 -0
- package/dist/js/scripts/entity_graph.schema.json +218 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -8,7 +8,13 @@ Essential Source of Schemas and Examples (ESSE) contains data format definitions
|
|
|
8
8
|
|
|
9
9
|
Although the schemas are used to facilitate the operations of [mat3ra.com](https://mat3ra.com), they are designed to be generic and can be used in other applications. The open-source packages developed by Mat3ra.com use the schemas available in this repository.
|
|
10
10
|
|
|
11
|
-
The latest variants of schemas and examples are available at [schemas.mat3ra.com](https://schemas.mat3ra.com/)
|
|
11
|
+
The latest variants of schemas and examples are available at [schemas.mat3ra.com](https://schemas.mat3ra.com/), which has three surfaces:
|
|
12
|
+
|
|
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
|
+
- **[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.
|
|
16
|
+
|
|
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.
|
|
12
18
|
|
|
13
19
|
ESSE has a dual-nature as both a Python and a Node.js package.
|
|
14
20
|
|
|
@@ -110,7 +116,7 @@ console.log(result); // {"a": "x", "b": 1}
|
|
|
110
116
|
|
|
111
117
|
## 3. Directory Structure
|
|
112
118
|
|
|
113
|
-
ESSE contains 3 main directories, [schema](schema), [example](example) and [src](src) outlined below.
|
|
119
|
+
ESSE contains 3 main directories, [schema](schema), [example](example) and [src](src) outlined below, plus [docs](docs) (documentation sources) and [plan](plan) (design documents).
|
|
114
120
|
|
|
115
121
|
### 3.1. Schema
|
|
116
122
|
|
|
@@ -130,6 +136,14 @@ This directory contains the examples formed according to the schemas and impleme
|
|
|
130
136
|
|
|
131
137
|
This directory contains Python and Javascript interfaces implementing the functionality to access and validate schemas and examples.
|
|
132
138
|
|
|
139
|
+
### 3.4. docs
|
|
140
|
+
|
|
141
|
+
Markdown sources for the [concept documentation](https://schemas.mat3ra.com/docs/), rendered to HTML at deploy time. Pages may embed generated fragments (`<!-- generated:name -->`) that are expanded from the entity graph, so counts and relationship listings cannot drift from the schemas.
|
|
142
|
+
|
|
143
|
+
### 3.5. plan
|
|
144
|
+
|
|
145
|
+
Design documents, filed by status (`upcoming/`, `review/`, `implemented/`, `context/`) per the convention in [mat3ra/agents](https://github.com/mat3ra/agents/blob/main/AGENTS.md). See [plan/README.md](plan/README.md).
|
|
146
|
+
|
|
133
147
|
## 4. Conventions
|
|
134
148
|
|
|
135
149
|
### 4.1. Generative vs Non-generative keys
|
|
@@ -146,7 +160,7 @@ The following outlines the development process workflow:
|
|
|
146
160
|
2. Edit code and commit changes.
|
|
147
161
|
3. Pre commit is used to regenerate the modules.
|
|
148
162
|
4. Push the changes to GitHub.
|
|
149
|
-
5. GH workflow is used to generate the fully resolved file (without "$ref"s and "$allOf" etc.) and examples and publish them to [schemas.mat3ra.com](http://schemas.mat3ra.com/).
|
|
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.
|
|
150
164
|
6. Publish the new version of the package to PyPI and npm.
|
|
151
165
|
|
|
152
166
|
The [pre-commit](.husky/pre-commit) is using both JS and PY runtime(s) to regenerate the schemas and examples.
|
|
@@ -197,6 +211,8 @@ Other suggestions:
|
|
|
197
211
|
- Use unique IDs for schemas
|
|
198
212
|
- Do not use circular references in the schemas, instead leave the type as object and add explanation to description.
|
|
199
213
|
|
|
214
|
+
The schema lint (`npm run lint-entity-graph`, also part of `npm test`) enforces both of the above along with `$id` conventions, JSON-pointer targets and manifest references. [Contributing a schema](https://schemas.mat3ra.com/docs/contributing-a-schema.html) walks through adding one end to end.
|
|
215
|
+
|
|
200
216
|
## Links
|
|
201
217
|
|
|
202
218
|
[^1]: [Data-centric online ecosystem for digital materials science](https://arxiv.org/pdf/1902.10838.pdf)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type EntityGraph } from "./buildEntityGraph";
|
|
2
|
+
export interface DocsPage {
|
|
3
|
+
slug: string;
|
|
4
|
+
title: string;
|
|
5
|
+
order: number;
|
|
6
|
+
summary: string;
|
|
7
|
+
body: string;
|
|
8
|
+
}
|
|
9
|
+
interface FrontMatter {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
}
|
|
12
|
+
/** Splits a leading `---` block off the document. Enough for title/order/summary. */
|
|
13
|
+
export declare function parseFrontMatter(source: string): {
|
|
14
|
+
data: FrontMatter;
|
|
15
|
+
body: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function expandFragments(body: string, graph: EntityGraph): string;
|
|
18
|
+
export declare function buildDocsPages(outputDir: string): DocsPage[];
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,419 @@
|
|
|
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.parseFrontMatter = parseFrontMatter;
|
|
7
|
+
exports.expandFragments = expandFragments;
|
|
8
|
+
exports.buildDocsPages = buildDocsPages;
|
|
9
|
+
/**
|
|
10
|
+
* Renders the concept documentation in `docs/` to static HTML for the site.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately small: `marked` plus the site's own chrome, no static site generator.
|
|
13
|
+
* Ten pages do not justify a toolchain, and the repository's web surface is otherwise
|
|
14
|
+
* build-free.
|
|
15
|
+
*
|
|
16
|
+
* Pages may embed generated fragments (`<!-- generated:name -->`), which are expanded from
|
|
17
|
+
* the entity graph at build time. Prose stays prose in git while the published pages carry
|
|
18
|
+
* live numbers, so the documentation cannot quietly drift from the schemas it describes.
|
|
19
|
+
*/
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const marked_1 = require("marked");
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const buildEntityGraph_1 = require("./buildEntityGraph");
|
|
24
|
+
const REPOSITORY_ROOT = path_1.default.resolve(__dirname, "../../../");
|
|
25
|
+
const DOCS_SOURCE_DIR = path_1.default.join(REPOSITORY_ROOT, "docs");
|
|
26
|
+
/** Splits a leading `---` block off the document. Enough for title/order/summary. */
|
|
27
|
+
function parseFrontMatter(source) {
|
|
28
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
29
|
+
if (!match)
|
|
30
|
+
return { data: {}, body: source };
|
|
31
|
+
const data = {};
|
|
32
|
+
match[1].split(/\r?\n/).forEach((line) => {
|
|
33
|
+
const separator = line.indexOf(":");
|
|
34
|
+
if (separator === -1)
|
|
35
|
+
return;
|
|
36
|
+
data[line.slice(0, separator).trim()] = line
|
|
37
|
+
.slice(separator + 1)
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/^["']|["']$/g, "");
|
|
40
|
+
});
|
|
41
|
+
return { data, body: source.slice(match[0].length) };
|
|
42
|
+
}
|
|
43
|
+
// ── Generated fragments ───────────────────────────────────────────────────────
|
|
44
|
+
function mapLink(id) {
|
|
45
|
+
return `[\`${id}\`](../map/#/entity/${encodeURIComponent(id)})`;
|
|
46
|
+
}
|
|
47
|
+
function table(headers, rows) {
|
|
48
|
+
return [
|
|
49
|
+
`| ${headers.join(" | ")} |`,
|
|
50
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
51
|
+
...rows.map((row) => `| ${row.join(" | ")} |`),
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
function relationshipFragment(graph, id) {
|
|
55
|
+
const node = graph.nodes.find((candidate) => candidate.id === id);
|
|
56
|
+
if (!node)
|
|
57
|
+
throw new Error(`entity-relationships fragment references unknown schema "${id}"`);
|
|
58
|
+
const describe = (kind, direction) => graph.edges
|
|
59
|
+
.filter((edge) => edge.kind === kind && edge[direction === "target" ? "source" : "target"] === id)
|
|
60
|
+
.map((edge) => {
|
|
61
|
+
const other = direction === "target" ? edge.target : edge.source;
|
|
62
|
+
return `${mapLink(other)}${edge.label ? ` \`${edge.label}\`` : ""}`;
|
|
63
|
+
});
|
|
64
|
+
const sections = [];
|
|
65
|
+
const extendsList = describe("extends", "target");
|
|
66
|
+
const containsList = describe("contains", "target");
|
|
67
|
+
const variantList = describe("variant", "target");
|
|
68
|
+
const usedBy = graph.edges
|
|
69
|
+
.filter((edge) => edge.target === id)
|
|
70
|
+
.map((edge) => mapLink(edge.source));
|
|
71
|
+
if (extendsList.length)
|
|
72
|
+
sections.push(`**Extends** — ${extendsList.join(", ")}`);
|
|
73
|
+
if (containsList.length)
|
|
74
|
+
sections.push(`**Contains** — ${containsList.join(", ")}`);
|
|
75
|
+
if (variantList.length > 0) {
|
|
76
|
+
sections.push(variantList.length > 12
|
|
77
|
+
? `**Variants** — ${variantList.length} schemas, see the map`
|
|
78
|
+
: `**Variants** — ${variantList.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
if (usedBy.length) {
|
|
81
|
+
sections.push(usedBy.length > 12
|
|
82
|
+
? `**Used by** — ${usedBy.length} schemas, see the map`
|
|
83
|
+
: `**Used by** — ${usedBy.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
return sections.length ? sections.map((line) => `${line}\n`).join("\n") : "_No references._";
|
|
86
|
+
}
|
|
87
|
+
function mixinUsageFragment(graph) {
|
|
88
|
+
const mixins = new Map();
|
|
89
|
+
graph.edges
|
|
90
|
+
.filter((edge) => edge.kind === "extends")
|
|
91
|
+
.forEach((edge) => {
|
|
92
|
+
const target = graph.nodes.find((node) => node.id === edge.target);
|
|
93
|
+
if (!target || (target.layer !== "in-memory-entity" && target.layer !== "system"))
|
|
94
|
+
return;
|
|
95
|
+
if (!mixins.has(edge.target))
|
|
96
|
+
mixins.set(edge.target, []);
|
|
97
|
+
mixins.get(edge.target).push(edge.source);
|
|
98
|
+
});
|
|
99
|
+
const rows = [...mixins.entries()]
|
|
100
|
+
.sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))
|
|
101
|
+
.slice(0, 15)
|
|
102
|
+
.map(([mixin, users]) => [mapLink(mixin), String(users.length)]);
|
|
103
|
+
return table(["Mixin", "Schemas extending it"], rows);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The corpus categorizes along more than one scheme, and the difference matters when
|
|
107
|
+
* reading a `*_category` path. This is derived rather than asserted: the tiered
|
|
108
|
+
* vocabulary is the transitive `extends` closure into `core/reusable/categories`, so
|
|
109
|
+
* a materials schema that adopted tiers tomorrow would move rows without an edit here.
|
|
110
|
+
*/
|
|
111
|
+
function categorizationSchemesFragment(graph) {
|
|
112
|
+
const parents = new Map();
|
|
113
|
+
graph.edges
|
|
114
|
+
.filter((edge) => edge.kind === "extends")
|
|
115
|
+
.forEach((edge) => {
|
|
116
|
+
if (!parents.has(edge.source))
|
|
117
|
+
parents.set(edge.source, []);
|
|
118
|
+
parents.get(edge.source).push(edge.target);
|
|
119
|
+
});
|
|
120
|
+
const CATEGORIES = "core/reusable/categories";
|
|
121
|
+
const reachesCategories = (id, seen = new Set()) => {
|
|
122
|
+
var _a;
|
|
123
|
+
if (id === CATEGORIES)
|
|
124
|
+
return true;
|
|
125
|
+
if (seen.has(id))
|
|
126
|
+
return false;
|
|
127
|
+
seen.add(id);
|
|
128
|
+
return ((_a = parents.get(id)) !== null && _a !== void 0 ? _a : []).some((parent) => reachesCategories(parent, seen));
|
|
129
|
+
};
|
|
130
|
+
const countUnder = (prefix) => graph.nodes.filter((node) => node.id.startsWith(prefix)).length;
|
|
131
|
+
const carriers = graph.edges
|
|
132
|
+
.filter((edge) => edge.target === CATEGORIES && edge.kind === "contains")
|
|
133
|
+
.map((edge) => edge.source)
|
|
134
|
+
.sort();
|
|
135
|
+
const domains = ["models-category", "methods-category", "materials-category"];
|
|
136
|
+
const rows = domains.map((domain) => {
|
|
137
|
+
const prefix = `${domain}/`;
|
|
138
|
+
const total = countUnder(prefix);
|
|
139
|
+
const rest = graph.nodes.filter((node) => node.id.startsWith(prefix) && !reachesCategories(node.id));
|
|
140
|
+
const tiered = total - rest.length;
|
|
141
|
+
if (!tiered)
|
|
142
|
+
return [`\`${domain}\``, String(total), "none — compositional"];
|
|
143
|
+
// Today the untiered remainder is entirely enum holders; say so only while true.
|
|
144
|
+
const allEnums = rest.every((node) => /\/enum-options$|\/enums$/.test(node.id));
|
|
145
|
+
const remainder = rest.length
|
|
146
|
+
? ` + ${rest.length} ${allEnums ? "enum holders" : "other"}`
|
|
147
|
+
: "";
|
|
148
|
+
return [`\`${domain}\``, String(total), `${tiered} tiered${remainder}`];
|
|
149
|
+
});
|
|
150
|
+
rows.push([
|
|
151
|
+
"`materials-category-components`",
|
|
152
|
+
String(countUnder("materials-category-components/")),
|
|
153
|
+
`${countUnder("materials-category-components/entities/")} entities + ${countUnder("materials-category-components/operations/")} operations`,
|
|
154
|
+
]);
|
|
155
|
+
const carrierList = carriers.map(mapLink).join(", ");
|
|
156
|
+
return `${table(["Category domain", "Schemas", "Scheme"], rows)}
|
|
157
|
+
|
|
158
|
+
Entities carrying a tiered \`categories\` field: ${carrierList || "_none_"}.`;
|
|
159
|
+
}
|
|
160
|
+
function layerInventoryFragment(graph) {
|
|
161
|
+
const rows = Object.entries(graph.meta.layerCounts)
|
|
162
|
+
.sort((a, b) => b[1] - a[1])
|
|
163
|
+
.map(([layer, count]) => [`\`${layer}\``, String(count)]);
|
|
164
|
+
return table(["Layer", "Schemas"], rows);
|
|
165
|
+
}
|
|
166
|
+
function hubTableFragment(graph) {
|
|
167
|
+
const rows = [...graph.nodes]
|
|
168
|
+
.sort((a, b) => b.inDegree - a.inDegree || a.id.localeCompare(b.id))
|
|
169
|
+
.slice(0, 10)
|
|
170
|
+
.map((node) => [mapLink(node.id), String(node.inDegree)]);
|
|
171
|
+
return table(["Schema", "Referenced by"], rows);
|
|
172
|
+
}
|
|
173
|
+
function expandFragments(body, graph) {
|
|
174
|
+
return body.replace(/<!--\s*generated:([^\s]+)\s*-->/g, (_match, name) => {
|
|
175
|
+
const [fragment, argument] = name.split(":");
|
|
176
|
+
switch (fragment) {
|
|
177
|
+
case "corpus-totals":
|
|
178
|
+
return table(["Schemas", "References", "extends", "contains", "variant"], [
|
|
179
|
+
[
|
|
180
|
+
String(graph.meta.nodeCount),
|
|
181
|
+
String(graph.meta.edgeCount),
|
|
182
|
+
String(graph.meta.edgeCountsByKind.extends),
|
|
183
|
+
String(graph.meta.edgeCountsByKind.contains),
|
|
184
|
+
String(graph.meta.edgeCountsByKind.variant),
|
|
185
|
+
],
|
|
186
|
+
]);
|
|
187
|
+
case "layer-inventory":
|
|
188
|
+
return layerInventoryFragment(graph);
|
|
189
|
+
case "categorization-schemes":
|
|
190
|
+
return categorizationSchemesFragment(graph);
|
|
191
|
+
case "hub-table":
|
|
192
|
+
return hubTableFragment(graph);
|
|
193
|
+
case "mixin-usage":
|
|
194
|
+
return mixinUsageFragment(graph);
|
|
195
|
+
case "example-coverage":
|
|
196
|
+
return `${graph.meta.schemasWithExample} of ${graph.meta.nodeCount} schemas (${Math.round((graph.meta.schemasWithExample / graph.meta.nodeCount) * 100)}%) have a mirror example`;
|
|
197
|
+
case "entity-relationships":
|
|
198
|
+
return relationshipFragment(graph, argument);
|
|
199
|
+
default:
|
|
200
|
+
throw new Error(`Unknown generated fragment "${name}"`);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// ── Rendering ─────────────────────────────────────────────────────────────────
|
|
205
|
+
function escapeHtml(value) {
|
|
206
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
207
|
+
}
|
|
208
|
+
function renderPage(page, pages, html) {
|
|
209
|
+
const nav = pages
|
|
210
|
+
.map((item) => `<a href="${item.slug}.html"${item.slug === page.slug ? ' class="current"' : ""}>` +
|
|
211
|
+
`${escapeHtml(item.title)}</a>`)
|
|
212
|
+
.join("\n ");
|
|
213
|
+
return `<!DOCTYPE html>
|
|
214
|
+
<html lang="en">
|
|
215
|
+
<head>
|
|
216
|
+
<meta charset="UTF-8">
|
|
217
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
218
|
+
<title>${escapeHtml(page.title)} — ESSE</title>
|
|
219
|
+
<meta name="description" content="${escapeHtml(page.summary)}">
|
|
220
|
+
<link rel="stylesheet" href="docs.css">
|
|
221
|
+
</head>
|
|
222
|
+
<body>
|
|
223
|
+
<div id="titlebar">
|
|
224
|
+
<span class="app-name">ESSE</span>
|
|
225
|
+
<nav id="surfaces">
|
|
226
|
+
<a href="index.html" class="current">Docs</a>
|
|
227
|
+
<a href="../index.html">Explorer</a>
|
|
228
|
+
<a href="../map/index.html">Map</a>
|
|
229
|
+
</nav>
|
|
230
|
+
</div>
|
|
231
|
+
<div id="workspace">
|
|
232
|
+
<aside id="docs-nav">
|
|
233
|
+
<div class="nav-title">Concepts</div>
|
|
234
|
+
<nav>
|
|
235
|
+
${nav}
|
|
236
|
+
</nav>
|
|
237
|
+
</aside>
|
|
238
|
+
<main id="docs-content">
|
|
239
|
+
<article>
|
|
240
|
+
${html}
|
|
241
|
+
</article>
|
|
242
|
+
</main>
|
|
243
|
+
</div>
|
|
244
|
+
</body>
|
|
245
|
+
</html>
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
const DOCS_STYLESHEET = `/* Concept documentation — shares the explorer's palette. */
|
|
249
|
+
:root {
|
|
250
|
+
--bg-editor: #1e1e1e;
|
|
251
|
+
--bg-sidebar: #252526;
|
|
252
|
+
--bg-inactive: #2d2d2d;
|
|
253
|
+
--bg-hover: #2a2d2e;
|
|
254
|
+
--text-primary: #d4d4d4;
|
|
255
|
+
--text-secondary: #abb2bf;
|
|
256
|
+
--text-muted: #858585;
|
|
257
|
+
--border: #3e3e3e;
|
|
258
|
+
--accent: #4ea1ff;
|
|
259
|
+
--code: #9cdcfe;
|
|
260
|
+
}
|
|
261
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
262
|
+
body {
|
|
263
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
264
|
+
background: var(--bg-editor);
|
|
265
|
+
color: var(--text-primary);
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
min-height: 100vh;
|
|
269
|
+
line-height: 1.6;
|
|
270
|
+
}
|
|
271
|
+
#titlebar {
|
|
272
|
+
height: 34px;
|
|
273
|
+
background: #323233;
|
|
274
|
+
border-bottom: 1px solid #111;
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
padding: 0 14px;
|
|
278
|
+
gap: 16px;
|
|
279
|
+
font-size: 13px;
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
position: sticky;
|
|
282
|
+
top: 0;
|
|
283
|
+
z-index: 5;
|
|
284
|
+
}
|
|
285
|
+
#titlebar .app-name { color: var(--text-primary); font-weight: 600; }
|
|
286
|
+
#surfaces { display: flex; gap: 14px; }
|
|
287
|
+
#surfaces a { color: var(--text-muted); text-decoration: none; }
|
|
288
|
+
#surfaces a:hover { color: var(--text-primary); }
|
|
289
|
+
#surfaces a.current { color: var(--text-primary); border-bottom: 2px solid var(--accent); }
|
|
290
|
+
#workspace { display: flex; flex: 1; align-items: flex-start; }
|
|
291
|
+
#docs-nav {
|
|
292
|
+
width: 250px;
|
|
293
|
+
flex-shrink: 0;
|
|
294
|
+
background: var(--bg-sidebar);
|
|
295
|
+
border-right: 1px solid var(--border);
|
|
296
|
+
padding: 16px 0;
|
|
297
|
+
position: sticky;
|
|
298
|
+
top: 34px;
|
|
299
|
+
align-self: stretch;
|
|
300
|
+
}
|
|
301
|
+
.nav-title {
|
|
302
|
+
padding: 0 16px 8px;
|
|
303
|
+
font-size: 11px;
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
text-transform: uppercase;
|
|
306
|
+
letter-spacing: 0.1em;
|
|
307
|
+
color: var(--text-muted);
|
|
308
|
+
}
|
|
309
|
+
#docs-nav nav { display: flex; flex-direction: column; }
|
|
310
|
+
#docs-nav a {
|
|
311
|
+
padding: 6px 16px;
|
|
312
|
+
color: var(--text-secondary);
|
|
313
|
+
text-decoration: none;
|
|
314
|
+
font-size: 13px;
|
|
315
|
+
border-left: 2px solid transparent;
|
|
316
|
+
}
|
|
317
|
+
#docs-nav a:hover { background: var(--bg-hover); color: var(--text-primary); }
|
|
318
|
+
#docs-nav a.current {
|
|
319
|
+
background: var(--bg-inactive);
|
|
320
|
+
color: var(--text-primary);
|
|
321
|
+
border-left-color: var(--accent);
|
|
322
|
+
}
|
|
323
|
+
#docs-content { flex: 1; min-width: 0; padding: 32px 40px 80px; }
|
|
324
|
+
article { max-width: 68ch; }
|
|
325
|
+
article h1 { font-size: 27px; font-weight: 600; margin-bottom: 18px; text-wrap: balance; }
|
|
326
|
+
article h2 {
|
|
327
|
+
font-size: 19px;
|
|
328
|
+
font-weight: 600;
|
|
329
|
+
margin: 30px 0 10px;
|
|
330
|
+
padding-top: 10px;
|
|
331
|
+
border-top: 1px solid var(--border);
|
|
332
|
+
text-wrap: balance;
|
|
333
|
+
}
|
|
334
|
+
article h3 { font-size: 15px; font-weight: 600; margin: 20px 0 6px; }
|
|
335
|
+
article p, article ul, article ol { margin-bottom: 13px; }
|
|
336
|
+
article ul, article ol { padding-left: 22px; }
|
|
337
|
+
article li { margin-bottom: 4px; }
|
|
338
|
+
article a { color: var(--accent); text-decoration: none; }
|
|
339
|
+
article a:hover { text-decoration: underline; }
|
|
340
|
+
article code {
|
|
341
|
+
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
|
342
|
+
font-size: 0.87em;
|
|
343
|
+
background: var(--bg-inactive);
|
|
344
|
+
color: var(--code);
|
|
345
|
+
padding: 1px 5px;
|
|
346
|
+
border-radius: 3px;
|
|
347
|
+
}
|
|
348
|
+
article pre {
|
|
349
|
+
background: var(--bg-sidebar);
|
|
350
|
+
border: 1px solid var(--border);
|
|
351
|
+
border-radius: 4px;
|
|
352
|
+
padding: 12px 14px;
|
|
353
|
+
overflow-x: auto;
|
|
354
|
+
margin-bottom: 14px;
|
|
355
|
+
}
|
|
356
|
+
article pre code { background: none; padding: 0; color: var(--text-primary); font-size: 12.5px; }
|
|
357
|
+
article blockquote {
|
|
358
|
+
border-left: 3px solid var(--accent);
|
|
359
|
+
padding: 2px 0 2px 14px;
|
|
360
|
+
margin-bottom: 14px;
|
|
361
|
+
color: var(--text-secondary);
|
|
362
|
+
}
|
|
363
|
+
.table-wrap { overflow-x: auto; margin-bottom: 16px; }
|
|
364
|
+
article table { border-collapse: collapse; font-size: 13px; min-width: 100%; }
|
|
365
|
+
article th, article td {
|
|
366
|
+
border: 1px solid var(--border);
|
|
367
|
+
padding: 6px 10px;
|
|
368
|
+
text-align: left;
|
|
369
|
+
vertical-align: top;
|
|
370
|
+
}
|
|
371
|
+
article th { background: var(--bg-sidebar); font-weight: 600; }
|
|
372
|
+
article td:not(:first-child) { font-variant-numeric: tabular-nums; }
|
|
373
|
+
article hr { border: none; border-top: 1px solid var(--border); margin: 26px 0; }
|
|
374
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
375
|
+
@media (max-width: 760px) {
|
|
376
|
+
#workspace { flex-direction: column; }
|
|
377
|
+
#docs-nav { width: 100%; position: static; border-right: none; border-bottom: 1px solid var(--border); }
|
|
378
|
+
#docs-content { padding: 20px; }
|
|
379
|
+
}
|
|
380
|
+
`;
|
|
381
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
382
|
+
function buildDocsPages(outputDir) {
|
|
383
|
+
const { graph } = (0, buildEntityGraph_1.buildEntityGraph)();
|
|
384
|
+
const pages = fs_1.default
|
|
385
|
+
.readdirSync(DOCS_SOURCE_DIR)
|
|
386
|
+
.filter((entry) => entry.endsWith(".md"))
|
|
387
|
+
.map((entry) => {
|
|
388
|
+
var _a, _b;
|
|
389
|
+
const source = fs_1.default.readFileSync(path_1.default.join(DOCS_SOURCE_DIR, entry), "utf-8");
|
|
390
|
+
const { data, body } = parseFrontMatter(source);
|
|
391
|
+
if (!data.title)
|
|
392
|
+
throw new Error(`docs/${entry} is missing a "title" in front matter`);
|
|
393
|
+
return {
|
|
394
|
+
slug: entry.replace(/^\d+-/, "").replace(/\.md$/, ""),
|
|
395
|
+
title: data.title,
|
|
396
|
+
order: Number((_a = data.order) !== null && _a !== void 0 ? _a : 999),
|
|
397
|
+
summary: (_b = data.summary) !== null && _b !== void 0 ? _b : "",
|
|
398
|
+
body,
|
|
399
|
+
};
|
|
400
|
+
})
|
|
401
|
+
.sort((a, b) => a.order - b.order);
|
|
402
|
+
// Render everything before writing anything: an unknown fragment or a broken page
|
|
403
|
+
// should fail the build outright, not leave a half-written site behind.
|
|
404
|
+
const rendered = pages.map((page) => {
|
|
405
|
+
const expanded = expandFragments(page.body, graph);
|
|
406
|
+
const html = marked_1.marked.parse(expanded, { async: false })
|
|
407
|
+
// Wide tables scroll inside their own container rather than the page body.
|
|
408
|
+
.replace(/<table>/g, '<div class="table-wrap"><table>')
|
|
409
|
+
.replace(/<\/table>/g, "</table></div>");
|
|
410
|
+
return { page, html: renderPage(page, pages, html) };
|
|
411
|
+
});
|
|
412
|
+
const targetDir = path_1.default.join(outputDir, "docs");
|
|
413
|
+
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
414
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, "docs.css"), DOCS_STYLESHEET, "utf8");
|
|
415
|
+
rendered.forEach(({ page, html }) => {
|
|
416
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, `${page.slug}.html`), html, "utf8");
|
|
417
|
+
});
|
|
418
|
+
return pages;
|
|
419
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { JSONSchema } from "../esse/utils";
|
|
2
|
+
export type EntityGraphEdgeKind = "extends" | "contains" | "variant";
|
|
3
|
+
export type EntityGraphLayer = "primitive" | "abstract" | "reusable" | "reference" | "definition" | "in-memory-entity" | "system" | "entity" | "entity-component" | "category" | "directory" | "application-parsing";
|
|
4
|
+
export interface EntityGraphNodeManifest {
|
|
5
|
+
name: string;
|
|
6
|
+
isResult?: boolean;
|
|
7
|
+
isMonitor?: boolean;
|
|
8
|
+
defaultUnits?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface EntityGraphNode {
|
|
11
|
+
/** Schema `$id`, dash-separated, e.g. "in-memory-entity/named-defaultable". */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Source path relative to the repository root, e.g. "schema/material.json". */
|
|
14
|
+
path: string;
|
|
15
|
+
/**
|
|
16
|
+
* Path of the resolved copy on the published site. Derived from `$id` the same way
|
|
17
|
+
* JSONSchemasGenerator derives it, which is NOT always the source path: source
|
|
18
|
+
* directories may contain dashes (`properties_directory/non-scalar`) that become
|
|
19
|
+
* underscores once the id round-trips through `$id`.
|
|
20
|
+
*/
|
|
21
|
+
publishedPath: string;
|
|
22
|
+
title: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** Top-level source directory, or "(root)" for schemas at the top of `schema/`. */
|
|
25
|
+
domain: string;
|
|
26
|
+
layer: EntityGraphLayer;
|
|
27
|
+
/** For "entity-component" nodes, the root entity the component belongs to. */
|
|
28
|
+
ownerEntity?: string;
|
|
29
|
+
inDegree: number;
|
|
30
|
+
outDegree: number;
|
|
31
|
+
propertyCount: number;
|
|
32
|
+
hasExample: boolean;
|
|
33
|
+
manifest?: EntityGraphNodeManifest;
|
|
34
|
+
/** Map coordinates, baked at build time so the client renders without laying out. */
|
|
35
|
+
x?: number;
|
|
36
|
+
y?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface EntityGraphEdge {
|
|
39
|
+
source: string;
|
|
40
|
+
target: string;
|
|
41
|
+
kind: EntityGraphEdgeKind;
|
|
42
|
+
/** Property name for "contains" edges; "[]" suffix marks a reference through `items`. */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** JSON pointer when the `$ref` carried a fragment, e.g. "/physicsBased". */
|
|
45
|
+
pointer?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface EntityGraphMeta {
|
|
48
|
+
nodeCount: number;
|
|
49
|
+
edgeCount: number;
|
|
50
|
+
edgeCountsByKind: Record<EntityGraphEdgeKind, number>;
|
|
51
|
+
layerCounts: Record<string, number>;
|
|
52
|
+
sameDocumentRefCount: number;
|
|
53
|
+
schemasWithExample: number;
|
|
54
|
+
isolatedNodeCount: number;
|
|
55
|
+
}
|
|
56
|
+
export interface EntityGraph {
|
|
57
|
+
meta: EntityGraphMeta;
|
|
58
|
+
nodes: EntityGraphNode[];
|
|
59
|
+
edges: EntityGraphEdge[];
|
|
60
|
+
}
|
|
61
|
+
export interface EntityGraphLintResult {
|
|
62
|
+
failures: string[];
|
|
63
|
+
warnings: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Isolated schemas at the time the lint baseline was taken; growth is reported, not failed.
|
|
67
|
+
* These are leaf definitions and externally-consumed formats that nothing else references.
|
|
68
|
+
*/
|
|
69
|
+
export declare const ISOLATED_NODE_BASELINE = 34;
|
|
70
|
+
/**
|
|
71
|
+
* Maps a schema `$id` to the path of its resolved copy on the published site.
|
|
72
|
+
*
|
|
73
|
+
* Exactly invertible by {@link publishedPathToSchemaId}: an `$id` is a path with underscores
|
|
74
|
+
* replaced by dashes, so no `$id` ever contains an underscore and the mapping is injective.
|
|
75
|
+
*
|
|
76
|
+
* What is *not* recoverable is the **source** path, because a source directory may contain a
|
|
77
|
+
* literal dash (`properties_directory/non-scalar`) that the round-trip through `$id` turns into
|
|
78
|
+
* an underscore. Read `node.path` for that.
|
|
79
|
+
*/
|
|
80
|
+
export declare function schemaIdToPublishedPath(schemaId: string): string;
|
|
81
|
+
/** Recovers a schema `$id` from the path of its published copy. */
|
|
82
|
+
export declare function publishedPathToSchemaId(publishedPath: string): string;
|
|
83
|
+
/** Builds the published-path to `$id` lookup, for callers that prefer a map to a function. */
|
|
84
|
+
export declare function buildPublishedPathIndex(nodes: EntityGraphNode[]): Record<string, string>;
|
|
85
|
+
/** Derives the `$id` a schema at this source path must declare, per `setSchemaIds`. */
|
|
86
|
+
export declare function sourcePathToSchemaId(sourcePath: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Assigns a layer to every schema. The classification is total by contract: an
|
|
89
|
+
* unclassifiable path is a lint failure, so a newly added top-level directory forces a
|
|
90
|
+
* deliberate decision here instead of silently landing in a catch-all bucket.
|
|
91
|
+
*/
|
|
92
|
+
export declare function classifyLayer(schemaId: string): {
|
|
93
|
+
layer: EntityGraphLayer;
|
|
94
|
+
ownerEntity?: string;
|
|
95
|
+
} | undefined;
|
|
96
|
+
/** Resolves a JSON pointer within a document, returning undefined when it does not exist. */
|
|
97
|
+
export declare function resolveJsonPointer(document: unknown, pointer: string): unknown;
|
|
98
|
+
interface RawReference {
|
|
99
|
+
ref: string;
|
|
100
|
+
/** Absent when no structural keyword encloses the `$ref` — an L4 failure. */
|
|
101
|
+
kind?: EntityGraphEdgeKind;
|
|
102
|
+
label?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Collects every `$ref` in a schema together with the relationship it expresses.
|
|
106
|
+
*
|
|
107
|
+
* The relationship comes from the innermost enclosing structural keyword: a `$ref` inside
|
|
108
|
+
* `properties.foo.allOf[0]` extends, while one inside `allOf[0].properties.foo` is contained.
|
|
109
|
+
* The enclosing property name travels with the reference regardless of kind, so a union
|
|
110
|
+
* under `properties.data.oneOf` is recorded as a variant *of `data`*.
|
|
111
|
+
*/
|
|
112
|
+
export declare function collectReferences(schema: JSONSchema): RawReference[];
|
|
113
|
+
export interface BuildEntityGraphResult {
|
|
114
|
+
graph: EntityGraph;
|
|
115
|
+
lint: EntityGraphLintResult;
|
|
116
|
+
}
|
|
117
|
+
export declare function buildEntityGraph(): BuildEntityGraphResult;
|
|
118
|
+
/**
|
|
119
|
+
* L10 — validates the emitted graph against its schema.
|
|
120
|
+
*
|
|
121
|
+
* That schema lives here rather than in `schema/`: it describes a build artifact, not an
|
|
122
|
+
* entity of digital materials science, so it has no business in the corpus this tool
|
|
123
|
+
* measures. Imported rather than read from disk so it resolves identically whether the
|
|
124
|
+
* module runs from source or from the transpiled copy in `dist/`.
|
|
125
|
+
*/
|
|
126
|
+
export declare function validateEntityGraph(graph: EntityGraph): string[];
|
|
127
|
+
/** Writes `graph.json` into the given directory, creating it when missing. */
|
|
128
|
+
export declare function writeEntityGraph(graph: EntityGraph, outputDir: string): string;
|
|
129
|
+
export {};
|