diff --git a/.sync/PORTING.md b/.sync/PORTING.md index 4f3f4a50..804dda02 100644 --- a/.sync/PORTING.md +++ b/.sync/PORTING.md @@ -177,13 +177,16 @@ material. Reproduce its *intent* in b24ui by editing files under `src/` only. re-inspected here** — treat "upstream has no such parameter" as an inference from b24ui's own history rather than a verified fact, and re-check before acting on it. What is certain is that replaying upstream's four-parameter - signature wholesale drops behaviour shipped in v2.8.0 while **nothing goes - red**: `useTokenSearch` appears nowhere under `test/`. (Reverting the signature - alone does fail `typecheck`, since `CommandPalette.vue` passes five arguments; - it is a port that replays both files that goes quiet.) Several §2 rules here - name a guarding spec and some — locales, playground-manifest mirroring — do - not; this one does not, and #363 tracks the gap. Port the intent, keep the - fifth argument, and prefer adding that coverage over trusting this paragraph. + signature wholesale drops behaviour shipped in v2.8.0. Until #369 that + happened while **nothing went red** — `useTokenSearch` appeared nowhere under + `test/`, and reverting the signature alone only failed `typecheck`, since + `CommandPalette.vue` passes five arguments; a port that replayed both files + went quiet. Now guarded by `describe('useTokenSearch')` in + `test/utils/search.spec.ts`, which fails against a revert to upstream's + four-parameter signature, and by `describe('highlighting through fuse')` in + `test/components/CommandPalette.spec.ts`, which fails if the fifth argument + stops being forwarded from `processGroupItems`. Port the intent and keep the + fifth argument; trust those two specs over this paragraph. - **`skills/` is b24ui-authored — never replay upstream skill or doc prose into it.** The package was seeded from nuxt/ui's skill, and every defect the #93 audit found was an inherited upstream idiom rather than an ordinary typo: @@ -358,3 +361,4 @@ forward, since every commit between the two would then never be judged. - 2026-08-11 — review of PR #347 (issue #339): added the §2 **`highlight()` takes a fifth `useTokenSearch` argument** invariant, and corrected the `.sync/nuxt-ui.json` summary for `2a172ef` that asserted "highlight signature matches 1:1". The divergence has been in the tree since v2.8.0 and was never recorded: `c502157b` added `tokens`/`minTokenLength` and `6743f793` the parameter itself, both to `src/runtime/utils/fuse.ts`, and the port in `557a5178` renamed the file to `search.ts` — so a pickaxe on the current path returns only `557a5178`, a genuine upstream port, unless you pass `--follow`. That rename, not the trailer convention, is what hid it; `Upstream:` trailers are too rare (16 of ~3200 commits) to carry an inference either way. It has no test coverage; #363 tracks that. Worth recording how the error was found: the "byte-identical with upstream" premise originated **here**, in `595923b9` (PR #338), was repeated in #339, and was inherited in good faith by the external contributor whose PR prompted the check — nuxt/ui itself has still not been inspected, so the divergence is established from b24ui's history alone. Last reviewed: 2026-08-11. - 2026-08-12 — the sync is manual by decision; the automation is removed. Deleted `.sync/PLAN.md` (the dispatcher/porter/on-merge design, its phase plan and its cron) and `.sync/RUNBOOK.md` (an incident playbook whose every row diagnosed one of those workflows). Dropped `sync_enabled` from the ledger — a kill-switch for a dispatcher that will not exist reads as "the sync is off" to anyone who finds it, which was already misleading while this file's own procedure ran twelve ports past it — and `stats`, Phase-4 telemetry that was never written to (`noop_ratio: 0` against an actual 47/226). Folded the one runbook row that survives manual work into §6: a cursor SHA that vanishes under an upstream force-push must be moved to the nearest surviving ancestor with a tracking issue, never skipped forward. §6 now spells out the procedure that was previously only implied by the workflows — parent-order reconstruction, verbatim diffs, the gate order with `docs:generate` and `deploy.yml`'s env, ledger reconciliation including the last-entry case, and the `behind` rebase. Also corrected `color-map.json`: `warning` mapped to `air-primary-alert`, the same token as `error`, so the table said the two upstream colors were interchangeable; `air-primary-warning` exists and is used 50 times in `src/theme/`. Last reviewed: 2026-08-12. - 2026-08-12 — rebuilt `icon-map.json` and gave it a guard (the content of the closed PR #67, verified rather than imported). The map is now *derived*: for every icon key both sides define — `src/theme/icons.ts` upstream, `src/runtime/dictionary/icons.ts` here — the row is (upstream's lucide name → whatever our dictionary maps that key to), 37 pairs from a 43×39 key intersection at cursor `3dbca02`. Beware the obvious shortcut when re-checking this: the installed `@nuxt/ui@4.8.2` in `node_modules` (pulled in transitively by `nuxtseo-layer-devtools`) is **older than the sync cursor** and is missing keys — three separate reviewers read it and concluded `star` was fabricated and the intersection was 36. Read the raw file at the cursor SHA instead. The derivation turned up three errors in the values #67 proposed, each of which resolves to a real icon and so would have failed no import: `i-lucide-rotate-cw` for what upstream calls `i-lucide-rotate-ccw` (`reload`), `i-lucide-circle-check` for `copyCheck`'s `i-lucide-copy-check`, and `i-lucide-refresh-cw`, which no upstream key uses. It also surfaced seven derivable pairs #67 missed — `drag`, `panelClose`, `panelOpen`, `star`, `stop`, `copyCheck`, `reload` — and, separately, `i-lucide-terminal`, the **only** `i-lucide-*` literal upstream hardcodes under `src/` (`src/theme/prose/code-icon.ts`), which neither the old map nor #67 had even though `prose/CodeIcon.vue` has answered it all along. `error` and `success` gained judgement rows rather than staying unmapped: our `caution` carries a `// this for error` comment, and `copyCheck` already owns the glyph `success` would want. The five entries #67 dropped (`activity`, `arrow-up-to-line`, `house`, `settings`, `user`) are kept — they match no key on either side, which is the hardcoded-literal case the map exists for. **Correcting the record on the five values #67 changed** (`check`, `chevronDown`, `chevronUp`, `minus`, `x`): they are wrong because the map must agree with the dictionary, *not* — as an earlier draft of this entry claimed — because the library never renders them. It does. `Checkbox.vue` renders `main/CheckIcon` and `actions/Minus20Icon`, `Badge.vue` renders `actions/Cross20Icon`, `Button.vue` renders `outline/ChevronDownSIcon`; roughly half of the icon paths under `src/` are hardcoded in components that never read the dictionary, which is #380. That discovery also reshaped the guard: `test/utils/icon-map.spec.ts` allows any icon used anywhere in `src/` rather than only the dictionary's — the narrower rule rejected `terminal`, a correct row — while separately requiring every *derived* row to equal what its semantic key resolves to. That last check is the one with teeth: without it, pointing `i-lucide-check` at another icon the dictionary genuinely uses passed every other assertion. It guards wrong rows, not stale ones; nothing here notices if upstream renames a default. No `.sync/log/` or ledger entry, since this is not a port of an upstream commit — same as #343, #346, #351 and #377. Last reviewed: 2026-08-12. +- 2026-08-12 — coverage for #363: gave the §2 **`highlight()` takes a fifth `useTokenSearch` argument** invariant a guard. It was recorded during the review of #347 but left untested, and the bullet said so. The parameter and the token-search logic around it are b24ui-only — `c502157b` added `tokens`/`minTokenLength` and `6743f793` the parameter itself, both against the file's old name `src/runtime/utils/fuse.ts`, both shipped in v2.8.0. Immediately before `c502157b` the function took four parameters and computed no `minTokenLength` at all, which is what marks it as locally authored; the `Upstream:` trailer convention is too sparse (16 of ~3200 commits) to carry an inference either way. The later port `557a5178` renamed `fuse.ts` to `search.ts` and carried the divergence across, so a pickaxe on the current path returns only that port — pass `--follow` to see the two commits that introduced it. Until now it had no test at all, so replaying upstream's four-parameter signature would have dropped a shipped feature with nothing going red. Upstream itself has not been re-inspected; treat "upstream has no such parameter" as an inference from b24ui's own history. Last reviewed: 2026-08-12. diff --git a/test/components/CommandPalette.spec.ts b/test/components/CommandPalette.spec.ts index 720e9846..c34c2eb7 100644 --- a/test/components/CommandPalette.spec.ts +++ b/test/components/CommandPalette.spec.ts @@ -317,4 +317,43 @@ describe('CommandPalette', () => { window.HTMLElement.prototype.scrollIntoView = original } }) + + describe('highlighting through fuse', () => { + // Everything above sets `labelHtml`/`suffixHtml`/`descriptionHtml` by hand, + // which is exactly the path that skips `highlight()`. These mount the real + // wiring instead: `processGroupItems` forwards + // `fuse.fuseOptions.useTokenSearch` as the fifth argument, and Fuse only + // populates `matches` when `includeMatches` is set — which this component + // leaves to the caller, so no other case in this file reaches `highlight()` + // at all. + function paletteWith(label: string, searchTerm: string, useTokenSearch?: boolean, threshold = 0.1) { + return mountSuspended(CommandPalette, { + props: { + groups: [{ id: 'g', items: [{ label }] }], + searchTerm, + fuse: { fuseOptions: { includeMatches: true, useTokenSearch, threshold, ignoreLocation: true } } + } as any + }) + } + + it('marks the match', async () => { + const wrapper = await paletteWith('alpha beta', 'alpha') + + expect(wrapper.find('mark').exists()).toBe(true) + expect(wrapper.find('mark').text()).toContain('alpha') + }) + + it('marks each word of a multi-word query only when token search is on', async () => { + // Fuse returns the two words as separate regions, six and four characters + // long. Off, the threshold is the whole query — ten — and neither reaches + // it, so nothing is marked; on, it drops to the shortest word and both do. + // An exact-match fixture would not show the difference: Fuse then returns + // one region as long as the query, which clears either threshold. + const off = await paletteWith('alpha xxxx beta', 'alpha beta', false, 0.4) + const on = await paletteWith('alpha xxxx beta', 'alpha beta', true, 0.4) + + expect(off.find('mark').exists()).toBe(false) + expect(on.find('mark').exists()).toBe(true) + }) + }) }) diff --git a/test/utils/search.spec.ts b/test/utils/search.spec.ts index ffc1503f..02266f14 100644 --- a/test/utils/search.spec.ts +++ b/test/utils/search.spec.ts @@ -61,6 +61,52 @@ describe('highlight', () => { expect(highlight({ label: 'foo' }, 'foo', 'label')).toBeUndefined() }) + describe('useTokenSearch', () => { + // b24ui-only fifth argument (see `.sync/PORTING.md` §2). It only moves + // `minTokenLength`, the threshold a region must reach to be marked at all: + // off, that is the length of the whole search term; on, the length of the + // shortest word in it — so multi-word queries highlight each word instead of + // nothing. + function highlightWith(value: string, indices: [number, number][], searchTerm: string, useTokenSearch?: boolean) { + return highlight({ label: value, matches: [{ key: 'label', value, indices }] }, searchTerm, 'label', undefined, useTokenSearch) + } + + it('marks a region shorter than the whole term', () => { + // The region is 5 long. `alpha beta` is 10, so the full-term threshold + // rejects it; the shortest token, `beta`, is 4, so token search accepts it. + expect(highlightWith('alpha beta', [[0, 4]], 'alpha beta', true)).toBe('alpha beta') + }) + + it('leaves that same region unmarked when off', () => { + expect(highlightWith('alpha beta', [[0, 4]], 'alpha beta', false)).toBe('alpha beta') + }) + + it('is off by default — the path `CommandPalette` takes unless `fuseOptions` opts in', () => { + expect(highlightWith('alpha beta', [[0, 4]], 'alpha beta')).toBe('alpha beta') + }) + + it('still refuses a region shorter than the shortest token', () => { + // `be` is 2; the shortest token `beta` is 4. Token search lowers the bar, + // it does not remove it. + expect(highlightWith('alpha beta', [[6, 7]], 'alpha beta', true)).toBe('alpha beta') + }) + + it('falls back to the whole term when the tokenizer matches nothing', () => { + // `/[\p{L}\p{M}\p{N}_]+/gu` finds no token in an emoji-only query, so + // `tokens` is empty and the threshold stays `searchTerm.length` — 2 code + // units for one emoji — rather than collapsing to zero and marking + // everything. + expect(highlightWith('\u{1F600}\u{1F600}\u{1F600}', [[0, 1]], '\u{1F600}', true)) + .toBe('\u{1F600}\u{1F600}\u{1F600}') + }) + + it('takes the shortest token, not the first or the longest', () => { + // Tokens are `considerable` (12) and `ab` (2); only a 2-long threshold + // admits this region. + expect(highlightWith('xy considerable', [[0, 1]], 'considerable ab', true)).toBe('xy considerable') + }) + }) + describe('truncation from the start', () => { // Matches a high surrogate not followed by a low one, or a low surrogate not // preceded by a high one — i.e. half of an astral character.