Skip to content

fix(core): keep a front-matter type that names no type as a property - #2260

Merged
aicayzer merged 4 commits into
mainfrom
fix/099-foreign-type
Oct 21, 2026
Merged

aicayzer merged 4 commits into
mainfrom
fix/099-foreign-type

Conversation

@aicayzer

@aicayzer aicayzer commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A Markdown note whose front matter says type: reference, as many Obsidian vaults do, was held with "reference is not a type this copy holds" and never synced. A type identifier always has at least two dot-separated segments (types/id-grammar), so a type value that the grammar refuses can never name a Marfa type. A folder now makes such a note a document of its default type, keeps the value as the item property type, and writes the file's type line back as the person wrote it. A value that is a valid identifier but names a type the copy does not hold, such as a mistyped core.notes, is still refused.

Changes

  • Contract (folders.md):
    • New statements: folders/foreign-type-property (a type value that is not an identifier is carried as the property type, and the item's type is unchanged), folders/foreign-type-write (a write shows that property in the type line in place of the item's type) and folders/foreign-type-empty (an empty type: line stays empty).
    • Narrowed folders/retype-unsuited to a type identifier the copy does not hold, with its reason.
    • Updated folders/own-field-lines and folders/default-document-type to match.
    • Removed "an empty or non-text type" from folders/own-field-validation: such a line is no longer unreadable.
    • folders/uncarried-preserve now excepts a type property that the type line replaces with a non-identifier value, so the person's word wins over a type property an app set. folders/foreign-type-property cross-references it.
  • Core (folder/fields.rs): names_type decides whether a type value is an identifier, using the grammar's own validator. read carries any other value (string, number, list, empty or null) as a property. lines_of shows that property in the type line, and keeps an empty line empty. carried says which reserved-name properties a file carries. presentation compares a non-identifier value as written instead of trimmed.
  • Core (folder/push.rs): a whole save no longer puts back a held type property that the file carries, so changing the line to a real type or removing it clears the property. A held reserved-name property is put back only where the file read no value under that name, so a word the person writes replaces an app's identifier-valued type property instead of being overwritten by it.
  • Core (folder/mod.rs, one line): the render passes the file's front matter to lines_of. PR fix(core): finish a push after a key change, keep waits at hand-over #2255 also changes this file, but not this line.
  • Core (hydrate.rs, catalog.rs, scaffold.rs): type_id is the single check for "a type identifier and not a .* pattern". The two places that spelled it out now call it.
  • Tests: a new a type line that names no type group (eight fixtures, including replaces an app's type property with the word the person writes on the line) in device/folders-frontmatter.test.ts, unit tests in fields.rs, and Untyped.md (type:) taken out of the unreadable cases of reads own-field lines in each form a file can hold them.

Testing

  • Review fix: replaces an app's type property with the word the person writes on the line seeds a note whose type property is core.bookmark (the file shows type: core.note), edits the line to type: reference and expects an update with properties.type: "reference" and the line kept. Before the fix it failed: no update was sent, so the write-back would have reverted the line. It passes with the fix.
  • The first seven new device fixtures failed on main first. The create was flagged refused, and each edge case was flagged unreadable. All seven pass with the change.
  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test -p marfa-core -p marfa-cli: all clean, 1,242 tests passed (rerun after the review fix: all passed).
  • Device folder suites (folders-frontmatter, folders, folders-contract-a, -b, -c, -extra-c): 355 of 355 passed. After the review fix, folders-frontmatter, folders and folders-contract-a, -b, -c: 333 of 333 passed.
  • spec-citations, spec-form and spec-statements: 48 passed. After the review fix, spec-form and spec-citations: 34 passed. tsc --noEmit and Prettier on conformance are clean.
  • End to end against a real server (core/scripts/server-up.sh, the built marfa binary, two folder directories as two machines):
    • Push: Reading.md (type: reference), Stub.md (type:) and Book.md (type: [book, source]) became core.note items with properties type: "reference", type: null and type: ["book", "source"]. Misspelt.md (type: core.notes) was held with "core.notes is not a type this copy holds".
    • Write-back: each file's type line was written back unchanged. A second push sent nothing and left every file byte for byte the same.
    • Second directory: writes type: reference the same. Its YAML layout for the other lines differs: type: ~ for the empty line, block lists, and property order. Every new file is written this way, so this change does not cause it.
    • Edits: changing type: reference to type: core.bookmark retyped the item and dropped the property. Changing the list to type: article sent only the property. Removing the type: line cleared the property and left the type as core.note. The second directory followed the retype and the new word. I did not pull it again after the removal.
    • Edge cases: the real server accepts a property named type holding a string, a list, a number, an empty string or null.
  • Not tested: the review fix end to end against a real server (covered by the device fixture only). The Node and Swift bindings, which share this core code. The cli/folder.test.ts scenario suite was not run.

Notes for reviewers

  • Review depth: full, it changes what a folder writes to a person's files.
  • Where the value lives: the item property type. The server forbids type only as a field a type declares (types/field-shadows-item), not as an item property, and stores one as written. So no new name is needed, and the line round-trips exactly. If a client sets a type property that is itself a valid identifier, the file does not carry it. The file shows the item's type, and the property is kept on a whole save (folders/uncarried-preserve), unless the person writes a non-identifier word on the line: that word replaces the property.
  • While a non-identifier type property is present, the file does not show the item's Marfa type. A retype made elsewhere does not change the line. This is deliberate, so a person's vault stays as they wrote it.
  • Trimming: an identifier is still read trimmed, as before. A non-identifier value is kept exactly as written, including whitespace.
  • A run worth knowing about: in the end-to-end run, one push stopped with read_view_changed; hydrate again after a second folder was added under the same key. The next push delivered the queued edit. The read view is outside this change, and fix(core): finish a push after a key change, keep waits at hand-over #2255 rebuilds the copy within the same push. I did not reproduce it on main.
  • Docs for the release pass:
    • The folders guide needs an "Obsidian vaults" note: a type line that is not a Marfa type identifier becomes the property type on a note of the folder's default type, and the file keeps its line.
    • The front matter reference should say which type values are read as a type and which are kept as a property.
    • Remove any line that says an empty or non-text type makes a file unreadable.

Checklist

  • The title is a scoped Conventional Commit.
  • New behavior has a test that was seen failing first, or no test was needed and the reason is above.
  • Docs, comments and the contract still say what is true after this change.

@aicayzer aicayzer left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: ready to merge

Summary. A note whose front-matter type is a word Marfa could never use as a type, as Obsidian vaults write, now syncs as the folder's default type with the word kept as a property and written back unchanged; a valid identifier the copy does not hold is still refused. Safe to merge.

First draft: 4/5. 1 blocking issue found, fixed in c226c718e; one Reason corrected in 56ed122a9.

Found.

  • Blocking: where an app had set the item's type property to a valid identifier, a person who changed the line to a word lost it: the held property overwrote the word, nothing was sent, and the write-back reverted the file (reproduced). Fixed: the word on the line wins, stated in folders/uncarried-preserve, with a fixture seen failing first.
  • folders/foreign-type-empty gave an untrue reason for an empty value. Corrected.

Checked. A retype made on another device is not undone by the next scan (run): the line stays and no type change is sent. Create, write-back, a second device, a retype in either direction, rewording and removing the line keep or carry the person's value. hydrate::type_id gives the same answer as the checks it replaced. Numbers, lists, empty values, whitespace, wrong case, a., .b and null all become a property.

Trade-off, by decision. While a word is on the line the file does not show the item's Marfa type.

@aicayzer
aicayzer marked this pull request as ready for review October 21, 2026 10:44
@aicayzer
aicayzer enabled auto-merge (squash) October 21, 2026 10:44
@aicayzer
aicayzer merged commit 22858c8 into main Oct 21, 2026
53 checks passed
@aicayzer
aicayzer deleted the fix/099-foreign-type branch October 21, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant