@metaobjectsdev/metadata 1.0.13 → 1.1.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.
Files changed (233) hide show
  1. package/README.md +2 -0
  2. package/dist/attr-schema-validate.d.ts.map +1 -1
  3. package/dist/attr-schema-validate.js +9 -12
  4. package/dist/attr-schema-validate.js.map +1 -1
  5. package/dist/constants.d.ts +1 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +1 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/core/attr/meta-attr-filter.d.ts.map +1 -1
  10. package/dist/core/attr/meta-attr-filter.js +8 -2
  11. package/dist/core/attr/meta-attr-filter.js.map +1 -1
  12. package/dist/core/attr/meta-attr.d.ts +14 -0
  13. package/dist/core/attr/meta-attr.d.ts.map +1 -1
  14. package/dist/core/attr/meta-attr.js +23 -1
  15. package/dist/core/attr/meta-attr.js.map +1 -1
  16. package/dist/core/field/shared-enum.d.ts +21 -0
  17. package/dist/core/field/shared-enum.d.ts.map +1 -0
  18. package/dist/core/field/shared-enum.js +49 -0
  19. package/dist/core/field/shared-enum.js.map +1 -0
  20. package/dist/core/identity/meta-identity.d.ts +1 -1
  21. package/dist/core/identity/meta-identity.d.ts.map +1 -1
  22. package/dist/core/identity/meta-identity.js +8 -13
  23. package/dist/core/identity/meta-identity.js.map +1 -1
  24. package/dist/core/index.d.ts +2 -0
  25. package/dist/core/index.d.ts.map +1 -1
  26. package/dist/core/index.js +3 -0
  27. package/dist/core/index.js.map +1 -1
  28. package/dist/core/object/object-constants.d.ts +14 -1
  29. package/dist/core/object/object-constants.d.ts.map +1 -1
  30. package/dist/core/object/object-constants.js +18 -0
  31. package/dist/core/object/object-constants.js.map +1 -1
  32. package/dist/core/object/object-definition.embedded.d.ts.map +1 -1
  33. package/dist/core/object/object-definition.embedded.js +80 -0
  34. package/dist/core/object/object-definition.embedded.js.map +1 -1
  35. package/dist/core/relationship/derive-m2m-fields.d.ts +1 -1
  36. package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
  37. package/dist/core/relationship/derive-m2m-fields.js +15 -44
  38. package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
  39. package/dist/core/relationship/find-reference.d.ts.map +1 -1
  40. package/dist/core/relationship/find-reference.js +4 -2
  41. package/dist/core/relationship/find-reference.js.map +1 -1
  42. package/dist/core/relationship/resolve-relationship-reference.d.ts +6 -5
  43. package/dist/core/relationship/resolve-relationship-reference.d.ts.map +1 -1
  44. package/dist/core/relationship/resolve-relationship-reference.js +9 -7
  45. package/dist/core/relationship/resolve-relationship-reference.js.map +1 -1
  46. package/dist/core/reporting/meta-dimension.d.ts +13 -0
  47. package/dist/core/reporting/meta-dimension.d.ts.map +1 -0
  48. package/dist/core/reporting/meta-dimension.js +30 -0
  49. package/dist/core/reporting/meta-dimension.js.map +1 -0
  50. package/dist/core/reporting/meta-measure.d.ts +24 -0
  51. package/dist/core/reporting/meta-measure.d.ts.map +1 -0
  52. package/dist/core/reporting/meta-measure.js +54 -0
  53. package/dist/core/reporting/meta-measure.js.map +1 -0
  54. package/dist/core/reporting/meta-segment.d.ts +6 -0
  55. package/dist/core/reporting/meta-segment.d.ts.map +1 -0
  56. package/dist/core/reporting/meta-segment.js +13 -0
  57. package/dist/core/reporting/meta-segment.js.map +1 -0
  58. package/dist/core/reporting/report-accessors.d.ts +27 -0
  59. package/dist/core/reporting/report-accessors.d.ts.map +1 -0
  60. package/dist/core/reporting/report-accessors.js +56 -0
  61. package/dist/core/reporting/report-accessors.js.map +1 -0
  62. package/dist/core/reporting/report-describe.d.ts +59 -0
  63. package/dist/core/reporting/report-describe.d.ts.map +1 -0
  64. package/dist/core/reporting/report-describe.js +182 -0
  65. package/dist/core/reporting/report-describe.js.map +1 -0
  66. package/dist/core/reporting/report-read-model.d.ts +34 -0
  67. package/dist/core/reporting/report-read-model.d.ts.map +1 -0
  68. package/dist/core/reporting/report-read-model.js +165 -0
  69. package/dist/core/reporting/report-read-model.js.map +1 -0
  70. package/dist/core/reporting/report-shape.d.ts +103 -0
  71. package/dist/core/reporting/report-shape.d.ts.map +1 -0
  72. package/dist/core/reporting/report-shape.js +398 -0
  73. package/dist/core/reporting/report-shape.js.map +1 -0
  74. package/dist/core/reporting/reporting-constants.d.ts +65 -0
  75. package/dist/core/reporting/reporting-constants.d.ts.map +1 -0
  76. package/dist/core/reporting/reporting-constants.js +84 -0
  77. package/dist/core/reporting/reporting-constants.js.map +1 -0
  78. package/dist/core/reporting/reporting-definition.embedded.d.ts +3 -0
  79. package/dist/core/reporting/reporting-definition.embedded.d.ts.map +1 -0
  80. package/dist/core/reporting/reporting-definition.embedded.js +185 -0
  81. package/dist/core/reporting/reporting-definition.embedded.js.map +1 -0
  82. package/dist/core/requirement/resolve-claim.d.ts +8 -0
  83. package/dist/core/requirement/resolve-claim.d.ts.map +1 -1
  84. package/dist/core/requirement/resolve-claim.js +21 -0
  85. package/dist/core/requirement/resolve-claim.js.map +1 -1
  86. package/dist/core/yaml-desugar.d.ts.map +1 -1
  87. package/dist/core/yaml-desugar.js +12 -82
  88. package/dist/core/yaml-desugar.js.map +1 -1
  89. package/dist/core-types.d.ts.map +1 -1
  90. package/dist/core-types.js +28 -1
  91. package/dist/core-types.js.map +1 -1
  92. package/dist/docs-ref.d.ts +19 -0
  93. package/dist/docs-ref.d.ts.map +1 -0
  94. package/dist/docs-ref.js +37 -0
  95. package/dist/docs-ref.js.map +1 -0
  96. package/dist/errors.d.ts +1 -1
  97. package/dist/errors.d.ts.map +1 -1
  98. package/dist/errors.js +17 -6
  99. package/dist/errors.js.map +1 -1
  100. package/dist/fmt.d.ts.map +1 -1
  101. package/dist/fmt.js +3 -1
  102. package/dist/fmt.js.map +1 -1
  103. package/dist/index.d.ts +15 -4
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +18 -4
  106. package/dist/index.js.map +1 -1
  107. package/dist/loader/declared-duplicate-fields.d.ts +19 -0
  108. package/dist/loader/declared-duplicate-fields.d.ts.map +1 -0
  109. package/dist/loader/declared-duplicate-fields.js +97 -0
  110. package/dist/loader/declared-duplicate-fields.js.map +1 -0
  111. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  112. package/dist/loader/meta-data-loader.js +18 -1
  113. package/dist/loader/meta-data-loader.js.map +1 -1
  114. package/dist/loader/reporting-validation.d.ts +11 -0
  115. package/dist/loader/reporting-validation.d.ts.map +1 -0
  116. package/dist/loader/reporting-validation.js +883 -0
  117. package/dist/loader/reporting-validation.js.map +1 -0
  118. package/dist/loader/resolve-references.d.ts +8 -0
  119. package/dist/loader/resolve-references.d.ts.map +1 -0
  120. package/dist/loader/resolve-references.js +132 -0
  121. package/dist/loader/resolve-references.js.map +1 -0
  122. package/dist/loader/validation-passes.d.ts.map +1 -1
  123. package/dist/loader/validation-passes.js +14 -9
  124. package/dist/loader/validation-passes.js.map +1 -1
  125. package/dist/loader/validation-registry.d.ts.map +1 -1
  126. package/dist/loader/validation-registry.js +33 -10
  127. package/dist/loader/validation-registry.js.map +1 -1
  128. package/dist/metamodel-docs/index.js +3 -3
  129. package/dist/metamodel-docs/index.js.map +1 -1
  130. package/dist/metamodel-docs/render.js +4 -4
  131. package/dist/metamodel-docs/render.js.map +1 -1
  132. package/dist/naming-refs.d.ts +102 -50
  133. package/dist/naming-refs.d.ts.map +1 -1
  134. package/dist/naming-refs.js +223 -97
  135. package/dist/naming-refs.js.map +1 -1
  136. package/dist/naming.d.ts +42 -7
  137. package/dist/naming.d.ts.map +1 -1
  138. package/dist/naming.js +43 -5
  139. package/dist/naming.js.map +1 -1
  140. package/dist/object-serializer.d.ts.map +1 -1
  141. package/dist/object-serializer.js +6 -9
  142. package/dist/object-serializer.js.map +1 -1
  143. package/dist/parser-core.d.ts.map +1 -1
  144. package/dist/parser-core.js +18 -48
  145. package/dist/parser-core.js.map +1 -1
  146. package/dist/persistence/db/db-definition.embedded.js +2 -2
  147. package/dist/persistence/db/db-definition.embedded.js.map +1 -1
  148. package/dist/persistence/source/meta-source.d.ts +5 -3
  149. package/dist/persistence/source/meta-source.d.ts.map +1 -1
  150. package/dist/persistence/source/meta-source.js +8 -6
  151. package/dist/persistence/source/meta-source.js.map +1 -1
  152. package/dist/registry-manifest-exclusions.js +2 -2
  153. package/dist/registry-manifest-exclusions.js.map +1 -1
  154. package/dist/registry-manifest.d.ts +1 -1
  155. package/dist/registry-manifest.js +1 -1
  156. package/dist/release-docs.d.ts +11 -0
  157. package/dist/release-docs.d.ts.map +1 -0
  158. package/dist/release-docs.js +48 -0
  159. package/dist/release-docs.js.map +1 -0
  160. package/dist/serializer-json.d.ts +15 -1
  161. package/dist/serializer-json.d.ts.map +1 -1
  162. package/dist/serializer-json.js +55 -17
  163. package/dist/serializer-json.js.map +1 -1
  164. package/dist/shared/base-types.d.ts +7 -1
  165. package/dist/shared/base-types.d.ts.map +1 -1
  166. package/dist/shared/base-types.js +9 -0
  167. package/dist/shared/base-types.js.map +1 -1
  168. package/dist/shared/meta-data.d.ts +10 -0
  169. package/dist/shared/meta-data.d.ts.map +1 -1
  170. package/dist/shared/meta-data.js +22 -1
  171. package/dist/shared/meta-data.js.map +1 -1
  172. package/dist/shared/meta-root.d.ts +13 -1
  173. package/dist/shared/meta-root.d.ts.map +1 -1
  174. package/dist/shared/meta-root.js +19 -1
  175. package/dist/shared/meta-root.js.map +1 -1
  176. package/dist/super-resolve.d.ts +4 -1
  177. package/dist/super-resolve.d.ts.map +1 -1
  178. package/dist/super-resolve.js +44 -65
  179. package/dist/super-resolve.js.map +1 -1
  180. package/dist/vocabulary-catalog.js +2 -2
  181. package/dist/vocabulary-catalog.js.map +1 -1
  182. package/package.json +6 -1
  183. package/src/attr-schema-validate.ts +9 -13
  184. package/src/constants.ts +1 -0
  185. package/src/core/attr/meta-attr-filter.ts +8 -1
  186. package/src/core/attr/meta-attr.ts +24 -1
  187. package/src/core/field/shared-enum.ts +53 -0
  188. package/src/core/identity/meta-identity.ts +9 -12
  189. package/src/core/index.ts +5 -0
  190. package/src/core/object/object-constants.ts +19 -0
  191. package/src/core/object/object-definition.embedded.ts +80 -0
  192. package/src/core/relationship/derive-m2m-fields.ts +15 -48
  193. package/src/core/relationship/find-reference.ts +4 -2
  194. package/src/core/relationship/resolve-relationship-reference.ts +11 -9
  195. package/src/core/reporting/meta-dimension.ts +41 -0
  196. package/src/core/reporting/meta-measure.ts +75 -0
  197. package/src/core/reporting/meta-segment.ts +14 -0
  198. package/src/core/reporting/report-accessors.ts +73 -0
  199. package/src/core/reporting/report-describe.ts +206 -0
  200. package/src/core/reporting/report-read-model.ts +177 -0
  201. package/src/core/reporting/report-shape.ts +493 -0
  202. package/src/core/reporting/reporting-constants.ts +104 -0
  203. package/src/core/reporting/reporting-definition.embedded.ts +192 -0
  204. package/src/core/requirement/resolve-claim.ts +20 -0
  205. package/src/core/yaml-desugar.ts +9 -89
  206. package/src/core-types.ts +41 -0
  207. package/src/docs-ref.ts +41 -0
  208. package/src/errors.ts +17 -6
  209. package/src/fmt.ts +3 -1
  210. package/src/index.ts +72 -4
  211. package/src/loader/declared-duplicate-fields.ts +114 -0
  212. package/src/loader/meta-data-loader.ts +25 -1
  213. package/src/loader/reporting-validation.ts +1137 -0
  214. package/src/loader/resolve-references.ts +144 -0
  215. package/src/loader/validation-passes.ts +13 -9
  216. package/src/loader/validation-registry.ts +37 -9
  217. package/src/metamodel-docs/index.ts +3 -3
  218. package/src/metamodel-docs/render.ts +4 -4
  219. package/src/naming-refs.ts +237 -94
  220. package/src/naming.ts +75 -11
  221. package/src/object-serializer.ts +7 -9
  222. package/src/parser-core.ts +19 -56
  223. package/src/persistence/db/db-definition.embedded.ts +2 -2
  224. package/src/persistence/source/meta-source.ts +8 -6
  225. package/src/registry-manifest-exclusions.ts +2 -2
  226. package/src/registry-manifest.ts +1 -1
  227. package/src/release-docs.ts +48 -0
  228. package/src/serializer-json.ts +79 -12
  229. package/src/shared/base-types.ts +9 -0
  230. package/src/shared/meta-data.ts +22 -1
  231. package/src/shared/meta-root.ts +20 -1
  232. package/src/super-resolve.ts +49 -63
  233. package/src/vocabulary-catalog.ts +2 -2
@@ -0,0 +1,206 @@
1
+ // The reporting vocabulary in words (FR-044): what a dimension groups by, what a measure
2
+ // computes, which rows a segment or report is scoped to, and why a report is not served.
3
+ //
4
+ // One home for the wording. The docs generators print these sentences on the markdown model pages
5
+ // (codegen-ts) and on the HTML site (docs-site); both read them from here, so the two
6
+ // surfaces cannot say different things about the same node.
7
+ //
8
+ // Every function returns PLAIN TEXT whose identifiers sit in single-backtick spans
9
+ // ("sum of `Invoice.amountCents` where segment `paid`"). A caller turns the spans into
10
+ // its own markup. Nothing here derives SQL: a description says what a column means, never
11
+ // how the view computes it.
12
+ //
13
+ // Browser-safe: no Node-only import.
14
+
15
+ import type { MetaData } from "../../shared/meta-data.js";
16
+ import type { MetaRoot } from "../../shared/meta-root.js";
17
+ import { TYPE_DIMENSION, TYPE_MEASURE, TYPE_SEGMENT } from "../../shared/base-types.js";
18
+ import { AGG_ALL } from "../../persistence/origin/origin-constants.js";
19
+ import type { MetaObject } from "../object/meta-object.js";
20
+ import { OBJECT_REPORT_ATTR_FILTER, OBJECT_REPORT_ATTR_SEGMENT, OBJECT_REPORT_ATTR_SPINE } from "../object/object-constants.js";
21
+ import {
22
+ DIMENSION_SUBTYPE_TIME,
23
+ MEASURE_SUBTYPE_RATIO,
24
+ REPORTING_ATTR_AGG,
25
+ REPORTING_ATTR_DEFAULT,
26
+ REPORTING_ATTR_DENOMINATOR,
27
+ REPORTING_ATTR_DISTINCT,
28
+ REPORTING_ATTR_FILTER,
29
+ REPORTING_ATTR_GRAINS,
30
+ REPORTING_ATTR_NUMERATOR,
31
+ REPORTING_ATTR_OF,
32
+ REPORTING_ATTR_SEGMENT,
33
+ REPORTING_ATTR_VIA,
34
+ } from "./reporting-constants.js";
35
+ import { reportSpine } from "./report-accessors.js";
36
+ import { reportReadSource } from "./report-read-model.js";
37
+ import { reportSpineEntity, type ReportField, type ReportShape } from "./report-shape.js";
38
+
39
+ /** Inline code. A backtick cannot sit inside a single-backtick span, so it is dropped. */
40
+ function tick(text: string): string {
41
+ return `\`${text.replace(/`/g, "")}\``;
42
+ }
43
+
44
+ function stringList(v: unknown): string[] {
45
+ if (Array.isArray(v)) return v.filter((x): x is string => typeof x === "string");
46
+ return typeof v === "string" ? [v] : [];
47
+ }
48
+
49
+ /** A row-scope filter as the loader holds it: compact JSON in declared order, in a code span. */
50
+ export function describeFilter(filter: unknown): string {
51
+ return tick(JSON.stringify(filter ?? {}));
52
+ }
53
+
54
+ /**
55
+ * "segment `paid` and filter `{…}`": the rows a measure or a report is scoped to, or
56
+ * undefined when it declares neither. The two combine by AND, as the lowering combines them.
57
+ */
58
+ export function describeRowScope(segment: unknown, filter: unknown): string | undefined {
59
+ const parts: string[] = [];
60
+ if (typeof segment === "string" && segment !== "") parts.push(`segment ${tick(segment)}`);
61
+ if (filter !== undefined && filter !== null) parts.push(`filter ${describeFilter(filter)}`);
62
+ return parts.length > 0 ? parts.join(" and ") : undefined;
63
+ }
64
+
65
+ /** "`Program.title` via `Purchase.program`": the column a dimension groups by. */
66
+ function dimensionColumn(dim: MetaData): string {
67
+ // ADR-0039: resolving reads, here and in every describer below, so a node that
68
+ // `extends` another is described by its effective attributes. References are
69
+ // printed as the author wrote them (ADR-0059), not as the loader resolved them.
70
+ const of = dim.authoredAttr(REPORTING_ATTR_OF);
71
+ const via = dim.authoredAttr(REPORTING_ATTR_VIA);
72
+ const column = tick(typeof of === "string" ? of : "");
73
+ return typeof via === "string" && via !== "" ? `${column} via ${tick(via)}` : column;
74
+ }
75
+
76
+ /** A dimension as its entity declares it: the column, and for a time dimension its grains. */
77
+ export function describeDimension(dim: MetaData): string {
78
+ const column = dimensionColumn(dim);
79
+ if (dim.subType !== DIMENSION_SUBTYPE_TIME) return column;
80
+ return `${column}; grains: ${stringList(dim.attr(REPORTING_ATTR_GRAINS)).join(", ")}`;
81
+ }
82
+
83
+ /**
84
+ * A dimension as ONE report column. A time dimension is truncated to the grain the report
85
+ * picked; reports are UTC only (there is no time-zone vocabulary). After a `via` clause
86
+ * the truncation is set off by a comma, so it reads as applying to the column and not to
87
+ * the relationship path.
88
+ */
89
+ export function describeDimensionColumn(dim: MetaData, grain?: string): string {
90
+ const column = dimensionColumn(dim);
91
+ if (grain === undefined || grain === "") return column;
92
+ const joiner = column.includes(" via ") ? "," : "";
93
+ return `${column}${joiner} truncated to ${grain}, UTC`;
94
+ }
95
+
96
+ /**
97
+ * "; `0` when there is nothing to aggregate": the clause a measure's `@default` adds (R9), or
98
+ * "" when it declares none. The declared integer is printed as written.
99
+ */
100
+ function defaultClause(measure: MetaData): string {
101
+ const declared = measure.attr(REPORTING_ATTR_DEFAULT);
102
+ return typeof declared === "number" ? `; ${tick(String(declared))} when there is nothing to aggregate` : "";
103
+ }
104
+
105
+ /**
106
+ * A measure in words: the aggregate and its row scope, or the ratio and its null rule. A
107
+ * `@default` ends either with the value it reads when there is nothing to aggregate; a
108
+ * defaulted ratio is never null, so its null rule gives way to that clause.
109
+ */
110
+ export function describeMeasure(measure: MetaData): string {
111
+ const fallback = defaultClause(measure);
112
+ if (measure.subType === MEASURE_SUBTYPE_RATIO) {
113
+ const numerator = measure.attr(REPORTING_ATTR_NUMERATOR);
114
+ const denominator = measure.attr(REPORTING_ATTR_DENOMINATOR);
115
+ const quotient = `${tick(String(numerator ?? ""))} / ${tick(String(denominator ?? ""))}`;
116
+ return fallback !== "" ? `${quotient}${fallback}` : `${quotient}, null when the denominator is 0`;
117
+ }
118
+ const columns = stringList(measure.authoredAttr(REPORTING_ATTR_OF)).map(tick);
119
+ const scope = describeRowScope(measure.attr(REPORTING_ATTR_SEGMENT), measure.attr(REPORTING_ATTR_FILTER));
120
+ if (columns.length === 0) {
121
+ // @agg any / all with no @of (loader rule M0): the row condition is the quantified predicate.
122
+ const quantifier = measure.attr(REPORTING_ATTR_AGG) === AGG_ALL ? "every row" : "some row";
123
+ return `true when ${quantifier} has ${scope ?? ""}${fallback}`;
124
+ }
125
+ const of = columns.length === 1 ? columns[0]! : `(${columns.join(", ")})`;
126
+ const distinct = measure.attr(REPORTING_ATTR_DISTINCT) === true ? "distinct " : "";
127
+ return `${String(measure.attr(REPORTING_ATTR_AGG) ?? "")} of ${distinct}${of}${scope !== undefined ? ` where ${scope}` : ""}${fallback}`;
128
+ }
129
+
130
+ /** A segment in words: its filter. */
131
+ export function describeSegment(segment: MetaData): string {
132
+ return describeFilter(segment.attr(REPORTING_ATTR_FILTER));
133
+ }
134
+
135
+ /** One derived report column's definition, from the dimension or measure it comes from. */
136
+ export function describeReportField(field: ReportField): string {
137
+ if (field.dimension !== undefined) return describeDimensionColumn(field.dimension, field.grain);
138
+ return field.measure !== undefined ? describeMeasure(field.measure) : "";
139
+ }
140
+
141
+ /**
142
+ * Where a report with `@spine` takes its rows (R8): "one row per distinct dimension tuple
143
+ * among the rows of `Program`, reached by `Purchase.program`, including those no `Purchase`
144
+ * refers to". Undefined for a report without `@spine`, whose rows are the groups of its
145
+ * `@from` rows and need no sentence. The spine is printed as declared.
146
+ */
147
+ export function describeReportRows(shape: ReportShape, root: MetaRoot): string | undefined {
148
+ const entity = reportSpineEntity(shape.report, shape.from, root);
149
+ // Printed as the author wrote it (ADR-0059).
150
+ const authored = reportSpine(shape.report) !== undefined ? shape.report.authoredAttr(OBJECT_REPORT_ATTR_SPINE) : undefined;
151
+ const spine = typeof authored === "string" ? authored : undefined;
152
+ if (spine === undefined || entity === undefined) return undefined;
153
+ return (
154
+ `one row per distinct dimension tuple among the rows of ${tick(entity.name)}, reached by ${tick(spine)}, ` +
155
+ `including those no ${tick(shape.from.name)} refers to`
156
+ );
157
+ }
158
+
159
+ /**
160
+ * A report's row scope ({@link describeRowScope} over its `@segment` and `@filter`), or
161
+ * undefined when it declares neither. Under `@spine` the scope chooses which rows of `@from`
162
+ * are aggregated and never removes a row of the report, so it reads "aggregating only
163
+ * segment `paid`".
164
+ */
165
+ export function describeReportRowScope(report: MetaObject): string | undefined {
166
+ // ADR-0039: resolving.
167
+ const scope = describeRowScope(report.attr(OBJECT_REPORT_ATTR_SEGMENT), report.attr(OBJECT_REPORT_ATTR_FILTER));
168
+ if (scope === undefined) return undefined;
169
+ return reportSpine(report) !== undefined ? `aggregating only ${scope}` : scope;
170
+ }
171
+
172
+ /** The neutral logical type of a derived column: its Table B subtype, `[]` for an array. */
173
+ export function reportFieldTypeName(field: ReportField): string {
174
+ // ADR-0039: resolvedIsArray() is the resolving read of the native array flag.
175
+ return field.typeSource?.resolvedIsArray() === true ? `${field.subType}[]` : field.subType;
176
+ }
177
+
178
+ /**
179
+ * Why a report is not served, or undefined when it is. The loader refuses an abstract
180
+ * report (R11) and a read source whose `@kind` is not `view` (R10), so the one way a loaded
181
+ * report goes unserved is declaring no read source at all (Plan 3 Table A).
182
+ */
183
+ export function reportNotServedReason(report: MetaObject): string | undefined {
184
+ return reportReadSource(report) === undefined ? "Not served: declares no view source" : undefined;
185
+ }
186
+
187
+ const DIMENSION_ATTRS = [REPORTING_ATTR_OF, REPORTING_ATTR_VIA] as const;
188
+ const TIME_DIMENSION_ATTRS = [...DIMENSION_ATTRS, REPORTING_ATTR_GRAINS] as const;
189
+ const AGGREGATE_ATTRS = [
190
+ REPORTING_ATTR_AGG, REPORTING_ATTR_OF, REPORTING_ATTR_DISTINCT, REPORTING_ATTR_SEGMENT, REPORTING_ATTR_FILTER,
191
+ REPORTING_ATTR_DEFAULT,
192
+ ] as const;
193
+ const RATIO_ATTRS = [REPORTING_ATTR_NUMERATOR, REPORTING_ATTR_DENOMINATOR, REPORTING_ATTR_DEFAULT] as const;
194
+ const SEGMENT_ATTRS = [REPORTING_ATTR_FILTER] as const;
195
+
196
+ /**
197
+ * The attributes the describer for `node` reads, so a documentation coverage audit can
198
+ * mark exactly those as rendered and no others. Kept beside the describers: a describer
199
+ * that starts reading a new attribute adds it here. Empty for any other node.
200
+ */
201
+ export function reportingDescribedAttrs(node: MetaData): readonly string[] {
202
+ if (node.type === TYPE_DIMENSION) return node.subType === DIMENSION_SUBTYPE_TIME ? TIME_DIMENSION_ATTRS : DIMENSION_ATTRS;
203
+ if (node.type === TYPE_MEASURE) return node.subType === MEASURE_SUBTYPE_RATIO ? RATIO_ATTRS : AGGREGATE_ATTRS;
204
+ if (node.type === TYPE_SEGMENT) return SEGMENT_ATTRS;
205
+ return [];
206
+ }
@@ -0,0 +1,177 @@
1
+ // A report's READ MODEL (FR-044): a detached object carrying one real `field.*`
2
+ // child per derived field (Table B) and a copy of the report's own read-only source.
3
+ //
4
+ // WHY IT EXISTS
5
+ //
6
+ // An `object.report` declares no fields: its read shape is derived from its
7
+ // dimensions and measures. A metadata-driven runtime walks an object's field
8
+ // children in a dozen places (column list, filter and sort resolution, the name
9
+ // map, every read coercion). Rather than teach each of them what a report is, the
10
+ // runtime reads a report through this model and sees ordinary fields.
11
+ //
12
+ // WHY IT IS DETACHED
13
+ //
14
+ // The model is never added to the root: it has no parent, `root.objects()` does
15
+ // not list it, and the canonical serializer, `fmt`, codegen and every other tree
16
+ // walker never see it. Nothing in the loaded tree is mutated to build it; in
17
+ // particular the report's own source node is COPIED, not re-parented
18
+ // (`addChild` rewrites the child's parent). The nodes are constructed directly,
19
+ // not through the registry, so the sealed registry is not involved and no
20
+ // vocabulary is added: every node is an already-registered `type.subType`.
21
+ //
22
+ // It keeps the report's name, package and `object.report` subtype, so a consumer
23
+ // holding it can still tell it is a report (no identity, read-only).
24
+
25
+ import { TypeId } from "../../registry.js";
26
+ import { TYPE_FIELD } from "../../shared/base-types.js";
27
+ import type { MetaRoot } from "../../shared/meta-root.js";
28
+ import { isReadOnlySource } from "../../shared/node-guards.js";
29
+ import { MetaSource } from "../../persistence/source/meta-source.js";
30
+ import { SOURCE_ATTR_ROLE, SOURCE_ROLE_PRIMARY } from "../../persistence/source/source-constants.js";
31
+ import { FIELD_ATTR_DB_COLUMN_TYPE, FIELD_ATTR_LOCAL_TIME } from "../../persistence/db/db-constants.js";
32
+ import { MetaObject } from "../object/meta-object.js";
33
+ import { MetaField } from "../field/meta-field.js";
34
+ import {
35
+ FIELD_ATTR_CURRENCY,
36
+ FIELD_ATTR_FILTERABLE,
37
+ FIELD_ATTR_INT_VALUE_MAP,
38
+ FIELD_ATTR_MAX_LENGTH,
39
+ FIELD_ATTR_PRECISION,
40
+ FIELD_ATTR_REQUIRED,
41
+ FIELD_ATTR_SCALE,
42
+ FIELD_ATTR_STORAGE,
43
+ FIELD_ATTR_VALUES,
44
+ FIELD_SUBTYPE_MAP,
45
+ } from "../field/field-constants.js";
46
+ import { opsForField } from "../query/query-constants.js";
47
+ import { reportShape, type ReportField } from "./report-shape.js";
48
+
49
+ /**
50
+ * Table B: the type-shaping attrs a derived field carries from its `typeSource`,
51
+ * read with the RESOLVING accessor (ADR-0039) so a value the `@of` field inherits
52
+ * through `extends` is carried too. `@dbColumnType` and `isArray` are handled
53
+ * separately below. Nothing else is carried: no `@column`, `@required`,
54
+ * `@default`, validators or views.
55
+ */
56
+ const CARRIED_ATTRS = [
57
+ FIELD_ATTR_CURRENCY,
58
+ FIELD_ATTR_VALUES,
59
+ FIELD_ATTR_INT_VALUE_MAP,
60
+ FIELD_ATTR_MAX_LENGTH,
61
+ FIELD_ATTR_PRECISION,
62
+ FIELD_ATTR_SCALE,
63
+ FIELD_ATTR_LOCAL_TIME,
64
+ FIELD_ATTR_STORAGE,
65
+ ] as const;
66
+
67
+ /** True for a derived field whose source field is an array or a `field.map`. */
68
+ function isArrayOrMap(field: MetaField): boolean {
69
+ return field.resolvedIsArray() || field.subType === FIELD_SUBTYPE_MAP;
70
+ }
71
+
72
+ function derivedField(f: ReportField): MetaField {
73
+ const field = new MetaField(new TypeId(TYPE_FIELD, f.subType), f.name);
74
+ // From the derived shape, never from the type source: a `min` of a required column
75
+ // is still nullable, and a dimension reached by `@via` is nullable.
76
+ field.setAttr(FIELD_ATTR_REQUIRED, f.required);
77
+ const src = f.typeSource;
78
+ if (src !== undefined) {
79
+ for (const name of CARRIED_ATTRS) {
80
+ const value = src.attr(name);
81
+ if (value !== undefined) field.setAttr(name, value);
82
+ }
83
+ // ADR-0039: own — `@dbColumnType` is the one deliberately own-only attr (a
84
+ // physical column-type override is never inherited), and every consumer reads
85
+ // it with `ownAttr`. So it is read own from the type source and set OWN here:
86
+ // the derived field carries exactly what the `@of` field itself declares, and
87
+ // nothing its supers declare. A field that `extends` another does not get the
88
+ // parent's `@dbColumnType` either, so this matches how a projection field
89
+ // would see it.
90
+ const dbColumnType = src.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE);
91
+ if (dbColumnType !== undefined) field.setAttr(FIELD_ATTR_DB_COLUMN_TYPE, dbColumnType);
92
+ // `isArray` is a native flag, not an attr; resolvedIsArray() is its resolving read.
93
+ if (src.resolvedIsArray()) field.setIsArray(true);
94
+ }
95
+ // Table C (Plan 3): a report author has no node to put @filterable on, so every
96
+ // derived field that has a filter band is filterable, and (the sort allowlist follows
97
+ // `@filterable`) sortable. Set on this detached model only; no vocabulary is added and the
98
+ // declared tree is not touched. EXCEPT an array or a map: an array column cannot be compared
99
+ // with a scalar (`text[] = text` is a Postgres error), and the entity rule already refuses
100
+ // `@filterable` on an array field, so a derived field whose source is either is neither
101
+ // filterable nor sortable, in every port.
102
+ if (!isArrayOrMap(field) && opsForField(field).length > 0) field.setAttr(FIELD_ATTR_FILTERABLE, true);
103
+ return field;
104
+ }
105
+
106
+ /**
107
+ * The source a report is READ from: its own read-only source with `@role: primary`,
108
+ * else its first own read-only source. Undefined when it declares none (Table A:
109
+ * not lowered, not served).
110
+ *
111
+ * This is the rule that NAMES the lowered view — `viewName` / `projectionViewSource`
112
+ * in codegen-ts's `projection/extract-view-spec.ts`, reached for a report through
113
+ * `projectionViewName`. It is restated here because the metadata package cannot
114
+ * depend on a codegen package; the two must stay the same rule, or the runtime
115
+ * reads a relation the lowering did not create.
116
+ *
117
+ * What the loader permits, measured: a report may declare several read-only sources
118
+ * (a `@role: replica` view beside its primary view loads clean, in either order);
119
+ * `@role` defaults to `primary`; a report whose sources include no primary is
120
+ * `ERR_SOURCE_NO_PRIMARY` and a writable source on a report is refused. So for every
121
+ * model that loads, the primary branch fires. The first-read-only fallback covers a
122
+ * tree built in code, and keeps this rule identical to the lowering's.
123
+ */
124
+ export function reportReadSource(report: MetaObject): MetaSource | undefined {
125
+ // ADR-0039: own — source classification reads the sources the report declares
126
+ // ITSELF, exactly as the lowering's `viewName` does.
127
+ const readOnly = report.ownChildren().filter(isReadOnlySource);
128
+ return readOnly.find((s) => s.role === SOURCE_ROLE_PRIMARY) ?? readOnly[0];
129
+ }
130
+
131
+ /**
132
+ * A detached copy of a source node: same `type.subType`, name and effective attrs,
133
+ * and nothing else (attrs only — the loaded node is never re-parented).
134
+ *
135
+ * The copy is the model's ONLY source, and it is pinned to `@role: primary`: the
136
+ * runtime resolves an object's table through `primaryRdbSource`, which considers
137
+ * primary sources only, so this is what makes the read land on the selected
138
+ * source's physical name rather than on a default table name nobody declared.
139
+ */
140
+ function copySource(source: MetaSource): MetaSource {
141
+ const copy = new MetaSource(source.typeId, source.name);
142
+ // ADR-0039: resolving — the copy carries the source's effective configuration
143
+ // (@kind, the physical-name alias, @schema, @unmanaged, @sql).
144
+ for (const [name, value] of source.attrs()) copy.setAttr(name, value);
145
+ copy.setAttr(SOURCE_ATTR_ROLE, SOURCE_ROLE_PRIMARY);
146
+ return copy;
147
+ }
148
+
149
+ const READ_MODELS = new WeakMap<MetaObject, MetaObject>();
150
+
151
+ /**
152
+ * The read model of an `object.report`: one field per Table B row, in Table B
153
+ * order, plus a copy of the source the report is read from (see
154
+ * `reportReadSource`) when it declares one (Table A). A sourceless report yields a model with no source: it has a shape
155
+ * and no view, and the caller decides what that means (the runtime refuses to
156
+ * serve it).
157
+ *
158
+ * Cached per report node; the model is frozen. Throws what `reportShape` throws
159
+ * when a reference does not resolve.
160
+ */
161
+ export function reportReadModel(report: MetaObject, root: MetaRoot): MetaObject {
162
+ const cached = READ_MODELS.get(report);
163
+ if (cached !== undefined) return cached;
164
+
165
+ const shape = reportShape(report, root);
166
+ const model = new MetaObject(report.typeId, report.name);
167
+ if (report.package !== undefined) model.setPackage(report.package);
168
+ if (report.fileDefaultPackage !== undefined) model.setFileDefaultPackage(report.fileDefaultPackage);
169
+ for (const f of shape.fields) model.addChild(derivedField(f));
170
+
171
+ const source = reportReadSource(report);
172
+ if (source !== undefined) model.addChild(copySource(source));
173
+
174
+ model.freeze();
175
+ READ_MODELS.set(report, model);
176
+ return model;
177
+ }