@memoarchitect/tools 0.7.2 → 0.8.7

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 (160) hide show
  1. package/README.md +1 -1
  2. package/corpus/README.md +63 -0
  3. package/corpus/sysml-v2-release/bnf/SysML-graphical-bnf.kgbnf +2264 -0
  4. package/package.json +4 -3
  5. package/packages/tools/lib/analysis/dsm.d.ts +1 -1
  6. package/packages/tools/lib/analysis/dsm.d.ts.map +1 -1
  7. package/packages/tools/lib/analysis/dsm.js +1 -1
  8. package/packages/tools/lib/analysis/dsm.js.map +1 -1
  9. package/packages/tools/lib/bin/memo.js +25 -0
  10. package/packages/tools/lib/bin/memo.js.map +1 -1
  11. package/packages/tools/lib/browser/index.d.ts +2 -0
  12. package/packages/tools/lib/browser/index.d.ts.map +1 -1
  13. package/packages/tools/lib/browser/index.js +1 -0
  14. package/packages/tools/lib/browser/index.js.map +1 -1
  15. package/packages/tools/lib/commands/dev.d.ts.map +1 -1
  16. package/packages/tools/lib/commands/dev.js +37 -15
  17. package/packages/tools/lib/commands/dev.js.map +1 -1
  18. package/packages/tools/lib/commands/dhf-lint.d.ts +10 -0
  19. package/packages/tools/lib/commands/dhf-lint.d.ts.map +1 -0
  20. package/packages/tools/lib/commands/dhf-lint.js +72 -0
  21. package/packages/tools/lib/commands/dhf-lint.js.map +1 -0
  22. package/packages/tools/lib/commands/dhf.d.ts.map +1 -1
  23. package/packages/tools/lib/commands/dhf.js +7 -1
  24. package/packages/tools/lib/commands/dhf.js.map +1 -1
  25. package/packages/tools/lib/commands/standards.d.ts +28 -0
  26. package/packages/tools/lib/commands/standards.d.ts.map +1 -0
  27. package/packages/tools/lib/commands/standards.js +203 -0
  28. package/packages/tools/lib/commands/standards.js.map +1 -0
  29. package/packages/tools/lib/commands/validate.d.ts.map +1 -1
  30. package/packages/tools/lib/commands/validate.js +13 -2
  31. package/packages/tools/lib/commands/validate.js.map +1 -1
  32. package/packages/tools/lib/dhf/document-compiler.d.ts.map +1 -1
  33. package/packages/tools/lib/dhf/document-compiler.js +13 -8
  34. package/packages/tools/lib/dhf/document-compiler.js.map +1 -1
  35. package/packages/tools/lib/dhf/document-registry.d.ts +16 -2
  36. package/packages/tools/lib/dhf/document-registry.d.ts.map +1 -1
  37. package/packages/tools/lib/dhf/document-registry.js +74 -23
  38. package/packages/tools/lib/dhf/document-registry.js.map +1 -1
  39. package/packages/tools/lib/dhf/index.d.ts +3 -0
  40. package/packages/tools/lib/dhf/index.d.ts.map +1 -1
  41. package/packages/tools/lib/dhf/index.js +4 -0
  42. package/packages/tools/lib/dhf/index.js.map +1 -1
  43. package/packages/tools/lib/dhf/query-engine.d.ts +12 -1
  44. package/packages/tools/lib/dhf/query-engine.d.ts.map +1 -1
  45. package/packages/tools/lib/dhf/query-engine.js +2 -1
  46. package/packages/tools/lib/dhf/query-engine.js.map +1 -1
  47. package/packages/tools/lib/dhf/query-executor.d.ts +72 -5
  48. package/packages/tools/lib/dhf/query-executor.d.ts.map +1 -1
  49. package/packages/tools/lib/dhf/query-executor.js +471 -81
  50. package/packages/tools/lib/dhf/query-executor.js.map +1 -1
  51. package/packages/tools/lib/dhf/script-runner.js +6 -6
  52. package/packages/tools/lib/dhf/script-runner.js.map +1 -1
  53. package/packages/tools/lib/dhf/standards-block.d.ts +40 -0
  54. package/packages/tools/lib/dhf/standards-block.d.ts.map +1 -0
  55. package/packages/tools/lib/dhf/standards-block.js +223 -0
  56. package/packages/tools/lib/dhf/standards-block.js.map +1 -0
  57. package/packages/tools/lib/dhf/standards-library.d.ts +123 -0
  58. package/packages/tools/lib/dhf/standards-library.d.ts.map +1 -0
  59. package/packages/tools/lib/dhf/standards-library.js +295 -0
  60. package/packages/tools/lib/dhf/standards-library.js.map +1 -0
  61. package/packages/tools/lib/dhf/standards-report.d.ts +189 -0
  62. package/packages/tools/lib/dhf/standards-report.d.ts.map +1 -0
  63. package/packages/tools/lib/dhf/standards-report.js +384 -0
  64. package/packages/tools/lib/dhf/standards-report.js.map +1 -0
  65. package/packages/tools/lib/dhf/template-lint.d.ts +42 -0
  66. package/packages/tools/lib/dhf/template-lint.d.ts.map +1 -0
  67. package/packages/tools/lib/dhf/template-lint.js +421 -0
  68. package/packages/tools/lib/dhf/template-lint.js.map +1 -0
  69. package/packages/tools/lib/dhf/template-resolver.d.ts.map +1 -1
  70. package/packages/tools/lib/dhf/template-resolver.js +17 -5
  71. package/packages/tools/lib/dhf/template-resolver.js.map +1 -1
  72. package/packages/tools/lib/importer/cameo-importer.js +2 -2
  73. package/packages/tools/lib/importer/cameo-importer.js.map +1 -1
  74. package/packages/tools/lib/importer/ea-importer.d.ts.map +1 -1
  75. package/packages/tools/lib/importer/ea-importer.js +4 -3
  76. package/packages/tools/lib/importer/ea-importer.js.map +1 -1
  77. package/packages/tools/lib/importer/owl-importer.d.ts.map +1 -1
  78. package/packages/tools/lib/importer/owl-importer.js +2 -3
  79. package/packages/tools/lib/importer/owl-importer.js.map +1 -1
  80. package/packages/tools/lib/language/generated/ast.d.ts +1155 -55
  81. package/packages/tools/lib/language/generated/ast.d.ts.map +1 -1
  82. package/packages/tools/lib/language/generated/ast.js +922 -9
  83. package/packages/tools/lib/language/generated/ast.js.map +1 -1
  84. package/packages/tools/lib/language/generated/grammar.d.ts.map +1 -1
  85. package/packages/tools/lib/language/generated/grammar.js +3643 -427
  86. package/packages/tools/lib/language/generated/grammar.js.map +1 -1
  87. package/packages/tools/lib/model/builder.d.ts.map +1 -1
  88. package/packages/tools/lib/model/builder.js +725 -43
  89. package/packages/tools/lib/model/builder.js.map +1 -1
  90. package/packages/tools/lib/model/kind-registry.d.ts +20 -0
  91. package/packages/tools/lib/model/kind-registry.d.ts.map +1 -1
  92. package/packages/tools/lib/model/kind-registry.js +38 -0
  93. package/packages/tools/lib/model/kind-registry.js.map +1 -1
  94. package/packages/tools/lib/model/manifest.d.ts +8 -0
  95. package/packages/tools/lib/model/manifest.d.ts.map +1 -1
  96. package/packages/tools/lib/model/manifest.js +3 -0
  97. package/packages/tools/lib/model/manifest.js.map +1 -1
  98. package/packages/tools/lib/model/naming.d.ts +57 -0
  99. package/packages/tools/lib/model/naming.d.ts.map +1 -0
  100. package/packages/tools/lib/model/naming.js +69 -0
  101. package/packages/tools/lib/model/naming.js.map +1 -0
  102. package/packages/tools/lib/model/native-project.d.ts.map +1 -1
  103. package/packages/tools/lib/model/native-project.js +10 -0
  104. package/packages/tools/lib/model/native-project.js.map +1 -1
  105. package/packages/tools/lib/model/ontology-loader.d.ts.map +1 -1
  106. package/packages/tools/lib/model/ontology-loader.js +3 -0
  107. package/packages/tools/lib/model/ontology-loader.js.map +1 -1
  108. package/packages/tools/lib/model/relationship-legality.d.ts +17 -0
  109. package/packages/tools/lib/model/relationship-legality.d.ts.map +1 -1
  110. package/packages/tools/lib/model/relationship-legality.js.map +1 -1
  111. package/packages/tools/lib/model/relationship-registry.d.ts +77 -0
  112. package/packages/tools/lib/model/relationship-registry.d.ts.map +1 -1
  113. package/packages/tools/lib/model/relationship-registry.js +217 -2
  114. package/packages/tools/lib/model/relationship-registry.js.map +1 -1
  115. package/packages/tools/lib/model/semantic.d.ts +46 -0
  116. package/packages/tools/lib/model/semantic.d.ts.map +1 -1
  117. package/packages/tools/lib/model/semantic.js +3 -0
  118. package/packages/tools/lib/model/semantic.js.map +1 -1
  119. package/packages/tools/lib/model/short-id.d.ts.map +1 -1
  120. package/packages/tools/lib/model/short-id.js +1 -3
  121. package/packages/tools/lib/model/short-id.js.map +1 -1
  122. package/packages/tools/lib/model/view-deriver.d.ts +10 -1
  123. package/packages/tools/lib/model/view-deriver.d.ts.map +1 -1
  124. package/packages/tools/lib/model/view-deriver.js +103 -5
  125. package/packages/tools/lib/model/view-deriver.js.map +1 -1
  126. package/packages/tools/lib/protocol/messages.d.ts +85 -3
  127. package/packages/tools/lib/protocol/messages.d.ts.map +1 -1
  128. package/packages/tools/lib/server/dev-server.d.ts.map +1 -1
  129. package/packages/tools/lib/server/dev-server.js +158 -5
  130. package/packages/tools/lib/server/dev-server.js.map +1 -1
  131. package/packages/tools/lib/server/package-source.d.ts +26 -0
  132. package/packages/tools/lib/server/package-source.d.ts.map +1 -0
  133. package/packages/tools/lib/server/package-source.js +81 -0
  134. package/packages/tools/lib/server/package-source.js.map +1 -0
  135. package/packages/tools/lib/server/package-writer.d.ts +94 -0
  136. package/packages/tools/lib/server/package-writer.d.ts.map +1 -0
  137. package/packages/tools/lib/server/package-writer.js +317 -0
  138. package/packages/tools/lib/server/package-writer.js.map +1 -0
  139. package/packages/tools/lib/server/persistor.d.ts +20 -0
  140. package/packages/tools/lib/server/persistor.d.ts.map +1 -1
  141. package/packages/tools/lib/server/persistor.js +29 -8
  142. package/packages/tools/lib/server/persistor.js.map +1 -1
  143. package/packages/tools/lib/server/relationship-writer.d.ts +13 -1
  144. package/packages/tools/lib/server/relationship-writer.d.ts.map +1 -1
  145. package/packages/tools/lib/server/relationship-writer.js +59 -0
  146. package/packages/tools/lib/server/relationship-writer.js.map +1 -1
  147. package/packages/tools/lib/server/sysml-notation.d.ts +1 -1
  148. package/packages/tools/lib/server/sysml-notation.d.ts.map +1 -1
  149. package/packages/tools/lib/server/sysml-notation.js +17 -0
  150. package/packages/tools/lib/server/sysml-notation.js.map +1 -1
  151. package/packages/tools/lib/server/view-layout-store.d.ts.map +1 -1
  152. package/packages/tools/lib/server/view-layout-store.js +1 -0
  153. package/packages/tools/lib/server/view-layout-store.js.map +1 -1
  154. package/packages/tools/lib/validator/behavior-validator.d.ts +1 -1
  155. package/packages/tools/lib/validator/behavior-validator.js +1 -1
  156. package/packages/tools/lib/validator/constraint-eval.d.ts +16 -0
  157. package/packages/tools/lib/validator/constraint-eval.d.ts.map +1 -1
  158. package/packages/tools/lib/validator/constraint-eval.js +102 -0
  159. package/packages/tools/lib/validator/constraint-eval.js.map +1 -1
  160. package/packages/tools/src/grammar/memo-sysml.langium +415 -25
@@ -14,10 +14,22 @@
14
14
  // group_by: layer
15
15
  // ```
16
16
  //
17
+ // A query selects elements by default. `select: relationships` selects links
18
+ // instead — the shape every clause-traceability and allocation table has, where
19
+ // a row is the edge and the columns come from both of its ends:
20
+ //
21
+ // ```memo-query
22
+ // select: relationships
23
+ // kind: conformsTo
24
+ // where: target.package == "memo_artifacts_standards_iec_62304"
25
+ // columns: source, target.clauseNumber, target.title
26
+ // ```
27
+ //
17
28
  // The block is replaced with rendered markdown (table / list / matrix / grouped).
18
29
  // Reuses QueryContext from query-engine.ts so all model access is consistent.
19
30
  // ─────────────────────────────────────────────────────────────────────────────
20
31
  import * as yaml from 'yaml';
32
+ import { toModelType } from '../model/naming.js';
21
33
  // ─── Parse a memo-query block ─────────────────────────────────────────────────
22
34
  export function parseMemoQuery(blockContent) {
23
35
  try {
@@ -30,59 +42,349 @@ export function parseMemoQuery(blockContent) {
30
42
  return null;
31
43
  }
32
44
  }
33
- // ─── Execute a query against the model context ────────────────────────────────
34
- export function executeQuery(spec, ctx) {
35
- let elements = [];
36
- // 1. Select by kind(s)
37
- if (spec.kind) {
38
- const kinds = Array.isArray(spec.kind) ? spec.kind : [spec.kind];
39
- elements = ctx.elementsByKinds(kinds);
45
+ // ─── Strict validation ────────────────────────────────────────────────────────
46
+ //
47
+ // A query directive this engine does not understand must never be ignored.
48
+ // `where:` used to fall through to `return elements` when no pattern matched,
49
+ // so a filter the parser could not read produced the FULL, unfiltered set —
50
+ // a compliance table that looks authoritative and is not. Everything below
51
+ // exists to turn that class of silence into an error.
52
+ //
53
+ // Validation is deliberately separate from execution so `memo dhf lint` can
54
+ // check a template without loading a model.
55
+ // ──────────────────────────────────────────────────────────────────────────────
56
+ /** A query directive that cannot be executed as written. */
57
+ export class MemoQueryError extends Error {
58
+ source;
59
+ constructor(message, source) {
60
+ super(source ? `${source}: ${message}` : message);
61
+ this.source = source;
62
+ this.name = 'MemoQueryError';
40
63
  }
41
- else {
42
- elements = ctx.allElements();
64
+ }
65
+ const KNOWN_KEYS = new Set([
66
+ 'select', 'kind', 'where', 'traverse', 'display', 'columns', 'sort', 'empty',
67
+ 'group_by', 'limit', 'row_kind', 'col_kind', 'label', 'value',
68
+ ]);
69
+ const KNOWN_DISPLAYS = new Set(['table', 'list', 'matrix', 'grouped', 'count', 'metric']);
70
+ const KNOWN_SELECTS = new Set(['elements', 'relationships']);
71
+ /** True when this spec's rows are relationships rather than elements. */
72
+ export function selectsRelationships(spec) {
73
+ return spec.select === 'relationships';
74
+ }
75
+ /**
76
+ * Fields a relationship row answers without consulting its attribute map.
77
+ *
78
+ * `source` and `target` are the endpoint *names*, which is what a table column
79
+ * wants; `source.<field>` / `target.<field>` reach the endpoint element itself.
80
+ */
81
+ export const RELATIONSHIP_FIELDS = new Set([
82
+ 'id', 'type', 'kind', 'source', 'target', 'sourceId', 'targetId',
83
+ 'sourceEnd', 'targetEnd', 'file', 'flowItem',
84
+ ]);
85
+ /** The `source` / `target` prefix of a dotted path, or undefined if it has none. */
86
+ export function endpointOf(field) {
87
+ const dot = field.indexOf('.');
88
+ if (dot === -1)
89
+ return undefined;
90
+ const head = field.slice(0, dot);
91
+ return head === 'source' || head === 'target' ? head : undefined;
92
+ }
93
+ const WHERE_PATTERNS = [
94
+ { re: /^([\w.]+)\s*==\s*["']?([^"']*)["']?$/, op: '==' },
95
+ { re: /^([\w.]+)\s*!=\s*["']?([^"']*)["']?$/, op: '!=' },
96
+ { re: /^([\w.]+)\s+contains\s+["']?([^"']*)["']?$/i, op: 'contains' },
97
+ ];
98
+ /**
99
+ * Parse a `where:` expression, or return null if this engine cannot evaluate it.
100
+ *
101
+ * Only a single comparison is supported. Boolean composition, parenthesised
102
+ * groups and `starts with` are all unsupported — callers must treat null as an
103
+ * error, never as "no filter". A dotted endpoint path (`target.layer`) parses,
104
+ * but means nothing to an element query; `validateQuerySpec` rejects it there.
105
+ */
106
+ export function parseWhereClause(where) {
107
+ const expr = where.trim();
108
+ for (const { re, op } of WHERE_PATTERNS) {
109
+ const m = expr.match(re);
110
+ if (m)
111
+ return { field: m[1], op, value: m[2] };
43
112
  }
44
- // 2. Apply where filter
45
- if (spec.where) {
46
- elements = applyFilter(elements, spec.where);
113
+ return null;
114
+ }
115
+ // ─── Enum-qualified values ────────────────────────────────────────────────────
116
+ //
117
+ // `builder.ts` stores an enum attribute as the reference the author wrote, and
118
+ // every enum-typed attribute in the ontology and its exemplars is written
119
+ // qualified — the model holds `"RequirementKind::software"`, never `"software"`.
120
+ // So the qualified form is THE spelling, and a template writes
121
+ // `where: requirementKind == "RequirementKind::software"`.
122
+ //
123
+ // Comparison is exact. The alternative — accepting the bare member name too —
124
+ // buys two spellings for one value, which is the defect this area exists to
125
+ // remove, and it costs the reader the only clue to which enum is meant:
126
+ // `criticality == "high"` and `severity == "high"` are indistinguishable,
127
+ // `CriticalityKind::high` is not.
128
+ //
129
+ // Exact comparison alone would resurrect the silent failure, though: a bare
130
+ // value simply matches nothing and the section renders its `empty:` message.
131
+ // So a bare value that IS a member of the enum actually stored on that field is
132
+ // an error naming the qualified spelling — the filter is not evaluated on a
133
+ // guess. Same rule as everywhere else here: fail, never quietly narrow.
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+ /** The member name of an enum reference: `RequirementKind::software` → `software`. */
136
+ export function unqualifyEnum(value) {
137
+ const sep = value.lastIndexOf('::');
138
+ return sep === -1 ? value : value.slice(sep + 2);
139
+ }
140
+ /**
141
+ * The qualified value stored on the compared field whose member name is
142
+ * `queryValue`, if any row has one — i.e. the spelling the author meant to
143
+ * write. Reads through the row's own field resolver so it works the same for a
144
+ * relationship endpoint (`target.requirementKind`) as for an element.
145
+ */
146
+ function qualifiedSpellingOf(rows, read, queryValue) {
147
+ if (queryValue.includes('::'))
148
+ return undefined;
149
+ for (const row of rows) {
150
+ const stored = String(read(row) ?? '');
151
+ if (stored.includes('::') && unqualifyEnum(stored) === queryValue)
152
+ return stored;
153
+ }
154
+ return undefined;
155
+ }
156
+ /** Explain why a `where:` expression is unsupported, for an actionable message. */
157
+ function diagnoseWhere(where) {
158
+ const expr = where.trim();
159
+ if (/\b(and|or)\b/i.test(expr)) {
160
+ return 'boolean composition (`and` / `or`) is not supported — use a single comparison, or narrow with `kind:`';
47
161
  }
162
+ if (/\bstarts with\b|\bends with\b|\bmatches\b/i.test(expr)) {
163
+ return 'only `==`, `!=` and `contains` are supported';
164
+ }
165
+ return 'expected `field == value`, `field != value`, or `field contains value`';
166
+ }
167
+ /**
168
+ * Why a dotted path cannot be used here, or undefined when it can.
169
+ *
170
+ * Dotted paths exist for relationship rows, which have no useful flat fields of
171
+ * their own: everything a compliance table wants to show lives on one of the two
172
+ * ends. An element row has no ends, so a dotted path there is a query that will
173
+ * render "—" in every cell — the silence this engine exists to turn into errors.
174
+ */
175
+ function diagnoseDottedPath(path, relationships) {
176
+ if (!path.includes('.'))
177
+ return undefined;
178
+ if (!relationships) {
179
+ return 'a dotted path names a relationship endpoint, and this query selects elements — add `select: relationships`';
180
+ }
181
+ if (!endpointOf(path)) {
182
+ return 'a dotted path must start with `source.` or `target.`';
183
+ }
184
+ return undefined;
185
+ }
186
+ /**
187
+ * Validate a query spec against what the engine can execute.
188
+ * Returns a list of human-readable problems; empty means executable.
189
+ */
190
+ export function validateQuerySpec(spec) {
191
+ const problems = [];
192
+ const relationships = selectsRelationships(spec);
193
+ for (const key of Object.keys(spec)) {
194
+ if (!KNOWN_KEYS.has(key)) {
195
+ problems.push(`unknown directive \`${key}:\` (known: ${[...KNOWN_KEYS].sort().join(', ')})`);
196
+ }
197
+ }
198
+ if (spec.select !== undefined && !KNOWN_SELECTS.has(spec.select)) {
199
+ problems.push(`unknown \`select: ${spec.select}\` (known: ${[...KNOWN_SELECTS].sort().join(', ')})`);
200
+ }
201
+ // A relationship type is stored lower-camel (`AllocatedTo` → `allocatedTo`)
202
+ // and matched exactly, so the PascalCase name of the connection def selects
203
+ // nothing at all. Same rule, and the same message, as `traverse:`.
204
+ if (relationships) {
205
+ for (const type of kindList(spec)) {
206
+ if (/^[A-Z]/.test(type)) {
207
+ problems.push(`\`kind: ${type}\` names a relationship type in PascalCase — use \`${pascalToCamel(type)}\``);
208
+ }
209
+ }
210
+ }
211
+ if (spec.where !== undefined) {
212
+ if (typeof spec.where !== 'string') {
213
+ problems.push('`where:` must be a string');
214
+ }
215
+ else {
216
+ const clause = parseWhereClause(spec.where);
217
+ if (!clause) {
218
+ problems.push(`cannot evaluate \`where: ${spec.where}\` — ${diagnoseWhere(spec.where)}`);
219
+ }
220
+ else {
221
+ const why = diagnoseDottedPath(clause.field, relationships);
222
+ if (why)
223
+ problems.push(`cannot evaluate \`where: ${spec.where}\` — ${why}`);
224
+ }
225
+ }
226
+ }
227
+ if (spec.sort !== undefined) {
228
+ if (typeof spec.sort !== 'string') {
229
+ problems.push('`sort:` must be a string');
230
+ }
231
+ else if (/\s/.test(spec.sort.trim())) {
232
+ problems.push(`cannot evaluate \`sort: ${spec.sort}\` — only a bare field name is supported (no \`asc\`/\`desc\`)`);
233
+ }
234
+ else {
235
+ const why = diagnoseDottedPath(spec.sort.trim(), relationships);
236
+ if (why)
237
+ problems.push(`cannot evaluate \`sort: ${spec.sort}\` — ${why}`);
238
+ }
239
+ }
240
+ for (const col of resolveColumns(spec)) {
241
+ if (/\s+as\s+/i.test(col)) {
242
+ problems.push(`column \`${col}\` uses an alias — \`as\` is not supported`);
243
+ continue;
244
+ }
245
+ const why = diagnoseDottedPath(col, relationships);
246
+ if (why)
247
+ problems.push(`column \`${col}\` — ${why}`);
248
+ }
249
+ if (spec.group_by !== undefined) {
250
+ const why = diagnoseDottedPath(String(spec.group_by), relationships);
251
+ if (why)
252
+ problems.push(`\`group_by: ${spec.group_by}\` — ${why}`);
253
+ }
254
+ if (spec.display !== undefined && !KNOWN_DISPLAYS.has(spec.display)) {
255
+ problems.push(`unknown \`display: ${spec.display}\` (known: ${[...KNOWN_DISPLAYS].sort().join(', ')})`);
256
+ }
257
+ if (spec.traverse !== undefined) {
258
+ const parts = String(spec.traverse).trim().split(/\s+/);
259
+ if (relationships) {
260
+ // Traversal walks *from* an element along an edge. The rows here are
261
+ // already the edges, so there is nothing to walk from — rather than
262
+ // pick an end silently, say so.
263
+ problems.push('`traverse:` selects elements reached from a seed, so it cannot be combined with `select: relationships`');
264
+ }
265
+ else if (parts.length !== 2 || !['outgoing', 'incoming'].includes(parts[0])) {
266
+ problems.push(`cannot evaluate \`traverse: ${spec.traverse}\` — expected \`outgoing <relType>\` or \`incoming <relType>\``);
267
+ }
268
+ else if (parts[1] !== '*' && /^[A-Z]/.test(parts[1])) {
269
+ problems.push(`\`traverse: ${spec.traverse}\` names a relationship type in PascalCase — use \`${pascalToCamel(parts[1])}\``);
270
+ }
271
+ }
272
+ return problems;
273
+ }
274
+ /** `kind:` as a list, whichever of the two spellings the block used. */
275
+ export function kindList(spec) {
276
+ if (spec.kind === undefined)
277
+ return [];
278
+ return Array.isArray(spec.kind) ? spec.kind : [spec.kind];
279
+ }
280
+ const pascalToCamel = toModelType;
281
+ // ─── Commented-out blocks ─────────────────────────────────────────────────────
282
+ //
283
+ // A template may park a query it cannot run yet inside an HTML comment, with a
284
+ // visible TODO beside it — the honest way to ship a section whose query shape
285
+ // the engine does not support yet. A commented block renders nothing, so it
286
+ // must neither be executed nor linted; otherwise "park it until the engine
287
+ // catches up" is not available and the alternative is a table that renders
288
+ // empty and reads like a clean audit.
289
+ // ─────────────────────────────────────────────────────────────────────────────
290
+ const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
291
+ /** Character ranges of the HTML comments in `content`. */
292
+ export function htmlCommentRanges(content) {
293
+ const ranges = [];
294
+ for (const m of content.matchAll(HTML_COMMENT_RE)) {
295
+ ranges.push([m.index, m.index + m[0].length]);
296
+ }
297
+ return ranges;
298
+ }
299
+ export function isCommentedOut(ranges, offset) {
300
+ return ranges.some(([start, end]) => offset >= start && offset < end);
301
+ }
302
+ /** Throw if the spec cannot be executed as written. */
303
+ export function assertExecutable(spec, source) {
304
+ const problems = validateQuerySpec(spec);
305
+ if (problems.length > 0) {
306
+ throw new MemoQueryError(problems.join('; '), source);
307
+ }
308
+ }
309
+ // ─── Execute a query against the model context ────────────────────────────────
310
+ export function executeQuery(spec, ctx, source) {
311
+ assertExecutable(spec, source);
312
+ return selectsRelationships(spec)
313
+ ? executeRelationshipQuery(spec, ctx)
314
+ : executeElementQuery(spec, ctx);
315
+ }
316
+ function executeElementQuery(spec, ctx) {
317
+ // 1. Select by kind(s)
318
+ let elements = spec.kind ? ctx.elementsByKinds(kindList(spec)) : ctx.allElements();
319
+ // 2. Apply where filter
320
+ if (spec.where)
321
+ elements = applyFilter(elements, spec.where, ctx);
48
322
  // 3. Traverse relationships
49
323
  if (spec.traverse && elements.length > 0) {
50
324
  elements = applyTraverse(elements, spec.traverse, ctx);
51
325
  }
52
326
  // 4. Sort
53
- if (spec.sort) {
54
- const field = spec.sort;
55
- elements = [...elements].sort((a, b) => {
56
- const av = getField(a, field) ?? '';
57
- const bv = getField(b, field) ?? '';
58
- return String(av).localeCompare(String(bv));
59
- });
60
- }
327
+ if (spec.sort)
328
+ elements = sortRows(elements, spec.sort, el => getField(el, spec.sort));
61
329
  // 5. Limit
62
- if (spec.limit && spec.limit > 0) {
330
+ if (spec.limit && spec.limit > 0)
63
331
  elements = elements.slice(0, spec.limit);
64
- }
65
332
  return elements;
66
333
  }
67
- function applyFilter(elements, where) {
68
- // Supports: "field == value", "field != value", "field contains value"
69
- const eqMatch = where.match(/^(\w+)\s*==\s*["']?([^"']*)["']?$/);
70
- const neqMatch = where.match(/^(\w+)\s*!=\s*["']?([^"']*)["']?$/);
71
- const containsMatch = where.match(/^(\w+)\s+contains\s+["']?([^"']*)["']?$/i);
72
- if (eqMatch) {
73
- const [, field, value] = eqMatch;
74
- return elements.filter(el => String(getField(el, field) ?? '') === value);
75
- }
76
- if (neqMatch) {
77
- const [, field, value] = neqMatch;
78
- return elements.filter(el => String(getField(el, field) ?? '') !== value);
334
+ /**
335
+ * Rows are the links themselves.
336
+ *
337
+ * `kind:` names relationship types here, resolved through the same lower-camel
338
+ * index the builder writes (`AllocatedTo` → `allocatedTo`) — the PascalCase
339
+ * spelling is rejected in validation rather than silently matching nothing.
340
+ */
341
+ function executeRelationshipQuery(spec, ctx) {
342
+ let rels = spec.kind
343
+ ? kindList(spec).flatMap(type => ctx.relationshipsByType(type))
344
+ : ctx.allRelationships();
345
+ if (spec.where)
346
+ rels = applyFilter(rels, spec.where, ctx);
347
+ if (spec.sort)
348
+ rels = sortRows(rels, spec.sort, rel => getRelField(rel, spec.sort, ctx));
349
+ if (spec.limit && spec.limit > 0)
350
+ rels = rels.slice(0, spec.limit);
351
+ return rels;
352
+ }
353
+ function sortRows(rows, _field, read) {
354
+ // Numeric-aware, because the fields documents sort on are numbered: clause
355
+ // "14" belongs after "9", and "REQ-CS-2" before "REQ-CS-10". Plain
356
+ // lexicographic ordering put clause 14 at the top of a conformance matrix,
357
+ // which reads as a broken table long before anyone works out why.
358
+ return [...rows].sort((a, b) => String(read(a) ?? '').localeCompare(String(read(b) ?? ''), undefined, { numeric: true }));
359
+ }
360
+ function applyFilter(rows, where, ctx) {
361
+ const clause = parseWhereClause(where);
362
+ // Never fall through to the unfiltered set: a filter the engine cannot read
363
+ // must fail, not quietly widen the result to everything.
364
+ if (!clause)
365
+ throw new MemoQueryError(`cannot evaluate \`where: ${where}\` — ${diagnoseWhere(where)}`);
366
+ const { field, op, value } = clause;
367
+ const read = (row) => rowField(row, field, ctx);
368
+ // A bare enum member never matches, because the model stores the qualified
369
+ // reference. Say so instead of returning an empty — or, for `!=`, a
370
+ // complete — set that reads like a real answer.
371
+ if (op === '==' || op === '!=') {
372
+ const qualified = qualifiedSpellingOf(rows, read, value);
373
+ if (qualified) {
374
+ throw new MemoQueryError(`\`${field} ${op} "${value}"\` names an enum member without its enum, and the model stores `
375
+ + `\`${qualified}\` — write \`${field} ${op} "${qualified}"\``);
376
+ }
79
377
  }
80
- if (containsMatch) {
81
- const [, field, value] = containsMatch;
82
- const lv = value.toLowerCase();
83
- return elements.filter(el => String(getField(el, field) ?? '').toLowerCase().includes(lv));
378
+ switch (op) {
379
+ case '==':
380
+ return rows.filter(row => String(read(row) ?? '') === value);
381
+ case '!=':
382
+ return rows.filter(row => String(read(row) ?? '') !== value);
383
+ case 'contains': {
384
+ const lv = value.toLowerCase();
385
+ return rows.filter(row => String(read(row) ?? '').toLowerCase().includes(lv));
386
+ }
84
387
  }
85
- return elements;
86
388
  }
87
389
  function applyTraverse(seeds, traverse, ctx) {
88
390
  // "outgoing mitigates" | "incoming derivedFrom" | "outgoing *"
@@ -116,6 +418,13 @@ function getField(el, field) {
116
418
  return el.kind;
117
419
  if (field === 'layer')
118
420
  return el.layer;
421
+ // The declaring package. Structural, one per source file, and the fact that
422
+ // says which standard a clause belongs to — the standards library is one
423
+ // package per standard, so `target.package == "…_iec_62304"` is the exact
424
+ // per-standard selector a clause-conformance table needs. Clause *numbers*
425
+ // are not: "5" is a clause of seven different standards in this library.
426
+ if (field === 'package')
427
+ return el.package || '';
119
428
  if (field === 'doc' || field === 'description')
120
429
  return el.doc || '';
121
430
  // Try attributes map
@@ -123,20 +432,61 @@ function getField(el, field) {
123
432
  return el.attributes[field];
124
433
  return undefined;
125
434
  }
435
+ /** True when a row is a relationship rather than an element. */
436
+ function isRelationshipRow(row) {
437
+ return 'sourceId' in row;
438
+ }
439
+ /** Read `field` off a row, whichever kind of row it is. */
440
+ function rowField(row, field, ctx) {
441
+ return isRelationshipRow(row) ? getRelField(row, field, ctx) : getField(row, field);
442
+ }
443
+ /**
444
+ * Read a field off a relationship.
445
+ *
446
+ * Bare `source` / `target` are the endpoint names, which is what a table cell
447
+ * wants. `source.<field>` / `target.<field>` resolve the endpoint element and
448
+ * read the field off it — the reason relationship rows need dotted paths at
449
+ * all, since a link carries almost nothing on itself.
450
+ */
451
+ function getRelField(rel, field, ctx) {
452
+ const endpoint = endpointOf(field);
453
+ if (endpoint) {
454
+ const el = ctx.element(endpoint === 'source' ? rel.sourceId : rel.targetId);
455
+ return el ? getField(el, field.slice(field.indexOf('.') + 1)) : undefined;
456
+ }
457
+ switch (field) {
458
+ case 'source': return ctx.elementName(rel.sourceId);
459
+ case 'target': return ctx.elementName(rel.targetId);
460
+ case 'id': return rel.id;
461
+ // `kind` is the element word for "what is this"; a template that writes
462
+ // it against a link means the link's type, and answering is kinder than
463
+ // rendering a dash.
464
+ case 'kind':
465
+ case 'type': return rel.type;
466
+ case 'sourceId': return rel.sourceId;
467
+ case 'targetId': return rel.targetId;
468
+ case 'sourceEnd': return rel.sourceEnd;
469
+ case 'targetEnd': return rel.targetEnd;
470
+ case 'file': return rel.file;
471
+ case 'flowItem': return rel.flowItem;
472
+ default: return rel.attributes?.[field];
473
+ }
474
+ }
126
475
  // ─── Render query results as markdown ─────────────────────────────────────────
127
- export function renderQueryResult(spec, elements, ctx) {
128
- if (elements.length === 0) {
476
+ export function renderQueryResult(spec, rows, ctx) {
477
+ if (rows.length === 0) {
129
478
  return spec.empty ? `\n_${spec.empty}_\n` : '\n_No results found._\n';
130
479
  }
480
+ const read = (row, field) => rowField(row, field, ctx);
131
481
  const display = spec.display || 'table';
132
482
  switch (display) {
133
- case 'table': return renderTable(spec, elements);
134
- case 'list': return renderList(spec, elements);
135
- case 'grouped': return renderGrouped(spec, elements);
136
- case 'matrix': return renderMatrix(spec, elements, ctx);
137
- case 'count': return `\n**${elements.length}** ${spec.kind || 'elements'}\n`;
138
- case 'metric': return renderMetric(spec, elements);
139
- default: return renderTable(spec, elements);
483
+ case 'table': return renderTable(spec, rows, read);
484
+ case 'list': return renderList(spec, rows, read);
485
+ case 'grouped': return renderGrouped(spec, rows, read);
486
+ case 'matrix': return renderMatrix(spec, rows, read);
487
+ case 'count': return `\n**${rows.length}** ${spec.kind || (selectsRelationships(spec) ? 'relationships' : 'elements')}\n`;
488
+ case 'metric': return renderMetric(spec, rows, read);
489
+ default: return renderTable(spec, rows, read);
140
490
  }
141
491
  }
142
492
  function resolveColumns(spec) {
@@ -145,78 +495,118 @@ function resolveColumns(spec) {
145
495
  return spec.columns;
146
496
  return spec.columns.split(',').map(c => c.trim());
147
497
  }
148
- return ['name', 'kind', 'layer', 'doc'];
498
+ // A relationship row has no name/layer of its own; what it has is two ends.
499
+ return selectsRelationships(spec)
500
+ ? ['source', 'type', 'target']
501
+ : ['name', 'kind', 'layer', 'doc'];
149
502
  }
150
- function renderTable(spec, elements) {
503
+ function renderTable(spec, rows, read) {
151
504
  const cols = resolveColumns(spec);
152
- const header = '| ' + cols.map(c => capitalize(c)).join(' | ') + ' |';
505
+ const header = '| ' + cols.map(c => columnHeader(c)).join(' | ') + ' |';
153
506
  const sep = '| ' + cols.map(() => '---').join(' | ') + ' |';
154
- const rows = elements.map(el => {
507
+ const body = rows.map(row => {
155
508
  const cells = cols.map(c => {
156
- const val = getField(el, c);
509
+ const val = read(row, c);
157
510
  return val ? String(val).replace(/\|/g, '\\|') : '—';
158
511
  });
159
512
  return '| ' + cells.join(' | ') + ' |';
160
513
  });
161
- return '\n' + [header, sep, ...rows].join('\n') + '\n';
514
+ return '\n' + [header, sep, ...body].join('\n') + '\n';
162
515
  }
163
- function renderList(spec, elements) {
164
- return '\n' + elements.map(el => `- **${el.name}** _(${el.kind})_ — ${el.doc || el.layer || ''}`).join('\n') + '\n';
516
+ function renderList(spec, rows, read) {
517
+ const line = (row) => selectsRelationships(spec)
518
+ ? `- **${read(row, 'source')}** → **${read(row, 'target')}** _(${read(row, 'type')})_`
519
+ : `- **${read(row, 'name')}** _(${read(row, 'kind')})_ — ${read(row, 'doc') || read(row, 'layer') || ''}`;
520
+ return '\n' + rows.map(line).join('\n') + '\n';
165
521
  }
166
- function renderGrouped(spec, elements) {
167
- const groupField = spec.group_by || 'layer';
522
+ function renderGrouped(spec, rows, read) {
523
+ const groupField = spec.group_by || (selectsRelationships(spec) ? 'type' : 'layer');
168
524
  const groups = new Map();
169
- for (const el of elements) {
170
- const key = String(getField(el, groupField) ?? 'Other');
525
+ for (const row of rows) {
526
+ const key = String(read(row, groupField) ?? 'Other');
171
527
  if (!groups.has(key))
172
528
  groups.set(key, []);
173
- groups.get(key).push(el);
529
+ groups.get(key).push(row);
174
530
  }
175
531
  const parts = [];
176
- for (const [key, els] of groups) {
177
- parts.push(`\n**${key}** (${els.length})\n`);
178
- parts.push(renderTable(spec, els));
532
+ for (const [key, group] of groups) {
533
+ parts.push(`\n**${key}** (${group.length})\n`);
534
+ parts.push(renderTable(spec, group, read));
179
535
  }
180
536
  return parts.join('');
181
537
  }
182
- function renderMatrix(spec, elements, ctx) {
538
+ function renderMatrix(spec, rows, read) {
183
539
  // Row/col breakdown by two fields
184
540
  const rowField = spec.row_kind || 'kind';
185
541
  const colField = spec.col_kind || 'layer';
186
- const rowVals = [...new Set(elements.map(el => String(getField(el, rowField) ?? '?')))].sort();
187
- const colVals = [...new Set(elements.map(el => String(getField(el, colField) ?? '?')))].sort();
542
+ const rowVals = [...new Set(rows.map(r => String(read(r, rowField) ?? '?')))].sort();
543
+ const colVals = [...new Set(rows.map(r => String(read(r, colField) ?? '?')))].sort();
188
544
  const header = '| | ' + colVals.join(' | ') + ' |';
189
545
  const sep = '| --- | ' + colVals.map(() => '---').join(' | ') + ' |';
190
- const rows = rowVals.map(rv => {
546
+ const body = rowVals.map(rv => {
191
547
  const cells = colVals.map(cv => {
192
- const count = elements.filter(el => String(getField(el, rowField) ?? '?') === rv &&
193
- String(getField(el, colField) ?? '?') === cv).length;
548
+ const count = rows.filter(r => String(read(r, rowField) ?? '?') === rv &&
549
+ String(read(r, colField) ?? '?') === cv).length;
194
550
  return count > 0 ? String(count) : '—';
195
551
  });
196
552
  return `| **${rv}** | ${cells.join(' | ')} |`;
197
553
  });
198
- return '\n' + [header, sep, ...rows].join('\n') + '\n';
554
+ return '\n' + [header, sep, ...body].join('\n') + '\n';
199
555
  }
200
- function renderMetric(spec, elements) {
556
+ function renderMetric(spec, rows, read) {
201
557
  const label = spec.label || 'Count';
202
- const value = spec.value ? elements.reduce((sum, el) => {
203
- const v = Number(getField(el, spec.value) ?? 0);
558
+ const value = spec.value ? rows.reduce((sum, row) => {
559
+ const v = Number(read(row, spec.value) ?? 0);
204
560
  return sum + (isNaN(v) ? 0 : v);
205
- }, 0) : elements.length;
561
+ }, 0) : rows.length;
206
562
  return `\n**${label}:** ${value}\n`;
207
563
  }
208
564
  function capitalize(s) {
209
565
  return s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ');
210
566
  }
567
+ /**
568
+ * Header text for a column. A dotted endpoint path becomes readable words
569
+ * ("target.clauseNumber" → "Target Clause Number"), which is why relationship
570
+ * tables need no column aliases. Flat column headers are left exactly as they
571
+ * were, so no existing document's tables change.
572
+ */
573
+ function columnHeader(col) {
574
+ if (!col.includes('.'))
575
+ return capitalize(col);
576
+ return col
577
+ .split('.')
578
+ .map(seg => capitalize(seg.replace(/([a-z0-9])([A-Z])/g, '$1 $2')))
579
+ .join(' ');
580
+ }
211
581
  // ─── Process all memo-query blocks in a markdown string ───────────────────────
212
582
  const QUERY_BLOCK_RE = /```memo-query\n([\s\S]*?)```/g;
213
- export function processMemoQueryBlocks(content, ctx) {
214
- return content.replace(QUERY_BLOCK_RE, (_match, blockContent) => {
583
+ export function processMemoQueryBlocks(content, ctx, options = {}) {
584
+ const { source, onError = 'throw' } = options;
585
+ const commented = htmlCommentRanges(content);
586
+ let blockIndex = 0;
587
+ return content.replace(QUERY_BLOCK_RE, (match, blockContent, offset) => {
588
+ // Leave a parked block exactly as written — it is documentation of a
589
+ // query the engine cannot run yet, not a query to run.
590
+ if (isCommentedOut(commented, offset))
591
+ return match;
592
+ blockIndex++;
593
+ const where = source ? `${source} (memo-query block ${blockIndex})` : `memo-query block ${blockIndex}`;
215
594
  const spec = parseMemoQuery(blockContent);
216
- if (!spec)
217
- return `\n> ⚠️ Invalid memo-query block\n`;
218
- const elements = executeQuery(spec, ctx);
219
- return renderQueryResult(spec, elements, ctx);
595
+ if (!spec) {
596
+ if (onError === 'throw')
597
+ throw new MemoQueryError('block is not valid YAML', where);
598
+ return `\n> ⚠️ **${where}** — block is not valid YAML\n`;
599
+ }
600
+ try {
601
+ const elements = executeQuery(spec, ctx, where);
602
+ return renderQueryResult(spec, elements, ctx);
603
+ }
604
+ catch (error) {
605
+ if (onError === 'throw')
606
+ throw error;
607
+ const message = error instanceof Error ? error.message : String(error);
608
+ return `\n> ⚠️ **${message}**\n`;
609
+ }
220
610
  });
221
611
  }
222
612
  //# sourceMappingURL=query-executor.js.map