Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
40f4124
feat(CodeLookup): memory store — IndexedDB usage counts with lazy ser…
horner Jul 24, 2026
6bf7fec
test(CodeLookup): memory store coverage — isolation, seed merge, delt…
horner Jul 24, 2026
945895b
feat(CodeLookup): memory picklist — frequently-used codes menu on emp…
horner Jul 24, 2026
551e1d3
docs(CodeLookup): memory picklist story and README
horner Jul 24, 2026
913aaf6
chore: check off code-memory plan milestones 1-5
horner Jul 24, 2026
6e84bd1
feat(CodeLookup): keep remembered codes ranked first while typing
horner Jul 31, 2026
93de170
feat(CodeLookup): YAML import/export for memory buckets
horner Jul 31, 2026
558cf80
feat(CodeLookup): device-trust storage modes and identity gate for me…
horner Aug 1, 2026
8d4276c
feat(storybook): session-identity and device-trust toolbar globals
horner Aug 1, 2026
3b17c17
style(storybook): icon-only toolbar globals
horner Aug 1, 2026
89a910c
style(storybook): show the current value in each toolbar button
horner Aug 1, 2026
28c530f
feat(storybook): add nurse, reception and patient identities
horner Aug 1, 2026
2c344ac
fix(CodeLookup): address review on memory YAML, TTL and safelist
horner Aug 1, 2026
b1d4b9c
feat(CodeLookup): memory on by default from the provider identity
horner Aug 1, 2026
f9a1d6b
fix(lockfile): restore ychart resolution lost when submodule was absent
garrity-miepub Aug 4, 2026
13022f1
fix(CodeLookup): fail closed — session memory only in a real browser …
garrity-miepub Aug 4, 2026
a62c339
Merge branch 'main' into code-memory
garrity-miepub Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 68 additions & 17 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,21 @@ const withBrand: Decorator = (Story, context) => {
// explicit `codeLookup` / `renderCodeSearch` prop) demonstrates offline coded
// search. Stories that inject their own config still win (explicit overrides
// context); pass `codeLookup={false}` in a story to demo the plain-text opt-out.
//
// The `user` / `device` toolbar globals drive the memory picklist's two gates,
// and double as the reference for how an app wires them: one decision at the
// mount point, not per component.
const withCodeLookup: Decorator = (Story, context) => {
const locale = (context.globals.locale as string) || 'en';
const userId = (context.globals.user as string) || 'anonymous';
const trusted = context.globals.device === 'trusted';
return (
<CodeLookupProvider component={CodeLookup} indexUrl="/codify" locale={locale}>
<CodeLookupProvider
component={CodeLookup}
indexUrl="/codify"
locale={locale}
memory={{ userId, storage: trusted ? 'local' : 'session' }}
>
<Story />
</CodeLookupProvider>
);
Expand All @@ -264,24 +275,28 @@ const preview: Preview = {
theme: 'light',
density: 'standard',
locale: 'en',
user: 'anonymous',
device: 'public',
},
// The bar stays one glyph wide but still shows the current value: `title` is
// the emoji (or a per-item icon) and the wording moves to the dropdown's
// `right` column.
globalTypes: {
brand: {
name: 'Brand',
description: 'Switch between brand themes',
toolbar: {
icon: 'paintbrush',
items: [
{ value: 'bluehive', title: '🐝 BlueHive' },
{ value: 'ccme', title: '🌿 ccMe' },
{ value: 'default', title: '⚪ Default' },
{ value: 'enterprise-health', title: '🏥 Enterprise Health' },
{ value: 'mieweb', title: '🟢 MIE Web' },
{ value: 'ozwell', title: '🤖 Ozwell' },
{ value: 'waggleline', title: '🍯 Waggleline' },
{ value: 'webchart', title: '🟠 WebChart' },
{ value: 'bluehive', title: '🐝', right: 'BlueHive' },
{ value: 'ccme', title: '🌿', right: 'ccMe' },
{ value: 'default', title: '⚪', right: 'Default' },
{ value: 'enterprise-health', title: '🏥', right: 'Enterprise Health' },
{ value: 'mieweb', title: '🟢', right: 'MIE Web' },
{ value: 'ozwell', title: '🤖', right: 'Ozwell' },
{ value: 'waggleline', title: '🍯', right: 'Waggleline' },
{ value: 'webchart', title: '🟠', right: 'WebChart' },
],
dynamicTitle: true,
},
},
theme: {
Expand All @@ -293,7 +308,7 @@ const preview: Preview = {
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' },
],
dynamicTitle: true,
dynamicTitle: false,
},
},
density: {
Expand All @@ -302,10 +317,10 @@ const preview: Preview = {
toolbar: {
icon: 'collapse',
items: [
{ value: 'standard', title: 'Standard' },
{ value: 'condensed', title: 'Condensed' },
{ value: 'standard', icon: 'grow', title: 'Standard' },
{ value: 'condensed', icon: 'collapse', title: 'Condensed' },
],
dynamicTitle: true,
dynamicTitle: false,
},
},
locale: {
Expand All @@ -314,10 +329,46 @@ const preview: Preview = {
toolbar: {
icon: 'globe',
items: [
{ value: 'en', title: '🇺🇸 English' },
{ value: 'es', title: '🇪🇸 Español (sample)' },
{ value: 'en', title: '🇺🇸', right: 'English' },
{ value: 'es', title: '🇪🇸', right: 'Español (sample)' },
],
dynamicTitle: true,
},
},
user: {
name: 'Signed in as',
description:
'Simulated session identity. CodeLookup only remembers picked codes for a real user.',
toolbar: {
icon: 'user',
items: [
{ value: 'anonymous', title: '🚫', right: 'Not signed in' },
{ value: 'alice', title: '👩‍⚕️', right: 'Dr. Alice' },
{ value: 'bob', title: '👨‍⚕️', right: 'Dr. Bob' },
{ value: 'nurse', title: '💉', right: 'Nurse Nia' },
{ value: 'reception', title: '🧑‍💼', right: 'Reception Rae' },
{ value: 'patient', title: '🤒', right: 'Patient Pat' },
],
},
},
device: {
name: 'Device',
description:
'Simulates the deployment’s device-trust decision (not an end-user setting): whether picked codes may be cached on this machine.',
toolbar: {
icon: 'lock',
items: [
{
value: 'public',
icon: 'unlock',
title: 'Public kiosk — nothing stored',
},
{
value: 'trusted',
icon: 'lock',
title: 'Trusted workstation — cached',
},
],
dynamicTitle: false,
},
},
},
Expand Down
105 changes: 105 additions & 0 deletions code-memory-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# CodeLookup "Memory" — frequently-used codes picklist

Personal MRU/frequency picklist for CodeLookup: on focus with an **empty query**, show a
"Frequently used" menu of the user's most-picked codes, scoped by **(userId, context)**.
Counts persist in IndexedDB; an optional `serverUrl` seeds the list and receives lazily
flushed count deltas. On by default wherever the provider names a signed-in user —
`memory={false}` opts a provider or one box out.

> Working agreement: complete a milestone → check its boxes → run its verification →
> commit with the listed message (ask before committing, per repo policy).

## Decisions (locked)

- **Storage: IndexedDB** — house pattern (`src/components/AI/voiceprintStore.ts`,
`src/components/SuperChat/render/attachmentCache.ts`): promise-wrapped, graceful
no-IDB degradation, testable with `fake-indexeddb`.
- **Server sync: full sync** — GET seed once per (user, context) per session, merge
`max(count)`; debounced POST of count deltas; `sendBeacon` on `pagehide`.
- **Identity: developer-supplied `userId`** — missing → `'anonymous'` bucket.
Multi-user in one browser = separate buckets keyed `userId|context|fullid`.
- **Phase 1 = empty-query menu only** — personal ranking boost while typing is phase 2.
- **API: single prop** — `memory?: false | CodeLookupMemoryConfig`; absent/false = off.

## API sketch

- `CodeLookupMemoryConfig { context: string; userId?: string; serverUrl?: string; limit?: number /* default 8 */ }`
- `CodeLookupProps` gains `memory?: false | CodeLookupMemoryConfig`
- `CodeLookupProviderConfig` gains optional `memory` defaults (`userId`, `serverUrl`);
per-instance `context` always comes from the prop.
- IDB: db `mieweb-codelookup-memory`, store `usage`, key `` `${userId}|${context}|${fullid}` ``,
record `{ userId, context, fullid, label, codetype, fullcode, domain, count, lastUsed, pendingDelta }`
(full display fields stored — no shard rehydration needed).
- Sync protocol:
- `GET {serverUrl}?user={userId}&context={context}` → `[{fullid,label,codetype,fullcode,domain,count}]`
- `POST {serverUrl}` body `{user, context, deltas:[{fullid,label,codetype,fullcode,domain,delta}]}`

---

## Milestone 1 — memory store (pure TS, no React)

`src/components/CodeLookup/memoryStore.ts`, modeled on `voiceprintStore.ts`.

- [x] IDB open/upgrade helper with `hasIndexedDB()` guard; all APIs no-op cleanly without IDB (SSR/tests)
- [x] `recordUse(scope, result: CodifyResult)` — increment `count` + `pendingDelta`, set `lastUsed`
- [x] `getTopCodes(scope, limit): Promise<MemoryEntry[]>` — sort `count` desc, `lastUsed` tiebreak
- [x] `seedFromServer(scope, serverUrl)` — once per scope per session; merge `max(count)`
- [x] `scheduleFlush(scope, serverUrl)` — debounced POST of pending deltas; `sendBeacon` on `pagehide`; reset `pendingDelta` on success
- [x] `clearMemory(scope)` — reset for a (user, context) bucket

**Commit:** `feat(CodeLookup): memory store — IndexedDB usage counts with lazy server sync`

## Milestone 2 — store tests

`src/components/CodeLookup/memoryStore.test.ts` (vitest + `fake-indexeddb`, already a devDep).

- [x] record → getTopCodes ordering (count desc, lastUsed tiebreak, limit)
- [x] multi-user isolation (alice vs bob, same browser/db)
- [x] context isolation (`med-orders` vs `presenting-meds`, same user)
- [x] seed merge (server counts vs local counts → max wins; new entries added)
- [x] delta flush: mocked fetch receives correct payload; `pendingDelta` reset; failure keeps deltas
- [x] IDB-unavailable → all APIs resolve as no-ops (no throw)

**Verify:** `pnpm vitest run src/components/CodeLookup/memoryStore.test.ts`
**Commit:** `test(CodeLookup): memory store coverage — isolation, seed merge, delta flush`

## Milestone 3 — CodeLookup wiring

`src/components/CodeLookup/CodeLookup.tsx` + `context.tsx` + `index.ts`.

- [x] `memory` prop; resolve scope from prop + provider defaults (`context.tsx`: add `memory` defaults to `CodeLookupProviderConfig`, instance prop wins)
- [x] On mount / scope change: `seedFromServer` (if `serverUrl`) then load `getTopCodes` into state
- [x] Empty-query branch (`!q` in the debounced search effect): show memory entries flagged `viaMemory` instead of clearing/closing; `onFocus` opens when entries exist; `dropdownOpen` gains memory disjunct
- [x] "Frequently used" group header row (aria-labelled listbox group); existing keyboard-nav machinery reused unchanged
- [x] `pick(r)`: `recordUse` + `scheduleFlush` + refresh cached top list
- [x] Respect `initialQuery`/`skipSearchRef` seeded path — menu only on true empty-query focus
- [x] Export `CodeLookupMemoryConfig`, `MemoryEntry`, `clearMemory` from the component barrel (`index.ts`) — NOT `src/index.ts` (worker bundling constraint)

**Verify:** `pnpm vitest run src/components/CodeLookup/engine.test.ts` (no regressions) + typecheck
**Commit:** `feat(CodeLookup): memory picklist — frequently-used codes menu on empty-query focus`

## Milestone 4 — story + docs

- [x] `WithMemory` story in `CodeLookup.stories.tsx`: alice/bob user toggle × orders/presenting context toggle demonstrating isolation; stubbed `fetch` for `serverUrl` (or omit serverUrl and demo local-only)
- [x] `README.md`: document prop, scope semantics (user + context), sync protocol, storage, disable, multi-user
- [x] Manual pass in Storybook (http://localhost:6006): pick codes → refocus shows count-ordered picklist; user/context switch isolates; page reload persists

**Commit:** `docs(CodeLookup): memory picklist story and README`

## Milestone 5 — final checks

- [x] `pnpm typecheck`
- [x] `pnpm lint`
- [x] `pnpm format:fix`
- [x] Full `pnpm vitest run src/components/CodeLookup/`
- [ ] Open PR

---

## Phase 2 (explicitly deferred — do not build now)

- Personal-count boost in `searchShards` ranking while typing (compose with population `docPrior`)
— *partially delivered*: matching remembered codes are merged ahead of index hits in the
component (`matchMemory`); in-engine score composition is still deferred.
- Wrapper components (MedicationEditor, OrderEditor, AllergyManager, esheet fields) forwarding `memory` contexts through their `codeLookup` configs
- Reference server endpoint implementation
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export default [
// IndexedDB
indexedDB: 'readonly',
IDBDatabase: 'readonly',
IDBFactory: 'readonly',
IDBKeyRange: 'readonly',
IDBObjectStore: 'readonly',
IDBTransactionMode: 'readonly',
IDBOpenDBRequest: 'readonly',
Expand Down
Loading
Loading