@metaobjectsdev/docs-site 0.15.8-rc.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.
Files changed (104) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +62 -0
  3. package/assets/site.css +13 -0
  4. package/assets/site.js +25 -0
  5. package/dist/badges.d.ts +14 -0
  6. package/dist/badges.d.ts.map +1 -0
  7. package/dist/badges.js +21 -0
  8. package/dist/badges.js.map +1 -0
  9. package/dist/builders/extras.d.ts +23 -0
  10. package/dist/builders/extras.d.ts.map +1 -0
  11. package/dist/builders/extras.js +70 -0
  12. package/dist/builders/extras.js.map +1 -0
  13. package/dist/builders/index-data.d.ts +52 -0
  14. package/dist/builders/index-data.d.ts.map +1 -0
  15. package/dist/builders/index-data.js +115 -0
  16. package/dist/builders/index-data.js.map +1 -0
  17. package/dist/builders/object-data.d.ts +97 -0
  18. package/dist/builders/object-data.d.ts.map +1 -0
  19. package/dist/builders/object-data.js +336 -0
  20. package/dist/builders/object-data.js.map +1 -0
  21. package/dist/builders/output-data.d.ts +26 -0
  22. package/dist/builders/output-data.d.ts.map +1 -0
  23. package/dist/builders/output-data.js +35 -0
  24. package/dist/builders/output-data.js.map +1 -0
  25. package/dist/builders/package-data.d.ts +48 -0
  26. package/dist/builders/package-data.d.ts.map +1 -0
  27. package/dist/builders/package-data.js +142 -0
  28. package/dist/builders/package-data.js.map +1 -0
  29. package/dist/builders/prompt-data.d.ts +31 -0
  30. package/dist/builders/prompt-data.d.ts.map +1 -0
  31. package/dist/builders/prompt-data.js +68 -0
  32. package/dist/builders/prompt-data.js.map +1 -0
  33. package/dist/coverage.d.ts +19 -0
  34. package/dist/coverage.d.ts.map +1 -0
  35. package/dist/coverage.js +26 -0
  36. package/dist/coverage.js.map +1 -0
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +3 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/link-check.d.ts +2 -0
  42. package/dist/link-check.d.ts.map +1 -0
  43. package/dist/link-check.js +30 -0
  44. package/dist/link-check.js.map +1 -0
  45. package/dist/link-graph.d.ts +54 -0
  46. package/dist/link-graph.d.ts.map +1 -0
  47. package/dist/link-graph.js +198 -0
  48. package/dist/link-graph.js.map +1 -0
  49. package/dist/load.d.ts +11 -0
  50. package/dist/load.d.ts.map +1 -0
  51. package/dist/load.js +40 -0
  52. package/dist/load.js.map +1 -0
  53. package/dist/mermaid.d.ts +56 -0
  54. package/dist/mermaid.d.ts.map +1 -0
  55. package/dist/mermaid.js +136 -0
  56. package/dist/mermaid.js.map +1 -0
  57. package/dist/mustache-highlight.d.ts +10 -0
  58. package/dist/mustache-highlight.d.ts.map +1 -0
  59. package/dist/mustache-highlight.js +55 -0
  60. package/dist/mustache-highlight.js.map +1 -0
  61. package/dist/package-docs.d.ts +12 -0
  62. package/dist/package-docs.d.ts.map +1 -0
  63. package/dist/package-docs.js +34 -0
  64. package/dist/package-docs.js.map +1 -0
  65. package/dist/scaffold.d.ts +7 -0
  66. package/dist/scaffold.d.ts.map +1 -0
  67. package/dist/scaffold.js +24 -0
  68. package/dist/scaffold.js.map +1 -0
  69. package/dist/site.d.ts +23 -0
  70. package/dist/site.d.ts.map +1 -0
  71. package/dist/site.js +219 -0
  72. package/dist/site.js.map +1 -0
  73. package/dist/yaml-comments.d.ts +6 -0
  74. package/dist/yaml-comments.d.ts.map +1 -0
  75. package/dist/yaml-comments.js +49 -0
  76. package/dist/yaml-comments.js.map +1 -0
  77. package/package.json +45 -0
  78. package/src/badges.ts +26 -0
  79. package/src/builders/extras.ts +61 -0
  80. package/src/builders/index-data.ts +95 -0
  81. package/src/builders/object-data.ts +261 -0
  82. package/src/builders/output-data.ts +35 -0
  83. package/src/builders/package-data.ts +134 -0
  84. package/src/builders/prompt-data.ts +61 -0
  85. package/src/coverage.ts +29 -0
  86. package/src/index.ts +3 -0
  87. package/src/link-check.ts +24 -0
  88. package/src/link-graph.ts +204 -0
  89. package/src/load.ts +47 -0
  90. package/src/mermaid.ts +142 -0
  91. package/src/mustache-highlight.ts +43 -0
  92. package/src/package-docs.ts +33 -0
  93. package/src/scaffold.ts +26 -0
  94. package/src/site.ts +289 -0
  95. package/src/yaml-comments.ts +33 -0
  96. package/templates/chrome-foot.mustache +17 -0
  97. package/templates/chrome-head.mustache +21 -0
  98. package/templates/coverage.html.mustache +24 -0
  99. package/templates/enums.html.mustache +14 -0
  100. package/templates/index.html.mustache +54 -0
  101. package/templates/object.html.mustache +46 -0
  102. package/templates/output.html.mustache +16 -0
  103. package/templates/package.html.mustache +43 -0
  104. package/templates/prompt.html.mustache +28 -0
@@ -0,0 +1,261 @@
1
+ import type { MetaData } from "@metaobjectsdev/metadata";
2
+ import { LinkGraph, fqnOf, type Ref } from "../link-graph";
3
+ import type { CoverageTracker } from "../coverage";
4
+ import { esc, badge } from "../badges";
5
+ import { inheritanceTree, erDiagramRich, flowchartDomain, RICH_MAX, type ErEdge, type ErNode, type ErAttr } from "../mermaid";
6
+
7
+ // capped box attributes for the rich neighborhood ERD: PK → FKs(target) → enums → required, ≤6 + overflow count
8
+ function neighborAttrs(o: MetaData): { attrs: ErAttr[]; more: number } {
9
+ const pk = new Set<string>(), fk = new Map<string, string>();
10
+ for (const id of o.childrenOfType("identity")) {
11
+ const flds = id.attr("fields");
12
+ const names = Array.isArray(flds) ? flds.map(String) : flds !== undefined ? [String(flds)] : [];
13
+ if (id.subType === "primary") names.forEach((n) => pk.add(n));
14
+ if (id.subType === "reference") { const tgt = String(id.attr("references") ?? "").split("::").pop() ?? ""; names.forEach((n) => fk.set(n, tgt)); }
15
+ }
16
+ const fields = o.childrenOfType("field");
17
+ const isEnum = (f: MetaData) => Array.isArray(f.attr("values"));
18
+ const isReq = (f: MetaData) => f.attr("required") === true || f.attr("required") === "true";
19
+ const attrs: ErAttr[] = []; const seen = new Set<string>();
20
+ const push = (f: MetaData, key: ErAttr["key"], note: string) => { if (seen.has(f.name) || attrs.length >= 6) return; seen.add(f.name); attrs.push({ type: f.subType, name: f.name, key, note }); };
21
+ for (const f of fields) if (pk.has(f.name)) push(f, "PK", "");
22
+ for (const f of fields) if (fk.has(f.name)) push(f, "FK", fk.get(f.name)!);
23
+ for (const f of fields) if (isEnum(f)) push(f, "", "enum");
24
+ for (const f of fields) if (isReq(f)) push(f, "", "req");
25
+ const relevant = fields.filter((f) => pk.has(f.name) || fk.has(f.name) || isEnum(f) || isReq(f)).length;
26
+ return { attrs, more: Math.max(0, relevant - attrs.length) };
27
+ }
28
+
29
+ export interface EnumValue { value: string; deflt: boolean; desc: string; }
30
+ export interface FieldRow { name: string; type: string; isArray: boolean; required: boolean; badgesHtml: string; desc: string; enumValues: EnumValue[]; refHref?: string | undefined; refName?: string | undefined; inheritedFrom?: { name: string; href: string } | undefined; anchor: string; }
31
+ export interface IndexRow { name: string; kind: string; fields: string; extra: string; unique: boolean; }
32
+ export interface ValidatorRow { scope: "field" | "object"; subject: string; rule: string; // human-readable, HTML-escaped
33
+ }
34
+ export interface RelationRow { name: string; toName: string; toHref: string; cardinality: string; }
35
+ export interface OriginRow { field: string; from: string; via: string; }
36
+ export interface HierRow { name: string; href: string; level: number; self: boolean; }
37
+ export interface ObjectPageData {
38
+ name: string; kindBadge: string; isAbstract: boolean; isView: boolean; generation: string;
39
+ pkg: string; href: string; breadcrumbHtml: string; desc: string; tableName?: string | undefined; pkHtml?: string | undefined;
40
+ ownFields: FieldRow[]; inheritedFields: FieldRow[]; indexes: IndexRow[]; validators: ValidatorRow[];
41
+ relations: RelationRow[]; origins: OriginRow[]; hierarchy: HierRow[]; inheritanceMermaid?: string | undefined;
42
+ neighborhoodMermaid?: string | undefined; neighborhoodLegend?: { pkg: string; fill: string; stroke: string }[] | undefined; neighborhoodMore?: number | undefined;
43
+ referencedBy: { name: string; href: string; via: string }[];
44
+ references: { name: string; href: string; via: string }[]; usedByTemplates: { name: string; href: string }[];
45
+ sourceFile: string;
46
+ }
47
+
48
+ function fieldRow(f: MetaData, ownerHref: string, g: LinkGraph, cov: CoverageTracker, ctxPkg: string): FieldRow {
49
+ cov.consumeNode(f);
50
+ const a = (n: string) => { const v = f.attr(n); if (v !== undefined) cov.consumeAttr(f, n); return v; };
51
+ const bits: string[] = [];
52
+ const reqVal = a("required"); const required = reqVal === true || reqVal === "true";
53
+ if (required) bits.push(badge({ text: "required", cls: "badge-soft badge-error" }));
54
+ if (a("deprecated") !== undefined) bits.push(badge({ text: "deprecated", cls: "badge-soft badge-warning" }));
55
+ const len = a("maxLength"); if (len !== undefined) bits.push(badge({ text: `≤${len}`, cls: "badge-soft badge-neutral" }));
56
+ const dbt = a("dbColumnType"); if (dbt !== undefined) bits.push(badge({ text: String(dbt), cls: "badge-soft badge-neutral" }));
57
+ const def = a("default"); if (def !== undefined) bits.push(badge({ text: `default ${def}`, cls: "badge-soft badge-neutral" }));
58
+ if (a("xmlText") !== undefined) bits.push(badge({ text: "@xmlText", cls: "badge-soft badge-neutral" }));
59
+ // enum values as data (rendered per-value in the template)
60
+ const valuesAttr = a("values");
61
+ const enumValues: EnumValue[] = Array.isArray(valuesAttr)
62
+ ? valuesAttr.map((v) => ({ value: esc(String(v)), deflt: String(def ?? "") === String(v), desc: "" }))
63
+ : [];
64
+ if (enumValues.length) bits.push(badge({ text: "enum", cls: "badge-soft badge-accent" }));
65
+ // field-level validators as badges
66
+ for (const v of f.childrenOfType("validator")) {
67
+ cov.consumeNode(v);
68
+ if (v.subType === "regex") { cov.consumeAttr(v, "pattern"); bits.push(badge({ text: `regex ${v.attr("pattern")}`, cls: "badge-soft badge-neutral" })); }
69
+ if (v.subType === "numeric") {
70
+ const mm = ["min", "max"].filter((k) => v.attr(k) !== undefined).map((k) => { cov.consumeAttr(v, k); return `${k}=${v.attr(k)}`; });
71
+ if (mm.length) bits.push(badge({ text: mm.join(" "), cls: "badge-soft badge-neutral" }));
72
+ }
73
+ }
74
+ // reference vs containment badge. A field of subType `object` with an objectRef CONTAINS a nested
75
+ // object (composition); a scalar field with an objectRef REFERENCES another object by id.
76
+ let refHref: string | undefined, refName: string | undefined;
77
+ const oref = a("objectRef");
78
+ if (typeof oref === "string") {
79
+ const t = g.byFqn(oref.includes("::") ? oref : `${ctxPkg}::${oref}`) ?? g.byFqn(oref);
80
+ if (t) {
81
+ refHref = g.relHref(ownerHref, t.href); refName = t.name;
82
+ const contains = f.subType === "object";
83
+ bits.push(badge({
84
+ text: contains ? `⊃ ${t.name}` : `→ ${t.name}`,
85
+ cls: contains ? "badge-soft badge-secondary" : "badge-soft badge-info",
86
+ href: refHref,
87
+ title: contains ? "contains (nested object)" : "reference",
88
+ }));
89
+ }
90
+ }
91
+ const desc = esc(a("description") ?? "");
92
+ return { name: f.name, type: f.subType, isArray: f.resolvedIsArray(), required, badgesHtml: bits.join(" "), desc, enumValues, refHref, refName, inheritedFrom: undefined, anchor: `f-${f.name}` };
93
+ }
94
+
95
+ // Neighborhood edge label: relationship edges show their name + (M:N) junction + onDelete; extends/others show via.
96
+ function edgeLabelFor(r: Ref): string {
97
+ if (r.kind === "extends") return "extends";
98
+ if (r.kind === "relationship") {
99
+ const junction = r.through ? ` · M:N via ${r.through.split("::").pop()}` : "";
100
+ const od = r.onDelete ? ` · ${r.onDelete}` : "";
101
+ return `${r.via}${junction}${od}`;
102
+ }
103
+ return r.via;
104
+ }
105
+
106
+ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker): ObjectPageData {
107
+ const dn = g.byFqn(fqn);
108
+ if (!dn || dn.kind !== "object") throw new Error(`not an object: ${fqn}`);
109
+ const o = dn.node;
110
+ cov.consumeNode(o);
111
+ cov.consumeAttr(o, "description");
112
+
113
+ // inheritance hierarchy rows (ancestors nearest-last so level increases downward) + self + direct children
114
+ const anc = g.ancestors(fqn); // nearest-first
115
+ const hierarchy: HierRow[] = [];
116
+ anc.slice().reverse().forEach((n, i) => hierarchy.push({ name: n.name, href: g.relHref(dn.href, n.href), level: i, self: false }));
117
+ const selfLevel = anc.length;
118
+ hierarchy.push({ name: dn.name, href: "", level: selfLevel, self: true });
119
+ const kids = g.extendedBy(fqn).slice().sort((a, b) => a.name.localeCompare(b.name));
120
+ for (const k of kids) hierarchy.push({ name: k.name, href: g.relHref(dn.href, k.href), level: selfLevel + 1, self: false });
121
+ const inheritanceMermaid = hierarchy.length > 1
122
+ ? inheritanceTree(hierarchy.map((h) => ({ name: h.name, level: h.level, self: h.self }))) : undefined;
123
+
124
+ // storage: table vs view, generation, pk
125
+ let tableName: string | undefined, isView = false, pkHtml: string | undefined, generation = "";
126
+ for (const s of o.childrenOfType("source")) {
127
+ cov.consumeNode(s);
128
+ const t = s.attr("table"); if (t !== undefined) { cov.consumeAttr(s, "table"); tableName = String(t); }
129
+ const kind = s.attr("kind"); if (kind !== undefined) { cov.consumeAttr(s, "kind"); if (String(kind) === "view") isView = true; }
130
+ }
131
+ // indexes: identity (pk/secondary) + index.lookup with tuning detail
132
+ const indexes: IndexRow[] = [];
133
+ for (const id of o.childrenOfType("identity")) {
134
+ cov.consumeNode(id);
135
+ const flds = id.attr("fields"); if (flds !== undefined) cov.consumeAttr(id, "fields");
136
+ const fields = Array.isArray(flds) ? flds.join(", ") : String(flds ?? "");
137
+ if (id.subType === "primary") {
138
+ pkHtml = `<code>${esc(fields)}</code>`;
139
+ const gen = id.attr("generation"); if (gen !== undefined) { cov.consumeAttr(id, "generation"); generation = String(gen); }
140
+ indexes.push({ name: id.name, kind: "primary", fields, extra: "", unique: true });
141
+ } else if (id.subType === "secondary") {
142
+ indexes.push({ name: id.name, kind: "unique", fields, extra: "", unique: true });
143
+ } else if (id.subType === "reference") {
144
+ const ref = id.attr("references"); if (ref !== undefined) cov.consumeAttr(id, "references");
145
+ const enf = id.attr("enforce"); if (enf !== undefined) cov.consumeAttr(id, "enforce");
146
+ indexes.push({ name: id.name, kind: "fk", fields, extra: [ref ? `→ ${esc(String(ref))}` : "", enf === false || enf === "false" ? "logical" : ""].filter(Boolean).join(" · "), unique: false });
147
+ }
148
+ }
149
+ for (const ix of o.childrenOfType("index")) {
150
+ cov.consumeNode(ix);
151
+ const flds = ix.attr("fields"); if (flds !== undefined) cov.consumeAttr(ix, "fields");
152
+ const fields = Array.isArray(flds) ? flds.join(", ") : String(flds ?? "");
153
+ const extras: string[] = [];
154
+ for (const k of ["orders", "where", "expr", "using"]) {
155
+ const v = ix.attr(k); if (v !== undefined) { cov.consumeAttr(ix, k); extras.push(esc(`${k} ${Array.isArray(v) ? v.join(",") : v}`)); }
156
+ }
157
+ const uniq = ix.attr("unique"); if (uniq !== undefined) cov.consumeAttr(ix, "unique");
158
+ indexes.push({ name: ix.name, kind: "index", fields, extra: extras.join(" · "), unique: uniq === true || uniq === "true" });
159
+ }
160
+ indexes.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
161
+
162
+ // validators: field-level shown as badges already; collect OBJECT-level here
163
+ const validators: ValidatorRow[] = [];
164
+ for (const v of o.childrenOfType("validator")) {
165
+ cov.consumeNode(v);
166
+ const at = (k: string) => { const x = v.attr(k); if (x !== undefined) cov.consumeAttr(v, k); return x; };
167
+ let rule = "";
168
+ if (v.subType === "comparison") rule = `${esc(String(at("left") ?? ""))} ${esc(String(at("op") ?? ""))} ${esc(String(at("right") ?? ""))}`;
169
+ else if (v.subType === "requiredWhen") rule = `${esc(String(at("field") ?? ""))} required when ${esc(String(at("when") ?? ""))} = ${esc(String(at("equals") ?? ""))}`;
170
+ else if (v.subType === "presentIff") rule = `${esc(String(at("field") ?? ""))} present iff ${esc(String(at("when") ?? ""))} = ${esc(String(at("equals") ?? ""))}`;
171
+ else if (v.subType === "atLeastOne") { const fs = at("fields"); rule = `at least one of ${esc(Array.isArray(fs) ? fs.map(String).join(", ") : String(fs ?? ""))}`; }
172
+ else rule = esc(v.subType);
173
+ validators.push({ scope: "object", subject: v.name || v.subType, rule });
174
+ }
175
+ validators.sort((a, b) => a.subject.localeCompare(b.subject));
176
+
177
+ // relationships
178
+ const relations: RelationRow[] = g.relationshipsOf(fqn).map((r) => {
179
+ const t = g.byFqn(r.toFqn);
180
+ return { name: r.name, toName: t?.name ?? r.toFqn, toHref: t ? g.relHref(dn.href, t.href) : "", cardinality: r.cardinality };
181
+ });
182
+ for (const rel of o.childrenOfType("relationship")) { cov.consumeNode(rel); cov.consumeAttr(rel, "objectRef"); cov.consumeAttr(rel, "cardinality"); }
183
+
184
+ // origin provenance
185
+ const origins: OriginRow[] = g.originsOf(fqn).map((r) => ({ field: r.field, from: esc(r.from), via: esc(r.via) }))
186
+ .sort((a, b) => a.field.localeCompare(b.field));
187
+ for (const f of o.childrenOfType("field")) for (const org of f.childrenOfType("origin")) { cov.consumeNode(org); cov.consumeAttr(org, "from"); cov.consumeAttr(org, "via"); }
188
+
189
+ // fields own vs inherited
190
+ const ownNames = new Set(o.ownChildren().filter((c) => c.type === "field").map((c) => c.name));
191
+ const ownFields: FieldRow[] = [], inheritedFields: FieldRow[] = [];
192
+ for (const f of o.childrenOfType("field")) {
193
+ const row = fieldRow(f, dn.href, g, cov, dn.pkg);
194
+ if (ownNames.has(f.name)) ownFields.push(row);
195
+ else {
196
+ for (let s = o.superResolved; s; s = s.superResolved) {
197
+ if (s.ownChildren().some((c) => c.type === "field" && c.name === f.name)) {
198
+ const t = g.byFqn(fqnOf(s));
199
+ row.inheritedFrom = t ? { name: t.name, href: g.relHref(dn.href, t.href) } : { name: s.name, href: "#" };
200
+ break;
201
+ }
202
+ }
203
+ inheritedFields.push(row);
204
+ }
205
+ }
206
+
207
+ // neighborhood diagram — rich (attrs + domain-fill/role-stroke) when small, simple domain map when large.
208
+ // Cap the neighbor count so hub entities (30+ neighbors) don't produce an unreadable megadiagram;
209
+ // the full set is always available in the Referenced-by / References sections below.
210
+ const NB_MAX = 16;
211
+ // include every traversal kind (fk, field.object, origin, relationship, extends) so projections and
212
+ // value objects are never orphaned; the dedicated Inheritance section still shows the full chain.
213
+ const nbCandidates = new Map<string, { node: typeof dn; edge: ErEdge }>();
214
+ for (const r of g.refsFrom(fqn)) { const t = g.byFqn(r.to); if (t && !nbCandidates.has(r.to)) nbCandidates.set(r.to, { node: t, edge: { parent: t.name, child: dn.name, label: edgeLabelFor(r), cardinality: r.cardinality } }); }
215
+ for (const r of g.refsTo(fqn)) { const s = g.byFqn(r.from); if (s && s.kind === "object" && !nbCandidates.has(r.from)) nbCandidates.set(r.from, { node: s, edge: { parent: dn.name, child: s.name, label: edgeLabelFor(r), cardinality: r.cardinality } }); }
216
+ const sortedNeighbors = [...nbCandidates.entries()].sort(([, a], [, b]) => a.node.name.localeCompare(b.node.name));
217
+ const neighborhoodMore = Math.max(0, sortedNeighbors.length - NB_MAX);
218
+ const nbNodes = new Map<string, typeof dn>(); const nbEdges: ErEdge[] = [];
219
+ nbNodes.set(fqn, dn);
220
+ for (const [k, { node, edge }] of sortedNeighbors.slice(0, NB_MAX)) { nbNodes.set(k, node); nbEdges.push(edge); }
221
+ const roleOf = (n: typeof dn): ErNode["role"] =>
222
+ fqnOf(n.node) === fqn ? "focal"
223
+ : n.node.childrenOfType("source").some((s) => String(s.attr("kind") ?? "") === "view") ? "view"
224
+ : n.pkg !== dn.pkg ? "external" : "normal";
225
+ let neighborhoodMermaid: string | undefined;
226
+ let neighborhoodLegend: { pkg: string; fill: string; stroke: string }[] | undefined;
227
+ if (nbEdges.length > 0) {
228
+ if (nbNodes.size <= RICH_MAX) {
229
+ const erNodes: ErNode[] = [...nbNodes.values()].map((n) => { const { attrs, more } = neighborAttrs(n.node); return { name: n.name, pkg: n.pkg, role: roleOf(n), kind: n.node.subType, attrs, more }; });
230
+ neighborhoodMermaid = erDiagramRich(erNodes, nbEdges);
231
+ } else {
232
+ const r = flowchartDomain([...nbNodes.values()].map((n) => ({ name: n.name, pkg: n.pkg, kind: n.node.subType })), nbEdges.map((e) => ({ from: e.parent, to: e.child, label: e.label, ...(e.cardinality === "many" ? { style: "dashed" as const } : {}) })));
233
+ neighborhoodMermaid = r.mermaid; neighborhoodLegend = r.legend;
234
+ }
235
+ }
236
+
237
+ // backlinks / forward refs (non-extends)
238
+ const referencedBy = g.refsTo(fqn).filter((r) => r.kind !== "extends").map((r) => { const s = g.byFqn(r.from)!; return { name: s.name, href: g.relHref(dn.href, s.href), via: r.via }; }).sort((a, b) => a.name.localeCompare(b.name) || a.via.localeCompare(b.via));
239
+ const references = g.refsFrom(fqn).filter((r) => r.kind !== "extends").map((r) => { const t = g.byFqn(r.to)!; return { name: t.name, href: g.relHref(dn.href, t.href), via: r.via }; }).sort((a, b) => a.name.localeCompare(b.name) || a.via.localeCompare(b.via));
240
+
241
+ // used-by templates (unchanged BFS)
242
+ const usedByTemplates: { name: string; href: string }[] = [];
243
+ for (const t of g.nodes().filter((n) => n.kind !== "object")) {
244
+ const seen = new Set<string>(); const q = g.refsFrom(fqnOf(t.node)).filter((r) => r.kind === "payload").map((r) => r.to); let hit = false;
245
+ while (q.length && seen.size < 100) { const cur = q.shift()!; if (seen.has(cur)) continue; seen.add(cur); if (cur === fqn) { hit = true; break; } for (const r of g.refsFrom(cur)) if (r.kind === "field") q.push(r.to); }
246
+ if (hit) usedByTemplates.push({ name: t.name, href: g.relHref(dn.href, t.href) });
247
+ }
248
+
249
+ const src = (o.source as { files?: string[] }).files?.[0] ?? "";
250
+ const crumbs = [
251
+ `<a href="${esc(g.relHref(dn.href, "index.html"))}">index</a>`,
252
+ `<a href="${esc(g.relHref(dn.href, `${dn.pkgPath}/index.html`))}">${esc(dn.pkg)}</a>`,
253
+ esc(dn.name),
254
+ ];
255
+ return {
256
+ name: dn.name, kindBadge: o.subType, isAbstract: o.isAbstract, isView, generation,
257
+ pkg: dn.pkg, href: dn.href, breadcrumbHtml: crumbs.join(" / "), desc: esc(o.attr("description") ?? ""),
258
+ tableName, pkHtml, ownFields, inheritedFields, indexes, validators, relations, origins,
259
+ hierarchy, inheritanceMermaid, neighborhoodMermaid, neighborhoodLegend, neighborhoodMore, referencedBy, references, usedByTemplates, sourceFile: src,
260
+ };
261
+ }
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { LinkGraph } from "../link-graph";
4
+ import type { CoverageTracker } from "../coverage";
5
+ import type { CommentDocs } from "../yaml-comments";
6
+ import { esc } from "../badges";
7
+
8
+ export interface OutputPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; format: string; kind: string; textRef: string; textRefResolves: boolean; payloadName: string; payloadHref: string; desc: string; fields: { name: string; type: string; isArray: boolean; wire: string; note: string; refHtml: string }[]; }
9
+
10
+ export function buildOutputPage(fqn: string, g: LinkGraph, cov: CoverageTracker, docs: CommentDocs, sourceDirs: string[] = []): OutputPageData {
11
+ const dn = g.byFqn(fqn)!;
12
+ const t = dn.node;
13
+ cov.consumeNode(t); cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef"); cov.consumeAttr(t, "format");
14
+ const format = String(t.attr("format") ?? "text");
15
+ const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload");
16
+ const payload = pRef ? g.byFqn(pRef.to) : undefined;
17
+ const fields = (payload?.node.childrenOfType("field") ?? []).map((f) => {
18
+ const ref = f.attr("objectRef");
19
+ const target = typeof ref === "string" ? (g.byFqn(ref.includes("::") ? ref : `${dn.pkg}::${ref}`) ?? g.byFqn(ref)) : undefined;
20
+ const hasXmlText = f.attr("xmlText") !== undefined;
21
+ const wire = hasXmlText ? "@xmlText body" : target ? "nested" : format === "xml" ? (f.resolvedIsArray() ? "element" : "attr") : "property";
22
+ const noteAttr = f.attr("description");
23
+ const note = esc(noteAttr ?? docs.fieldNote.get(`${payload!.name}.${f.name}`) ?? "");
24
+ if (noteAttr !== undefined) cov.consumeAttr(f, "description");
25
+ return { name: f.name, type: f.subType, isArray: f.resolvedIsArray(), wire, note,
26
+ refHtml: target ? `<a href="${esc(g.relHref(dn.href, target.href))}" class="link">${esc(target.name)}</a>` : "" };
27
+ });
28
+ const textRef = String(t.attr("textRef") ?? "");
29
+ const textRefResolves = sourceDirs.some((d) => existsSync(join(d, ...textRef.split("/")) + ".mustache"));
30
+ return { name: dn.name, pkg: dn.pkg, href: dn.href,
31
+ breadcrumbHtml: `<a href="${esc(g.relHref(dn.href, "index.html"))}">index</a> / <a href="${esc("index.html")}">${esc(dn.pkg)}</a> / ${esc(dn.name)}`,
32
+ format, kind: String(t.attr("kind") ?? "document"), textRef, textRefResolves,
33
+ payloadName: payload?.name ?? "", payloadHref: payload ? esc(g.relHref(dn.href, payload.href)) : "",
34
+ desc: payload ? esc(payload.node.attr("description") ?? docs.objectDesc.get(payload.name) ?? "") : "", fields };
35
+ }
@@ -0,0 +1,134 @@
1
+ import type { MetaData } from "@metaobjectsdev/metadata";
2
+ import { LinkGraph, fqnOf } from "../link-graph";
3
+ import type { CoverageTracker } from "../coverage";
4
+ import { esc } from "../badges";
5
+ import { erDiagramRich, flowchartDomain, domainColor, RICH_MAX, type ErEdge, type ErAttr, type ErNode } from "../mermaid";
6
+ import { harvestPackageDocs, keyEntities } from "../package-docs";
7
+
8
+ // capped box attributes for the rich package ERD: PK → FKs(target) → enums → required, ≤6 + overflow count
9
+ // (mirrors neighborAttrs in object-data.ts — kept local to avoid cross-builder coupling)
10
+ function pkgAttrs(o: MetaData): { attrs: ErAttr[]; more: number } {
11
+ const pk = new Set<string>(), fk = new Map<string, string>();
12
+ for (const id of o.childrenOfType("identity")) {
13
+ const flds = id.attr("fields");
14
+ const names = Array.isArray(flds) ? flds.map(String) : flds !== undefined ? [String(flds)] : [];
15
+ if (id.subType === "primary") names.forEach((n) => pk.add(n));
16
+ if (id.subType === "reference") { const tgt = String(id.attr("references") ?? "").split("::").pop() ?? ""; names.forEach((n) => fk.set(n, tgt)); }
17
+ }
18
+ const fields = o.childrenOfType("field");
19
+ const isEnum = (f: MetaData) => Array.isArray(f.attr("values"));
20
+ const isReq = (f: MetaData) => f.attr("required") === true || f.attr("required") === "true";
21
+ const attrs: ErAttr[] = []; const seen = new Set<string>();
22
+ const push = (f: MetaData, key: ErAttr["key"], note: string) => { if (seen.has(f.name) || attrs.length >= 6) return; seen.add(f.name); attrs.push({ type: f.subType, name: f.name, key, note }); };
23
+ for (const f of fields) if (pk.has(f.name)) push(f, "PK", "");
24
+ for (const f of fields) if (fk.has(f.name)) push(f, "FK", fk.get(f.name)!);
25
+ for (const f of fields) if (isEnum(f)) push(f, "", "enum");
26
+ for (const f of fields) if (isReq(f)) push(f, "", "req");
27
+ const relevant = fields.filter((f) => pk.has(f.name) || fk.has(f.name) || isEnum(f) || isReq(f)).length;
28
+ return { attrs, more: Math.max(0, relevant - attrs.length) };
29
+ }
30
+
31
+ export interface ObjRow { name: string; href: string; kind: string; table: string; fieldCount: number; extendsName: string; }
32
+ export interface TplRow { name: string; href: string; payloadName: string; payloadHref: string; format: string; textRef: string; }
33
+ export interface PackagePageData {
34
+ pkg: string; pkgPath: string; tree: string; breadcrumbHtml: string;
35
+ title: string; descHtml: string;
36
+ keyCards: { name: string; href: string; inbound: number }[];
37
+ erdMermaid: string; erdLegend: { pkg: string; fill: string; stroke: string }[];
38
+ abstracts: ObjRow[]; objects: ObjRow[]; prompts: TplRow[]; outputs: TplRow[];
39
+ referencedBy: { pkg: string; href: string; n: number }[];
40
+ }
41
+
42
+ export function buildPackagePage(pkg: string, g: LinkGraph, cov: CoverageTracker, sourceDirs?: string[]): PackagePageData {
43
+ const dirs = sourceDirs ?? [];
44
+ const members = g.nodes().filter((n) => n.pkg === pkg);
45
+ const pageHref = `${members[0]!.pkgPath}/index.html`;
46
+ const objRow = (n: (typeof members)[0]): ObjRow => ({
47
+ name: n.name, href: `${n.name}.html`, kind: n.node.subType,
48
+ table: String(n.node.childrenOfType("source").map((s) => s.attr("table")).find((t) => t !== undefined) ?? ""),
49
+ fieldCount: n.node.childrenOfType("field").length,
50
+ extendsName: n.node.superResolved?.name ?? "",
51
+ });
52
+ const tplRow = (n: (typeof members)[0]): TplRow => {
53
+ cov.consumeNode(n.node); cov.consumeAttr(n.node, "payloadRef"); cov.consumeAttr(n.node, "textRef"); cov.consumeAttr(n.node, "format");
54
+ const p = g.refsFrom(fqnOf(n.node)).find((r) => r.kind === "payload");
55
+ const pt = p ? g.byFqn(p.to) : undefined;
56
+ return { name: n.name, href: `${n.name}.html`, payloadName: pt?.name ?? String(n.node.attr("payloadRef") ?? ""), payloadHref: pt ? g.relHref(pageHref, pt.href) : "", format: String(n.node.attr("format") ?? "text"), textRef: String(n.node.attr("textRef") ?? "") };
57
+ };
58
+ const objs = members.filter((m) => m.kind === "object");
59
+ const abstracts = objs.filter((m) => m.node.isAbstract).map(objRow).sort((a, b) => a.name.localeCompare(b.name));
60
+ const objects = objs.filter((m) => !m.node.isAbstract).map(objRow).sort((a, b) => a.name.localeCompare(b.name));
61
+ const prompts = members.filter((m) => m.kind === "prompt").map(tplRow).sort((a, b) => a.name.localeCompare(b.name));
62
+ const outputs = members.filter((m) => m.kind === "output").map(tplRow).sort((a, b) => a.name.localeCompare(b.name));
63
+
64
+ // authored prose + key-entity cards
65
+ const pd = harvestPackageDocs(dirs).get(pkg);
66
+ const title = esc(pd?.title ?? pkg.split("::").pop() ?? pkg);
67
+ const descHtml = esc(pd?.description ?? "");
68
+ const keyCards = keyEntities(pkg, g).map((k) => ({ name: k.name, href: g.relHref(pageHref, k.href), inbound: k.inbound }));
69
+
70
+ // package ERD — collect internal objects + external neighbor objects + edges
71
+ const extNodes = new Map<string, typeof members[0]>();
72
+ const erdEdges: ErEdge[] = [];
73
+ for (const m of objs) {
74
+ for (const r of g.refsFrom(fqnOf(m.node))) {
75
+ if (r.kind === "extends") continue;
76
+ const t = g.byFqn(r.to); if (!t || t.kind !== "object") continue;
77
+ erdEdges.push({ parent: t.name, child: m.name, label: r.via });
78
+ if (t.pkg !== pkg) extNodes.set(r.to, t);
79
+ }
80
+ for (const r of g.refsTo(fqnOf(m.node))) {
81
+ if (r.kind === "extends") continue;
82
+ const s = g.byFqn(r.from); if (!s || s.kind !== "object" || s.pkg === pkg) continue;
83
+ erdEdges.push({ parent: m.name, child: s.name, label: r.via });
84
+ extNodes.set(r.from, s);
85
+ }
86
+ }
87
+ // deduplicate edges
88
+ const deduped = erdEdges.filter((e, i) => erdEdges.findIndex((x) => x.parent === e.parent && x.child === e.child && x.label === e.label) === i);
89
+
90
+ // mode switch: rich (≤RICH_MAX) vs domain flowchart (large)
91
+ const totalNodes = objs.length + extNodes.size;
92
+ let erdMermaid: string;
93
+ let erdLegend: { pkg: string; fill: string; stroke: string }[] = [];
94
+ if (totalNodes <= RICH_MAX) {
95
+ const erNodes: ErNode[] = [
96
+ ...objs.map((m) => { const { attrs, more } = pkgAttrs(m.node); return { name: m.name, pkg: m.pkg, role: "normal" as ErNode["role"], kind: m.node.subType, attrs, more }; }),
97
+ ...[...extNodes.values()].map((m) => { const { attrs, more } = pkgAttrs(m.node); return { name: m.name, pkg: m.pkg, role: "external" as ErNode["role"], kind: m.node.subType, attrs, more }; }),
98
+ ];
99
+ erdMermaid = erDiagramRich(erNodes, deduped);
100
+ // build legend from unique packages
101
+ const pkgsSeen = new Map<string, typeof erdLegend[0]>();
102
+ for (const n of erNodes) {
103
+ if (!pkgsSeen.has(n.pkg)) {
104
+ const dc = domainColor(n.pkg);
105
+ pkgsSeen.set(n.pkg, { pkg: n.pkg, fill: dc.fill, stroke: dc.stroke });
106
+ }
107
+ }
108
+ erdLegend = [...pkgsSeen.values()].sort((a, b) => a.pkg.localeCompare(b.pkg));
109
+ } else {
110
+ const flowNodes = [
111
+ ...objs.map((m) => ({ name: m.name, pkg: m.pkg, kind: m.node.subType })),
112
+ ...[...extNodes.values()].map((m) => ({ name: m.name, pkg: m.pkg, kind: m.node.subType })),
113
+ ];
114
+ const r = flowchartDomain(flowNodes, deduped.map((e) => ({ from: e.parent, to: e.child, label: e.label })));
115
+ erdMermaid = r.mermaid;
116
+ erdLegend = r.legend;
117
+ }
118
+
119
+ // inbound package backlinks
120
+ const inbound = new Map<string, number>();
121
+ for (const m of objs) for (const r of g.refsTo(fqnOf(m.node))) {
122
+ const s = g.byFqn(r.from);
123
+ if (s && s.pkg !== pkg && r.kind !== "extends") inbound.set(s.pkg, (inbound.get(s.pkg) ?? 0) + 1);
124
+ }
125
+ const referencedBy = [...inbound.entries()].sort().map(([p, n]) => ({ pkg: p, href: g.relHref(pageHref, `${p.split("::").join("/")}/index.html`), n }));
126
+
127
+ return {
128
+ pkg, pkgPath: members[0]!.pkgPath, tree: members[0]!.tree,
129
+ breadcrumbHtml: `<a href="${g.relHref(pageHref, "index.html")}">index</a> / ${esc(pkg)}`,
130
+ title, descHtml, keyCards,
131
+ erdMermaid, erdLegend,
132
+ abstracts, objects, prompts, outputs, referencedBy,
133
+ };
134
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { LinkGraph } from "../link-graph";
4
+ import type { CoverageTracker } from "../coverage";
5
+ import { highlightMustache } from "../mustache-highlight";
6
+ import { esc } from "../badges";
7
+
8
+ export interface PayloadTreeRow { indent: number; name: string; type: string; isArray: boolean; anchor: string; desc: string; refHtml: string; }
9
+ export interface PromptPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; attrsHtml: string; desc: string; payloadName: string; payloadHref: string; payloadTree: PayloadTreeRow[]; sourceHtml?: string | undefined; sourceMissingNote?: string | undefined; tocHtml?: string | undefined; packageFiles: { file: string; html: string }[]; }
10
+
11
+ export function buildPromptPage(fqn: string, g: LinkGraph, cov: CoverageTracker, sourceDirs: string[]): PromptPageData {
12
+ const dn = g.byFqn(fqn)!;
13
+ const t = dn.node;
14
+ cov.consumeNode(t);
15
+ const attrs: string[] = [];
16
+ for (const a of ["format", "maxTokens", "requiredSlots", "model", "responseRef", "maxChars", "promptStyle"]) {
17
+ const v = t.attr(a); if (v !== undefined) { cov.consumeAttr(t, a); attrs.push(`<span class="badge badge-ghost badge-sm">@${esc(a)} ${esc(v)}</span>`); }
18
+ }
19
+ cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef");
20
+ // payload tree (root + one nested level)
21
+ const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload");
22
+ const payload = pRef ? g.byFqn(pRef.to) : undefined;
23
+ const tree: PayloadTreeRow[] = [];
24
+ const fieldsOf = (o: NonNullable<typeof payload>, indent: number, prefix: string) => {
25
+ for (const f of o.node.childrenOfType("field")) {
26
+ const ref = f.attr("objectRef");
27
+ const target = typeof ref === "string" ? (g.byFqn(ref.includes("::") ? ref : `${o.pkg}::${ref}`) ?? g.byFqn(ref)) : undefined;
28
+ tree.push({ indent, name: prefix + f.name, type: f.subType, isArray: f.resolvedIsArray(), anchor: `f-${prefix}${f.name}`,
29
+ desc: esc(f.attr("description") ?? ""),
30
+ refHtml: target ? `<a href="${esc(g.relHref(dn.href, target.href))}" class="link">${esc(target.name)}</a>` : "" });
31
+ if (target && indent === 0) fieldsOf(target, 1, `${f.name}.`);
32
+ }
33
+ };
34
+ if (payload) fieldsOf(payload, 0, "");
35
+ const anchors = new Map(tree.map((r) => [r.name, `#${r.anchor}`]));
36
+ // source resolution
37
+ const textRef = String(t.attr("textRef") ?? "");
38
+ let sourceHtml: string | undefined, tocHtml: string | undefined, sourceMissingNote: string | undefined;
39
+ let pkgDir: string | undefined;
40
+ for (const d of sourceDirs) {
41
+ const p = join(d, ...textRef.split("/")) + ".mustache";
42
+ if (existsSync(p)) { pkgDir = dirname(p);
43
+ const r = highlightMustache(readFileSync(p, "utf8"), (path) => anchors.get(path));
44
+ sourceHtml = r.html;
45
+ tocHtml = r.toc.map((s) => `<a href="#${esc(s.anchor)}" class="link">${esc(s.name)}</a>`).join(" · ");
46
+ break;
47
+ }
48
+ }
49
+ if (!sourceHtml) sourceMissingNote = `text ref ${esc(textRef)} does not resolve under the metadata roots (forward-pointing ref).`;
50
+ // other mustache files in the template's package dir
51
+ const packageFiles: { file: string; html: string }[] = [];
52
+ if (pkgDir) for (const f of readdirSync(pkgDir).filter((f) => f.endsWith(".mustache")).sort()) {
53
+ if (join(pkgDir, f) === join(pkgDir, `${textRef.split("/").pop()}.mustache`)) continue;
54
+ packageFiles.push({ file: f, html: highlightMustache(readFileSync(join(pkgDir, f), "utf8"), (p) => anchors.get(p)).html });
55
+ }
56
+ return { name: dn.name, pkg: dn.pkg, href: dn.href,
57
+ breadcrumbHtml: `<a href="${esc(g.relHref(dn.href, "index.html"))}">index</a> / <a href="${esc("index.html")}">${esc(dn.pkg)}</a> / ${esc(dn.name)}`,
58
+ attrsHtml: attrs.join(" "), desc: esc(t.attr("description") ?? ""),
59
+ payloadName: payload?.name ?? "", payloadHref: payload ? esc(g.relHref(dn.href, payload.href)) : "",
60
+ payloadTree: tree, sourceHtml, sourceMissingNote, tocHtml, packageFiles };
61
+ }
@@ -0,0 +1,29 @@
1
+ import type { MetaData, MetaRoot } from "@metaobjectsdev/metadata";
2
+
3
+ export interface CoverageRow { key: string; count: number; consumed: boolean; }
4
+ export interface CoverageReport { kinds: CoverageRow[]; attrs: CoverageRow[]; warnings: string[]; }
5
+
6
+ export class CoverageTracker {
7
+ private kinds = new Set<string>();
8
+ private attrs = new Set<string>();
9
+ consumeNode(n: MetaData): void { this.kinds.add(`${n.type}.${n.subType}`); }
10
+ consumeAttr(n: MetaData, a: string): void { this.attrs.add(`${n.type}:@${a}`); }
11
+ report(root: MetaRoot): CoverageReport {
12
+ const kindCount = new Map<string, number>();
13
+ const attrCount = new Map<string, number>();
14
+ const walk = (n: MetaData) => {
15
+ kindCount.set(`${n.type}.${n.subType}`, (kindCount.get(`${n.type}.${n.subType}`) ?? 0) + 1);
16
+ for (const [name] of n.ownAttrs()) {
17
+ attrCount.set(`${n.type}:@${name}`, (attrCount.get(`${n.type}:@${name}`) ?? 0) + 1);
18
+ }
19
+ for (const c of n.ownChildren()) walk(c);
20
+ };
21
+ for (const c of root.ownChildren()) walk(c);
22
+ const rows = (m: Map<string, number>, seen: Set<string>) =>
23
+ [...m.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([key, count]) => ({ key, count, consumed: seen.has(key) }));
24
+ const kinds = rows(kindCount, this.kinds);
25
+ const attrs = rows(attrCount, this.attrs);
26
+ const warnings = [...kinds, ...attrs].filter((r) => !r.consumed).map((r) => `coverage: ${r.key} (${r.count}) not rendered by any page`);
27
+ return { kinds, attrs, warnings };
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { generateSite } from "./site";
2
+ export type { SiteOptions, SiteResult } from "./site";
3
+ export { SITE_TEMPLATE_NAMES, SITE_ASSET_NAMES, readSiteFile } from "./scaffold";
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, normalize } from "node:path";
3
+
4
+ export function checkLinks(outDir: string, pages: string[]): string[] {
5
+ const errs: string[] = [];
6
+ const idCache = new Map<string, Set<string>>();
7
+ const idsOf = (p: string) => {
8
+ if (!idCache.has(p)) idCache.set(p, new Set([...readFileSync(p, "utf8").matchAll(/id="([^"]+)"/g)].map((m) => m[1]!)));
9
+ return idCache.get(p)!;
10
+ };
11
+ for (const page of pages) {
12
+ const html = readFileSync(join(outDir, page), "utf8");
13
+ for (const m of html.matchAll(/href="([^"]+)"/g)) {
14
+ const href = m[1]!;
15
+ if (/^(https?:|mailto:|#$)/.test(href)) continue;
16
+ const [file = "", anchor] = href.split("#");
17
+ const target = file === "" ? page : normalize(join(dirname(page), file));
18
+ const abs = join(outDir, target);
19
+ if (!existsSync(abs)) { errs.push(`${page} -> ${href}`); continue; }
20
+ if (anchor && !idsOf(abs).has(anchor)) errs.push(`${page} -> ${href}`);
21
+ }
22
+ }
23
+ return errs;
24
+ }