The picker was add-only — calling it 'Edit' but never letting you unlink was
the real wrong. Reworked ElementSelectionModal into a toggle list: every
element of the type shown, already-linked ones pre-checked, click toggles
link/unlink, checked-first ordering, search box for long lists. Multi-link
confirms a set on Done; single-link is radio-style (pick sets + closes) with a
Clear-link option to unset. NoteLinker computes current links (resolving
[[Name]] via the path-aware resolver) to pre-check, and writeLink now REPLACES
the field with exactly the chosen set instead of merge-only.
Gate: tsc clean, build clean, 60/60 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE bug: NoteLinker.writeLink wrote raw UUIDs into link fields — the one
write path never updated for the 3.0.0 readability change, so linking via
the command produced unreadable uuid fields. Now writes [[Name]] wikilinks
(sanitized to match the on-disk basename), merging by name AND id so an
existing raw-id or wikilink entry isn't duplicated — old raw-id links
self-heal to wikilinks on next edit. Merge logic unit-verified (add / re-add /
raw-id-upgrade / colon-name).
Modal UX (Captain's asks): field picker shows each field's current link count
(empty vs filled), sorts single links first then empty-before-filled, dims
already-filled fields, Add/Edit button label. README notes native Property
autocomplete as a second linking path.
Gate: tsc clean, build clean, 60/60 tests + merge unit-check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wake the dormant vault/element-file.ts write path and make YAML frontmatter
the note format for new writes, with read-tolerance for legacy span notes.
Pure logic (Obsidian-free, unit-tested under `npm test`):
- vault/element-transform.ts: frontmatter<->payload, span parse, span->frontmatter,
link normalization (single string / multi list, never comma-joined), extension-key
passthrough (atlas_/shadow_/x_), body<->description|story mapping, read-before-PATCH diff.
- test/element-transform.test.ts + tsconfig.test.json: 16 node:test cases incl. a real
span-format Character fixture. Wired to `npm test` (pretest compiles to test-dist/).
Wiring:
- element-file.ts: readElement preserves extension fields + maps body to story for
Narrative (R3/R5); frontmatter-first with legacy span fallback; writeElement round-trips
extension keys and respects count-suffixed folders.
- DownloadWorldCommand + CreateElementCommand: emit frontmatter via writeElement (no more
Handlebars element templates).
- NoteLinker: reworked onto metadataCache + SDK FIELD_SCHEMA; writes link IDs into
frontmatter (new FieldSelectionModal picks the field). NameChanger syncs frontmatter name.
- SaveElementCommand: read-before-PATCH (GET, diff, PATCH only changed fields) (R7).
- ExportWorldCommand + CopyWorldCommand: read frontmatter via readElement, span as fallback
(mixed worlds upload/copy correctly).
- MigrateWorldCommand: "Migrate world notes to frontmatter" — backup-first (abort on failure),
idempotent, end report modal (converted/skipped/failed).
Deferred (see docs report): PasteWorld still emits span; ValidateWorld is still a span linter
(diagnostic only, no writes). docs/SMOKE-CHECKLIST.md is the manual release gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>