Skip to content

Store bodies as HackMD does and flag title drift - #18

Merged
jserv merged 8 commits into
mainfrom
dev
Oct 9, 2026
Merged

jserv merged 8 commits into
mainfrom
dev

Conversation

@jserv

@jserv jserv commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Bodies and titles now follow how HackMD actually stores them. HackMD reads \r\n and a lone \r back as \n (measured 2026-10-08), so a body sent with CRLF could never be confirmed by its read-back, and a working file saved with CRLF looked changed on every sync check. Bodies a caller supplies and every local file read for sync are now converted first; a body read from HackMD and resent goes back exactly as read. An explicit title always wins and later body edits never rename a note, so pull and push now report title_drift when the body's own title (plain front matter, or an H1 that is its first line of text) differs from the listed one. Anything the parser cannot read for certain gives none, since a wrong suggestion would rename a note. Read-back now checks an explicit title too.

Pull also reports changes, a bounded diff of what it replaced in the local file. LocalFiles::write_atomic takes an Expect (the size and modification time of the file the caller read, or absent), checked just before the rename, so an editor's save during a pull, or into a conflict's *.remote.md, is refused as a conflict instead of lost. Three smaller commits keep upstream error details intact when no token is set, skip sizing a note list the cache will not keep, and fold RemoteSource into list_notes.

Verified with cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and make check (344 lib tests, 11 stdio tests); the third commit also passes clippy and its 325 lib tests on its own. A probe ran 2.56 million generated four-line bodies through the title parser: no panic, and no title carrying markup. The live suites were not run.

Left out: how HackMD derives a title beyond the measured precedence (inline markup, repeated spaces) is unmeasured, so the parser stays narrow until it is. A conflict snapshot is still removed if its sidecar write fails right after an editor saved into it; that predates this change.


Summary by cubic

Stores note bodies in the form HackMD actually keeps and reports title drift, so a CRLF file no longer looks changed on every sync check and a renamed H1 no longer diverges from the listing silently.

Bodies and titles

  • Converts \r\n and lone \r to \n in every caller-supplied body and every local file read for sync; HackMD reads those back as \n, so a resent body goes back exactly as read.
  • An explicit title always wins and later body edits never rename a note; hackmd_pull_note and hackmd_push_note report title_drift when the body's own title differs from the listed one, only when the parser can read that title for certain (plain front matter, or an H1 that is the body's first line of text). Push reports it only on a push or a no-op, so a missing title_drift does not mean the titles match.

Conflict safety

  • Pull reports changes, a bounded diff of what it replaced locally, and none when the old file was unreadable, oversized, not UTF-8, missing, or already matched.
  • LocalFiles::write_atomic takes an Expect — the size and modification time of the file the caller read — checked just before the rename, so an editor's save during a pull or into a conflict's *.remote.md is refused as a conflict instead of lost; a dangling symlink standing where nothing was expected is refused for the same reason.
  • The front-matter parser trusts only the indented lines a value can actually take, so stray YAML can never hide or fake a title:.
  • Preserves upstream error details when no token is set, skips sizing a note list the cache will not keep, and folds RemoteSource into list_notes.

Written for commit 1312293. Summary will update on new commits.

View guided diff Turn on auto-fix

jserv added 4 commits October 9, 2026 14:48
bounded_body redacts the token from an upstream error body with
str::replace, and an empty pattern matches between every character, so
with no token every detail came back with "[REDACTED]" between each
letter. An empty token is now left alone. Config never builds one in
production, but the function no longer depends on that.
put_notes measured a workspace's note list before checking whether the
cache keeps anything, so with a zero TTL, which the tests use, it walked
every note only to throw the size away.
RemoteSource repeated NoteSource without tracked, only so list_notes
could hand remote sources to list_remote. list_notes now matches on
NoteSource itself. Tracked records still skip the limit check, which
their own listing makes, and each remote source keeps its default order
and account-wide rules.
HackMD reads \r\n and a lone \r back as \n (measured 2026-10-08), so a
body sent with CRLF could never be confirmed by its read-back, and a
working file saved with CRLF looked changed on every check. Bodies a
caller supplies (content, create, a patch result) and every local file
read for sync are converted first. A body read from HackMD and resent
goes back exactly as read.

An explicit title always wins and later body edits never rename a note,
so an edited H1 leaves the listing behind. Pull and push now report
title_drift, the title the body gives, when it differs from the listed
one. It is read only from plain front matter or an H1 that is the
body's first line of text; anything else, including YAML or Markdown
the parser does not decode, gives none, since a wrong suggestion would
rename a note. Read-back now checks an explicit title too, which the
old precedence notes had excluded.

Pull also reports changes, a bounded diff of what it replaced in the
local file. LocalFiles::write_atomic takes an Expect, the size and
modification time of the file the caller read, or Absent, checked just
before the rename, so an editor's save during a pull or under a
conflict's *.remote.md is refused as a conflict instead of lost. A
snapshot an editor only resaved with CRLF still counts as the server's.
cubic-dev-ai[bot]

This comment was marked as resolved.

jserv added 4 commits October 9, 2026 15:02
Adding stamp left read_stamped's description above it, so stamp's doc
opened with a byte limit and a file handle it does not have, and
read_stamped had none. Each now carries its own.
A write that expects no file refuses a dangling symlink, but the
message said the file changed after it was read and to read it again,
when nothing was read and reading cannot help. It now names both
causes and asks the caller to check the path.
Any key but title let indented and dash lines follow it, so an indented
line after a closed quote or flow value, or a mapping under a plain
scalar, was skipped as part of that key. The block is then YAML that
does not parse, and HackMD takes the H1, while a later title: still
read as the title. Nested lines now follow only an empty value, indented
text only a block scalar or a plain scalar it folds into, and anything
else gives no title.
docs/tools.md promised title_drift whenever the body's title differs
from the listed one, but it is left out when the title cannot be read
for certain, and push leaves it out of conflict and remote_changed
results. A client could take a missing field as proof the titles match;
the docs now say when it is reported.
@jserv
jserv merged commit 3475279 into main Oct 9, 2026
11 checks passed
@jserv
jserv deleted the dev branch October 9, 2026 07:12
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