Skip to content

spaces: page aliases and unlinked mentions - #439

Open
nyblnet wants to merge 1 commit into
mainfrom
spaces-mentions
Open

spaces: page aliases and unlinked mentions#439
nyblnet wants to merge 1 commit into
mainfrom
spaces-mentions

Conversation

@nyblnet

@nyblnet nyblnet commented Sep 10, 2026

Copy link
Copy Markdown
Owner

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 to spaces/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.ts untouched).

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() emits alias-collision naming 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 v flag expresses this as one set difference and is not used — v is 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>York collapses to New York and 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.

synthetic space buildIndex one page's mentions all pairs
100 pages, 252KB 0.2ms 1.3ms 62ms
500 pages, 1.26MB 1.1ms 3.4ms 417ms
1000 pages, 2.5MB 1.9ms 6.5ms 1000ms

The 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/main and 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 (new mentions rig: 59 assertions); --manifest ok
  • tsc -b clean; build:single + shell-gate clean; test-sync-spaces 3/3
  • i18n: 7 new strings, all 8 catalogs, packed.ts grepped — 100% every locale
  • 13 sabotages, each guard removed in turn and watched to go red, then restored. The self-mention guard is doubled, so removing either half proves nothing — both had to go before the rig noticed, and that is now written next to the code.
  • Browser-verified on a uniquely-named copy over http://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 — synthetic input events do not reach the [[ handler, which is exactly the instrument that lies. No console errors.

Notes for review

  • .github/workflows/ci.yml and scripts/test-spaces.mjs are the shared surface, claimed by path. Not optional: test-spaces.mjs exits 2 on a rig that exists but is unlisted, and --manifest exits 2 on one listed but unregistered. One step, appended at the tail.
  • scripts/test-spaces-mentions.ts is new rather than an extension of test-spaces-model.ts, to keep out of a contended file.
  • docs/DECISIONS.md currently ends with a Claude-Session: URL from an earlier session. Left alone — not mine to rewrite — but flagging it, since it is the attribution the standing rule forbids.

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.
@github-actions

Copy link
Copy Markdown

Build size

main (0100083) → spaces-mentions (b006413)

app base PR change
bento/slides 677.9 KiB 677.9 KiB 0.0 KiB (0.00%)
bento/spaces 271.3 KiB 276.2 KiB +4.9 KiB (+1.80%)
bento/dash 424.1 KiB 424.1 KiB 0.0 KiB (0.00%)

Updated: 2026-09-10T00:22:50Z

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