spaces: page aliases and unlinked mentions - #439
Open
nyblnet wants to merge 1 commit into
Open
Conversation
A page can answer to more than one name, and a page's names are found in
prose that never linked to it.
Aliases resolve at LINK time, in one place — `nameIndex` in the new
spaces/src/mentions.ts — read by the `[[…]]` resolver, ⌘K and the `[[`
page picker. What lands in the file is an ordinary `#p/<id>` href, so
backlinks, the graph, export, print and the CRDT never learn that
aliases exist. All three surfaces or none: an alias that links but
cannot be searched reads as the search being broken.
`Page.aliases` is additive and absent by default; clearing the last one
deletes the key, so a page that had one and lost it is byte-identical to
one that never had it. Two pages may claim a name — a title beats an
alias, then document order — and validate() reports `alias-collision`
naming the page a link will actually reach.
Unlinked mentions sit under the backlinks with a one-click Link button.
Most of the work is refusal: not inside a code block, a code span, an
existing link, a URL or a mail address; not in a block that already
links to the target; not the page's own text; not part of a longer
word; and nothing shorter than three characters (two for Han/kana/
Hangul).
The word-boundary rule is the CJK decision. `(?<![\p{L}\p{N}_])` does
not degrade in Japanese, it returns zero forever — every neighbouring
kana is a letter — so a boundary is required only where the NAME's own
edge is a word character in a script that separates words. The `v` flag
says this in one term and is not used: it is Safari 17 against this
app's Safari 16.4 floor, and a regex that will not compile throws on
every page open rather than missing a match.
Not quadratic: reading a page scans the document once for that page's
names, so cost is the size of the space and not the count of pages.
Measured on a synthetic 1000-page, 2.5MB space — 6.5ms per page open
against 1.9ms for buildIndex; 1.3ms at 100 pages. `bento.mentions()`
adds the all-pairs answer for agents, read-only and off every paint
path.
scripts/test-spaces-mentions.ts covers it, negatives first; each guard
was removed in turn and watched to fail. Shell cost +5.0KB compressed.
Build size
Updated: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two features that are one idea: a page is known by more than one name, and those names appear in prose whether or not anybody typed
[[ ]]around them. New logic is confined tospaces/src/mentions.ts; the diffs against the files nine sibling branches are also touching are small (editor.ts+110,model.ts+23,main.ts+13,agent.ts+42,props.ts+40,markdown.tsuntouched).Aliases
Page.aliases—["NYC", "the Big Apple"]on a page titled New York — edited in the page panel as one comma-separated field.Resolved at link time, in one function (
nameIndex), read by the[[…]]resolver, ⌘K and the[[page picker. What gets written into the file is an ordinary#p/<id>href, so backlinks, the graph, export, print and the CRDT never learn that aliases exist. All three surfaces or none — an alias that links but cannot be searched means you file something under the name you use for it and then cannot find it by that name, which reads as the search being broken.Additive: absent by default, and clearing the last alias deletes the key rather than storing
[], so a page that had one and lost it is byte-identical to one that never had one. Verified through the shell's own serializer.Collisions are reported, never repaired. A title always beats an alias, then document order — identical in every replica.
validate()emitsalias-collisionnaming the page a[[link]]will actually reach.Unlinked mentions
Under the backlinks: every place this page's title or aliases appear as plain words elsewhere, with the sentence and a Link button that wraps exactly the words that matched.
Most of the work is what it refuses to find — a title inside a code block, a code span, an existing link, a URL or a mail address; a block that already links to the target; the page's own text; part of a longer word; anything under three characters (two for Han/kana/Hangul).
The matching rule, and CJK
The obvious boundary is
(?<![\p{L}\p{N}_]). Applied to Japanese it does not find fewer mentions — it finds none, ever, because every kana beside a name is a letter and the assertion never opens. That is a silent total failure in three of this app's nine locales, and it passes every test written in English.So a boundary is required only where the name's own edge character is a word character in a script that separates words.
私は東京に住んでいますmentions東京. The stated cost, because it is real: a Han name also matches inside a longer Han compound (京都inside東京都). Both halves are asserted.Two mechanical notes: the
vflag expresses this as one set difference and is not used —vis Safari 17 against this app's Safari 16.4 floor (DecompressionStream), and a regex the engine cannot compile throws on every page open rather than missing a match. And the run scanner separates skipped regions with U+0000, not a space: with a space,New<a>x</a>Yorkcollapses toNew Yorkand reports a mention nobody wrote.Cost — measured, not assumed
"Every page's title against every page's text" is N×M and the panel never computes it. Opening a page scans the document once for that page's names, so cost is the size of the space, not the number of pages in it.
buildIndexThe all-pairs column is
bento.mentions(), the read-only agent surface, and is on no paint path. One measurement corrected a claim in an earlier draft of these comments: the four ways of spelling the boundary all run within noise of each other (0.03–0.05ms per 210KB pass), so the choice is the browser floor and nothing else. The real cost I had introduced was an unanchored email pattern in the URL mask — 7ms → 21ms — now gated.Shell: +5.0KB compressed (277,769 → 282,781 B), measured by building
origin/mainand this branch back to back. The size rig reports +24KB against a reference that has drifted behind other merged work.Verification
node scripts/test-spaces.mjs— all 9 rigs pass (newmentionsrig: 59 assertions);--manifestoktsc -bclean;build:single+shell-gateclean;test-sync-spaces3/3packed.tsgrepped — 100% every localehttp://127.0.0.1:5417, marker-checked (typeof bento.mentions === 'function'). All five negatives held in the real shell; the Link button spliced exactly the right occurrence and the mention became a backlink; ⌘K and the[[picker both found the page by alias with real keystrokes — syntheticinputevents do not reach the[[handler, which is exactly the instrument that lies. No console errors.Notes for review
.github/workflows/ci.ymlandscripts/test-spaces.mjsare the shared surface, claimed by path. Not optional:test-spaces.mjsexits 2 on a rig that exists but is unlisted, and--manifestexits 2 on one listed but unregistered. One step, appended at the tail.scripts/test-spaces-mentions.tsis new rather than an extension oftest-spaces-model.ts, to keep out of a contended file.docs/DECISIONS.mdcurrently ends with aClaude-Session:URL from an earlier session. Left alone — not mine to rewrite — but flagging it, since it is the attribution the standing rule forbids.