@mat3ra/esse 2026.8.18-2 → 2026.8.25-1

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
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 {};