@agentshouse/mdmodel 0.1.0-c4fe1bc82bc55 → 0.1.0-c520180b7a33f

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 (137) hide show
  1. package/API.md +264 -250
  2. package/README.md +21 -22
  3. package/dist/blocks.d.ts.map +1 -1
  4. package/dist/blocks.js +35 -20
  5. package/dist/definition.d.ts +17 -1
  6. package/dist/definition.d.ts.map +1 -1
  7. package/dist/definition.js +321 -45
  8. package/dist/divide.d.ts.map +1 -1
  9. package/dist/divide.js +27 -17
  10. package/dist/document.d.ts +6 -3
  11. package/dist/document.d.ts.map +1 -1
  12. package/dist/document.js +326 -171
  13. package/dist/event.d.ts +78 -0
  14. package/dist/event.d.ts.map +1 -0
  15. package/dist/event.js +230 -0
  16. package/dist/fields.d.ts +2 -0
  17. package/dist/fields.d.ts.map +1 -1
  18. package/dist/fields.js +7 -0
  19. package/dist/index.d.ts +24 -10
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +14 -7
  22. package/dist/kinds.d.ts +19 -0
  23. package/dist/kinds.d.ts.map +1 -0
  24. package/dist/kinds.js +161 -0
  25. package/dist/links.d.ts +3 -0
  26. package/dist/links.d.ts.map +1 -1
  27. package/dist/links.js +108 -51
  28. package/dist/paths.d.ts +10 -1
  29. package/dist/paths.d.ts.map +1 -1
  30. package/dist/paths.js +118 -12
  31. package/dist/pattern.d.ts +10 -0
  32. package/dist/pattern.d.ts.map +1 -0
  33. package/dist/pattern.js +355 -0
  34. package/dist/project.d.ts +1 -0
  35. package/dist/project.d.ts.map +1 -1
  36. package/dist/project.js +38 -15
  37. package/dist/query/language.d.ts +223 -0
  38. package/dist/query/language.d.ts.map +1 -0
  39. package/dist/query/language.js +565 -0
  40. package/dist/query/match.d.ts +14 -0
  41. package/dist/query/match.d.ts.map +1 -0
  42. package/dist/query/match.js +121 -0
  43. package/dist/query/rows.d.ts +32 -0
  44. package/dist/query/rows.d.ts.map +1 -0
  45. package/dist/query/rows.js +161 -0
  46. package/dist/query/sql.d.ts +39 -0
  47. package/dist/query/sql.d.ts.map +1 -0
  48. package/dist/query/sql.js +279 -0
  49. package/dist/references.d.ts +2 -2
  50. package/dist/references.d.ts.map +1 -1
  51. package/dist/references.js +6 -1
  52. package/dist/reserved.d.ts +5 -0
  53. package/dist/reserved.d.ts.map +1 -0
  54. package/dist/reserved.js +20 -0
  55. package/dist/schema.d.ts +1 -0
  56. package/dist/schema.d.ts.map +1 -1
  57. package/dist/schema.js +57 -6
  58. package/dist/types.d.ts +57 -2
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/validate.d.ts +31 -2
  61. package/dist/validate.d.ts.map +1 -1
  62. package/dist/validate.js +400 -115
  63. package/dist/yaml.d.ts +1 -2
  64. package/dist/yaml.d.ts.map +1 -1
  65. package/dist/yaml.js +75 -32
  66. package/fixtures/README.md +27 -7
  67. package/fixtures/cases/conditional-section/context.json +1 -0
  68. package/fixtures/cases/conditional-section/expected.json +6 -0
  69. package/fixtures/cases/conditional-section/library/questions/hosting.md +9 -0
  70. package/fixtures/cases/conditional-section/library/questions/payments.md +12 -0
  71. package/fixtures/cases/conditional-section/schema.yaml +14 -0
  72. package/fixtures/cases/cross-room-link/context.json +1 -0
  73. package/fixtures/cases/cross-room-link/expected.json +1 -0
  74. package/fixtures/cases/cross-room-link/library/clients/acme.md +6 -0
  75. package/fixtures/cases/cross-room-link/schema.yaml +3 -0
  76. package/fixtures/cases/cross-room-reference/context.json +1 -0
  77. package/fixtures/cases/cross-room-reference/expected.json +1 -0
  78. package/fixtures/cases/cross-room-reference/library/clients/acme.md +9 -0
  79. package/fixtures/cases/cross-room-reference/schema.yaml +9 -0
  80. package/fixtures/cases/link-into-a-reserved-name/context.json +1 -0
  81. package/fixtures/cases/link-into-a-reserved-name/expected.json +5 -0
  82. package/fixtures/cases/link-into-a-reserved-name/library/notes/a.md +6 -0
  83. package/fixtures/cases/link-into-a-reserved-name/schema.yaml +3 -0
  84. package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/context.json +1 -0
  85. package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/expected.json +5 -0
  86. package/fixtures/cases/link-into-a-reserved-name-from-an-untyped-document/library/notes/b.md +5 -0
  87. package/fixtures/cases/member-on-an-untyped-document/context.json +1 -0
  88. package/fixtures/cases/member-on-an-untyped-document/expected.json +6 -0
  89. package/fixtures/cases/member-on-an-untyped-document/library/note.md +4 -0
  90. package/fixtures/cases/member-on-another-type/context.json +1 -0
  91. package/fixtures/cases/member-on-another-type/expected.json +6 -0
  92. package/fixtures/cases/member-on-another-type/library/note.md +4 -0
  93. package/fixtures/cases/member-on-another-type/schema.yaml +7 -0
  94. package/fixtures/cases/reference-into-a-reserved-name/context.json +1 -0
  95. package/fixtures/cases/reference-into-a-reserved-name/expected.json +6 -0
  96. package/fixtures/cases/reference-into-a-reserved-name/library/notes/a.md +4 -0
  97. package/fixtures/cases/reference-into-a-reserved-name/library/settings/brief.md +1 -0
  98. package/fixtures/cases/reference-into-a-reserved-name/schema.yaml +3 -0
  99. package/fixtures/cases/room-md-declares-a-type/context.json +1 -0
  100. package/fixtures/cases/room-md-declares-a-type/expected.json +6 -0
  101. package/fixtures/cases/room-md-declares-a-type/library/ROOM.md +5 -0
  102. package/fixtures/cases/typed-path-names-another-type/context.json +11 -0
  103. package/fixtures/cases/typed-path-names-another-type/expected.json +6 -0
  104. package/fixtures/cases/typed-path-names-another-type/library/settings/prefs/one.md +3 -0
  105. package/fixtures/cases/typed-path-names-another-type/schema.yaml +6 -0
  106. package/fixtures/cases/typed-path-names-its-type/context.json +11 -0
  107. package/fixtures/cases/typed-path-names-its-type/expected.json +3 -0
  108. package/fixtures/cases/typed-path-names-its-type/library/settings/prefs/deeper/two.md +1 -0
  109. package/fixtures/cases/typed-path-names-its-type/library/settings/prefs/one.md +4 -0
  110. package/fixtures/cases/typed-path-names-its-type/schema.yaml +6 -0
  111. package/fixtures/cases/typed-path-names-no-type/context.json +11 -0
  112. package/fixtures/cases/typed-path-names-no-type/expected.json +6 -0
  113. package/fixtures/cases/typed-path-names-no-type/library/settings/prefs/one.md +1 -0
  114. package/fixtures/cases/typed-path-names-no-type/schema.yaml +6 -0
  115. package/fixtures/cases/unnamed-typed-path-checks-its-fields/context.json +11 -0
  116. package/fixtures/cases/unnamed-typed-path-checks-its-fields/expected.json +6 -0
  117. package/fixtures/cases/unnamed-typed-path-checks-its-fields/library/rooms/team/tasks/plan.md +4 -0
  118. package/fixtures/cases/unnamed-typed-path-checks-its-fields/schema.yaml +5 -0
  119. package/fixtures/cases/unnamed-typed-path-names-a-type/context.json +11 -0
  120. package/fixtures/cases/unnamed-typed-path-names-a-type/expected.json +6 -0
  121. package/fixtures/cases/unnamed-typed-path-names-a-type/library/rooms/team/tasks/plan.md +6 -0
  122. package/fixtures/cases/unnamed-typed-path-names-a-type/schema.yaml +5 -0
  123. package/fixtures/cases/unnamed-typed-path-reads-its-type/context.json +11 -0
  124. package/fixtures/cases/unnamed-typed-path-reads-its-type/expected.json +3 -0
  125. package/fixtures/cases/unnamed-typed-path-reads-its-type/library/rooms/team/plan.md +1 -0
  126. package/fixtures/cases/unnamed-typed-path-reads-its-type/library/rooms/team/tasks/plan.md +5 -0
  127. package/fixtures/cases/unnamed-typed-path-reads-its-type/schema.yaml +5 -0
  128. package/fixtures/cases/unnamed-typed-path-without-a-body/context.json +11 -0
  129. package/fixtures/cases/unnamed-typed-path-without-a-body/expected.json +5 -0
  130. package/fixtures/cases/unnamed-typed-path-without-a-body/library/rooms/team/tasks/plan.md +5 -0
  131. package/fixtures/cases/unnamed-typed-path-without-a-body/schema.yaml +5 -0
  132. package/fixtures/division.json +26 -26
  133. package/fixtures/queries.json +913 -0
  134. package/package.json +10 -3
  135. package/dist/condition.d.ts +0 -149
  136. package/dist/condition.d.ts.map +0 -1
  137. package/dist/condition.js +0 -491
package/dist/document.js CHANGED
@@ -1,24 +1,35 @@
1
1
  import { ARRAY_INDEX, pointerSegments } from './definition.js';
2
- import { hrefOccurrences } from './links.js';
3
- import { relative, retarget, SHELL_ROOMS, shellTarget, target } from './paths.js';
2
+ import { destinations, hrefOccurrences, linkInventory, } from './links.js';
3
+ import { destinationPath, relative, retarget, roomPath, roomTarget, SHELL_ROOMS, shellTarget, target, } from './paths.js';
4
4
  import { references } from './references.js';
5
5
  import { headingSpans, sectionsRecord } from './sections.js';
6
6
  import { refusal } from './refuse.js';
7
7
  import { bodyStartAfterYaml, bodyStartOf, equalValue, extractFrontmatter, frontmatterValue, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError, } from './yaml.js';
8
- const internals = new WeakMap();
8
+ const HELD = Symbol('held');
9
+ function heldOf(document) {
10
+ return document[HELD];
11
+ }
12
+ export function inventoryOf(document) {
13
+ const held = heldOf(document);
14
+ if (held?.inventory !== undefined)
15
+ return held.inventory;
16
+ const inventory = linkInventory(document.content.slice(bodyStartOf(document.content)));
17
+ if (held !== undefined)
18
+ held.inventory = inventory;
19
+ return inventory;
20
+ }
9
21
  function refuse(file, rule, location) {
10
22
  return location === undefined ? refusal(file, rule) : refusal(file, rule, { location });
11
23
  }
12
24
  function freezeDocument(content, type, fields, preamble, sections, internal) {
13
- const document = Object.freeze({
25
+ const held = { internal, inventory: undefined };
26
+ return Object.freeze(Object.defineProperty({
14
27
  content,
15
28
  type,
16
29
  fields: Object.freeze(fields),
17
30
  preamble,
18
31
  sections: Object.freeze(sections),
19
- });
20
- internals.set(document, internal);
21
- return document;
32
+ }, HELD, { value: held }));
22
33
  }
23
34
  function declaredType(fields) {
24
35
  const value = fields.type;
@@ -28,19 +39,10 @@ function declaredType(fields) {
28
39
  return undefined;
29
40
  return value;
30
41
  }
31
- export function parse(content, file = '') {
42
+ function opened(content, file) {
32
43
  const fence = extractFrontmatter(content);
33
- if (fence.kind === 'none') {
34
- const spans = headingSpans(content, 0);
35
- return {
36
- ok: true,
37
- document: freezeDocument(content, undefined, {}, content.slice(0, spans[0]?.headingStart), sectionsRecord(spans, content), {
38
- root: null,
39
- yamlEnd: 0,
40
- bodyStart: 0,
41
- }),
42
- };
43
- }
44
+ if (fence.kind === 'none')
45
+ return { ok: true, root: null, fields: {}, yamlEnd: 0 };
44
46
  if (fence.kind === 'unclosed')
45
47
  return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
46
48
  try {
@@ -49,17 +51,7 @@ export function parse(content, file = '') {
49
51
  if (fields === null || typeof fields !== 'object' || Array.isArray(fields)) {
50
52
  return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
51
53
  }
52
- const bodyStart = bodyStartAfterYaml(content, fence.yamlEnd);
53
- const spans = headingSpans(content, bodyStart);
54
- const record = fields;
55
- return {
56
- ok: true,
57
- document: freezeDocument(content, declaredType(record), record, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), {
58
- root,
59
- yamlEnd: fence.yamlEnd,
60
- bodyStart,
61
- }),
62
- };
54
+ return { ok: true, root, fields: fields, yamlEnd: fence.yamlEnd };
63
55
  }
64
56
  catch (error) {
65
57
  if (error instanceof YamlError)
@@ -67,66 +59,29 @@ export function parse(content, file = '') {
67
59
  throw error;
68
60
  }
69
61
  }
70
- function splice(content, start, end, insert) {
71
- return content.slice(0, start) + insert + content.slice(end);
72
- }
73
- function applyField(document, name, value, prior) {
74
- const internal = internals.get(document);
75
- if (internal === undefined || internal.root === null) {
76
- return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };
77
- }
78
- if (!equalValue(document.fields[name], prior)) {
79
- return { outcome: 'stale', document };
80
- }
81
- if (!frontmatterValue(value)) {
82
- return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };
83
- }
84
- let nextContent;
85
- const existing = mapEntry(internal.root, name);
86
- if (existing !== undefined) {
87
- nextContent = splice(document.content, existing.value.start, existing.value.end, serializeJs(value));
88
- }
89
- else if (document.content[internal.root.start] === '{') {
90
- nextContent = splice(document.content, internal.root.end - 1, internal.root.end - 1, `${internal.root.entries.length === 0 ? '' : ', '}${serializeKey(name)}: ${serializeJs(value)}`);
91
- }
92
- else {
93
- const nl = lineEnding(document.content);
94
- const lineStart = document.content.lastIndexOf('\n', internal.root.start - 1) + 1;
95
- const indent = document.content.slice(lineStart, internal.root.start);
96
- const line = `${indent}${serializeKey(name)}: ${serializeJs(value)}${nl}`;
97
- const before = document.content.slice(0, internal.yamlEnd);
98
- const prefix = before.endsWith('\n') || before.endsWith('\r\n') ? '' : nl;
99
- nextContent = splice(document.content, internal.yamlEnd, internal.yamlEnd, `${prefix}${line}`);
100
- }
101
- const parsed = parse(nextContent);
102
- if (!parsed.ok)
103
- return { outcome: 'refuse', refusal: parsed.refusal };
104
- return { outcome: 'applied', document: parsed.document };
105
- }
106
- function applySection(document, name, value, prior) {
107
- const internal = internals.get(document);
108
- if (internal === undefined)
109
- return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };
110
- const spans = headingSpans(document.content, internal.bodyStart);
111
- const span = spans.find((item) => item.name === name);
112
- if (span === undefined)
113
- return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };
114
- const current = document.content.slice(span.bodyStart, span.bodyEnd);
115
- if (current !== prior)
116
- return { outcome: 'stale', document };
117
- const parsed = parse(splice(document.content, span.bodyStart, span.bodyEnd, value));
118
- if (!parsed.ok)
119
- return { outcome: 'refuse', refusal: parsed.refusal };
120
- return { outcome: 'applied', document: parsed.document };
62
+ export function parseFrontmatter(content, file = '') {
63
+ const read = opened(content, file);
64
+ if (!read.ok)
65
+ return read;
66
+ return {
67
+ ok: true,
68
+ frontmatter: Object.freeze({
69
+ content,
70
+ type: declaredType(read.fields),
71
+ fields: Object.freeze(read.fields),
72
+ }),
73
+ };
121
74
  }
122
- function applyPreamble(document, value, prior) {
123
- if (document.preamble !== prior)
124
- return { outcome: 'stale', document };
125
- const start = bodyStartOf(document.content);
126
- const parsed = parse(splice(document.content, start, start + document.preamble.length, value));
127
- if (!parsed.ok)
128
- return { outcome: 'refuse', refusal: parsed.refusal };
129
- return { outcome: 'applied', document: parsed.document };
75
+ export function parse(content, file = '') {
76
+ const read = opened(content, file);
77
+ if (!read.ok)
78
+ return read;
79
+ const bodyStart = read.root === null ? 0 : bodyStartAfterYaml(content, read.yamlEnd);
80
+ const spans = headingSpans(content, bodyStart);
81
+ return {
82
+ ok: true,
83
+ document: freezeDocument(content, declaredType(read.fields), read.fields, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), { root: read.root, yamlEnd: read.yamlEnd, bodyStart }),
84
+ };
130
85
  }
131
86
  function fitsBare(to) {
132
87
  if (to === '' || to.startsWith('<'))
@@ -155,58 +110,34 @@ function destinationText(to, bracketed) {
155
110
  return `<${to}>`;
156
111
  return null;
157
112
  }
158
- function relinked(content, bodyStart, href, to) {
159
- const written = retarget(href, to);
160
- let rewritten = '';
161
- let cursor = 0;
162
- for (const match of hrefOccurrences(content.slice(bodyStart))) {
163
- if (match.href !== href)
164
- continue;
165
- const text = destinationText(written, match.bracketed);
166
- if (text === null)
167
- return refuse('', 'unwritable-link', written);
168
- rewritten += content.slice(cursor, bodyStart + match.start);
169
- rewritten += text;
170
- cursor = bodyStart + match.end;
171
- }
172
- return rewritten + content.slice(cursor);
173
- }
174
- function shellRelinked(content, bodyStart, file, handle) {
175
- const hrefs = new Set(hrefOccurrences(content.slice(bodyStart)).map(({ href }) => href));
176
- let held = content;
177
- for (const href of hrefs) {
113
+ function shellRelinked(body, file, handle) {
114
+ const occurrences = hrefOccurrences(body);
115
+ const written = new Map();
116
+ for (const href of new Set(occurrences.map((occurrence) => occurrence.href))) {
178
117
  const path = shellTarget(href, handle);
179
118
  if (path === null)
180
119
  continue;
181
- const text = relinked(held, bodyStart, href, relative(file, path));
182
- if (typeof text !== 'string')
183
- return text;
184
- held = text;
120
+ const to = retarget(href, relative(file, path));
121
+ if (destinationText(to, false) === null)
122
+ return refuse('', 'unwritable-link', to);
123
+ written.set(href, to);
185
124
  }
186
- return held;
187
- }
188
- function reparsed(document, content) {
189
- if (typeof content !== 'string')
190
- return { outcome: 'refuse', refusal: content };
191
- if (content === document.content)
192
- return { outcome: 'applied', document };
193
- const parsed = parse(content);
194
- if (!parsed.ok)
195
- return { outcome: 'refuse', refusal: parsed.refusal };
196
- return { outcome: 'applied', document: parsed.document };
197
- }
198
- function applyLink(document, href, to) {
199
- const content = document.content;
200
- const bodyStart = bodyStartOf(content);
201
- if (!hrefOccurrences(content.slice(bodyStart)).some((occurrence) => occurrence.href === href))
202
- return { outcome: 'stale', document };
203
- return reparsed(document, relinked(content, bodyStart, href, to));
125
+ let rewritten = '';
126
+ let cursor = 0;
127
+ for (const match of occurrences) {
128
+ const to = written.get(match.href);
129
+ if (to === undefined)
130
+ continue;
131
+ rewritten += body.slice(cursor, match.start) + destinationText(to, match.bracketed);
132
+ cursor = match.end;
133
+ }
134
+ return rewritten + body.slice(cursor);
204
135
  }
205
- function nodeAt(root, pointer) {
136
+ function nodeAt(root, pointer, entryOf = mapEntry) {
206
137
  let node = root;
207
138
  for (const key of pointerSegments(pointer)) {
208
139
  if (node?.kind === 'map')
209
- node = mapEntry(node, key)?.value;
140
+ node = entryOf(node, key)?.value;
210
141
  else if (node?.kind === 'seq' && ARRAY_INDEX.test(key))
211
142
  node = node.items[Number(key)];
212
143
  else
@@ -221,37 +152,36 @@ function scalarText(source, value) {
221
152
  return JSON.stringify(value);
222
153
  return serializeJs(value);
223
154
  }
224
- function applyReference(document, pointer, value, prior) {
225
- const root = internals.get(document)?.root;
226
- const node = root === null || root === undefined ? undefined : nodeAt(root, pointer);
227
- if (node?.kind !== 'scalar' || node.value !== prior)
228
- return { outcome: 'stale', document };
229
- const text = scalarText(document.content[node.start], value);
230
- const parsed = parse(splice(document.content, node.start, node.end, text));
231
- if (!parsed.ok)
232
- return { outcome: 'refuse', refusal: parsed.refusal };
233
- return { outcome: 'applied', document: parsed.document };
234
- }
235
- export function relativize(document, file, handle, definition) {
236
- let held = document;
155
+ export function relativize(document, file, handle, definition, kept = new Set()) {
156
+ const relativized = (href) => {
157
+ const path = shellTarget(href, handle);
158
+ return path === null || kept.has(`${SHELL_ROOMS}${handle}/${path}`) ? null : path;
159
+ };
160
+ const changes = [];
237
161
  for (const { pointer, value } of definition === undefined
238
162
  ? []
239
163
  : references(document, definition)) {
240
- const path = shellTarget(value, handle);
241
- if (path === null)
242
- continue;
243
- const result = applyReference(held, pointer, retarget(value, relative(file, path)), value);
244
- if (result.outcome !== 'applied')
245
- return result;
246
- held = result.document;
164
+ const path = relativized(value);
165
+ if (path !== null)
166
+ changes.push({
167
+ kind: 'reference',
168
+ pointer,
169
+ value: retarget(value, relative(file, path)),
170
+ prior: value,
171
+ });
172
+ }
173
+ for (const href of new Set(destinations(inventoryOf(document)).map(({ href }) => href))) {
174
+ const path = relativized(href);
175
+ if (path !== null)
176
+ changes.push({ kind: 'link', href, to: relative(file, path) });
247
177
  }
248
- return reparsed(held, shellRelinked(held.content, bodyStartOf(held.content), file, handle));
178
+ return apply(document, changes);
249
179
  }
250
180
  export function relativizeBody(body, file, handle) {
251
- const relinkedBody = shellRelinked(body, 0, file, handle);
181
+ const relinkedBody = shellRelinked(body, file, handle);
252
182
  if (typeof relinkedBody !== 'string')
253
183
  return { outcome: 'refuse', refusal: relinkedBody };
254
- const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => href.startsWith(SHELL_ROOMS));
184
+ const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => destinationPath(href)?.startsWith(SHELL_ROOMS) === true && roomTarget(href) === null);
255
185
  if (elsewhere !== undefined)
256
186
  return { outcome: 'refuse', refusal: refuse(file, 'unresolved-link', elsewhere.href) };
257
187
  return { outcome: 'applied', body: relinkedBody };
@@ -260,11 +190,11 @@ export function relinkBody(body, file, moves) {
260
190
  let rewritten = '';
261
191
  let cursor = 0;
262
192
  for (const match of hrefOccurrences(body)) {
263
- const from = target(file, match.href);
193
+ const from = target(file, match.href) ?? roomPath(match.href);
264
194
  const to = from === null ? undefined : moves.get(from);
265
195
  if (to === undefined)
266
196
  continue;
267
- const written = retarget(match.href, relative(file, to));
197
+ const written = retarget(match.href, from.startsWith(SHELL_ROOMS) || to.startsWith(SHELL_ROOMS) ? to : relative(file, to));
268
198
  const text = destinationText(written, match.bracketed);
269
199
  if (text === null)
270
200
  return { outcome: 'refuse', refusal: refuse(file, 'unwritable-link', written) };
@@ -273,10 +203,10 @@ export function relinkBody(body, file, moves) {
273
203
  }
274
204
  return { outcome: 'applied', body: rewritten + body.slice(cursor) };
275
205
  }
276
- function linksIn(text, file) {
206
+ function linksIn(occurrences, file) {
277
207
  const found = [];
278
208
  const seen = new Set();
279
- for (const occurrence of hrefOccurrences(text)) {
209
+ for (const occurrence of occurrences) {
280
210
  if (seen.has(occurrence.href))
281
211
  continue;
282
212
  const resolved = target(file, occurrence.href);
@@ -288,25 +218,250 @@ function linksIn(text, file) {
288
218
  return found;
289
219
  }
290
220
  export function links(document, file) {
291
- return linksIn(document.content.slice(bodyStartOf(document.content)), file);
221
+ return linksIn(destinations(inventoryOf(document)), file);
292
222
  }
293
223
  export function bodyLinks(body, file) {
294
- return linksIn(body, file);
224
+ return linksIn(hrefOccurrences(body), file);
295
225
  }
296
- export function apply(document, change) {
226
+ function internalOf(document) {
227
+ const held = heldOf(document);
228
+ if (held !== undefined)
229
+ return held.internal;
230
+ const read = opened(document.content, '');
231
+ if (!read.ok)
232
+ return read.refusal;
233
+ return {
234
+ root: read.root,
235
+ yamlEnd: read.yamlEnd,
236
+ bodyStart: read.root === null ? 0 : bodyStartAfterYaml(document.content, read.yamlEnd),
237
+ };
238
+ }
239
+ function partOf(change) {
240
+ if (change.kind === 'field' || change.kind === 'section')
241
+ return `${change.kind} ${change.name}`;
297
242
  if (change.kind === 'link')
298
- return applyLink(document, change.href, change.to);
243
+ return `link ${change.href}`;
299
244
  if (change.kind === 'reference')
300
- return applyReference(document, change.pointer, change.value, change.prior);
301
- if (change.kind === 'preamble')
302
- return applyPreamble(document, change.value, change.prior);
303
- if (change.kind === 'field')
304
- return applyField(document, change.name, change.value, change.prior);
305
- return applySection(document, change.name, change.value, change.prior);
245
+ return `reference ${change.pointer}`;
246
+ return 'preamble';
247
+ }
248
+ function planner(document, internal) {
249
+ const content = document.content;
250
+ const keyed = new Map();
251
+ let spans;
252
+ let occurrences;
253
+ let ending;
254
+ let added = 0;
255
+ const entryOf = (map, key) => {
256
+ let index = keyed.get(map);
257
+ if (index === undefined) {
258
+ index = new Map();
259
+ for (const entry of map.entries)
260
+ if (!index.has(entry.key))
261
+ index.set(entry.key, entry);
262
+ keyed.set(map, index);
263
+ }
264
+ return index.get(key);
265
+ };
266
+ const field = (name, value, prior, at) => {
267
+ const root = internal.root;
268
+ if (root === null)
269
+ return refuse('', 'frontmatter-fence', name);
270
+ if (!equalValue(document.fields[name], prior))
271
+ return 'stale';
272
+ if (!frontmatterValue(value))
273
+ return refuse('', 'frontmatter-fence', name);
274
+ const existing = entryOf(root, name);
275
+ if (existing !== undefined) {
276
+ const held = existing.value;
277
+ const inline = held.start > existing.after && !/[\r\n]/.test(content.slice(existing.after, held.start));
278
+ return inline
279
+ ? [{ start: held.start, end: held.end, text: serializeJs(value), at }]
280
+ : [{ start: existing.after, end: held.end, text: ` ${serializeJs(value)}`, at }];
281
+ }
282
+ const entry = `${serializeKey(name)}: ${serializeJs(value)}`;
283
+ added += 1;
284
+ if (content[root.start] === '{') {
285
+ const separator = root.entries.length === 0 && added === 1 ? '' : ', ';
286
+ return [{ start: root.end - 1, end: root.end - 1, text: `${separator}${entry}`, at }];
287
+ }
288
+ const lineStart = content.lastIndexOf('\n', root.start - 1) + 1;
289
+ ending ??= lineEnding(content);
290
+ const line = `${content.slice(lineStart, root.start)}${entry}${ending}`;
291
+ return [{ start: internal.yamlEnd, end: internal.yamlEnd, text: line, at, entry: true }];
292
+ };
293
+ const section = (name, value, prior, at) => {
294
+ if (spans === undefined) {
295
+ spans = new Map();
296
+ for (const span of headingSpans(content, internal.bodyStart))
297
+ if (!spans.has(span.name))
298
+ spans.set(span.name, span);
299
+ }
300
+ const span = spans.get(name);
301
+ if (span === undefined)
302
+ return refuse('', 'required-section', name);
303
+ if (content.slice(span.bodyStart, span.bodyEnd) !== prior)
304
+ return 'stale';
305
+ return [{ start: span.bodyStart, end: span.bodyEnd, text: value, at }];
306
+ };
307
+ const link = (href, to, at) => {
308
+ if (occurrences === undefined) {
309
+ occurrences = new Map();
310
+ for (const occurrence of destinations(inventoryOf(document))) {
311
+ const same = occurrences.get(occurrence.href);
312
+ if (same === undefined)
313
+ occurrences.set(occurrence.href, [occurrence]);
314
+ else
315
+ same.push(occurrence);
316
+ }
317
+ }
318
+ const found = occurrences.get(href);
319
+ if (found === undefined)
320
+ return 'stale';
321
+ const written = retarget(href, to);
322
+ if (destinationText(written, false) === null)
323
+ return refuse('', 'unwritable-link', written);
324
+ return found.map((occurrence) => ({
325
+ start: internal.bodyStart + occurrence.start,
326
+ end: internal.bodyStart + occurrence.end,
327
+ text: destinationText(written, occurrence.bracketed),
328
+ at,
329
+ }));
330
+ };
331
+ const reference = (pointer, value, prior, at) => {
332
+ const node = internal.root === null ? undefined : nodeAt(internal.root, pointer, entryOf);
333
+ if (node?.kind !== 'scalar' || node.value !== prior)
334
+ return 'stale';
335
+ return [
336
+ { start: node.start, end: node.end, text: scalarText(content[node.start], value), at },
337
+ ];
338
+ };
339
+ return (change, at) => {
340
+ if (change.kind === 'field')
341
+ return field(change.name, change.value, change.prior, at);
342
+ if (change.kind === 'section')
343
+ return section(change.name, change.value, change.prior, at);
344
+ if (change.kind === 'link')
345
+ return link(change.href, change.to, at);
346
+ if (change.kind === 'reference')
347
+ return reference(change.pointer, change.value, change.prior, at);
348
+ if (document.preamble !== change.prior)
349
+ return 'stale';
350
+ const start = internal.bodyStart;
351
+ return [{ start, end: start + document.preamble.length, text: change.value, at }];
352
+ };
353
+ }
354
+ function overlapping(edits, stale) {
355
+ let reach = -1;
356
+ let owner = -1;
357
+ for (const edit of edits) {
358
+ if (edit.start < reach)
359
+ stale.add(Math.max(edit.at, owner));
360
+ if (edit.end > reach) {
361
+ reach = edit.end;
362
+ owner = edit.at;
363
+ }
364
+ }
365
+ }
366
+ function firstHeadings(content, bodyStart) {
367
+ const first = new Map();
368
+ for (const span of headingSpans(content, bodyStart)) {
369
+ if (!first.has(span.name))
370
+ first.set(span.name, span.headingStart);
371
+ }
372
+ return first;
373
+ }
374
+ function hidden(before, bodyStart, after, shifts) {
375
+ const placed = firstHeadings(after, bodyStartOf(after));
376
+ let shift = 0;
377
+ let index = 0;
378
+ for (const [name, at] of firstHeadings(before, bodyStart)) {
379
+ while (index < shifts.length && shifts[index][0] <= at) {
380
+ shift += shifts[index][1];
381
+ index += 1;
382
+ }
383
+ if (placed.get(name) !== at + shift)
384
+ return name;
385
+ }
386
+ return undefined;
387
+ }
388
+ function spliced(source, edits) {
389
+ const shifts = [];
390
+ const pieces = [];
391
+ let cursor = 0;
392
+ let entered = false;
393
+ let ended = false;
394
+ for (const edit of edits) {
395
+ const kept = source.slice(cursor, edit.start);
396
+ pieces.push(kept);
397
+ if (kept !== '')
398
+ ended = kept.endsWith('\n');
399
+ let text = edit.text;
400
+ if (edit.entry === true && !entered) {
401
+ entered = true;
402
+ if (!ended)
403
+ text = `${lineEnding(source)}${text}`;
404
+ }
405
+ pieces.push(text);
406
+ if (text !== '')
407
+ ended = text.endsWith('\n');
408
+ shifts.push([edit.start, text.length - (edit.end - edit.start)]);
409
+ cursor = edit.end;
410
+ }
411
+ pieces.push(source.slice(cursor));
412
+ return { content: pieces.join(''), shifts };
413
+ }
414
+ export function apply(document, changes) {
415
+ const internal = internalOf(document);
416
+ if (!('bodyStart' in internal))
417
+ return { outcome: 'refuse', refusal: internal };
418
+ const plan = planner(document, internal);
419
+ const edits = [];
420
+ const stale = new Set();
421
+ const named = new Set();
422
+ for (const [at, change] of changes.entries()) {
423
+ const planned = plan(change, at);
424
+ if (planned === 'stale') {
425
+ stale.add(at);
426
+ continue;
427
+ }
428
+ if (!Array.isArray(planned))
429
+ return { outcome: 'refuse', refusal: planned };
430
+ const part = partOf(change);
431
+ if (named.has(part)) {
432
+ stale.add(at);
433
+ continue;
434
+ }
435
+ named.add(part);
436
+ for (const edit of planned)
437
+ edits.push(edit);
438
+ }
439
+ edits.sort((left, right) => left.start - right.start || left.end - right.end || left.at - right.at);
440
+ overlapping(edits, stale);
441
+ if (stale.size > 0)
442
+ return { outcome: 'stale', document, stale: [...stale].sort((left, right) => left - right) };
443
+ const source = document.content;
444
+ const { content } = spliced(source, edits);
445
+ if (content === source)
446
+ return { outcome: 'applied', document };
447
+ const replaced = edits.filter((edit) => {
448
+ const kind = changes[edit.at].kind;
449
+ return kind === 'section' || kind === 'preamble';
450
+ });
451
+ if (replaced.length > 0) {
452
+ const checked = spliced(source, replaced);
453
+ const name = hidden(source, internal.bodyStart, checked.content, checked.shifts);
454
+ if (name !== undefined)
455
+ return { outcome: 'refuse', refusal: refuse('', 'overlapping-section', name) };
456
+ }
457
+ const parsed = parse(content);
458
+ if (!parsed.ok)
459
+ return { outcome: 'refuse', refusal: parsed.refusal };
460
+ return { outcome: 'applied', document: parsed.document };
306
461
  }
307
462
  export function serialize(document) {
308
463
  return document.content;
309
464
  }
310
465
  export function frontmatter(document) {
311
- return internals.get(document)?.root ?? null;
466
+ return heldOf(document)?.internal.root ?? null;
312
467
  }
@@ -0,0 +1,78 @@
1
+ import { type Definition } from './definition.js';
2
+ import type { Candidate } from './query/match.js';
3
+ import type { Document, VerifiedModel } from './types.js';
4
+ export type FactKind = 'string' | 'number' | 'boolean' | 'date' | 'time' | 'enum' | 'reference';
5
+ type Scalar = string | number | boolean;
6
+ export type Fact = {
7
+ readonly kind: FactKind;
8
+ readonly value: Scalar;
9
+ readonly member?: string;
10
+ } | {
11
+ readonly kind: 'prose';
12
+ } | {
13
+ readonly kind: 'null';
14
+ } | {
15
+ readonly kind: 'unreadable';
16
+ } | {
17
+ readonly kind: 'list';
18
+ readonly items: readonly Fact[];
19
+ } | {
20
+ readonly kind: 'object';
21
+ readonly fields: Readonly<Record<string, Fact>>;
22
+ };
23
+ export type DocumentFacts = {
24
+ readonly id: string;
25
+ readonly path: string;
26
+ readonly type: string | null;
27
+ readonly fields: Readonly<Record<string, Fact>>;
28
+ readonly sections: readonly string[];
29
+ };
30
+ export declare const TEXT_FIELD = "/text";
31
+ export declare const WRITE_DOORS: readonly ['browser', 'agent', 'client', 'function'];
32
+ export type WriteDoor = (typeof WRITE_DOORS)[number];
33
+ export type Actor = {
34
+ readonly user?: string;
35
+ readonly site?: string;
36
+ readonly app?: string;
37
+ readonly door?: WriteDoor;
38
+ };
39
+ export type DocumentEvent = {
40
+ readonly before: DocumentFacts | null;
41
+ readonly after: DocumentFacts | null;
42
+ readonly changed: readonly string[];
43
+ readonly by: Actor;
44
+ readonly moved_between_rooms: boolean;
45
+ };
46
+ export type ItemFacts = {
47
+ readonly delivery: string;
48
+ readonly source: string | null;
49
+ readonly address: string | null;
50
+ readonly container: string | null;
51
+ readonly author: string | null;
52
+ readonly headers: Readonly<Record<string, string>>;
53
+ readonly json: {
54
+ readonly value: unknown;
55
+ } | null;
56
+ };
57
+ export type MessageFacts = {
58
+ readonly sender: string;
59
+ readonly agent_function: string | null;
60
+ readonly path: string | null;
61
+ };
62
+ export type Resolved = {
63
+ readonly id: string;
64
+ readonly member?: string;
65
+ };
66
+ export declare function facts(document: Pick<Document, 'type' | 'fields' | 'sections'>, at: {
67
+ readonly id: string;
68
+ readonly path: string;
69
+ }, model: VerifiedModel, resolve: (target: string) => Resolved | undefined): DocumentFacts;
70
+ export declare function prosePaths(facts: DocumentFacts): string[];
71
+ export declare function members(document: DocumentFacts, path: string): readonly string[];
72
+ export declare function pointed(value: unknown, pointer: string): unknown;
73
+ export declare function valuedChange(definition: Definition, pointer: string): boolean;
74
+ export declare function datePointers(definition: Definition): string[];
75
+ export declare function memberPointers(definition: Definition, memberType: string): string[];
76
+ export declare function candidateOf(document: DocumentFacts): Candidate;
77
+ export {};
78
+ //# sourceMappingURL=event.d.ts.map