@metaobjectsdev/docs-site 1.0.13 → 1.1.1-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.
package/src/link-graph.ts CHANGED
@@ -1,11 +1,17 @@
1
- import type { MetaData, MetaObject, MetaRelationship } from "@metaobjectsdev/metadata";
2
- import { deriveM2MFields, resolveRelationshipReference, stripPackage } from "@metaobjectsdev/metadata";
1
+ import type { MetaData, MetaObject, MetaRelationship, MetaRoot } from "@metaobjectsdev/metadata";
2
+ import {
3
+ deriveM2MFields, resolveRelationshipReference,
4
+ OBJECT_REPORT_ATTR_FROM, OBJECT_SUBTYPE_REPORT, TYPE_OBJECT,
5
+ RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_THROUGH, IDENTITY_REFERENCE_ATTR_REFERENCES,
6
+ FIELD_ATTR_OBJECT_REF, ORIGIN_PASSTHROUGH_ATTR_FROM, TEMPLATE_ATTR_PAYLOAD_REF, TEMPLATE_ATTR_RESPONSE_REF,
7
+ refTarget,
8
+ } from "@metaobjectsdev/metadata";
3
9
  import { type LoadedModel, treeOf } from "./load.js";
4
10
 
5
11
  export interface DocNode { kind: "object" | "prompt" | "output"; name: string; pkg: string; pkgPath: string; href: string; node: MetaData; tree: string; }
6
12
  export interface Ref {
7
13
  from: string; to: string; via: string;
8
- kind: "field" | "fk" | "extends" | "payload" | "response" | "relationship" | "origin";
14
+ kind: "field" | "fk" | "extends" | "payload" | "response" | "relationship" | "origin" | "report";
9
15
  cardinality?: "one" | "many" | undefined;
10
16
  through?: string | undefined; // junction FQN (M:N)
11
17
  sourceJoinField?: string | undefined; // junction source FK (M:N)
@@ -30,8 +36,11 @@ export class LinkGraph {
30
36
  private _to = new Map<string, Ref[]>();
31
37
  private _extBy = new Map<string, DocNode[]>();
32
38
  private _origins = new Map<string, OriginRef[]>();
39
+ /** The loaded root the graph was built from (a report's derived columns resolve against it). */
40
+ readonly root: MetaRoot;
33
41
 
34
42
  constructor(model: LoadedModel) {
43
+ this.root = model.root;
35
44
  for (const o of model.root.ownChildren()) {
36
45
  let kind: DocNode["kind"] | undefined;
37
46
  if (o.type === "object") kind = "object";
@@ -48,16 +57,20 @@ export class LinkGraph {
48
57
  (this._from.get(r.from) ?? this._from.set(r.from, []).get(r.from)!).push(r);
49
58
  (this._to.get(r.to) ?? this._to.set(r.to, []).get(r.to)!).push(r);
50
59
  };
51
- const resolveRef = (raw: string, ctxPkg: string): string | undefined => {
52
- const cand = raw.includes("::") ? raw : `${ctxPkg}::${raw}`;
53
- return this._nodes.has(cand) ? cand : (this._nodes.has(raw) ? raw : undefined);
60
+ // ADR-0059 R7 — an edge points at the object the reference attribute is bound to,
61
+ // resolved where the attribute was declared, never re-resolved in an inheritor's package.
62
+ const bound = (node: MetaData, attr: string): string | undefined => {
63
+ const target = refTarget(node, attr);
64
+ if (target === undefined) return undefined;
65
+ const key = fqnOf(target);
66
+ return this._nodes.has(key) ? key : undefined;
54
67
  };
55
68
  // M:N through-junction edge. Derives the junction FK fields via the metadata
56
69
  // SSOT (deriveM2MFields) — hetero, directed (@sourceRefField), or symmetric
57
70
  // (@symmetric). On a derivation failure (ambiguous self-join) the edge is
58
71
  // SKIPPED — generation never fails.
59
- const addM2mEdge = (from: string, to: string, rel: MetaRelationship, obj: MetaObject, ctxPkg: string, onDelete: string | undefined, subtype: string | undefined): void => {
60
- const through = rel.through ? (resolveRef(rel.through, ctxPkg) ?? rel.through) : undefined;
72
+ const addM2mEdge = (from: string, to: string, rel: MetaRelationship, obj: MetaObject, onDelete: string | undefined, subtype: string | undefined): void => {
73
+ const through = rel.through ? (bound(rel, RELATIONSHIP_ATTR_THROUGH) ?? rel.through) : undefined;
61
74
  let sourceJoinField: string | undefined, targetJoinField: string | undefined;
62
75
  try {
63
76
  const f = deriveM2MFields(rel, obj, model.root);
@@ -72,11 +85,8 @@ export class LinkGraph {
72
85
  const fqn = fqnOf(dn.node);
73
86
  if (dn.kind === "object") {
74
87
  for (const f of dn.node.childrenOfType("field")) {
75
- const ref = f.attr("objectRef");
76
- if (typeof ref === "string") {
77
- const to = resolveRef(ref, dn.pkg);
78
- if (to) addRef({ from: fqn, to, via: f.name, kind: "field" });
79
- }
88
+ const to = bound(f, FIELD_ATTR_OBJECT_REF);
89
+ if (to) addRef({ from: fqn, to, via: f.name, kind: "field" });
80
90
  }
81
91
  // dn.node is typed MetaData; the enclosing dn.kind === "object" guard
82
92
  // guarantees it is a MetaObject at runtime. The `unknown` bridge is needed
@@ -88,15 +98,13 @@ export class LinkGraph {
88
98
  // reference whose (target, first-field) a belongs-to relationship covered.
89
99
  const coveredFk = new Set<string>();
90
100
  for (const rel of obj.relationships()) {
91
- const objectRef = rel.objectRef;
92
- if (typeof objectRef !== "string") continue;
93
- const to = resolveRef(objectRef, dn.pkg);
101
+ const to = bound(rel, RELATIONSHIP_ATTR_OBJECT_REF);
94
102
  if (!to) continue;
95
103
  const cardinality = rel.cardinality === "many" ? "many" : rel.cardinality === "one" ? "one" : undefined;
96
104
  const onDelete = rel.onDelete;
97
105
  const subtype = rel.subType;
98
106
  if (cardinality === "many" && rel.through !== undefined) {
99
- addM2mEdge(fqn, to, rel, obj, dn.pkg, onDelete, subtype); // Task 3 helper
107
+ addM2mEdge(fqn, to, rel, obj, onDelete, subtype); // Task 3 helper
100
108
  continue;
101
109
  }
102
110
  // belongs-to (1:N, one) — resolve the identity.reference this relationship
@@ -109,38 +117,39 @@ export class LinkGraph {
109
117
  // uses; ambiguous cases resolve to undefined and simply leave every candidate
110
118
  // reference to draw its own "fk" edge, which is the safe (extra-info, not
111
119
  // missing-edge) outcome for a docs graph.
112
- const target = stripPackage(objectRef);
113
- const match = resolveRelationshipReference(obj, rel.name, target, rel.sourceRefField);
120
+ const match = resolveRelationshipReference(
121
+ obj, rel.name, refTarget(rel, RELATIONSHIP_ATTR_OBJECT_REF), rel.sourceRefField,
122
+ );
114
123
  const fkField = match?.fields?.[0];
115
124
  if (fkField) coveredFk.add(`${to}::${fkField}`);
116
125
  addRef({ from: fqn, to, via: rel.name, kind: "relationship", cardinality, onDelete, subtype });
117
126
  }
118
127
  for (const id of dn.node.childrenOfType("identity")) {
119
128
  if (id.subType !== "reference") continue;
120
- const ref = id.attr("references");
121
- if (typeof ref === "string") {
122
- const to = resolveRef(ref, dn.pkg);
123
- if (to) {
124
- const fieldsValue = id.attr("fields") ?? id.name;
125
- const firstField = Array.isArray(fieldsValue) ? String(fieldsValue[0] ?? "") : String(fieldsValue);
126
- if (coveredFk.has(`${to}::${firstField}`)) continue; // superseded by a relationship edge
127
- const via = Array.isArray(fieldsValue) ? fieldsValue.join(", ") : String(fieldsValue);
128
- addRef({ from: fqn, to, via, kind: "fk" });
129
- }
130
- }
129
+ const to = bound(id, IDENTITY_REFERENCE_ATTR_REFERENCES);
130
+ if (!to) continue;
131
+ const fieldsValue = id.attr("fields") ?? id.name;
132
+ const firstField = Array.isArray(fieldsValue) ? String(fieldsValue[0] ?? "") : String(fieldsValue);
133
+ if (coveredFk.has(`${to}::${firstField}`)) continue; // superseded by a relationship edge
134
+ const via = Array.isArray(fieldsValue) ? fieldsValue.join(", ") : String(fieldsValue);
135
+ addRef({ from: fqn, to, via, kind: "fk" });
131
136
  }
132
137
  for (const f of dn.node.childrenOfType("field")) {
133
138
  for (const org of f.childrenOfType("origin")) {
134
139
  const from = String(org.attr("from") ?? ""), via = String(org.attr("via") ?? "");
135
140
  (this._origins.get(fqn) ?? this._origins.set(fqn, []).get(fqn)!).push({ field: f.name, from, via });
136
141
  // connect a projection to its source object so it is not orphaned on diagrams:
137
- // `from` is "pkg::Entity.field" — strip the trailing ".field" to get the source object ref.
138
- const dot = from.lastIndexOf(".");
139
- const srcRef = dot > 0 ? from.slice(0, dot) : "";
140
- const to = srcRef ? resolveRef(srcRef, dn.pkg) : undefined;
142
+ // `from` is "pkg::Entity.field"; its bound target is the source object.
143
+ const to = bound(org, ORIGIN_PASSTHROUGH_ATTR_FROM);
141
144
  if (to && to !== fqn) addRef({ from: fqn, to, via: `${f.name} (origin)`, kind: "origin" });
142
145
  }
143
146
  }
147
+ // FR-044: a report reads from its @from entity. The edge is what puts the report on
148
+ // that entity's page and diagrams, and keeps a report from reading as an orphan.
149
+ if (dn.node.type === TYPE_OBJECT && dn.node.subType === OBJECT_SUBTYPE_REPORT) {
150
+ const to = bound(dn.node, OBJECT_REPORT_ATTR_FROM);
151
+ if (to) addRef({ from: fqn, to, via: OBJECT_REPORT_ATTR_FROM, kind: "report" });
152
+ }
144
153
  const sup = dn.node.superResolved;
145
154
  if (sup) {
146
155
  const supFqn = fqnOf(sup);
@@ -148,18 +157,12 @@ export class LinkGraph {
148
157
  (this._extBy.get(supFqn) ?? this._extBy.set(supFqn, []).get(supFqn)!).push(dn);
149
158
  }
150
159
  } else {
151
- const p = dn.node.attr("payloadRef");
152
- if (typeof p === "string") {
153
- const to = resolveRef(p, dn.pkg);
154
- if (to) addRef({ from: fqn, to, via: "payloadRef", kind: "payload" });
155
- }
160
+ const toPayload = bound(dn.node, TEMPLATE_ATTR_PAYLOAD_REF);
161
+ if (toPayload) addRef({ from: fqn, to: toPayload, via: "payloadRef", kind: "payload" });
156
162
  // A prompt's @responseRef is the response value-object it PRODUCES — the
157
163
  // missing hop of the data-flow pipeline (prompt → response VO → entity).
158
- const r = dn.node.attr("responseRef");
159
- if (typeof r === "string") {
160
- const to = resolveRef(r, dn.pkg);
161
- if (to) addRef({ from: fqn, to, via: "responseRef", kind: "response" });
162
- }
164
+ const toResponse = bound(dn.node, TEMPLATE_ATTR_RESPONSE_REF);
165
+ if (toResponse) addRef({ from: fqn, to: toResponse, via: "responseRef", kind: "response" });
163
166
  }
164
167
  }
165
168
  }
package/src/site.ts CHANGED
@@ -189,6 +189,7 @@ export async function generateSite(opts: SiteOptions): Promise<SiteResult> {
189
189
  const objectTocHtml = (d: ObjectPageData): string => {
190
190
  const sections: { id: string; label: string; present: boolean }[] = [
191
191
  { id: "s-overview", label: "Overview", present: !!d.desc },
192
+ { id: "s-report", label: "Report", present: d.report !== undefined },
192
193
  { id: "s-fields", label: "Fields", present: d.ownFields.length > 0 },
193
194
  { id: "s-indexes", label: "Indexes &amp; keys", present: d.indexes.length > 0 },
194
195
  { id: "s-validators", label: "Validators", present: d.validators.length > 0 },
@@ -196,6 +197,7 @@ export async function generateSite(opts: SiteOptions): Promise<SiteResult> {
196
197
  { id: "s-provenance", label: "Field provenance", present: d.origins.length > 0 },
197
198
  { id: "s-inheritance", label: "Inheritance", present: !!d.inheritanceMermaid },
198
199
  { id: "s-neighborhood", label: "Neighborhood", present: !!d.neighborhoodMermaid },
200
+ { id: "s-reporting", label: "Reporting", present: d.reporting !== undefined },
199
201
  { id: "s-referenced-by", label: "Referenced by", present: d.referencedBy.length > 0 },
200
202
  ];
201
203
  return sections
@@ -11,7 +11,7 @@
11
11
  </div>
12
12
  <form method="dialog" class="modal-backdrop"><button>close</button></form>
13
13
  </dialog>
14
- <footer class="pl-footer text-xs opacity-40 px-10 pb-6">generated {{stamp}} · {{commit}} · metadata-docs</footer>
14
+ <footer class="pl-footer text-xs opacity-40 px-10 pb-6">generated{{#stamp}} {{stamp}}{{/stamp}}{{#commit}} · {{commit}}{{/commit}} · metadata-docs</footer>
15
15
  <script>window.__REL_ROOT__ = "{{relRoot}}";</script>
16
16
  <script src="{{relRoot}}assets/site.js"></script>
17
17
  </body></html>
@@ -8,8 +8,16 @@
8
8
  {{#pkHtml}} · pk {{{pkHtml}}}{{/pkHtml}}{{#generation}} · gen {{generation}}{{/generation}}{{#storageAttrs}} · {{name}} <code>{{value}}</code>{{/storageAttrs}}</div>
9
9
  {{#desc}}<section id="s-overview" class="mt-4"><p class="text-sm leading-relaxed">{{desc}}</p></section>{{/desc}}
10
10
  {{#objectAttrs.length}}<section id="s-attributes" class="mt-4 text-xs"><span class="opacity-50 mr-1">attributes</span>{{#objectAttrs}}<span class="badge badge-soft badge-ghost badge-xs mr-1 font-mono">@{{name}}={{value}}</span>{{/objectAttrs}}</section>{{/objectAttrs.length}}
11
+ {{#report}}
12
+ <section id="s-report" class="mt-6"><h2 class="text-lg font-semibold">Report</h2>
13
+ <div class="font-mono text-xs opacity-70 mt-1">from <a class="link font-mono" href="{{fromHref}}">{{fromName}}</a>{{#viewName}} · view <code>{{viewName}}</code>{{/viewName}}{{#notServed}} · <span class="badge badge-soft badge-warning badge-xs">{{notServed}}</span>{{/notServed}}{{#rowsHtml}} · rows: {{{rowsHtml}}}{{/rowsHtml}}{{#scopeHtml}} · row scope {{{scopeHtml}}}{{/scopeHtml}}</div>
14
+ <div class="overflow-x-auto"><table class="table table-xs"><thead><tr><th>Column</th><th>Type</th><th>Nullable</th><th>Role</th><th>Definition</th></tr></thead><tbody>
15
+ {{#columns}}<tr><td class="font-mono">{{name}}</td><td class="opacity-60">{{type}}</td><td class="opacity-60">{{nullable}}</td><td class="opacity-60">{{role}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
16
+ {{/columns}}
17
+ </tbody></table></div></section>
18
+ {{/report}}
11
19
 
12
- <section id="s-fields" class="mt-6"><h2 class="text-lg font-semibold">Fields</h2>
20
+ {{^report}}<section id="s-fields" class="mt-6"><h2 class="text-lg font-semibold">Fields</h2>
13
21
  <div class="overflow-x-auto"><table class="table table-xs"><thead><tr><th>Field</th><th>Type</th><th></th><th>Description</th></tr></thead><tbody>
14
22
  {{#ownFields}}<tr id="{{anchor}}"><td class="font-mono">{{name}}{{#isArray}}[]{{/isArray}}</td><td class="opacity-60">{{type}}</td><td>{{{badgesHtml}}}</td><td class="text-xs">{{desc}}</td></tr>
15
23
  {{#enumValues.length}}<tr><td></td><td colspan="3" class="pl-4">{{#enumValues}}<span class="badge badge-soft badge-accent badge-xs mr-1">{{value}}{{#deflt}} ★{{/deflt}}</span>{{/enumValues}}</td></tr>{{/enumValues.length}}
@@ -18,7 +26,7 @@
18
26
  </tbody></table></div>
19
27
  {{#inheritedFields.length}}<div class="collapse collapse-arrow bg-base-100 border border-base-300 mt-2"><input type="checkbox"><div class="collapse-title text-sm font-semibold opacity-70">Inherited fields ({{inheritedFields.length}})</div><div class="collapse-content"><table class="table table-xs opacity-80"><tbody>
20
28
  {{#inheritedFields}}<tr id="{{anchor}}"><td class="font-mono">{{name}}{{#isArray}}[]{{/isArray}} <span class="text-[10px] opacity-50">↑ <a class="link" href="{{inheritedFrom.href}}">{{inheritedFrom.name}}</a></span></td><td class="opacity-60">{{type}}</td><td>{{{badgesHtml}}}</td></tr>{{/inheritedFields}}
21
- </tbody></table></div></div>{{/inheritedFields.length}}</section>
29
+ </tbody></table></div></div>{{/inheritedFields.length}}</section>{{/report}}
22
30
 
23
31
  {{#indexes.length}}<section id="s-indexes" class="mt-6"><h2 class="text-lg font-semibold">Indexes &amp; keys</h2>
24
32
  <table class="table table-xs"><thead><tr><th>Name</th><th>Kind</th><th>Fields</th><th>Detail</th></tr></thead><tbody>
@@ -46,6 +54,14 @@
46
54
  <div class="text-[11px] opacity-50 mt-1"><span class="font-mono">▭</span> entity · <span class="font-mono">⬭</span> value object · <span class="font-mono">▱</span> view <span class="opacity-70">(ER boxes: dashed = value, dotted = view)</span></div>
47
55
  {{#neighborhoodMore}}<div class="text-xs opacity-50 mt-1">+{{neighborhoodMore}} more neighbor(s){{#referencedBy.length}} — see <a class="link" href="#s-referenced-by">Referenced by</a>{{/referencedBy.length}}.</div>{{/neighborhoodMore}}</section>{{/neighborhoodMermaid}}
48
56
 
49
- {{#referencedBy.length}}<section id="s-referenced-by" class="mt-6 text-sm"><span class="opacity-50">referenced by</span> {{#referencedBy}}<a class="link font-mono text-xs" href="{{href}}" title="{{via}}">{{name}}</a> {{/referencedBy}}</section>{{/referencedBy.length}}
57
+ {{#reporting}}<section id="s-reporting" class="mt-6"><h2 class="text-lg font-semibold">Reporting</h2>
58
+ <table class="table table-xs"><tbody>
59
+ {{#dimensions}}<tr><td class="text-xs opacity-50">dimension</td><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{kind}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
60
+ {{/dimensions}}{{#measures}}<tr><td class="text-xs opacity-50">measure</td><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{kind}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
61
+ {{/measures}}{{#segments}}<tr><td class="text-xs opacity-50">segment</td><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{kind}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
62
+ {{/segments}}</tbody></table>
63
+ {{#reports.length}}<div class="text-sm mt-1"><span class="opacity-50">reports</span> {{#reports}}<a class="link font-mono text-xs" href="{{href}}">{{name}}</a> {{/reports}}</div>{{/reports.length}}</section>
64
+
65
+ {{/reporting}}{{#referencedBy.length}}<section id="s-referenced-by" class="mt-6 text-sm"><span class="opacity-50">referenced by</span> {{#referencedBy}}<a class="link font-mono text-xs" href="{{href}}" title="{{via}}">{{name}}</a> {{/referencedBy}}</section>{{/referencedBy.length}}
50
66
  {{#usedByTemplates.length}}<div class="text-sm mt-1"><span class="opacity-50">used by templates</span> {{#usedByTemplates}}<a class="link font-mono text-xs" href="{{href}}">{{name}}</a> {{/usedByTemplates}}</div>{{/usedByTemplates.length}}
51
67
  <div class="text-[10px] opacity-40 mt-6 font-mono">{{sourceFile}}</div>