Skip to content

Accept legacy share links and report patch drift - #30

Merged
jserv merged 5 commits into
mainfrom
dev
Oct 17, 2026
Merged

jserv merged 5 commits into
mainfrom
dev

Conversation

@jserv

@jserv jserv commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Older course pages link notes as https://hackmd.io/s/<shortId>, which the tools refused. A short link names no workspace, so it is now found by its shortId: with team_path only that team is searched, without one the personal notes and then every team, and a shortId found more than once comes back as candidates rather than a guess. A trailing /download (the published-copy address) is accepted too. Patch edits on hackmd_update_note now report title_drift the way pull and push do, since renaming a note's H1 by patch never renames the note.

Supporting that meant telling an omitted team_path from the personal workspace: tool inputs now hold Option<Workspace>, and resolve_note_ref decides once what "none given" means for each kind of reference, which removes the guards that had let push search only the tracked team for a short link. On the wire nothing changes: null, an omitted field and the older tagged {"kind": "personal"} default all still mean "none given", and the team_path schema is the same nullable string.

Verified with make check (407 unit tests plus the integration targets) and cargo clippy --all-targets --all-features -- -D warnings, both clean. Reviewed by Codex, Agy and Claude across two rounds plus /code-review and /simplify. Live suites were not run. Left out: running team listings concurrently (risks 429 bursts on accounts in many teams) and title_drift for whole-body content replacements.


Summary by cubic

Accepts legacy https://hackmd.io/s/<shortId> share links as note references in every tool and reports title_drift on patch edits in hackmd_update_note, which previously kept that rename mismatch silent.

Legacy share links

  • With team_path only that team is searched; without one, personal notes and then every team are searched, using the existing 60-second caches.
  • A shortId found in more than one workspace returns candidates, never a guess, and a team whose list fails fails the search with an error suggesting team_path.
  • A trailing /download is ignored, and push cross-checks a short link across all workspaces so a link naming another workspace's note is a tracking mismatch.
  • Tool inputs now hold Option<Workspace> so an omitted team_path differs from the personal workspace; resolve_note_ref decides once what "none given" means for each reference kind. On the wire nothing changes: null, omission, and the older tagged {"kind": "personal"} still mean none given.

Patch title drift

  • A patch that renames the front-matter title: or a leading H1 reports the implied title as title_drift, judged against the read-back's title like push does.
  • Drift is reported only when the body before the patch matched the listed title, so a heading kept apart on purpose and a first H1 added to a body that had none stay silent.

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

View guided diff Turn on auto-fix

jserv added 4 commits October 17, 2026 22:10
A patch that renames a note's H1 or front-matter title leaves the
listed title as it was, since HackMD never renames a note for a body
edit. Pull and push already report that mismatch as title_drift; a
patch edit on hackmd_update_note did not, so the caller had to notice
it alone.

A patch now reports it the way push does. The body before the patch
stands in for the last-synced one: drift is reported only when that
body's title matched the listing, so a heading kept apart from the
title on purpose stays silent, as does a first H1 added to a body that
had none. It is judged against the title the read-back returns, so a
rename made while the patch was in flight is not reported. A patch
that changes nothing writes nothing and reports no drift.
Older course pages link notes as https://hackmd.io/s/<shortId>, a
share link HackMD redirects to the note's published address, and the
tools refused it. Such a link names no workspace, so the note is found
by its shortId: with team_path only that team is listed, and without
one the personal notes and then every team. A shortId found more than
once comes back as candidates, never a guess. A team whose list fails
fails the search, since the note may be there, and the error says that
team_path narrows it while keeping the kind of the error it wraps.

Push cross-checks its note_ref the same way, across every workspace,
so a short link naming another workspace's note is a tracking
mismatch rather than a not-found. Restore refuses a short link like
an @owner/slug URL, since trashed notes are never listed.

A trailing /download, the address that checks a published copy, is
dropped from a complete reference, so a slug that is itself download
still names its note. The host of a scheme-less hackmd.io reference
is matched case-insensitively, as the URL parser already did with a
scheme; the path stays case-sensitive.
Tool inputs read team_path as a Workspace, which defaults to the
personal one, so "no team_path given" and "the personal workspace"
were the same value. Each caller that needed the difference guessed at
it: a short link searched every workspace when it saw Personal, push
swapped Personal for the tracked note's workspace unless the reference
was a URL, and the tracked list treated Personal as "every workspace".
A guard missing from one of those lists is how push came to search
only the tracked team for a short link.

The inputs that take a team_path now hold Option<Workspace>, and
resolve_note_ref decides once what None means: personal for a bare ID,
every workspace for a short link, the URL's own for @owner/slug. Push
passes the tracked workspace as the home of a bare ID through
resolve_note_ref_from rather than rewriting team_path. The guards are
gone.

On the wire nothing changes for an omitted team_path, a null one
(which MCP clients send for an unset field), or a team path, and the
schema is the same nullable string. Only the older tagged form can say
{"kind": "personal"} explicitly, and it now means personal alone: a
short link searches personal notes only, the tracked list keeps
personal records only, push refuses it for a team-tracked note, and an
@owner/slug URL owned by someone else is refused as a mismatch.
The previous commit let the older tagged {"kind": "personal"} object
mean the personal workspace alone. Clients built against that older
schema send it as their default on every call, so they would have lost
team URLs, sync checks, pushes of team-tracked files and the full
tracked list. A team_path input now goes through models::given, which
reads null, omission and the tagged personal object alike as none
given, so only a team is ever Some. The refusal of an explicit
personal workspace beside another owner's URL could no longer happen
and is gone.

Short-link resolution now shares the slug path's candidate building
and its found, not found or ambiguous decision instead of copying
them. A patch edit keeps only the title of its read-back rather than
a second copy of the body while it judges drift.
cubic-dev-ai[bot]

This comment was marked as resolved.

The team_path docs said to omit it for personal notes, which does not
hold for an s/<shortId> link: omitted, it searches the personal notes
and then every team. The note_ref docs now also say a trailing
/download is ignored, and the update tool says a patch reports
title_drift only when the listed title matched the body's title
before the patch.

A test now reads ListNotesInput with no team_path, a null one, and
the older tagged personal object, and checks that history accepts
each rather than refusing it as a team.
@jserv
jserv merged commit 09bdb17 into main Oct 17, 2026
12 checks passed
@jserv
jserv deleted the dev branch October 17, 2026 14:56
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