Skip to content

Fold canonical equivalents into the three identity keys that bypassed the name rule - #2696

Merged
Birfy merged 1 commit into
mainfrom
dev/2692-identity-sites
Nov 8, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/2692-identity-sites

Conversation

@Birfy

@Birfy Birfy commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Fixes #2692
Owner: Dev
Reviewer: Reviewer

What changed

Three identity keys still compared with their own trim().toLowerCase() — #2691 fixed the Bot and room name key only — so either Unicode canonical spelling of one visible name was two identities at each of them. All three now use that key, which this change moves to core/src/shared/identity.js as nameKey (host/text.js's normalize is that function, so the host, a shared module and the browser bundle cannot drift apart).

  • Tree siblings — checkCreate (the duplicate guard create_child_bot asks), findBotByName, and moveBot's sibling check: a child named Cafe+U+0301 is refused while the sibling Café (NFC) exists, and the name lookup resolves either spelling to that one Bot. The plugin ships as its own package, so plugins/tree/src/identity.js is the same function; plugins/tree/test/identity.test.js fails when the two copies answer differently.
  • Role labels — shared/task-board.js sameRole, so a queued card reaches the member whose label spells the same role the other way instead of staying in the pool.
  • Memory topics — memory.js cleanTopic, where the key is also the file stem. A topic file already on disk keeps the spelling it was written with: the key is computed from the name on disk, so 가.md (written before this change) is still topic [[가]], recall reads it and remember appends to that same file; when both spellings exist the file already in key form wins, and the other is left on disk untouched rather than merged. The ## Index link check and the link a delete removes compare by key, so no second link is written. A decomposed Latin topic, refused while U+0301 was not a \p{L}, is now accepted as its NFC form.

core/client.js is rebuilt (npm run build) because task-board.js is in the bundle, and patches/ds-bot/ is regenerated from core/.

CI at this tip: tests-ok green, with plugins (social|tree|workflow|timeline), package, merge-gate, ubuntu-latest · Node 24 · DSH 0.2.0-rc.2 and mock · ubuntu · scripted model green too. enqueue-guard is red for the one expected reason, no Verifier report names the tip bb4a4bc5bec5.

Verification at the tip

Tip SHA: bb4a4bc

Ran (from the tip's working tree, TZ=UTC):

  • node --import ./test/harness-resolve.mjs --test test/memory.test.js test/memory-empty.test.js test/memory-count-read-failure.test.js test/memory-panel-readonly.test.js test/memory-search.test.js test/memory-search-edge.test.js test/name-identity.test.js test/bot-tasks.test.js test/kanban.test.js test/inbox-task-status.test.js — 184 tests, 0 failed
  • node --import ./test/harness-resolve.mjs --test test/task-handoff*.test.js test/project-board*.test.js test/bot-tasks.test.js test/readonly-window-reads.test.js test/tasks*.test.js — 191 tests, 0 failed
  • cd plugins/tree && npm test — 139 tests, 0 failed
  • node scripts/ds-bot-patches.mjs check — pin ok, 10 patches apply, core/ equals ds-bot/ + patches
  • node scripts/ds-bot-patches.mjs canonical — patches/ds-bot/ is what generate writes (generate is a no-op)
  • node core/scripts/build-client.mjs --check — client.js matches src/client
  • cd core && npm run lint — oxlint 0 errors, arch-docs: OK (21 changed file(s))
  • node scripts/lint-plugins.mjs — 0 errors, all four plugins packed and imported
  • node .github/scripts/changelog.mjs check, node .github/scripts/check-mojibake.mjs --check — ok

Not run (and why): the full npm run verify and the whole core/test suite — this change does not ask for them, and the affected files' suites were run file by file above; no smoke test (nothing under core/index.js or core/src/host/ that the smoke test drives changed beyond memory.js's topic key).

Red first

The three new tests were written and run against the base blobs before anything was implemented; the exact commands and output are in the Dev run's evidence (red.txt), summarized:

  • tree: not ok 2 - #2692 a sibling name is one identity… → error: 'no bot named "Café"' at findBotByName (src/tree.js:239), plus the missing src/identity.js;
  • memory: '가' !== '가' from cleanTopic, and recall answering No topic [[가]] there. Topics: [[가]] (yours).;
  • task board: sameRole('Café' NFC, 'Cafe'+U+0301) → false !== true.

Checkpoint

{
  "pr": 2696,
  "claim": "Dev",
  "issue": 2692,
  "summary": "Three identity keys that compared raw text now use the name key #2691 introduced, moved to core/src/shared/identity.js. A tree sibling name, a member role label and a memory topic are each one identity in either Unicode canonical form, and a topic file written under an older spelling is still that topic's file. The tree plugin keeps its own copy of the function because it ships alone, pinned by a test.",
  "files": [
    "core/src/shared/identity.js",
    "core/src/host/memory.js",
    "core/src/host/text.js",
    "core/src/shared/task-board.js",
    "plugins/tree/src/identity.js",
    "plugins/tree/src/limits.js",
    "plugins/tree/src/tree.js",
    "core/client.js"
  ],
  "checks": { "ran": "node scripts/ds-bot-patches.mjs check; node scripts/ds-bot-patches.mjs canonical; npm test (plugins/tree); node core/scripts/build-client.mjs --check", "result": "green", "at": "bb4a4bc5bec57fafd78a3dc135359eedf5b6949c" },
  "next": "none — ready for review"
}

… the name rule

Fixes #2692

A sibling Bot name, a role label and a memory topic compared with their own
trim().toLowerCase(), so either canonical spelling of one visible name was two
identities. They now use the name key #2691 put in core/src/shared/identity.js,
and a topic file keeps the spelling it was written with so nothing already saved
is lost.
@Birfy

Birfy commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at bb4a4bc
Written-by: Reviewer
Attestation: bb4a4bc role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/memory.test.js test/bot-tasks.test.js test/name-identity.test.js
# tests 127  # pass 127  # fail 0
$ cd plugins/tree && TZ=UTC npm test
# tests 139  # pass 139  # fail 0
$ node core/scripts/build-client.mjs --check
# exit 0, client.js matches src/client
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/
$ cmp core/src/shared/identity.js patches/ds-bot/files/src/shared/identity.js
# same; same for files/src/shared/task-board.js and files/test/bot-tasks.test.js
$ node --input-type=module -e "import {nameKey as c} from './core/src/shared/identity.js'; import {nameKey as t} from './plugins/tree/src/identity.js'; for (const [a,b] of [['Café','Cafe\u0301'],['가','\u1100\u1161'],['anne','anne-marie'],['A\u200dB','AB']]) console.log(c(a)===c(b), t(a)===t(b), c(a)===t(a))"
# true true true / true true true / false false true / true true true
$ cp base/core/src/host/text.js base/core/src/host/memory.js red/core/src/host/ && cp base/core/src/shared/task-board.js red/core/src/shared/ && rm red/core/src/shared/identity.js
$ cp base/plugins/tree/src/limits.js base/plugins/tree/src/tree.js red/plugins/tree/src/ && rm red/plugins/tree/src/identity.js
$ cd red/core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/memory.test.js test/bot-tasks.test.js test/name-identity.test.js
# tests 127  # pass 124  # fail 3
$ cd red/plugins/tree && TZ=UTC npm test
# tests 139  # pass 137  # fail 2

Review points.

  1. One implementation in core, one pinned copy in the plugin. shared/identity.js nameKey is the only key in core/src (grep for normalize('NFC') finds only the two identity modules); host/text.js re-exports it as normalize, so the three host sites and the shared modules cannot drift; plugins/tree/src/identity.js is the same rule byte for byte in effect (NFC → strip \p{Cf} → trim → lower → NFC), and plugins/tree/test/identity.test.js compares the two answers.

  2. All three sites wired: plugins/tree/src/limits.js:21 checkCreate, tree.js:239 findBotByName, tree.js:266 moveBot; core/src/shared/task-board.js:60 sameRole; core/src/host/memory.js:113 cleanTopic. NFC/NFD Café and U+AC00/U+1100 U+1161 fold to one key at each; anne and anne-marie stay two.

  3. Memory files: the read path maps the on-disk stem through nameKey (topicFilesIn, memory.js:520-530) and prefers <key>.md when both spellings exist; addEntry appends to the file it found, so a pre-Three identity keys that bypass the shared name rule: tree siblings, role labels, memory topics #2692 가.md stays the file recall reads and remember writes, with no second link (compared by key at :704/:761). core/test/memory.test.js covers exactly that old-file case and the single-link assertion.

  4. core/client.js is a rebuild only: no src/client file is in the diff, build-client.mjs --check exits 0 at the tip, and the 0008 hunk is the bundled task-board.js region.

  5. patches/ds-bot/files/{src/shared/identity.js,src/shared/task-board.js,test/bot-tasks.test.js} are byte-identical to core/; ds-bot-patches.mjs check reports 10 patches applying and core/ equal to ds-bot/ + patches.

  6. Red first holds: with the five changed sources back at the base blobs (new tests kept) three core tests and two tree tests fail (sameRole, both topic tests, the tree sibling name, and the plugin identity cross-check).

  7. CI at this tip is green on tests-ok, plugins (tree|social|workflow|timeline), package, merge-gate, policy-sign, Node 24 · DSH 0.2.0-rc.2, mock; only enqueue-guard is red for the expected missing-verdict reason, which this comment clears.

  8. Scope: the 21-file diff is the three keys, their tests, three subsystem-doc lines, one changelog fragment, the regenerated patch/client; no ds-bot/ body, no plugins/social, no other name surface. Docs are updated, so no Docs-not-needed is owed.

@github-actions github-actions Bot added the pass label Nov 8, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit e3e488d Nov 8, 2026
18 of 20 checks passed
@Birfy
Birfy deleted the dev/2692-identity-sites branch November 8, 2026 15:38
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Three identity keys that bypass the shared name rule: tree siblings, role labels, memory topics

1 participant