- PluginFiles is now just a README + settings note (templates retired), not
'managed automatically'.
- Upload World: reworded the link-preservation line to describe the actual
omit-whole-field-on-unresolved behavior (the audit fix), not the vague
'skipped'.
Cold pre-release audit (release-auditor, verified against source) found one
real regression + polish:
#1 DATA-LOSS (regression vs 2.3.0): the bulk upload path (Export/Upload World)
built a full-field PATCH from a SHORTENED link list when a [[Name]] didn't
resolve locally, stripping the server's copy of the dropped link. The span
path guards this (omit-field-on-unresolved); the frontmatter path lost it.
Fix: frontmatterToPayloadFields gains omitFieldOnUnresolved; readElement sets
it so any field with an unresolvable link is OMITTED (server value wins), not
shortened. Self-contained detection (input-count vs resolved-count) — no
reliance on a caller-supplied tracking array (a test caught that fragility).
4 new tests pin it.
#2 polish: migration now renders link fields as [[Name]] wikilinks (was raw
ids until next download) — scrapeIdToName resolver threaded into
apiDataToFrontmatter.
#3 polish: DownloadWorldCommand.findElementByIdInCategory matches frontmatter
first (was span-only), so re-download rename detection works on migrated
notes.
#4 (NameChanger self-write during download) left as documented no-op traffic.
Gate: tsc clean, build clean, 64/64 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
processFrontMatter updates the file but the open editor's Properties panel
lags until metadataCache re-indexes, so a just-linked/unlinked value didn't
show until you left and returned. After a link write, re-set the active view's
state to force a clean properties re-render (best-effort; the write already
succeeded regardless). Captain-verified live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- Template fetch retired: Create/Download/Paste no longer fetch or write the
legacy PluginFiles/Templates + Handlebars sets (nothing reads them — every
note write is frontmatter via writeElement). includeLegacyTemplates defaults
false; the two Create* template commands kept as a reversible margin.
- Read-only keys need no PIN: WorldDownloadModal detects ow_r_ / demo keys and
greys out the PIN field; validation accepts a blank PIN and sends "". Write
keys and private-world write keys still require the 4-digit PIN (unit-checked:
read keys skip, write keys don't — no auth hole). pin type: number -> string.
- Docs: README read-key note + all commands current; release notes reversioned
2.4.0 -> 3.0.0 (the frontmatter era: format change + folder bridge + span
retirement = a major).
Gate: tsc clean, build clean, 60/60 tests + pinless-detection unit check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PasteWorld was the last command still writing the legacy <span> format via
vault Handlebars templates — a broken command on a 2.4.0 vault, not a
deferred feature. Swapped its element-write path for writeElement (the same
frontmatter writer Download uses): builds an id->name map from the pasted
payload for [[Name]] wikilinks, reuses existing notes by embedded id (now
matching frontmatter id: too), preserves extension fields. World.md write
unchanged. Copy->Paste round-trips through one writer now.
Gate: tsc clean, build clean, 60/60 tests. (Write path is Obsidian-runtime,
identical contract to the Captain-smoke-verified Download path.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ValidateWorld: skip frontmatter notes instead of showing false 'missing
Id/Name' errors on migrated worlds (the span linter can't judge them; a
frontmatter-aware validator is future work). The user-facing lie is the
actual harm; this removes it without a full rewrite.
- README: frontmatter format + clickable [[wikilink]] fields, the Migrate
command + note-format/migration section, folder Export/Import commands,
folder-bridge in the ecosystem section. Voice preserved (Captain's), facts
brought to 2.4.0.
- 2.4.0 release-notes draft (the GitHub release body + Captain's gate).
Gate: tsc clean, build clean, 60/60 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The download id->name map sanitizes wikilink targets with WorldService's
sanitizeFileName (strips control chars + trailing dot/space, 'unnamed'
fallback), but writeElement named the actual file with a WEAKER regex
(colon/slash class only). A name with a trailing dot or control char would
name the file one way and the [[link]] another -> dangling link. Both now
use sanitizeFileName, so the wikilink always matches the on-disk basename.
Also files the download-resolver brief.
Gate: tsc clean, build clean, npm test 60/60 (orchestrator hands).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Link fields rendered as raw UUIDs after a real download: writeElement
resolved id->name via metadataCache, which is cold for freshly-written
sibling notes mid-bulk-write, so nearly every link stayed a bare id.
Fix (restores the old linkifyContent/buildVaultNameIndex approach for the
frontmatter format):
- WriteElementOpts gains an idToName map; when present writeElement uses it
instead of the cache-scanning buildIdToNameResolver (absent = back-compat).
- DownloadWorldCommand builds ONE complete id->basename map before the write
loop: seeded from the in-memory payload (freshest names) with a raw-disk
fallback (parseRawFrontmatterScalars, NO metadataCache) for ids outside an
incremental pull's changed set. Names pass through sanitizeFileName so the
written [[Name]] resolves by filename. Unknown id -> raw id (never dropped).
- parseRawFrontmatterScalars added to element-transform (pure, dep-free) +
unit tests; plus a download-style injected-map render test. 59/59 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Link fields now render as clickable [[Name]] wikilinks instead of raw
UUIDs (R1), empty fields are omitted (R3), and OW-internal fields (id,
image_url) sink to the bottom of the Properties panel (R4).
- element-transform.ts (pure): apiDataToFrontmatter takes an optional
id->name resolver (raw-id fallback keeps dangling/cross-world links);
frontmatterToPayloadFields takes an optional name->id resolver + an
unresolved sink (wikilinks resolve to ids, bare uuids pass through,
unresolvable reported not guessed). Empty-omit + name-first/id-last
ordering. Extension fields never wikilinked, never empty-omitted.
- element-file.ts (Obsidian): buildIdToNameResolver (write) +
path-aware buildWikilinkResolver via getFirstLinkpathDest (read);
writeElement rebuilds owned keys in R4 order; readElement logs any
dropped unresolved links.
- 13 new pure tests (write/read/empty-omit/order/round-trip/extension).
Migrate + Export consume the resolver-optional functions unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a destination choice to "Export as OnlyWorlds folder": keep the
vault-internal default (OW-folder-export/) and add "Choose a folder..."
that writes the conformant folder into any real OS directory -- point it
at the Atlas root for a one-step handoff.
- External sink uses node fs/promises + absolute paths (escapes the vault
sandbox, verified); vault sink unchanged (vault API).
- Serialization shared via planFiles(); only the sink differs.
- Native picker reached by a guarded multi-path probe; falls back to a
paste-absolute-path modal if no Electron dialog is reachable.
- Desktop-only: external option hidden on mobile.
- R3 safety: refuse if target exists or dest holds an open Atlas world
(.atlas / lock). Pure refusal logic in export-plan.ts, unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CORE-SMOKE.md: the 10-min pass. Records what Skeld verified by execution
(migration correctness on Captain's real MessWorld run — 22/22, 0 residue;
extension-field round-trip incl. nested objects, both via the plugin's own
transform) vs the human-only pass (download/save/migrate/folder-bridge).
atlas-handoff-design.md: why the plugin can't auto-detect the Atlas root
(browser FS-Access sandbox, opaque IDB handles, no shared path — confirmed
against Atlas source in the Forge), and that a destination picker is the
chosen smoothing (executor building it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captain's first smoke: Download World on a fresh vault wrote World.md and
nothing else, no errors. Cause: data.json outlives vault content — a stored
cursor at head means 'changes since' = zero, honestly written into an empty
vault. The watermark now sanity-checks against vault reality: no element
notes for the world -> cursor ignored, cold full pull.
Gate: tsc clean, build clean, npm test 34/34.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Export minted a FRESH world id every run (faithful to folder.js, wrong for
this context: azgaar conversions ARE new worlds each time; a vault world is
persistent). Consequences: every re-export lands in Atlas as a brand-new
world, and re-importing a re-export trips the never-merge guard against the
same world's own earlier import.
Fix: shared vault/world-id-marker.ts (adapter-only dotfile I/O, documented) —
export reads-or-mints-and-persists the id; import records the imported
folder's id as before. Re-exports are now identity-stable.
Gate: tsc clean, build clean, npm test 34/34 (orchestrator hands).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Obsidian's vault index excludes dot-prefixed files: getAbstractFileByPath on
.ow-world-id returns null forever and vault.create refuses the path, so the
R5 never-merge-two-worlds guard was inert as written — a same-name world with
a different id would import and silently merge. Marker read/write now goes
through vault.adapter (raw fs, dotfile-capable); unindexed files emit no vault
events so the self-write mark is unnecessary there.
Gate: tsc clean, build clean, npm test 34/34 (orchestrator hands).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two additive commands making the plugin the folder format's fourth
implementation (after Atlas, Magelet, azgaar-converter):
- Export as OnlyWorlds folder: walk a world's notes -> conformant folder
under OW-folder-export/ (vault-internal). world.json id+name, spatial
bucketing, body type/local_updated_at/created_at stamp, <slug>--<tail>
filenames. [[wikilink]] -> ow://type/uuid where names resolve.
- Import OnlyWorlds folder: read a world folder placed in the vault, mint
frontmatter notes via Phase B's writeElement. ow:// -> [[wikilink]],
skip-existing-id safety, abort on world-id mismatch, verbatim extension
fields.
Pure serializers in vault/folder-format.ts (no Obsidian imports), 13 new
tests: conformance pins for all four Atlas-ingest facts + in-memory
round-trip (object equality on ids/links/extension keys/story). Phase B's
21 tests stay green. Smoke section G appended.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
readElement returns id OUTSIDE .fields; the Phase B guards in Export and Copy
returned parsed.fields bare, so on a fully-migrated world Upload World would
silently skip EVERY element (the sweep's if-no-id-continue) while reporting
success, and Copy World would emit id-less JSON that breaks paste dedupe.
Both guards now return { id, ...fields }. Smoke step 17 strengthened to check
the uploaded count and spot-check ids in copied JSON, so this class stays dead
at the user layer too.
Gate: tsc clean, build clean, npm test 21/21 (orchestrator hands).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mint authentic old-format span fixtures via the REAL writer (real handlebars lib
+ verbatim upstream templates + linkify helper + [[id]]->[[Name]] rewrite) rather
than hand-written strings, per the R9 addendum. test/span-writer.ts (minter, with
disclosure that template STRINGS are transcribed but the RENDER is genuine) +
test/migration-roundtrip.test.ts (5 cases: Character multi-links, Narrative
body->story, apostrophe/HTML-entity, comma-in-linked-name, extension-field
round-trip).
The writer-minted apostrophe fixture caught a REAL bug hand-written fixtures
missed: the migration path did NOT decode HTML entities, so a pre-2.2.2 escaped
note ("The Kid's Family") would migrate with the corruption preserved. Fixed:
element-transform.ts parseSpanNote now decodes via the pure decodeHtmlEntities
(matching SaveElement/ExportWorld). Propagates to every span-read path (migration,
readElement span fallback). 21/21 tests pass.
SMOKE-CHECKLIST.md: added the copy/throwaway-vault-first caution as the first line.
Also folds in a working-tree safety change to MigrateWorldCommand + main.ts that I
did NOT author (a teammate added it mid-flight): migration takes auto-sync's
markSelfWrite guard so a user with auto-sync on doesn't mass-PATCH migrated notes
to the server (which would break backup-based reversibility). Sound change; added
the null-guard (this.autoSync?.) it was missing so tsc passes. Flagged to lead.
Gate: tsc clean, esbuild build succeeds, npm test 21/21.
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>
- All sync paths on /api/v2: Download World = /changes cursor walk (incremental
re-downloads, rewind detection, honest delete reporting); Upload World =
create/update sweep via /bulk + PATCH (server-only elements never deleted);
Save/auto-sync via shared V2Client. Legacy worldsync routes fully retired.
- Filename sanitization: Windows-illegal chars (':' etc.) no longer mangle
notes into NTFS streams; wikilinks use sanitized targets, names stay exact.
- Link safety: unresolvable links skip the whole field (server values kept,
surfaced in a toast); uuid-shaped wikilinks pass through as ids.
- Delete Element command: typed-name confirm -> server DELETE -> note to trash.
- Progress toasts for download/upload; README rewritten (v2 semantics +
ecosystem section).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Handlebars default-escaped field values into note files; every read shipped
the escaped form (failed [[link]] file lookups, escaped names stored via API).
noEscape on all 5 compile sites + entity decode at every extraction seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README documented the stale 'Export World' palette name and never listed
the restored Download World command. Fixed both; removed the dev-only
TEST-CHECKLIST.md (carried an absolute local path).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wrong-world write class: per-world commands fell back to the settings
key whenever a world's own World.md key was missing, silently writing to
whatever world that key named. The settings key survives a vault-content
clear (.obsidian/data.json), so a read-key download + stale settings key
routed rename/save to the settings world. Shared resolveWorldKey() now
uses the world's OWN key; settings is fallback ONLY when the world has
none, and the fallback warns.
Rename false-success: the toast fired on the local rename before the API
PATCH ran, so a read-key (or any failed) push still said 'success'. Now
the push runs first and the notice states plainly whether onlyworlds.com
was updated or only the vault.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Save Element creates new elements (create-on-404); Upload World renamed
with real error surfacing; Rename World pushes; Download World returns
(prefixed-key clean) with self-healing Map/Pin/Marker templates; upload
parser ships links as _id/_ids UUIDs; per-world key resolution ends the
wrong-world write class; scoped keys primary in all copy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The legacy ImportWorldCommand + WorldImportModal enforced 10-digit
numeric API keys (digit-strip listener, maxlength, length checks) and
would reject the new prefixed ow_* keys the OnlyWorlds platform issues
after its backend migration. The SDK settings flow (link world + sync)
covers the import job with opaque-string key handling; old 10-digit
keys keep working there too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Major rebuild. Plugin now uses @onlyworlds/sdk for API calls via Obsidian's
requestUrl, native PluginSettingTab for configuration, and a debounced auto-sync
engine that pushes element edits to onlyworlds.com after idle.
Changes
- Drop handrolled API code; route all writes through @onlyworlds/sdk subclass
- Native settings tab: API key, PIN, default world, sync mode, debounce
- PIN handling: persist in settings or cache per session; no per-save prompts
- Auto-sync toggle with configurable debounce (default 3s)
- Status bar + ribbon icon reflect sync state (idle/dirty/syncing/synced/error)
- Ribbon click opens menu (Sync / Settings / About) instead of firing sync
- Settings API key wins over per-world World.md (legacy fallback preserved)
- Add Map, Pin, Marker to element category enum (now 22)
- ESLint config (eslint-plugin-obsidianmd) scoped to new code
- Modernize build: TS 5.8, esbuild 0.25, stricter tsconfig
- Drop dead code: GraphViewExtensions, dead command imports, handleApiError
Compatibility
- Existing v1 vaults work unchanged. Span-tag format preserved.
- OnlyWorlds/Settings.md still readable; new settings tab takes precedence.
- isDesktopOnly: true. Mobile pending verification.