@mnstry/atelier 0.2.0-alpha.11 → 0.2.0-alpha.12

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 (81) hide show
  1. package/CHANGELOG.md +344 -0
  2. package/README.md +10 -6
  3. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  4. package/contracts/compat-baseline.json +1 -1
  5. package/docs/assurance-controls.md +3 -2
  6. package/docs/install.md +3 -3
  7. package/docs/local-services.md +62 -11
  8. package/docs/obsidian-contract.md +669 -35
  9. package/docs/obsidian-plugin.md +585 -0
  10. package/docs/obsidian.md +233 -50
  11. package/docs/release-engineering.md +7 -1
  12. package/docs/upgrade.md +23 -3
  13. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  14. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  15. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  16. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  17. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  18. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  19. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  20. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  21. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  22. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  23. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  24. package/fixtures/obsidian/edits/workspace.json +1 -1
  25. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  26. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  27. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  28. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  29. package/package.json +7 -3
  30. package/plugins/obsidian/main.js +444 -0
  31. package/plugins/obsidian/manifest.json +9 -0
  32. package/plugins/obsidian/styles.css +22 -0
  33. package/src/commands/obsidian.mjs +89 -10
  34. package/src/contracts/corpus.mjs +6 -0
  35. package/src/egress/forbidden-egress.mjs +2 -0
  36. package/src/project/private-state.mjs +10 -1
  37. package/src/projection/obsidian/contracts.mjs +224 -18
  38. package/src/projection/obsidian/edits/apply.mjs +12 -7
  39. package/src/projection/obsidian/edits/regions.mjs +137 -42
  40. package/src/projection/obsidian/materialize/index.mjs +6 -3
  41. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  42. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  43. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  44. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  45. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  46. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  47. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  48. package/src/projection/obsidian/proposals/observation.mjs +10 -5
  49. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  50. package/src/projection/obsidian/publication/index.mjs +2 -1
  51. package/src/projection/obsidian/publication/publisher.mjs +173 -26
  52. package/src/projection/obsidian/publication/transport.mjs +63 -8
  53. package/src/projection/obsidian/publication/vault-list.mjs +199 -0
  54. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  55. package/src/projection/obsidian/recovery/store.mjs +11 -4
  56. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  57. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  58. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  59. package/src/runtime/obsidian/app-capability.mjs +104 -12
  60. package/src/runtime/obsidian/app-production-seams.mjs +126 -39
  61. package/src/runtime/obsidian/app-registration.mjs +166 -0
  62. package/src/runtime/obsidian/engine.mjs +127 -11
  63. package/src/runtime/obsidian/extension-points.mjs +1 -1
  64. package/src/runtime/obsidian/index.mjs +5 -1
  65. package/src/runtime/obsidian/launch-plan.mjs +88 -0
  66. package/src/runtime/obsidian/lifecycle.mjs +105 -14
  67. package/src/runtime/obsidian/opening.mjs +227 -55
  68. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  69. package/src/runtime/obsidian/plugin-channel.mjs +281 -0
  70. package/src/runtime/obsidian/plugin-choice.mjs +170 -0
  71. package/src/runtime/obsidian/plugin-drift.mjs +76 -0
  72. package/src/runtime/obsidian/plugin-presence.mjs +48 -0
  73. package/src/runtime/obsidian/service-main.mjs +18 -5
  74. package/src/runtime/obsidian/service-record.mjs +36 -0
  75. package/src/runtime/obsidian/service-server.mjs +50 -12
  76. package/src/runtime/obsidian/service.mjs +100 -6
  77. package/src/runtime/obsidian/state-store.mjs +13 -1
  78. package/src/server/local-sidecar.mjs +24 -5
  79. package/src/server/server.mjs +18 -5
  80. package/src/upgrade/upgrade.mjs +1 -1
  81. package/templates/external-project-workspace/pack-lifecycle.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { scanMarkdownLinks } from '../../../graph/knowledge-graph.mjs'
2
- import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal } from '../contracts.mjs'
2
+ import { OBSIDIAN_EXT_KEY, ObsidianContractRefusal, manifestLayoutVersion } from '../contracts.mjs'
3
3
  import { readMarkdownLens, sha256Digest } from '../materialize/byte-lens.mjs'
4
4
  import { alignBodies, commonPrefixLength, commonSubsequenceLength, commonSuffixLength } from './align.mjs'
5
5
 
@@ -13,7 +13,8 @@ import { alignBodies, commonPrefixLength, commonSubsequenceLength, commonSuffixL
13
13
  // A note is three byte classes, in order:
14
14
  //
15
15
  // prefix bytes before regions.body.start: the source's byte order prefix
16
- // or front matter, verbatim
16
+ // or front matter, verbatim, and in layout 2 the generated
17
+ // identity lines (regions.identity) inside or as that front matter
17
18
  // authored the body, with canonical link and embed targets rewritten; each
18
19
  // rewrite is recorded as an inversion
19
20
  // generated relations, outside-selection rows and, before them, the closing
@@ -221,26 +222,41 @@ export function splitGeneratedTail({ noteEntry, publishedNoteBytes, editedNoteBy
221
222
  // Prefix
222
223
  // ---------------------------------------------------------------------------
223
224
 
224
- // Returns the edited authored BODY. The prefix must be byte-identical. A byte
225
- // order prefix is the one exception: it belongs to the source and is kept from
226
- // the source whether or not the editor kept it in the note.
227
- export function splitPrefix({ noteEntry, publishedNoteBytes, editedAuthored }) {
228
- const prefix = publishedNoteBytes.subarray(0, noteEntry.regions.body.start)
225
+ // The note's prefix as the source holds it: the published prefix without the
226
+ // generated identity lines, and whether the source has front matter of its own.
227
+ export function sourcePrefixOf({ noteEntry, publishedNoteBytes }) {
228
+ const bodyStart = noteEntry.regions.body.start
229
+ const prefix = publishedNoteBytes.subarray(0, bodyStart)
230
+ const identity = noteEntry.regions.identity ?? null
229
231
  const frontmatter = noteEntry.regions.frontmatter ?? null
232
+ if (identity === null || identity.end > bodyStart) return { prefix, sourcePrefix: prefix, hasFrontmatter: frontmatter !== null }
233
+ const sourcePrefix = Buffer.concat([prefix.subarray(0, identity.start), prefix.subarray(identity.end)])
234
+ // A front matter that is the identity block and nothing else was generated for a source that has none.
235
+ const generatedWhole = frontmatter !== null && frontmatter.start === identity.start && frontmatter.end === identity.end
236
+ return { prefix, sourcePrefix, hasFrontmatter: frontmatter !== null && !generatedWhole }
237
+ }
238
+
239
+ // Returns the edited authored BODY. The prefix must be byte-identical, the
240
+ // identity lines included: a change to them, as to any front matter, is not a
241
+ // body replacement. A byte order prefix is the one exception: it belongs to
242
+ // the source and is kept from the source whether or not the editor kept it in
243
+ // the note.
244
+ export function splitPrefix({ noteEntry, publishedNoteBytes, editedAuthored }) {
245
+ const { prefix, sourcePrefix, hasFrontmatter } = sourcePrefixOf({ noteEntry, publishedNoteBytes })
230
246
  let body
231
- if (frontmatter === null) {
247
+ if (noteEntry.regions.frontmatter === undefined) {
232
248
  body = startsWith(editedAuthored, UTF8_BOM) ? editedAuthored.subarray(UTF8_BOM.length) : editedAuthored
233
249
  } else {
234
- if (!startsWith(editedAuthored, prefix)) {
235
- refuse('unsupported-frontmatter-edit', 'the front matter of the note was edited; only the body can be replaced', { firstDifference: commonPrefixLength(editedAuthored, prefix) })
236
- }
237
- body = editedAuthored.subarray(prefix.length)
250
+ const withoutBom = startsWith(prefix, UTF8_BOM) ? prefix.subarray(UTF8_BOM.length) : null
251
+ if (startsWith(editedAuthored, prefix)) body = editedAuthored.subarray(prefix.length)
252
+ else if (withoutBom !== null && startsWith(editedAuthored, withoutBom)) body = editedAuthored.subarray(withoutBom.length)
253
+ else refuse('unsupported-frontmatter-edit', 'the front matter of the note was edited; only the body can be replaced', { firstDifference: commonPrefixLength(editedAuthored, prefix) })
238
254
  }
239
255
  // The body must not read as front matter of its own, and the front matter
240
256
  // must still end where it did, as the source lens reads the result.
241
257
  let lens = null
242
- try { lens = readMarkdownLens(Buffer.concat([frontmatter === null ? Buffer.alloc(0) : prefix, body])) } catch { /* refused below */ }
243
- const expectedEnd = frontmatter === null ? null : frontmatter.end
258
+ try { lens = readMarkdownLens(Buffer.concat([hasFrontmatter ? sourcePrefix : Buffer.alloc(0), body])) } catch { /* refused below */ }
259
+ const expectedEnd = hasFrontmatter ? sourcePrefix.length : null
244
260
  if (lens === null || (lens.frontmatter?.end ?? null) !== expectedEnd) {
245
261
  refuse('unsupported-frontmatter-edit', 'the edit changes where the front matter of the note begins or ends; only the body can be replaced')
246
262
  }
@@ -404,11 +420,53 @@ function assertCoherentPlacements({ units, placements, publishedBody, editedBody
404
420
  // The final invariant, independent of how the alignment went: every rewrite
405
421
  // of the published body is either substituted (intact) or gone (deleted), and
406
422
  // what is left of the edited body, which becomes source bytes verbatim, holds
407
- // no vault identity that the published authored text did not already hold.
408
- // A vault identity is the `--<hex>` suffix of a note or attachment path of
409
- // this view, wherever it appears: in a link, in code or in plain prose.
423
+ // no vault identity that the published authored text did not already hold,
424
+ // wherever it appears: in a link, in code or in plain prose. In layout 1 a
425
+ // vault identity is the `--<hex>` suffix of a note or attachment path of this
426
+ // view; in layout 2, where names are titles, it is the full vault path of one,
427
+ // as written or percent-encoded, with or without `.md`.
410
428
  const IDENTITY = /--[0-9a-f]{12,64}(?![0-9a-f])/g
411
429
 
430
+ const encodeSegment = (segment) => encodeURIComponent(segment).replace(/[()']/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
431
+ const latin1Of = (text) => Buffer.from(text, 'utf8').toString('latin1')
432
+ const vaultFilesOf = (manifest) => [...manifest.notes.map((note) => note.path), ...manifest.attachments.map((attachment) => attachment.path)]
433
+
434
+ // Returns (latin1 text) => [{ identity, index, length }].
435
+ function vaultIdentityFinder(manifest) {
436
+ if (manifestLayoutVersion(manifest) === 1) {
437
+ const known = new Set()
438
+ for (const filePath of vaultFilesOf(manifest)) for (const match of filePath.matchAll(IDENTITY)) known.add(match[0])
439
+ return (text) => [...text.matchAll(IDENTITY)].filter((match) => known.has(match[0])).map((match) => ({ identity: match[0], index: match.index, length: match[0].length }))
440
+ }
441
+ // Spellings in latin1, so an index is a byte offset. Every path starts with a
442
+ // repository folder, so only those need to be looked for.
443
+ const spellings = new Set()
444
+ const starts = new Set()
445
+ let longest = 0
446
+ for (const filePath of vaultFilesOf(manifest)) {
447
+ const forms = [filePath, filePath.split('/').map(encodeSegment).join('/')]
448
+ for (const form of forms) {
449
+ for (const spelling of form.endsWith('.md') ? [form, form.slice(0, -'.md'.length)] : [form]) {
450
+ const bytes = latin1Of(spelling)
451
+ spellings.add(bytes)
452
+ longest = Math.max(longest, bytes.length)
453
+ }
454
+ starts.add(latin1Of(`${form.split('/')[0]}/`))
455
+ }
456
+ }
457
+ return (text) => {
458
+ const found = []
459
+ for (const start of starts) {
460
+ for (let at = text.indexOf(start); at !== -1; at = text.indexOf(start, at + 1)) {
461
+ let match = null
462
+ for (let end = at + start.length + 1; end <= Math.min(text.length, at + longest); end += 1) if (spellings.has(text.slice(at, end))) match = end
463
+ if (match !== null) found.push({ identity: text.slice(at, match), index: at, length: match - at })
464
+ }
465
+ }
466
+ return found
467
+ }
468
+ }
469
+
412
470
  export function assertRewritesAccounted({ manifest, units, placements, publishedBody, editedBody }) {
413
471
  if (placements.length !== units.length || placements.some((placement) => placement.state !== 'intact' && placement.state !== 'deleted')) {
414
472
  refuse('unsupported-structural-edit', 'a rewritten link is neither kept nor deleted', { reason: 'rewrite-unaccounted' })
@@ -418,13 +476,12 @@ export function assertRewritesAccounted({ manifest, units, placements, published
418
476
  refuse('unsupported-structural-edit', 'a rewritten link is not where it was placed', { reason: 'rewrite-unaccounted' })
419
477
  }
420
478
  })
421
- const known = new Set()
422
- for (const item of [...manifest.notes, ...manifest.attachments]) for (const match of item.path.matchAll(IDENTITY)) known.add(match[0])
479
+ const find = vaultIdentityFinder(manifest)
423
480
  const identities = (body, holes) => {
424
481
  const found = []
425
482
  let cursor = 0
426
483
  for (const [start, end] of [...holes, [body.length, body.length]]) {
427
- for (const match of body.toString('latin1', cursor, start).matchAll(IDENTITY)) if (known.has(match[0])) found.push({ identity: match[0], start: cursor + match.index, end: cursor + match.index + match[0].length })
484
+ for (const item of find(body.toString('latin1', cursor, start))) found.push({ identity: item.identity, start: cursor + item.index, end: cursor + item.index + item.length })
428
485
  cursor = end
429
486
  }
430
487
  return found
@@ -440,29 +497,64 @@ export function assertRewritesAccounted({ manifest, units, placements, published
440
497
  }
441
498
 
442
499
  const folded = (value) => value.normalize('NFC').toLowerCase()
500
+ const parentFolder = (filePath) => (filePath.includes('/') ? filePath.slice(0, filePath.lastIndexOf('/')) : '')
443
501
 
444
502
  // Every spelling under which the view's notes and attachments can be linked
445
- // from inside the vault.
446
- function vaultTargets(manifest) {
447
- const targets = new Set()
448
- const paths = [...manifest.notes.map((note) => note.path), ...manifest.attachments.map((attachment) => attachment.path)]
449
- for (const filePath of paths) {
450
- const base = filePath.split('/').at(-1)
451
- for (const spelling of [filePath, base]) {
452
- targets.add(folded(spelling))
453
- if (spelling.endsWith('.md')) targets.add(folded(spelling.slice(0, -'.md'.length)))
503
+ // from inside the vault, as a predicate over a link target.
504
+ //
505
+ // Layout 1: a full path or a basename, with or without `.md`, or anything
506
+ // under `notes/` or `attachments/`.
507
+ //
508
+ // Layout 2 asks what the app would resolve: a target starting `./` or `../`
509
+ // is resolved against the folder of the note; then a vault file of the same
510
+ // name whose path is the target or ends with it, with or without `.md` (the
511
+ // app's exact and suffix matches). A target written from the vault root under
512
+ // a repository folder, where every file of the view is, names the vault even
513
+ // when no file answers it; a relative one that no file answers is an ordinary
514
+ // relative link, which means the same in the source.
515
+ function vaultTargets(manifest, notePath) {
516
+ const files = vaultFilesOf(manifest).map(folded)
517
+ if (manifestLayoutVersion(manifest) === 1) {
518
+ const targets = new Set()
519
+ for (const filePath of files) {
520
+ for (const spelling of [filePath, filePath.split('/').at(-1)]) {
521
+ targets.add(spelling)
522
+ if (spelling.endsWith('.md')) targets.add(spelling.slice(0, -'.md'.length))
523
+ }
524
+ }
525
+ return (written) => {
526
+ const target = folded(written.replace(/^(?:\.\/)+/, '').trim())
527
+ return targets.has(target) || target.startsWith('notes/') || target.startsWith('attachments/')
528
+ }
529
+ }
530
+ const byName = new Map()
531
+ for (const filePath of files) {
532
+ const name = filePath.split('/').at(-1)
533
+ if (!byName.has(name)) byName.set(name, [])
534
+ byName.get(name).push(filePath)
535
+ }
536
+ const topFolders = new Set(files.map((filePath) => filePath.split('/')[0]))
537
+ const folder = folded(parentFolder(notePath ?? ''))
538
+ return (written) => {
539
+ let target = folded(written.trim())
540
+ const relative = target.startsWith('./') || target.startsWith('../')
541
+ if (relative) {
542
+ let base = folder
543
+ if (target.startsWith('./../')) target = target.slice(2)
544
+ if (target.startsWith('./')) target = target.slice(2)
545
+ else while (target.startsWith('../')) { target = target.slice(3); base = parentFolder(base) }
546
+ target = base === '' ? target : `${base}/${target}`
454
547
  }
548
+ target = target.replace(/^\/+/, '')
549
+ if (!relative && target.includes('/') && topFolders.has(target.split('/')[0])) return true
550
+ return [target, `${target}.md`].some((spelling) => (byName.get(spelling.split('/').at(-1)) ?? []).some((filePath) => filePath.endsWith(spelling)))
455
551
  }
456
- return targets
457
552
  }
458
553
 
459
- function namesVaultFile(href, targets) {
554
+ function namesVaultFile(href, isVaultTarget) {
460
555
  const spellings = [href]
461
556
  try { spellings.push(decodeURIComponent(href)) } catch { /* not percent-encoded */ }
462
- return spellings.some((spelling) => {
463
- const target = folded(spelling.replace(/^<|>$/g, '').replace(/^(?:\.\/)+/, '').trim())
464
- return targets.has(target) || target.startsWith('notes/') || target.startsWith('attachments/')
465
- })
557
+ return spellings.some((spelling) => isVaultTarget(spelling.replace(/^<|>$/g, '')))
466
558
  }
467
559
 
468
560
  // Links and embeds of `bytes` (a whole authored note, so front matter is
@@ -497,8 +589,8 @@ function vaultLinkOccurrences(bytes, bodyStart, targets) {
497
589
  // link in the published body must still be the whole target of a link, and an
498
590
  // appended alias must still be what closes its link. Text typed against
499
591
  // either one changes what the link points at once the rewrite is inverted.
500
- export function findStructuralLinks({ manifest, notePrefix, publishedBody, editedBody, units, placements }) {
501
- const targets = vaultTargets(manifest)
592
+ export function findStructuralLinks({ manifest, notePath = null, notePrefix, publishedBody, editedBody, units, placements }) {
593
+ const targets = vaultTargets(manifest, notePath)
502
594
  const within = (occurrence, start, end) => start >= occurrence.targetStart && end <= occurrence.targetEnd
503
595
  const publishedLinks = vaultLinkOccurrences(Buffer.concat([notePrefix, publishedBody]), notePrefix.length, targets)
504
596
  const editedLinks = vaultLinkOccurrences(Buffer.concat([notePrefix, editedBody]), notePrefix.length, targets)
@@ -558,7 +650,10 @@ function lensWith(primitives, { manifest, repoId, nodeId, publishedNoteBytes, ed
558
650
  const bodyStart = noteEntry.regions.body.start
559
651
  const notePrefix = publishedNoteBytes.subarray(0, bodyStart)
560
652
  const sourceLens = readMarkdownLens(baseSourceBytes)
561
- if (sourceLens.body.start !== bodyStart || sourceLens.body.end !== baseSourceBytes.length || !baseSourceBytes.subarray(0, bodyStart).equals(notePrefix)) {
653
+ // The note's prefix is the source's, with the generated identity lines where the manifest records them.
654
+ const { sourcePrefix } = sourcePrefixOf({ noteEntry, publishedNoteBytes })
655
+ const sourceBodyStart = sourceLens.body.start
656
+ if (sourceBodyStart !== sourcePrefix.length || sourceLens.body.end !== baseSourceBytes.length || !baseSourceBytes.subarray(0, sourceBodyStart).equals(sourcePrefix)) {
562
657
  refuse('manifest-mismatch', 'the recorded body does not begin where the body of the base source begins')
563
658
  }
564
659
  const units = inversionUnits({ manifest, noteEntry, publishedNoteBytes, baseSourceBytes })
@@ -571,7 +666,7 @@ function lensWith(primitives, { manifest, repoId, nodeId, publishedNoteBytes, ed
571
666
  assertCoherentPlacements({ units, placements, publishedBody, editedBody })
572
667
  // Offsets in a refusal are offsets into the edited note.
573
668
  const shift = tail.authored.length - editedBody.length
574
- const structural = primitives.findStructuralLinks({ manifest, notePrefix: noteEntry.regions.frontmatter ? notePrefix : Buffer.alloc(0), publishedBody, editedBody, units, placements })
669
+ const structural = primitives.findStructuralLinks({ manifest, notePath: noteEntry.path, notePrefix: noteEntry.regions.frontmatter ? notePrefix : Buffer.alloc(0), publishedBody, editedBody, units, placements })
575
670
  const aliasTouched = structural.find((item) => item.alias)
576
671
  if (aliasTouched) {
577
672
  refuse('link-rewrite-edited', 'text was added to the alias the emitter appended to a link', { noteStart: aliasTouched.bodyStart + shift, noteEnd: aliasTouched.bodyEnd + shift })
@@ -590,10 +685,10 @@ function lensWith(primitives, { manifest, repoId, nodeId, publishedNoteBytes, ed
590
685
  }
591
686
 
592
687
  // Between two surviving rewrites the edited bytes ARE the new source bytes.
593
- const parts = [baseSourceBytes.subarray(0, bodyStart)]
688
+ const parts = [baseSourceBytes.subarray(0, sourceBodyStart)]
594
689
  const changedRanges = []
595
- let written = bodyStart
596
- let sourceCursor = bodyStart
690
+ let written = sourceBodyStart
691
+ let sourceCursor = sourceBodyStart
597
692
  let editedCursor = 0
598
693
  const segment = (editedTo, sourceTo) => {
599
694
  const next = editedBody.subarray(editedCursor, editedTo)
@@ -1,7 +1,10 @@
1
1
  // Materialization: the pure preparation layer for an Obsidian view. Nothing
2
2
  // here writes into a vault; publication is a separate protocol.
3
- export { EMITTER_VERSION, createPreparationCache, prepareView, withEligibility } from './prepare-view.mjs'
3
+ export { CURRENT_VAULT_LAYOUT, EMITTER_VERSION, createPreparationCache, prepareView, withEligibility } from './prepare-view.mjs'
4
4
  export { readMarkdownLens, assertStrictUtf8, sha256Digest } from './byte-lens.mjs'
5
- export { PATH_REGISTRY_SCHEMA, allocateWorkspacePaths, collisionKey, emptyPathRegistry } from './path-registry.mjs'
6
- export { POLICY_DISABLED_CORE_PLUGINS, POLICY_SETTINGS_PATH, SETTINGS_ROOT, isPolicySettingsPath, isUserOwnedSettingsPath, prepareSettings } from './settings.mjs'
5
+ export { PATH_REGISTRY_SCHEMA, allocateViewPaths, allocateWorkspacePaths, collisionKey, emptyPathRegistry } from './path-registry.mjs'
6
+ export {
7
+ COMMUNITY_PLUGINS_PATH, PLUGIN_OWNED_PATHS, POLICY_DISABLED_CORE_PLUGINS, POLICY_ENABLED_COMMUNITY_PLUGINS, POLICY_SETTINGS_PATH, POLICY_SETTINGS_PATHS, SETTINGS_ROOT,
8
+ isPluginOwnedPath, isPolicySettingsPath, isUserOwnedSettingsPath, preparePolicySettingsFile, prepareSettings,
9
+ } from './settings.mjs'
7
10
  export { stagePreparedView } from './stage.mjs'