structured-context 0.9.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 (112) hide show
  1. package/README.md +348 -0
  2. package/dist/commands/diagram.d.ts +5 -0
  3. package/dist/commands/diagram.js +12 -0
  4. package/dist/commands/docs.d.ts +1 -0
  5. package/dist/commands/docs.js +67 -0
  6. package/dist/commands/dump.d.ts +2 -0
  7. package/dist/commands/dump.js +6 -0
  8. package/dist/commands/plugins.d.ts +1 -0
  9. package/dist/commands/plugins.js +23 -0
  10. package/dist/commands/render.d.ts +6 -0
  11. package/dist/commands/render.js +35 -0
  12. package/dist/commands/schemas.d.ts +6 -0
  13. package/dist/commands/schemas.js +268 -0
  14. package/dist/commands/show.d.ts +4 -0
  15. package/dist/commands/show.js +7 -0
  16. package/dist/commands/spaces.d.ts +1 -0
  17. package/dist/commands/spaces.js +36 -0
  18. package/dist/commands/template-sync.d.ts +3 -0
  19. package/dist/commands/template-sync.js +13 -0
  20. package/dist/commands/validate-file.d.ts +28 -0
  21. package/dist/commands/validate-file.js +133 -0
  22. package/dist/commands/validate.d.ts +16 -0
  23. package/dist/commands/validate.js +349 -0
  24. package/dist/config.d.ts +38 -0
  25. package/dist/config.js +179 -0
  26. package/dist/constants.d.ts +6 -0
  27. package/dist/constants.js +6 -0
  28. package/dist/filter/augment-nodes.d.ts +23 -0
  29. package/dist/filter/augment-nodes.js +95 -0
  30. package/dist/filter/expand-include.d.ts +62 -0
  31. package/dist/filter/expand-include.js +181 -0
  32. package/dist/filter/filter-nodes.d.ts +21 -0
  33. package/dist/filter/filter-nodes.js +73 -0
  34. package/dist/filter/parse-expression.d.ts +20 -0
  35. package/dist/filter/parse-expression.js +60 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.js +161 -0
  38. package/dist/integrations/miro/cache.d.ts +21 -0
  39. package/dist/integrations/miro/cache.js +55 -0
  40. package/dist/integrations/miro/client.d.ts +99 -0
  41. package/dist/integrations/miro/client.js +118 -0
  42. package/dist/integrations/miro/layout.d.ts +28 -0
  43. package/dist/integrations/miro/layout.js +72 -0
  44. package/dist/integrations/miro/styles.d.ts +11 -0
  45. package/dist/integrations/miro/styles.js +65 -0
  46. package/dist/integrations/miro/sync.d.ts +8 -0
  47. package/dist/integrations/miro/sync.js +347 -0
  48. package/dist/plugin-api.d.ts +12 -0
  49. package/dist/plugin-api.js +7 -0
  50. package/dist/plugins/index.d.ts +3 -0
  51. package/dist/plugins/index.js +3 -0
  52. package/dist/plugins/loader.d.ts +21 -0
  53. package/dist/plugins/loader.js +104 -0
  54. package/dist/plugins/markdown/index.d.ts +48 -0
  55. package/dist/plugins/markdown/index.js +51 -0
  56. package/dist/plugins/markdown/parse-embedded.d.ts +90 -0
  57. package/dist/plugins/markdown/parse-embedded.js +663 -0
  58. package/dist/plugins/markdown/read-space.d.ts +7 -0
  59. package/dist/plugins/markdown/read-space.js +89 -0
  60. package/dist/plugins/markdown/render-bullets.d.ts +2 -0
  61. package/dist/plugins/markdown/render-bullets.js +42 -0
  62. package/dist/plugins/markdown/render-mermaid.d.ts +2 -0
  63. package/dist/plugins/markdown/render-mermaid.js +57 -0
  64. package/dist/plugins/markdown/template-sync.d.ts +16 -0
  65. package/dist/plugins/markdown/template-sync.js +294 -0
  66. package/dist/plugins/markdown/util.d.ts +19 -0
  67. package/dist/plugins/markdown/util.js +80 -0
  68. package/dist/plugins/util.d.ts +60 -0
  69. package/dist/plugins/util.js +7 -0
  70. package/dist/read/read-space.d.ts +2 -0
  71. package/dist/read/read-space.js +22 -0
  72. package/dist/read/resolve-graph-edges.d.ts +11 -0
  73. package/dist/read/resolve-graph-edges.js +201 -0
  74. package/dist/read/wikilink-utils.d.ts +16 -0
  75. package/dist/read/wikilink-utils.js +38 -0
  76. package/dist/render/registry.d.ts +13 -0
  77. package/dist/render/registry.js +22 -0
  78. package/dist/render/render.d.ts +4 -0
  79. package/dist/render/render.js +28 -0
  80. package/dist/schema/evaluate-rule.d.ts +30 -0
  81. package/dist/schema/evaluate-rule.js +82 -0
  82. package/dist/schema/metadata-contract.d.ts +538 -0
  83. package/dist/schema/metadata-contract.js +115 -0
  84. package/dist/schema/schema-refs.d.ts +22 -0
  85. package/dist/schema/schema-refs.js +168 -0
  86. package/dist/schema/schema.d.ts +27 -0
  87. package/dist/schema/schema.js +378 -0
  88. package/dist/schema/validate-graph.d.ts +24 -0
  89. package/dist/schema/validate-graph.js +141 -0
  90. package/dist/schema/validate-rules.d.ts +10 -0
  91. package/dist/schema/validate-rules.js +51 -0
  92. package/dist/schemas/_ost_strict.json +81 -0
  93. package/dist/schemas/_sctx_base.json +72 -0
  94. package/dist/schemas/general.json +261 -0
  95. package/dist/schemas/generated/_structured_context_schema_meta.json +191 -0
  96. package/dist/schemas/knowledge_wiki.json +206 -0
  97. package/dist/schemas/strict_ost.json +97 -0
  98. package/dist/space-graph.d.ts +28 -0
  99. package/dist/space-graph.js +82 -0
  100. package/dist/types.d.ts +145 -0
  101. package/dist/types.js +0 -0
  102. package/docs/concepts.md +391 -0
  103. package/docs/config.md +140 -0
  104. package/docs/rules.md +120 -0
  105. package/docs/schemas.md +340 -0
  106. package/package.json +69 -0
  107. package/schemas/_ost_strict.json +81 -0
  108. package/schemas/_sctx_base.json +72 -0
  109. package/schemas/general.json +261 -0
  110. package/schemas/generated/_structured_context_schema_meta.json +191 -0
  111. package/schemas/knowledge_wiki.json +206 -0
  112. package/schemas/strict_ost.json +97 -0
@@ -0,0 +1,663 @@
1
+ import { load as yamlLoad } from 'js-yaml';
2
+ import { toString as mdastToString } from 'mdast-util-to-string';
3
+ import remarkGfm from 'remark-gfm';
4
+ import remarkParse from 'remark-parse';
5
+ import { unified } from 'unified';
6
+ import { applyFieldMap, coerceDates } from './util';
7
+ /** Type values that identify a space_on_a_page container (not themselves space nodes). */
8
+ export const ON_A_PAGE_TYPES = ['ost_on_a_page', 'space_on_a_page'];
9
+ const DEFAULT_STATUS = 'identified';
10
+ /** Detect a bare wikilink `[[...]]` and return the inner target, or undefined. */
11
+ export function isWikilink(text) {
12
+ const match = text.match(/^\[\[(.+?)\]\]$/);
13
+ return match ? match[1] : undefined;
14
+ }
15
+ /** Evaluate a list of matchers against a heading title. */
16
+ function matchesPattern(title, lowerTitle, matchers) {
17
+ for (const matcher of matchers) {
18
+ if (matcher.startsWith('^') && matcher.endsWith('$')) {
19
+ if (new RegExp(matcher, 'i').test(title))
20
+ return true;
21
+ }
22
+ else if (matcher.startsWith('/') && matcher.endsWith('/')) {
23
+ if (new RegExp(matcher.slice(1, -1), 'i').test(title))
24
+ return true;
25
+ }
26
+ else if (lowerTitle === matcher.toLowerCase()) {
27
+ return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ /**
33
+ * Append a wikilink reference to a field array on a node.
34
+ * Creates the array if missing; throws if the field exists but is not an array.
35
+ */
36
+ function appendParentField(parentNode, field, linkRef) {
37
+ const fieldValue = parentNode.schemaData[field];
38
+ if (fieldValue === undefined) {
39
+ parentNode.schemaData[field] = [linkRef];
40
+ }
41
+ else if (Array.isArray(fieldValue)) {
42
+ fieldValue.push(linkRef);
43
+ }
44
+ else {
45
+ throw new Error(`Cannot append child link to field '${field}' on node '${parentNode.label}': ` +
46
+ `field exists but is not an array (found ${typeof fieldValue}). ` +
47
+ `Child link: ${linkRef}`);
48
+ }
49
+ }
50
+ /** Extract [key:: value] bracketed inline fields, return cleaned text and fields. */
51
+ export function extractBracketedFields(text) {
52
+ const fields = {};
53
+ const cleanText = text
54
+ .replace(/\[([^\]]+?):: *([^\]]*)\]/g, (_, key, value) => {
55
+ fields[key.trim()] = value.trim();
56
+ return '';
57
+ })
58
+ .trim();
59
+ return { cleanText, fields };
60
+ }
61
+ /**
62
+ * Extract unbracketed dataview fields (key:: value on own line).
63
+ * Keys must be identifier-style (letters, digits, hyphens, underscores - no spaces).
64
+ * Lines matching the pattern are consumed as fields; other lines kept as content.
65
+ */
66
+ export function extractUnbracketedFields(text) {
67
+ const fields = {};
68
+ const remaining = [];
69
+ for (const line of text.split('\n')) {
70
+ const match = line.match(/^([a-zA-Z][a-zA-Z0-9_-]*):: *(.*)$/);
71
+ if (match) {
72
+ fields[match[1].trim()] = match[2].trim();
73
+ }
74
+ else {
75
+ remaining.push(line);
76
+ }
77
+ }
78
+ return { remainingText: remaining.join('\n').trim(), fields };
79
+ }
80
+ /**
81
+ * Extract a trailing Obsidian block anchor from heading text.
82
+ * e.g. "My Title ^anchor-id" -> { cleanText: "My Title", anchor: "anchor-id" }
83
+ */
84
+ export function extractAnchor(text) {
85
+ const match = text.match(/\s+\^([a-zA-Z0-9][a-zA-Z0-9_-]*)$/);
86
+ if (match) {
87
+ return {
88
+ cleanText: text.slice(0, text.length - match[0].length).trim(),
89
+ anchor: match[1],
90
+ };
91
+ }
92
+ return { cleanText: text };
93
+ }
94
+ /**
95
+ * If the anchor name exactly matches a space node type (optionally followed by digits),
96
+ * return that type. Otherwise return undefined.
97
+ * Examples: "mission" -> "mission", "goal1" -> "goal", "myanchor" -> undefined
98
+ *
99
+ * Also checks relationship types (for parent-side relationships where child type may not be in hierarchy).
100
+ */
101
+ export function anchorToNodeType(anchor, hierarchy, relationships) {
102
+ for (const type of hierarchy) {
103
+ if (anchor === type || new RegExp(`^${type}\\d+$`).test(anchor)) {
104
+ return type;
105
+ }
106
+ }
107
+ // Check relationship types (for parent-side relationships)
108
+ if (relationships) {
109
+ for (const rel of relationships) {
110
+ if (anchor === rel.type || new RegExp(`^${rel.type}\\d+$`).test(anchor)) {
111
+ return rel.type;
112
+ }
113
+ }
114
+ }
115
+ return undefined;
116
+ }
117
+ /**
118
+ * Turn a full heading string into an Obsidian section-target key component.
119
+ * - normalizes observed Obsidian separators (#, ^, :, \) to spaces
120
+ * - compresses whitespace runs to single spaces
121
+ * - does _NOT_ (and should not) manipulate anchors or inline fields
122
+ */
123
+ export function normalizeHeadingSectionTarget(rawHeadingText) {
124
+ return rawHeadingText
125
+ .replace(/[#^:\\]/g, ' ')
126
+ .replace(/\s+/g, ' ')
127
+ .trim();
128
+ }
129
+ /**
130
+ * Returns the default space node type for a new heading based on its parent's effective type.
131
+ * The first heading in a document defaults to the first type in the hierarchy; each child is the next in sequence.
132
+ */
133
+ export function defaultNodeType(stack, hierarchy) {
134
+ if (stack.length === 0)
135
+ return hierarchy[0];
136
+ const parentType = stack[stack.length - 1].nodeType;
137
+ const idx = hierarchy.indexOf(parentType);
138
+ if (idx === -1 || idx >= hierarchy.length - 1) {
139
+ throw new Error(`No node type follows "${parentType}" - cannot determine type for child heading`);
140
+ }
141
+ return hierarchy[idx + 1];
142
+ }
143
+ function appendContent(node, text) {
144
+ if (!text)
145
+ return;
146
+ const existing = node.schemaData.content;
147
+ node.schemaData.content = existing ? `${existing}\n${text}` : text;
148
+ }
149
+ function processListItem(item, parentRef, contentTarget, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType, parentFieldAppend, activeNodeFieldAppend) {
150
+ const firstPara = item.children.find((c) => c.type === 'paragraph');
151
+ if (!firstPara) {
152
+ appendContent(contentTarget, `- ${mdastToString(item)}`);
153
+ return;
154
+ }
155
+ const rawText = mdastToString(firstPara);
156
+ // Wikilink detection: bare wikilinks populate a field without creating a node
157
+ const wikiTarget = isWikilink(rawText.trim());
158
+ if (wikiTarget) {
159
+ const linkRef = `[[${wikiTarget}]]`;
160
+ if (parentFieldAppend) {
161
+ appendParentField(parentFieldAppend.node, parentFieldAppend.field, linkRef);
162
+ return;
163
+ }
164
+ if (activeNodeFieldAppend) {
165
+ appendParentField(activeNodeFieldAppend.node, activeNodeFieldAppend.field, linkRef);
166
+ return;
167
+ }
168
+ // No field append context — fall through to node creation or content append
169
+ }
170
+ const { cleanText, fields: rawFields } = extractBracketedFields(rawText);
171
+ const fields = applyFieldMap(rawFields, fieldMap);
172
+ const type = fields.type ?? pendingType;
173
+ if (type) {
174
+ const dashIdx = cleanText.indexOf(' - ');
175
+ const title = (dashIdx >= 0 ? cleanText.slice(0, dashIdx) : cleanText).trim();
176
+ const summary = dashIdx >= 0 ? cleanText.slice(dashIdx + 3).trim() : undefined;
177
+ const schemaData = {
178
+ title,
179
+ type,
180
+ status: DEFAULT_STATUS,
181
+ ...fields,
182
+ };
183
+ if (parentRef && !parentFieldAppend)
184
+ schemaData.parent = parentRef;
185
+ if (summary)
186
+ schemaData.summary = summary;
187
+ const linkTargets = buildLinkTargets(title);
188
+ const newNode = {
189
+ label: makeLabel(title),
190
+ title,
191
+ schemaData,
192
+ linkTargets,
193
+ type,
194
+ };
195
+ nodes.push(newNode);
196
+ if (parentFieldAppend) {
197
+ appendParentField(parentFieldAppend.node, parentFieldAppend.field, `[[${linkTargets[0] ?? title}]]`);
198
+ }
199
+ const nestedParentRef = `[[${linkTargets[0] ?? title}]]`;
200
+ for (const child of item.children) {
201
+ if (child.type === 'list') {
202
+ for (const subItem of child.children) {
203
+ processListItem(subItem, nestedParentRef, newNode, nodes, makeLabel, buildLinkTargets, typeAliases, fieldMap, pendingType);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ else {
209
+ appendContent(contentTarget, `- ${rawText}`);
210
+ }
211
+ }
212
+ /**
213
+ * Extract space nodes from markdown body text.
214
+ *
215
+ * Used by both readSpaceOnAPage (single space_on_a_page file) and readSpaceDirectory
216
+ * (directory) to find embedded sub-nodes within a page's content.
217
+ */
218
+ export function extractEmbeddedNodes(body, options) {
219
+ const { pageTitle, pageType, metadata, fieldMap } = options;
220
+ const levels = metadata.hierarchy?.levels ?? [];
221
+ const hierarchy = levels.map((l) => l.type);
222
+ const relationships = metadata.relationships ?? [];
223
+ const typeAliases = metadata.typeAliases ?? {};
224
+ const isOnAPageMode = pageType === undefined || ON_A_PAGE_TYPES.includes(pageType);
225
+ const nodes = [];
226
+ // Preamble/root content sink - never added to nodes
227
+ const rootNode = {
228
+ label: '_root_',
229
+ title: '_root_',
230
+ schemaData: { type: 'space_on_a_page' },
231
+ linkTargets: [],
232
+ type: 'space_on_a_page',
233
+ };
234
+ const tree = unified().use(remarkParse).use(remarkGfm).parse(body);
235
+ // In typed-page mode: stack starts with the page's own virtual entry (depth 0).
236
+ // In space_on_a_page mode: stack starts empty (first heading has no parent).
237
+ const stack = !isOnAPageMode && pageTitle !== undefined
238
+ ? [{ depth: 0, title: pageTitle, nodeType: pageType, refTarget: pageTitle }]
239
+ : [];
240
+ let parseState = 'preamble';
241
+ let preambleNodeCount = 0;
242
+ const terminatedHeadings = [];
243
+ /**
244
+ * Returns the nearest typed parent context, skipping stack entries at depth >= headingDepth
245
+ * so that sibling headings don't masquerade as parents.
246
+ */
247
+ function getParentContextType(headingDepth) {
248
+ for (let i = stack.length - 1; i >= 0; i--) {
249
+ const entry = stack[i];
250
+ if (headingDepth !== undefined && entry.depth >= headingDepth)
251
+ continue;
252
+ if (entry.nodeType)
253
+ return entry.nodeType;
254
+ }
255
+ return undefined;
256
+ }
257
+ /** Convert a relationship definition to a normalised EmbeddingDefinition. */
258
+ function relationshipToEmbedding(rel) {
259
+ return {
260
+ parent: rel.parent,
261
+ type: rel.type,
262
+ field: rel.field,
263
+ fieldOn: rel.fieldOn,
264
+ multiple: rel.multiple,
265
+ templateFormat: rel.templateFormat ?? 'heading',
266
+ source: 'relationship',
267
+ matchers: rel.matchers,
268
+ embeddedTemplateFields: rel.embeddedTemplateFields,
269
+ };
270
+ }
271
+ /** Convert a hierarchy level to a normalised EmbeddingDefinition for child-level matching. */
272
+ function hierarchyLevelToEmbedding(level, parentType) {
273
+ return {
274
+ parent: parentType,
275
+ type: level.type,
276
+ field: level.field,
277
+ fieldOn: level.fieldOn,
278
+ multiple: level.multiple,
279
+ templateFormat: level.templateFormat ?? 'heading',
280
+ source: 'hierarchy',
281
+ matchers: level.matchers,
282
+ embeddedTemplateFields: level.embeddedTemplateFields,
283
+ };
284
+ }
285
+ /**
286
+ * Attempt to match a heading title to an embedding definition given the parent context type.
287
+ *
288
+ * Priority:
289
+ * 1. Relationships (explicit matchers or type name fallback)
290
+ * 2. Hierarchy child level (next level in hierarchy, using matchers or type name)
291
+ * 3. Hierarchy parent-level matching (immediate parent type — populates current node's field)
292
+ */
293
+ function matchEmbedding(title, parentType) {
294
+ if (!parentType)
295
+ return undefined;
296
+ const lowerTitle = title.toLowerCase();
297
+ // 1. Check relationships first (explicit matches)
298
+ for (const rel of relationships) {
299
+ if (rel.parent === parentType) {
300
+ if (rel.matchers && matchesPattern(title, lowerTitle, rel.matchers)) {
301
+ return relationshipToEmbedding(rel);
302
+ }
303
+ if (lowerTitle === rel.type.toLowerCase()) {
304
+ return relationshipToEmbedding(rel); // fallback implicit match
305
+ }
306
+ }
307
+ }
308
+ // 2. Check hierarchy child level matching
309
+ const parentIdx = hierarchy.indexOf(parentType);
310
+ if (parentIdx !== -1 && parentIdx < hierarchy.length - 1) {
311
+ const nextLevel = levels[parentIdx + 1];
312
+ if (nextLevel.matchers && matchesPattern(title, lowerTitle, nextLevel.matchers)) {
313
+ return hierarchyLevelToEmbedding(nextLevel, parentType);
314
+ }
315
+ if (lowerTitle === nextLevel.type.toLowerCase()) {
316
+ return hierarchyLevelToEmbedding(nextLevel, parentType);
317
+ }
318
+ }
319
+ // 3. Check parent-level matching: immediate parent type referenced from current node.
320
+ // e.g. if parentType is 'application' and heading is 'capabilities', match using
321
+ // the 'application' level's own field definition.
322
+ if (parentIdx > 0) {
323
+ const immediateParentLevel = levels[parentIdx - 1];
324
+ const matchesByMatchers = immediateParentLevel.matchers
325
+ ? matchesPattern(title, lowerTitle, immediateParentLevel.matchers)
326
+ : false;
327
+ const matchesByType = lowerTitle === immediateParentLevel.type.toLowerCase();
328
+ if (matchesByMatchers || matchesByType) {
329
+ const currentLevel = levels[parentIdx];
330
+ return {
331
+ parent: parentType,
332
+ type: immediateParentLevel.type,
333
+ field: currentLevel.field,
334
+ fieldOn: currentLevel.fieldOn,
335
+ multiple: currentLevel.multiple,
336
+ templateFormat: currentLevel.templateFormat ?? 'list',
337
+ source: 'hierarchy',
338
+ matchers: immediateParentLevel.matchers,
339
+ };
340
+ }
341
+ }
342
+ return undefined;
343
+ }
344
+ function makeLabel(title) {
345
+ return title;
346
+ }
347
+ /**
348
+ * Walk the stack backwards to find the deepest typed node entry (nodeType !== '').
349
+ * Untyped-heading placeholders (nodeType === '') are skipped so that typed headings
350
+ * beneath an untyped heading correctly inherit the last typed ancestor.
351
+ */
352
+ function currentParentRef() {
353
+ for (let i = stack.length - 1; i >= 0; i--) {
354
+ const entry = stack[i];
355
+ if (entry.nodeType === '')
356
+ continue;
357
+ return `[[${entry.refTarget}]]`;
358
+ }
359
+ return undefined;
360
+ }
361
+ /**
362
+ * Walk the stack from the second-to-last entry backwards to find the deepest typed node.
363
+ * Must be called AFTER the new heading is pushed to the stack so stack[-2] is its parent.
364
+ */
365
+ function resolveSemanticParent() {
366
+ for (let i = stack.length - 2; i >= 0; i--) {
367
+ if (stack[i].nodeType !== '') {
368
+ const refTarget = stack[i].refTarget;
369
+ return {
370
+ ref: `[[${refTarget}]]`,
371
+ node: nodes.find((n) => n.linkTargets.includes(refTarget)),
372
+ };
373
+ }
374
+ }
375
+ return { ref: undefined, node: undefined };
376
+ }
377
+ /**
378
+ * Emit the grouping heading node if not already emitted.
379
+ */
380
+ function flushGrouping(g) {
381
+ if (!g.emitted) {
382
+ nodes.push(g.headingNode);
383
+ g.emitted = true;
384
+ }
385
+ return g.headingNode;
386
+ }
387
+ function buildHeadingLinkTargets(rawHeadingText, title, anchor) {
388
+ if (!pageTitle) {
389
+ return [title];
390
+ }
391
+ const targets = [];
392
+ const sectionTarget = normalizeHeadingSectionTarget(rawHeadingText);
393
+ if (sectionTarget) {
394
+ targets.push(`${pageTitle}#${sectionTarget}`);
395
+ }
396
+ if (anchor) {
397
+ targets.push(`${pageTitle}#^${anchor}`);
398
+ }
399
+ return targets.length > 0 ? targets : [title];
400
+ }
401
+ function buildListItemLinkTargets(title) {
402
+ if (!pageTitle)
403
+ return [title];
404
+ const normalized = normalizeHeadingSectionTarget(title);
405
+ return normalized ? [`${pageTitle}#${normalized}`] : [title];
406
+ }
407
+ let grouping = null;
408
+ let activeNode = rootNode;
409
+ for (const child of tree.children) {
410
+ if (parseState === 'done') {
411
+ if (child.type === 'heading') {
412
+ const rawTitle = mdastToString(child);
413
+ const { cleanText: afterBracketed } = extractBracketedFields(rawTitle);
414
+ const { cleanText: title } = extractAnchor(afterBracketed);
415
+ terminatedHeadings.push(title);
416
+ }
417
+ continue;
418
+ }
419
+ if (child.type === 'thematicBreak') {
420
+ if (parseState === 'active')
421
+ parseState = 'done';
422
+ continue;
423
+ }
424
+ if (child.type === 'heading') {
425
+ const heading = child;
426
+ const depth = heading.depth;
427
+ if (depth > 5)
428
+ continue;
429
+ parseState = 'active';
430
+ const rawText = mdastToString(heading);
431
+ const { cleanText: afterBracketed, fields: rawInlineFields } = extractBracketedFields(rawText);
432
+ const inlineFields = applyFieldMap(rawInlineFields, fieldMap);
433
+ const { cleanText: title, anchor } = extractAnchor(afterBracketed);
434
+ const parentContextType = getParentContextType(depth);
435
+ const anchorType = anchor ? anchorToNodeType(anchor, hierarchy, relationships) : undefined;
436
+ const embeddingMatch = matchEmbedding(title, parentContextType);
437
+ const hasExplicitType = !!inlineFields.type;
438
+ const hasImpliedType = !!anchorType || !!embeddingMatch;
439
+ if (!isOnAPageMode && !hasExplicitType && !hasImpliedType) {
440
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
441
+ stack.pop();
442
+ }
443
+ stack.push({ depth, title, nodeType: '', refTarget: title });
444
+ // Discard any pending grouping (untyped heading has no implied type)
445
+ grouping = null;
446
+ continue;
447
+ }
448
+ if (isOnAPageMode && stack.length > 0) {
449
+ const topDepth = stack[stack.length - 1].depth;
450
+ if (depth > topDepth + 1) {
451
+ throw new Error(`Heading level skipped: jumped from H${topDepth} to H${depth} at "${title}"`);
452
+ }
453
+ }
454
+ while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
455
+ stack.pop();
456
+ }
457
+ const type = inlineFields.type ?? anchorType ?? embeddingMatch?.type ?? defaultNodeType(stack, hierarchy);
458
+ const parentRef = currentParentRef();
459
+ const schemaData = {
460
+ title,
461
+ type,
462
+ status: DEFAULT_STATUS,
463
+ ...inlineFields,
464
+ };
465
+ if (parentRef)
466
+ schemaData.parent = parentRef;
467
+ const linkTargets = buildHeadingLinkTargets(rawText, title, anchor);
468
+ const headingNode = {
469
+ label: makeLabel(title),
470
+ title,
471
+ schemaData,
472
+ linkTargets,
473
+ type,
474
+ };
475
+ // Push to stack BEFORE resolving semantic parent — stack[-2] is the correct parent.
476
+ const refTarget = linkTargets[0] ?? title;
477
+ stack.push({ depth, title, nodeType: type, refTarget });
478
+ // If this match came from a relationship/hierarchy and no explicit type was given,
479
+ // create a grouping — delay adding the node until we see the following content.
480
+ // Discard any previous grouping (not flushed — original agnostic-parse behaviour).
481
+ if (!hasExplicitType && !anchorType && embeddingMatch) {
482
+ grouping = {
483
+ definition: embeddingMatch,
484
+ semanticParent: resolveSemanticParent(),
485
+ headingNode,
486
+ emitted: false,
487
+ };
488
+ activeNode = headingNode;
489
+ }
490
+ else {
491
+ // Explicit type or anchor type: discard any pending grouping and emit immediately.
492
+ grouping = null;
493
+ nodes.push(headingNode);
494
+ activeNode = headingNode;
495
+ }
496
+ }
497
+ else if (parseState !== 'active') {
498
+ preambleNodeCount++;
499
+ }
500
+ else if (child.type === 'list') {
501
+ const parentRef = currentParentRef();
502
+ const list = child;
503
+ if (grouping) {
504
+ const { definition, semanticParent } = grouping;
505
+ const isParentSide = definition.fieldOn === 'parent';
506
+ const parentFieldAppendArg = isParentSide && semanticParent.node ? { node: semanticParent.node, field: definition.field } : undefined;
507
+ // Parent-level match: definition.type is an ancestor of definition.parent in hierarchy.
508
+ // e.g. definition.type='capabilities', definition.parent='application' → capabilities is above application.
509
+ const typeIdx = hierarchy.indexOf(definition.type);
510
+ const parentIdx = hierarchy.indexOf(definition.parent);
511
+ const isParentLevelMatch = definition.source === 'hierarchy' && typeIdx !== -1 && parentIdx !== -1 && typeIdx < parentIdx;
512
+ const activeNodeFieldAppendArg = isParentLevelMatch && semanticParent.node
513
+ ? { node: semanticParent.node, field: definition.field }
514
+ : undefined;
515
+ for (const item of list.children) {
516
+ processListItem(item, isParentSide ? undefined : semanticParent.ref, grouping.headingNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap, definition.type, parentFieldAppendArg, activeNodeFieldAppendArg);
517
+ }
518
+ grouping = null;
519
+ }
520
+ else {
521
+ for (const item of list.children) {
522
+ processListItem(item, parentRef, activeNode, nodes, makeLabel, buildListItemLinkTargets, typeAliases, fieldMap);
523
+ }
524
+ }
525
+ }
526
+ else if (child.type === 'table') {
527
+ const parentRef = currentParentRef();
528
+ const parentContextType = getParentContextType();
529
+ const table = child;
530
+ if (table.children && table.children.length > 0) {
531
+ const headerRow = table.children[0];
532
+ const rows = table.children.slice(1);
533
+ const columnNames = headerRow.children.map((cell) => mdastToString(cell).trim());
534
+ const firstColName = columnNames[0]?.toLowerCase();
535
+ let rowTypeStr;
536
+ let activeMatch = grouping?.definition;
537
+ if (activeMatch) {
538
+ rowTypeStr = activeMatch.type;
539
+ }
540
+ else if (firstColName) {
541
+ if (hierarchy.includes(firstColName) || typeAliases[firstColName]) {
542
+ rowTypeStr = firstColName;
543
+ }
544
+ else {
545
+ const rootRel = matchEmbedding(firstColName, parentContextType);
546
+ if (rootRel) {
547
+ rowTypeStr = rootRel.type;
548
+ activeMatch = rootRel;
549
+ }
550
+ }
551
+ }
552
+ if (!rowTypeStr && activeNode !== rootNode && activeNode.schemaData.type) {
553
+ const contextAsParentRel = matchEmbedding(firstColName || '', activeNode.schemaData.type);
554
+ if (contextAsParentRel) {
555
+ rowTypeStr = contextAsParentRel.type;
556
+ activeMatch = contextAsParentRel;
557
+ }
558
+ }
559
+ if (rowTypeStr) {
560
+ let semanticParentRef = parentRef;
561
+ let semanticParentNode;
562
+ if (grouping) {
563
+ // Use already-resolved semantic parent from grouping
564
+ semanticParentRef = grouping.semanticParent.ref;
565
+ semanticParentNode = grouping.semanticParent.node;
566
+ }
567
+ else if (activeMatch || rowTypeStr === parentContextType) {
568
+ for (let i = stack.length - 2; i >= 0; i--) {
569
+ if (stack[i].nodeType !== '') {
570
+ semanticParentRef = `[[${stack[i].refTarget}]]`;
571
+ const refTarget = stack[i].refTarget;
572
+ semanticParentNode = nodes.find((n) => n.linkTargets.includes(refTarget));
573
+ break;
574
+ }
575
+ }
576
+ }
577
+ const isParentSide = activeMatch?.fieldOn === 'parent';
578
+ const tableParentFieldAppend = isParentSide && semanticParentNode && activeMatch?.field
579
+ ? { node: semanticParentNode, field: activeMatch.field }
580
+ : undefined;
581
+ for (const row of rows) {
582
+ const cells = row.children;
583
+ if (!cells || cells.length === 0)
584
+ continue;
585
+ const titleRaw = mdastToString(cells[0]).trim();
586
+ const { cleanText: title, fields: rawInlineFields } = extractBracketedFields(titleRaw);
587
+ const inlineFields = applyFieldMap(rawInlineFields, fieldMap);
588
+ const schemaData = {
589
+ title,
590
+ type: rowTypeStr,
591
+ status: DEFAULT_STATUS,
592
+ ...inlineFields,
593
+ };
594
+ if (semanticParentRef && !tableParentFieldAppend)
595
+ schemaData.parent = semanticParentRef;
596
+ for (let i = 1; i < columnNames.length; i++) {
597
+ const colName = columnNames[i];
598
+ const cellContent = i < cells.length ? mdastToString(cells[i]).trim() : '';
599
+ if (colName && cellContent) {
600
+ const mappedColName = fieldMap?.[colName] ?? colName;
601
+ schemaData[mappedColName] = cellContent;
602
+ }
603
+ }
604
+ const linkTargets = buildListItemLinkTargets(title);
605
+ const rowNode = {
606
+ label: makeLabel(title),
607
+ title,
608
+ schemaData,
609
+ linkTargets,
610
+ type: rowTypeStr,
611
+ };
612
+ nodes.push(rowNode);
613
+ if (tableParentFieldAppend) {
614
+ appendParentField(tableParentFieldAppend.node, tableParentFieldAppend.field, `[[${linkTargets[0] ?? title}]]`);
615
+ }
616
+ }
617
+ grouping = null;
618
+ }
619
+ else {
620
+ appendContent(activeNode, mdastToString(child));
621
+ }
622
+ }
623
+ }
624
+ else {
625
+ // For any other content (paragraph, code, etc), if we had a grouping,
626
+ // it means the heading itself is the node. Flush it now.
627
+ if (grouping) {
628
+ flushGrouping(grouping);
629
+ grouping = null;
630
+ }
631
+ if (child.type === 'paragraph') {
632
+ const rawText = mdastToString(child);
633
+ const { cleanText: afterBracketed, fields: bracketedFields } = extractBracketedFields(rawText);
634
+ const { remainingText, fields: unbracketedFields } = extractUnbracketedFields(afterBracketed);
635
+ const allFields = applyFieldMap({ ...unbracketedFields, ...bracketedFields }, fieldMap);
636
+ if ('type' in allFields) {
637
+ throw new Error(`Type override via paragraph field is not supported at "${activeNode.schemaData.title ?? activeNode.label}". ` +
638
+ `Put [type:: ${allFields.type}] directly in the heading text.`);
639
+ }
640
+ Object.assign(activeNode.schemaData, allFields);
641
+ if (remainingText)
642
+ appendContent(activeNode, remainingText);
643
+ }
644
+ else if (child.type === 'code' && child.lang?.trim() === 'yaml') {
645
+ const code = child;
646
+ const parsed = yamlLoad(code.value);
647
+ if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
648
+ Object.assign(activeNode.schemaData, coerceDates(applyFieldMap(parsed, fieldMap)));
649
+ }
650
+ else if (Array.isArray(parsed)) {
651
+ throw new Error(`YAML block must be an object at "${activeNode.label}".`);
652
+ }
653
+ else {
654
+ appendContent(activeNode, code.value);
655
+ }
656
+ }
657
+ else {
658
+ appendContent(activeNode, mdastToString(child));
659
+ }
660
+ }
661
+ }
662
+ return { nodes, preambleNodeCount, terminatedHeadings };
663
+ }
@@ -0,0 +1,7 @@
1
+ import type { ParseResult, PluginContext } from '../util';
2
+ type ReadSpaceDirectoryOptions = {
3
+ includeOnAPageFiles?: boolean;
4
+ };
5
+ export declare function readSpaceOnAPage(context: PluginContext): ParseResult;
6
+ export declare function readSpaceDirectory(context: PluginContext, options?: ReadSpaceDirectoryOptions): Promise<ParseResult>;
7
+ export {};