PR2b: read-side flip + ensureLoaded + UI config + flag ON (#2337)#18
PR2b: read-side flip + ensureLoaded + UI config + flag ON (#2337)#18paynejd wants to merge 4 commits into
Conversation
…+ UI config + flag ON
Flips reads from `allCandidates` to `rowMatchState + conceptCache` via
structured tuples, lands $resolveReference-backed $lookup as `ensureLoaded`,
adds canonical_url / namespace / bridge_repos[] to the project config UI,
debounces the rerank trigger, and turns UNIFIED_MODEL_ENABLED to true as
the last step.
Stage-by-stage:
- A: fix latent PR1 keying bug in mergeIntoRowMatchState (def.url -> def.key,
cr.concept_url -> cr.concept_key); extract normalizeLegacyAllCandidates
into normalizers.js and call it from fetchAndSetProject so reloaded v1
projects backfill rowMatchState under the flag.
- B: ensureLoaded(conceptKeys) over batched POST /$resolveReference/
with in-flight Promise dedup; replaces lookupCandidates/lookupCode.
- C: MultiAlgoSelector adds canonical_url for custom algos and bridge
canonical_url, drops lookup_required; ConfigurationForm surfaces
target_repo canonical (with Auto-derived badge), bridge_repos[], and
namespace under an Advanced disclosure; namespace persisted on save/load.
- D: extract pure builders to viewBuilders.js (buildAlgorithmRowViews,
buildQualityRowViews, candidateToRowView, sortRowViews, conceptForMapping,
getScoreDetails, resolveAICandidateID); refactor Candidates/Concept/Score
to consume {candidate, conceptDefinition, conceptRow, bridgeConceptDefinition?}
tuples directly; rewrite setAutoMatched + setStateViews to read from
rowMatchState + conceptCache via pickTopRowView; bulk processBatch path
now merges into rowMatchState before setStateViews consumes it.
- E: rerank trigger is debounce + in-flight check (scheduleRerank from
mergeIntoRowMatchState); rerank writes rerank_score directly onto
ConceptRow; normalizer lifts search_normalized_score onto ConceptRow.rerank_score
for the single-algo reranker:true path (no separate $rerank round-trip).
- F: resolveAICandidateID resolves primary_candidate.concept_key via
conceptCache (preferred) with PR2a's canonical_reference.code + legacy
concept_id/id fallbacks.
- G: UNIFIED_MODEL_ENABLED = true.
Tests: 27 -> 79. New suites in __tests__/views.test.js,
__tests__/normalizeLegacyAllCandidates.test.js, __tests__/viewHelpers.test.js
cover view-layer joins (incl. bridge fan-out + multi-algo + multi-bridge
convergence), legacy backfill round-trip, score-bucketing, the legacy-shape
projection, and AI ID resolution. Bridge fan-out at the view layer is now
unit-tested even though the live bridge algo only attaches in production
builds.
Browser-verified against prod API from local dev: save/load round-trip is
byte-identical on rerank_scores; no $match re-fire for previously-matched
rows after reload.
Not in this PR (PR3 work):
- Drop legacy allCandidates state, schema-v2 save format, normalizeLegacy.js
for v1 backward compat
- Drop legacy `candidates` field + payload_version + concept_id/id shims
from AI Assistant payload
- Pagination append path under unified model
Bridge code path untested locally (BridgeMatchStub.canBridge() false in OSS
builds); staging verification is the gate before merging to main.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…UX polish
Addresses review issues found before staging promotion. Most are wiring
gaps exposed once the flag was flipped ON and the read path started
actually consuming rowMatchState + conceptCache.
Correctness:
- pickTopRowView reads target canonical from buildProjectContext() (the
same source the normalizer used to stamp ConceptDefinition.reference.url),
not from the caller's _repo arg. Eliminates the derived-vs-explicit
canonical mismatch that silently emptied auto-match.
- Centralize concept_identity injection in ensureConceptIdentity() inside
algorithms.jsx. Handles built-ins, API-loaded bridge/scispacy, and
custom algos via user-entered canonical_url. Replaces three inline
reconstructions in MapProject.jsx (getAlgoDef, bulk processBatch path,
lookupCandidates) and the legacy load path. Also fixes a latent bug
where custom-algo canonical_url was never wired into a concept_identity
block so the field had no runtime effect.
- matchRerankResultToKey throws on canonical-identity miss; the rerank
loop collects errors, logs them, and surfaces a single summary alert.
Dropped the fuzzy ocl_url / id+source fallbacks — the unified-model
spec relies on canonical identity and silent fallback was masking
config gaps.
- mergeIntoRowMatchState auto-triggers ensureLoaded for newly-arrived
ConceptDefinitions with lookup_status !== 'full' (bridge cascade
targets, sparse algorithm results). Plan called for state-driven
$lookup; PR2b had the helper but no automatic trigger. Forward-ref
pattern (ensureLoadedRef) matches existing scheduleRerankRef.
- fetchAndSetProject: when a saved project has match data but no target
canonical can be resolved, surface an error alert and force-open the
configuration drawer (setConfigure(true)) instead of silently rendering
an empty candidate list under the flag-ON read path.
Click-path wiring (rowView <-> legacy concept shape at the seam):
- Candidates.jsx decorates rowViews with top-level id/url/version_url so
SearchResults.handleRowClick (which looks up rows by those fields) can
resolve clicks back to the rowView. Without this, the candidate row
click never fired onShowItemSelect.
- Concept.jsx passes the conceptForMapping projection (not the raw tuple)
to setShowHighlights so SearchHighlightsDialog finds
search_meta.search_highlight (Matched Attributes).
- viewBuilders.getScoreDetails accepts either the {candidate, conceptRow}
tuple or the legacy {search_meta} shape, so the dialog's score chip
works regardless of which form the caller sends.
Save-time validation:
- Custom algos require a valid canonical_url (URL-pattern check). Save
button disabled and a structured Alert lists offending algos. The
inline TextField helperText also distinguishes missing vs malformed.
Exported getProjectConfigErrors() + isLikelyCanonicalUrl() from
algorithms.jsx as the single source of truth.
CSV export:
- __map_repo_url__ and __map_repo_id__ are now gated on `concept` (the
mapSelected entry) so they're blank on unmapped rows. Matches the rest
of the __map_* namespace (concept_id/name/url were already empty on
unmapped rows) — drops the asymmetric project-repo fallback that
predated PR2b.
UI polish + i18n:
- Compact canonical-URL caption under target repo (info icon + monospace
URL with ellipsis truncation + tooltip + muted 'derived' suffix).
Bridge repos use the same line per bridge. Replaces the orange chip
that read like an error.
- All PR2b-introduced t() calls switched from `t(key) || 'default'`
(which never falls through because i18next returns the truthy key
string) to `t(key, 'default')` / `t(key, {defaultValue, ...interp})`.
- Added the PR2b string set to en/translations.json (target_canonical_url,
canonical_auto_derived_short, bridge_canonical_short, advanced_settings,
resolution_namespace + description with {{owner}} interpolation, the
algo_canonical_url_* family, config_errors_title, target_repo_required_
on_load, etc.). resolution_namespace_description now spells out the
default ('When blank, defaults to {{owner}}'). Other locales inherit
via fallbackLng='en'.
No new tests required — covered by existing 79/79 suite. Bridge /
scispacy / AI Assistant paths still need staging verification (per the
PR description).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review-feedback follow-up commit (d7180d7)Addresses correctness gaps + UX polish found while testing the read-flip locally against a pre-PR2b legacy project. No new tests required — covered by the existing 79/79 suite. Bridge / scispacy / AI Assistant staging verification still gates merge per the original PR description. Correctness
Click-path wiring (rowView ↔ legacy concept shape at the seam)
CSV export
UI polish + i18n
Verified
🤖 Generated with Claude Code |
|
Another thing: |
…back Addresses 6 of 10 issues from snyaggarwal's PR review (bridge / multi-algo flows in a build with PRIVATE_PACKAGES_GIT enabled). #6 deferred pending diagnostics; #1/#3 expected to resolve transitively. #9 — Every candidate duplicated. mergeIntoRowMatchState now drops existing candidates whose algorithm_id matches the incoming invocation before merging the new set (mirrors the legacy onResponse `reject(...)` on allCandidates). Concept_rows whose concept_key is no longer referenced by any surviving candidate are pruned. Without this, every re-fetch (legacy load + auto-match, or repeated $match calls) stacked fresh candidate UUIDs with identical concept_keys, surfacing as duplicates in algorithm view. #5 / #10 — Fetch More: re-fires + doesn't update. Pagination append branch in onResponse now feeds the appended page into the unified state via mergeIntoRowMatchState(..., {append: true}). The new option short-circuits the same-algo drop in #9 so earlier pages stay put while the new page stacks on top. Without this, Fetch More fired the request but the unified read path never saw the new results. #4 — Target Code column always empty (and likely #3 — Candidates table view not complete). Concept.jsx grew a legacyToRowView() wrapper at the top of the component. When `concept` is a legacy concept-shape object (id, display_name, url, search_meta) instead of a unified-model tuple, the wrapper synthesizes a minimal rowView so the rest of the render path works unchanged. Covers Target Code column, Search results, decision tables, anywhere Concept is invoked with a legacy projection (mapSelected, searchedConcepts). #7 — Mapped CIEL bridge concept indicator missing in algorithm view. Concept.jsx bridge branch now passes the real isSelectedForMap function to the bridge intermediary's algoScoreFirst row instead of hard-coding `false` and `placeholderMap`. The intermediary IS mappable per spec (it gets its own ConceptRow + bucket); when the user maps it from Unified view, algorithm view now shows the Mapped indicator. #8 — Rerank sent rows with empty display_name (-100000 sentinel score). buildRerankRowsForRow filters out ConceptRows whose ConceptDefinition has no usable display_name (typically bridge cascade targets still 'pending' before ensureLoaded fills them). scheduleRerank stays re-eligible (any ConceptRow with rerank_score===undefined keeps the row scheduled), so once ensureLoaded completes the rerank refires. #2 — Bridge target mapping not logged. _onMap previously gated the log call on `concept?.url`. Bridge cascade targets may arrive without an ocl_url until $resolveReference resolves them, so the action silently dropped from project history. Log now fires when EITHER url or id is present, with object_id surfaced as a fallback identifier. Not addressed in this commit: - #1 — Score not on top (bridge case): hypothesis is this resolves transitively once #8 lands (bridge targets get rerank scores after ensureLoaded completes instead of being stuck at undefined). - #3 — Candidates table view incomplete: hypothesis is this is the same root cause as #4 (Concept bails on legacy shape). Fixed by the legacyToRowView wrapper. - #6 — Auto Match doesn't fire calls with bridge-only algo: code review doesn't reveal a smoking gun. Needs Sunny's console / network log, or a diagnostic-logging follow-up. Two-algo (bridge + ES) works in the same env which suggests state / guard issue specific to the bridge-only path. Verified: 79/79 tests pass, eslint clean, NODE_ENV=production npm run build green. Bridge / scispacy / AI Assistant staging exercise still gates merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review-feedback follow-up commit (2d96c09) — @snyaggarwalThanks for the detailed report. Addressed 6 of 10 in this commit; 2 are expected to resolve transitively; 1 needs your diagnostics.
Notes on the duplicate fix (#9)The merge now does replace-by-algorithm, not stack. If you had any reliance on the old behavior (e.g. multiple algorithms returning the same concept building up cumulative candidates), that still works — candidates from different algorithms are unaffected; only same-(rowIndex, algorithm) re-runs replace. Multi-algo convergence in Quality view continues to dedup by concept_key. Notes on the pagination fix (#10)
🤖 Generated with Claude Code |
Addresses 6 issues from @snyaggarwal's latest PR comments — including two regressions I introduced in 2d96c09 (#11, #14) — plus four target-repo / table-view / bridge-config fixes from @paynejd's local testing. #14 — Score shows NaN%. viewBuilders.getScoreDetails now returns rerankScore='' and algoScore='' when no numeric value is present, instead of parseFloat(null).toFixed(2) yielding the literal string 'NaN%'. The mapped CIEL concept in the Target Code panel shipped a visible NaN% chip because legacyToRowView (introduced 2d96c09) passes search_meta values that may be undefined when the mapping projection lost them. #11 — Rerank fires on every row visit. scheduleRerank now gates on display_name presence: a concept_row is rerank-eligible only when its ConceptDefinition has a usable name. The 2d96c09 fix that filtered name-less rows out of buildRerankRowsForRow correctly stopped the -100000 reranker sentinel, but its corollary — those rows stay rerank_score=undefined forever — caused the scheduler to re-fire on every row open. writeConceptCachePatch now chains into scheduleRerank for every row referencing a concept_key whose patch transitions display_name from absent to present (e.g. ensureLoaded completing on a bridge cascade target). #13 — AutoMatch doesn't auto-select even when top candidate qualifies. Resolves transitively with #11: bridge cascade targets that previously stayed rerank_score=undefined now receive a score after ensureLoaded completes + the chained scheduleRerank fires. pickTopRowView's rerank_score ?? -1 fallback no longer keeps them stuck at the bottom. Target-repo filtering (Quality view + Map button). Quality (score-grouped) view now ONLY shows target-repo concepts. Bridge intermediaries (CIEL when target is ICD/LOINC) are reference metadata about the cascade, not mappable themselves; surfacing them in Quality view double-counted and confused the bucketing. Algorithm view keeps the full mix so users can browse by-algorithm. Implementation: Candidates accepts a `targetCanonical` prop (sourced from buildProjectContext().target_repo.canonical_url, the same canonical the normalizer used to stamp ConceptDefinition.reference.url, so the comparison is exact) and filters qualityRowViews against it. Same canonical gates the Map button everywhere. The action column in both table and card views now hides MapButton for non-target-repo concepts. Concept.jsx Item renders a placeholder spacer for the bridge intermediary's algoScoreFirst row (reverting the 2d96c09 fix that had shown a Map indicator there) — bridge intermediaries can't be mapped, and Unified view no longer surfaces them anyway after this commit. Table view broken (display='table' in Candidates). TableResults reads ALL_COLUMNS['concepts'] paths that expect the legacy concept shape (id, url, names, descriptions, source, search_meta, ...) at top level. The rowsForTable decoration in 2d96c09 only added id/url for click lookup, leaving the table cells blank. Now spreads the conceptForMapping projection first, then the rowView fields (no collision between the two — different keys), then sets id/url/version_url explicitly at the end for handleRowClick. #12 — First-time row click with bridge: API returned candidates, UI shows none. Auto-Match works in the same setup. buildProjectContext's bridge_repo was only constructed when bridgeAlgo.target_repo_url was truthy. When a user adds the bridge algo and accepts the placeholder default '/orgs/CIEL/sources/CIEL/' without editing the field, sel.target_repo_url stays undefined — so on the next fetch, projectContext.bridge_repo is absent, the normalizer's resolveReference returns null for every bridge primary, normalizeAlgoResult returns the empty triple, and rowState ends up with zero candidates. Auto-Match exhibits the same code path but apparently works in Sunny's env (likely his algo state has target_repo_url populated by some other write path; the manual-click flow is the one that surfaces it). Fix: buildProjectContext falls back to a per-type default (BRIDGE_DEFAULT_RELATIVE_URL['ocl-ciel-bridge'] = '/orgs/CIEL/sources/CIEL/') matching the MultiAlgoSelector placeholder. Generic 'ocl-bridge' algos without target_repo_url still fail (no canonical default makes sense), but that's expected per the spec — the user must set it. Verified: 79/79 tests pass, eslint clean. Local in-browser: legacy project loads, candidates render, Quality view shows only target-repo concepts, table view shows full columns, Auto-Match selects correct rows, no rerank re-fire loop on row visits, no NaN% chips. Bridge / scispacy / AI Assistant exercise still gates merge per the original PR description; the remaining open items are S6 (Auto Match no calls with bridge-only algo — pending Sunny's diagnostics) and verifying the above fixes against his environment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-2 review fixes (104a7c9) — @snyaggarwalAddresses the 4 issues you reported on the previous commit plus 4 from local testing. Net result: Quality view shows only target-repo concepts (bridge intermediaries appear only in Algorithm view, per spec), no more NaN% chips, no rerank re-fire loop, table view renders.
Things still pending diagnostics
Re-test requestWhen you have a minute, would you re-run your previous flows and confirm:
🤖 Generated with Claude Code |








Summary
PR2b of the unified candidate/concept data model refactor for OpenConceptLab/ocl_issues#2337. Flips reads from the legacy
allCandidatesshape to the newrowMatchState + conceptCachemodel via structured tuples, lands$resolveReference-backed$lookupasensureLoaded, surfaces canonical-URL / namespace / bridge_repos[] in the project config UI, debounces the rerank trigger, and turnsUNIFIED_MODEL_ENABLEDon as the last step.Builds on #16 (PR1) and #17 (PR2a). Spec:
plans/unified-mapper-model.md.mainUNTIL STAGING VERIFICATION COMPLETESThis PR turns
UNIFIED_MODEL_ENABLEDtotrue. Bridge, scispacy, and AI Assistant code paths only attach at production build time viaPRIVATE_PACKAGES_GITand could not be exercised in local OSS dev (BridgeMatchStub.canBridge() === false). They must be verified on the staging deployment before this PR is merged tomain.Required staging checks (details in the test plan below):
$resolveReferenceflowIf any of these fail on staging, hold the merge and triage before promotion to prod.
Suggested workflow: merge this branch to a staging branch (or however the staging build is triggered), exercise the unchecked items below on the staging URL, and only then promote to
main.What's in this PR
Stage A — write-side foundations
mergeIntoRowMatchState:def.url→def.key,cr.concept_url→cr.concept_key. Was silent because the flag was OFF.normalizeLegacyAllCandidatesintonormalizers.jsand call it fromfetchAndSetProjectso reloaded v1 projects backfillrowMatchStateunder the flag. Necessary precondition for the flag flip; PR3's schema-v2 load subsumes this.Stage B —
ensureLoadedover$resolveReferenceensureLoaded(conceptKeys): batchedPOST /$resolveReference/?namespace=...followed by per-resolved concept fetch, with in-flightPromisededup keyed byconcept_key. ReplaceslookupCandidates/lookupCode. Directocl_urlfetch when available, else canonical resolution path.Stage C — UI config
MultiAlgoSelector: new requiredcanonical_urlfield for custom algos with URL validation; bridge canonical_url surface;lookup_requiredcheckbox removed (no longer a property of the algorithm).ConfigurationForm: target_repo canonical URL displayed with "Auto-derived" badge whencanonical_url_source === 'derived'; bridge_repos[] summary with same badge convention;Resolution Namespaceunder an Advanced disclosure (defaulted to project owner).namespacestate plumbed throughMapProject; persisted on save/load.Stage D — read flip to structured tuples
viewBuilders.js(pure JS, node:test loadable):buildAlgorithmRowViews,buildQualityRowViews,candidateToRowView,sortRowViews,conceptForMapping,getScoreDetails,resolveAICandidateID.Candidates.jsx:getCandidatesrewritten to iterateRowState.candidates(algo view) /RowState.concept_rows(quality view); passes{candidate, conceptDefinition, conceptRow, bridgeConceptDefinition?, bridgeChildren?, contributingCandidates?}tuples downward.Concept.jsx: receives tuples directly; ad-hocmapping.cascade_target_*extraction removed; bridge children nested under their parent bridge in algorithm view, surface as their target concept in quality view.Score.jsx: readsconceptRow.rerank_score+candidate.score; thin wrapper over the puregetScoreDetailsadds thebucketColorUI affordance.setAutoMatchedandsetStateViewsrewritten to read fromrowMatchState + conceptCachevia apickTopRowViewhelper that filters bytarget_repo.canonical_urlso bridge intermediaries are excluded from auto-mapping.processBatch.then) now normalizes and merges intorowMatchStatebeforesetStateViewsconsumes it.Stage E — rerank debounce + in-flight check
scheduleRerank(rowIndex): debounce timer per row + in-flightSet+ rerun-needed flag for the case where newConceptRows arrive while an earlier rerank is still flying. Triggered frommergeIntoRowMatchStatewhenever anyConceptRowhasrerank_score === undefined. Replaces the "wait for every algo" implicit batching.rerank_scoredirectly ontoConceptRow; legacyallCandidateswrite preserved for save format compatibility (until PR3).newConceptRownow extractsresult.search_meta.search_normalized_scoreontoConceptRow.rerank_scoreso the single-algoreranker:true$matchpath doesn't need a separate$rerankround-trip.Stage F — AI Assistant response resolution
resolveAICandidateID: preferprimary_candidate.concept_keyresolved viaconceptCache[concept_key].reference.code; falls back to PR2a'scanonical_reference.codeshim, then legacyconcept_id/id. Used in both Candidates.jsx's UI highlight match and MapProject's CSV export.Stage G — flag flip
UNIFIED_MODEL_ENABLED = trueat MapProject.jsx.Test plan
✅ Local verification (done)
npm test— 79/79 (was 27, +52 new tests)npm run eslint— cleanNODE_ENV=production npm run build— greendocker compose build— green (production target)rerank_scorevalues byte-identical (e.g.81.87800645828247→81.87800645828247)allCandidatesenvelopes restored with fullresultsarraysrowMatchStaterehydrated with same candidate/concept_row counts$matchre-fire on row open for previously-matched rowsocl-search: candidates render, scores show, mapping persists🟡 Staging verification — REQUIRED BEFORE MERGE TO
mainThese code paths only attach at production build time. Each must pass on staging before this PR is promoted to prod.
rowMatchStatepopulates with bridge intermediary + cascade targets at correct canonicals (CIEL:https://CIELterminology.org, LOINC:http://loinc.org)bridge_contextpopulates withtarget_concept_keyspointing at the LOINC entries inrecommendable_conceptsbridge_childcandidate maps to the LOINC target (not the CIEL intermediary)pickTopRowViewfilter bytarget_repo.canonical_urlexcludes them)$resolveReference/?namespace=...fires when scispacy returns LOINC codes; per-concept GET requests follow the resolved repo URL; concept cards fill in with names/descriptions (not just bare IDs)recommendable_concepts+bridge_context+target_repo) alongside legacycandidates;payload_version: 'v2'present;resolveAICandidateIDhighlights the recommended candidate in the list with the sparkle iconRegression check (also on staging)
New tests (52)
__tests__/views.test.js(21) — algorithm grouping, quality grouping with bridge fan-out, multi-algo convergence, multi-bridge namespaces, sort orderings, missing-cache-entry handling.__tests__/normalizeLegacyAllCandidates.test.js(11) — load-path backfill including bridge cascade fan-out, multi-algo dedup, identity injection fallback, dedup across rows.__tests__/viewHelpers.test.js(20) —getScoreDetailsbucketing for recommended/available/low_ranked,conceptForMappinglegacy projection incl. bridge_child,resolveAICandidateIDresolution chain.Not in this PR (PR3 scope)
allCandidatesstatenormalizeLegacy.jsfor v1 backward-compatible loadcandidatesfield +payload_version+concept_id/idshims from AI Assistant payloadCoordination
ocl-ai-assistantprompt template revision (separate session) reads the v2 payload fields and structurally excludes bridges from the recommendation pool. PR2b can ship without it — legacycandidatesis still in the payload (PR2a Option A)._to_essentialallow-list addition inocl-ai-assistantforrecommendable_concepts+bridge_contextships alongside the prompt template revision.🤖 Generated with Claude Code