Skip to content

PR2b: read-side flip + ensureLoaded + UI config + flag ON (#2337)#18

Open
paynejd wants to merge 4 commits into
mainfrom
issues#2337-pr2b
Open

PR2b: read-side flip + ensureLoaded + UI config + flag ON (#2337)#18
paynejd wants to merge 4 commits into
mainfrom
issues#2337-pr2b

Conversation

@paynejd
Copy link
Copy Markdown
Member

@paynejd paynejd commented May 11, 2026

Summary

PR2b of the unified candidate/concept data model refactor for OpenConceptLab/ocl_issues#2337. Flips reads from the legacy allCandidates shape to the new rowMatchState + conceptCache model via structured tuples, lands $resolveReference-backed $lookup as ensureLoaded, surfaces canonical-URL / namespace / bridge_repos[] in the project config UI, debounces the rerank trigger, and turns UNIFIED_MODEL_ENABLED on as the last step.

Builds on #16 (PR1) and #17 (PR2a). Spec: plans/unified-mapper-model.md.


⚠️ DO NOT MERGE TO main UNTIL STAGING VERIFICATION COMPLETES

This PR turns UNIFIED_MODEL_ENABLED to true. Bridge, scispacy, and AI Assistant code paths only attach at production build time via PRIVATE_PACKAGES_GIT and could not be exercised in local OSS dev (BridgeMatchStub.canBridge() === false). They must be verified on the staging deployment before this PR is merged to main.

Required staging checks (details in the test plan below):

  1. Bridge cascade fan-out end-to-end (CIEL → LOINC)
  2. Scispacy $resolveReference flow
  3. AI Assistant recommendation with v2 payload

If 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

  • Fix latent PR1 keying bug in mergeIntoRowMatchState: def.urldef.key, cr.concept_urlcr.concept_key. Was silent because the flag was OFF.
  • Extract normalizeLegacyAllCandidates into normalizers.js and call it from fetchAndSetProject so reloaded v1 projects backfill rowMatchState under the flag. Necessary precondition for the flag flip; PR3's schema-v2 load subsumes this.

Stage B — ensureLoaded over $resolveReference

  • ensureLoaded(conceptKeys): batched POST /$resolveReference/?namespace=... followed by per-resolved concept fetch, with in-flight Promise dedup keyed by concept_key. Replaces lookupCandidates / lookupCode. Direct ocl_url fetch when available, else canonical resolution path.

Stage C — UI config

  • MultiAlgoSelector: new required canonical_url field for custom algos with URL validation; bridge canonical_url surface; lookup_required checkbox removed (no longer a property of the algorithm).
  • ConfigurationForm: target_repo canonical URL displayed with "Auto-derived" badge when canonical_url_source === 'derived'; bridge_repos[] summary with same badge convention; Resolution Namespace under an Advanced disclosure (defaulted to project owner).
  • namespace state plumbed through MapProject; persisted on save/load.

Stage D — read flip to structured tuples

  • New viewBuilders.js (pure JS, node:test loadable): buildAlgorithmRowViews, buildQualityRowViews, candidateToRowView, sortRowViews, conceptForMapping, getScoreDetails, resolveAICandidateID.
  • Candidates.jsx: getCandidates rewritten to iterate RowState.candidates (algo view) / RowState.concept_rows (quality view); passes {candidate, conceptDefinition, conceptRow, bridgeConceptDefinition?, bridgeChildren?, contributingCandidates?} tuples downward.
  • Concept.jsx: receives tuples directly; ad-hoc mapping.cascade_target_* extraction removed; bridge children nested under their parent bridge in algorithm view, surface as their target concept in quality view.
  • Score.jsx: reads conceptRow.rerank_score + candidate.score; thin wrapper over the pure getScoreDetails adds the bucketColor UI affordance.
  • setAutoMatched and setStateViews rewritten to read from rowMatchState + conceptCache via a pickTopRowView helper that filters by target_repo.canonical_url so bridge intermediaries are excluded from auto-mapping.
  • Bulk match path (processBatch.then) now normalizes and merges into rowMatchState before setStateViews consumes it.

Stage E — rerank debounce + in-flight check

  • scheduleRerank(rowIndex): debounce timer per row + in-flight Set + rerun-needed flag for the case where new ConceptRows arrive while an earlier rerank is still flying. Triggered from mergeIntoRowMatchState whenever any ConceptRow has rerank_score === undefined. Replaces the "wait for every algo" implicit batching.
  • Rerank writes rerank_score directly onto ConceptRow; legacy allCandidates write preserved for save format compatibility (until PR3).
  • Normalizer Stage E lift: newConceptRow now extracts result.search_meta.search_normalized_score onto ConceptRow.rerank_score so the single-algo reranker:true $match path doesn't need a separate $rerank round-trip.

Stage F — AI Assistant response resolution

  • resolveAICandidateID: prefer primary_candidate.concept_key resolved via conceptCache[concept_key].reference.code; falls back to PR2a's canonical_reference.code shim, then legacy concept_id / id. Used in both Candidates.jsx's UI highlight match and MapProject's CSV export.

Stage G — flag flip

  • UNIFIED_MODEL_ENABLED = true at MapProject.jsx.

Test plan

✅ Local verification (done)

  • npm test — 79/79 (was 27, +52 new tests)
  • npm run eslint — clean
  • NODE_ENV=production npm run build — green
  • docker compose build — green (production target)
  • Save/load round-trip end-to-end against prod API from local dev:
    • 4 matched rows preserved across save → hard refresh → reload
    • rerank_score values byte-identical (e.g. 81.8780064582824781.87800645828247)
    • allCandidates envelopes restored with full results arrays
    • rowMatchState rehydrated with same candidate/concept_row counts
    • No $match re-fire on row open for previously-matched rows
  • Single-algo ocl-search: candidates render, scores show, mapping persists
  • Configuration form: canonical URL badge, namespace under Advanced, bridge canonical surfaces
  • Bridge fan-out at the view layer unit-tested (data shape exercised via fixtures; live algo waits for staging)

🟡 Staging verification — REQUIRED BEFORE MERGE TO main

These code paths only attach at production build time. Each must pass on staging before this PR is promoted to prod.

  • Bridge (CIEL → LOINC) — per PR2a's verification notes:
    • rowMatchState populates with bridge intermediary + cascade targets at correct canonicals (CIEL: https://CIELterminology.org, LOINC: http://loinc.org)
    • AI payload bridge_context populates with target_concept_keys pointing at the LOINC entries in recommendable_concepts
    • Quality view shows the LOINC target with both contributing candidates while the CIEL bridge appears as its own bucketed concept
    • Selecting a bridge_child candidate maps to the LOINC target (not the CIEL intermediary)
    • Auto-Match never selects a bridge intermediary (the pickTopRowView filter by target_repo.canonical_url excludes them)
  • Scispacy$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)
  • AI Assistant — request body shows v2 fields (recommendable_concepts + bridge_context + target_repo) alongside legacy candidates; payload_version: 'v2' present; resolveAICandidateID highlights the recommended candidate in the list with the sparkle icon

Regression check (also on staging)

  • Auto-match a project across all rows (multi-algo + bridge) → verify Mapped/Ready/Unmapped counts match expectations
  • Open AI Assistant recommendation on a multi-algo + bridge row → verify primary_candidate is from the target repo (not a bridge intermediary)
  • Save → reload an existing legacy v1 project → verify candidates render identically (the normalize-on-load path)

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) — getScoreDetails bucketing for recommended/available/low_ranked, conceptForMapping legacy projection incl. bridge_child, resolveAICandidateID resolution chain.

Not in this PR (PR3 scope)

  • Drop legacy allCandidates state
  • Schema-v2 save format + normalizeLegacy.js for v1 backward-compatible load
  • Drop legacy candidates field + payload_version + concept_id/id shims from AI Assistant payload
  • Pagination append path under the unified model

Coordination

  • The ocl-ai-assistant prompt template revision (separate session) reads the v2 payload fields and structurally excludes bridges from the recommendation pool. PR2b can ship without it — legacy candidates is still in the payload (PR2a Option A).
  • The _to_essential allow-list addition in ocl-ai-assistant for recommendable_concepts + bridge_context ships alongside the prompt template revision.

🤖 Generated with Claude Code

paynejd and others added 2 commits May 11, 2026 17:31
…+ 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>
@paynejd
Copy link
Copy Markdown
Member Author

paynejd commented May 12, 2026

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

  • Language zh added #1pickTopRowView canonical filter: now reads target_repo.canonical_url from buildProjectContext(), the same source the normalizer uses to stamp ConceptDefinition.reference.url. Eliminates the derived-vs-explicit mismatch that could silently empty auto-match.
  • [Snyk] Security upgrade nginx from 1.19-alpine to 1.29.4-alpine #2 — Custom-algo canonical_url: was decorative — user could enter a URL but nothing wired it into concept_identity. Centralized injection in new ensureConceptIdentity() helper (algorithms.jsx), used by getAlgoDef, bulk processBatch.then, lookupCandidates, and the legacy load path. Also adds Save-time validation: Save button disabled and an Alert lists offending algos when canonical_url is missing/malformed.
  • OpenConceptLab/ocl_issues#2315 | Updated Search Highlights dialog #7 — Rerank fail-loud: matchRerankResultToKey throws on canonical-identity miss instead of silently falling back to fuzzy ocl_url / id+source matches. The rerank loop collects errors, logs a single summary entry, and surfaces an alert to the user.
  • OpenConceptLab/ocl_issues#2388 | custom encoder model for reranking #8 — State-driven ensureLoaded: mergeIntoRowMatchState now auto-triggers ensureLoaded for newly-arrived ConceptDefinitions with lookup_status !== 'full' (bridge cascade targets, sparse algorithm results). Uses a forward-ref pattern mirroring scheduleRerankRef.
  • OpenConceptLab/ocl_issues#2485 | Copy project #13 — Missing target canonical on load: when a saved project has match data but no resolvable target canonical, surface an error alert and force-open the configuration drawer instead of silently rendering an empty candidate list.

Click-path wiring (rowView ↔ legacy concept shape at the seam)

  • Candidate row click → details modal: Candidates.jsx decorates rowViews with top-level id/url/version_url so SearchResults.handleRowClick can resolve clicks back to the rowView and fire onShowItemSelect.
  • Score chip click → Matched Attributes: Concept.jsx passes the conceptForMapping projection (not the raw tuple) to setShowHighlights so SearchHighlightsDialog finds search_meta.search_highlight.
  • Score chip unified score regression: viewBuilders.getScoreDetails now accepts either the {candidate, conceptRow} tuple or the legacy {search_meta} shape so the dialog works regardless of which form the caller sends.

CSV export

  • __map_repo_url__ and __map_repo_id__ gated on concept (the mapSelected entry) so they're blank on unmapped rows — matches the rest of the __map_* namespace. Dropped 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 warning-colored chip.
  • All PR2b-introduced t() calls switched from t(key) || 'default' (which never falls through — i18next returns the truthy key string when missing) to t(key, 'default') / t(key, {defaultValue, ...interp}).
  • Added the PR2b string set to en/translations.json. resolution_namespace_description now spells out the default behavior: "When blank, defaults to {{owner}}" — answers a reviewer question about whether namespace defaults to global or to the project owner (it's the project owner). Other locales inherit via fallbackLng: 'en'.

Verified

  • 79/79 unit tests pass, eslint clean, NODE_ENV=production npm run build green.
  • Live in-browser: legacy project loads, candidates render, row clicks open details, score chips open the highlights dialog with unified score + matched attributes, Auto-Match maps a row whose __map_algorithm__ correctly serializes to ocl-semantic via the unified-model projection.

🤖 Generated with Claude Code

@snyaggarwal
Copy link
Copy Markdown
Contributor

snyaggarwal commented May 12, 2026

Tried against 2 rows with CIEL bridge algo for ICD-11-WHO

  1. Score is not coming on top
Screenshot 2026-05-12 at 12 12 09 2. mapping to bridge target (ICD) is not getting logged Screenshot 2026-05-12 at 12 10 45 3. Candidates table view not complete Screenshot 2026-05-12 at 16 45 29 4. Target code on left is always empty Screenshot 2026-05-12 at 12 12 09 5. Fetch More in Candidates is not doing anything (no calls in network, infinite loading candidates...) Screenshot 2026-05-12 at 16 47 22
  1. Auto Match not firing any calls with just bridge algo. With two algos (bridge + ES Token Search) it did.

  2. Map a CIEL candidate from Unified candidates list and then go to algorithm view of candidates, no indicator in candidates that which CIEL concept was mapped

@snyaggarwal
Copy link
Copy Markdown
Contributor

Another thing:
This PR may send the rows to rerank API with no name (None) because bridge targets lookup may fail.
Reranker will assign -100000 score to those and they look weird in candidates list

@snyaggarwal
Copy link
Copy Markdown
Contributor

Trying 8 rows with only Semantic Algo, target CIEL -- Automatch worked here.

  1. every candidate is duplicated:
Screenshot 2026-05-12 at 17 11 06 2. Fetch More button fires the call but doesnt update the candidates list

…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>
@paynejd
Copy link
Copy Markdown
Member Author

paynejd commented May 12, 2026

Review-feedback follow-up commit (2d96c09) — @snyaggarwal

Thanks for the detailed report. Addressed 6 of 10 in this commit; 2 are expected to resolve transitively; 1 needs your diagnostics.

# Issue Status
9 Every candidate duplicated Fixed — mergeIntoRowMatchState now drops same-algo candidates on each invocation (mirrors the legacy reject(...) on allCandidates). Concept_rows pruned when no surviving candidate references them.
5/10 Fetch More re-fires + doesn't update Fixed — pagination append branch feeds appended results into rowMatchState via a new {append: true} option so earlier pages stay put.
4 Target code on left always empty FixedConcept.jsx now wraps legacy concept-shape input through a legacyToRowView() synthesizer at the top. One change covers Target Code column, decision tables, Search results, anywhere Concept is invoked with a legacy projection.
7 Mapped CIEL concept indicator missing in algorithm view Fixed — bridge intermediary's algoScoreFirst row was hard-coded isSelectedForMap={false}. Now passes the real function so the Mapped indicator renders when the user mapped the intermediary from Unified view.
8 Rerank sent rows with no name → -100000 FixedbuildRerankRowsForRow filters out ConceptRows whose ConceptDefinition has no usable display_name (typically pending bridge cascade targets). scheduleRerank stays re-eligible so the row reranks once ensureLoaded completes.
2 Bridge target mapping not logged Fixed_onMap no longer gates the log entry on concept?.url. Bridge cascade targets can lack ocl_url until $resolveReference resolves them; log now fires on url || id and surfaces object_id as fallback.
1 Score not on top (bridge case) Expected to resolve transitively via #8: once bridge targets get rerank_scores from the (now non-poisoned) rerank pass, they sort to the correct position. Worth re-checking after this commit.
3 Candidates table view not complete Expected to resolve transitively via #4: same root cause (Concept bailing on legacy shape). Worth re-checking.
6 Auto Match doesn't fire calls with bridge-only algo Pending — need diagnostics from you. Code review doesn't show a smoking gun; fetchBulkBridgeCandidates looks correct and canBridge should be true in your env. When you re-test, could you share: (a) the browser console + network tab during the bridge-only Auto Match, (b) whether the row was Unmapped when you clicked it (the bulk path filters rowStatuses.unmapped when autoMatchUnmappedOnly is true — a stale readyForReview entry would skip the row silently), and (c) whether bridge-only with autoMatchUnmappedOnly=false works?

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)

mergeIntoRowMatchState now takes an options arg: mergeIntoRowMatchState(rowIndex, normalized, {append: true}). The append path skips the same-algo drop and the orphan concept_row pruning. All existing callers default to append=false; only the offset > 0 branch in fetchAllCandidatesForRow.onResponse uses append=true.

🤖 Generated with Claude Code

@snyaggarwal
Copy link
Copy Markdown
Contributor

snyaggarwal commented May 12, 2026

  1. (with bridge) on candidates visit, it makes rerank call every time.
  • rerank -> save -> reload -> row click -> rerank again
  • rerank -> close -> open -> rerank
  1. No candidates rendered even when api returned candidates (this is the row I never ran earlier to get candidates)
Screenshot 2026-05-12 at 18 00 59
  1. AutoMatch ran and got the candidates but dint selected map when all criterias for top candidates matched

  2. Score is still NAN

Screenshot 2026-05-12 at 18 04 31

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>
@paynejd
Copy link
Copy Markdown
Member Author

paynejd commented May 12, 2026

Round-2 review fixes (104a7c9) — @snyaggarwal

Addresses 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.

# Issue Status
11 Rerank fires on every row visit FixedscheduleRerank now gates on whether the ConceptDefinition has a usable display_name. Rows that the previous-commit's buildRerankRowsForRow filter skips no longer stay eligible forever. writeConceptCachePatch chains into scheduleRerank when a lookup transitions to having a display_name, so once ensureLoaded completes for a pending bridge cascade target, the row gets a score.
14 Score shows NaN% FixedgetScoreDetails returns rerankScore: '' / algoScore: '' when no numeric value is present. The Target Code panel's chip will now render empty (or you can choose to hide it) rather than NaN%.
13 AutoMatch ran but didn't auto-select Expected to resolve transitively via #11 — cascade targets now receive rerank scores via the chained scheduleRerank, so pickTopRowView's sort no longer parks them at -1. Worth re-testing.
12 First-time row click w/ bridge: API returned data, UI shows none FixedbuildProjectContext now falls back to '/orgs/CIEL/sources/CIEL/' when bridgeAlgo.target_repo_url is missing (matches the MultiAlgoSelector placeholder). Without this, the normalizer's resolveReference returned null for every bridge primary and rowState ended up empty. Auto-Match happened to work because of state-ordering quirks; manual click surfaced the underlying bug.
Quality view should only show target-repo concepts FixedbuildQualityRowViews output is now filtered by conceptDefinition.reference.url === target_canonical (sourced from buildProjectContext()). Bridge intermediaries are gone from this view; they only appear in Algorithm view as parent groupings for their cascade children.
Algorithm view shows Map button on every candidate Fixed — Map action gated on the same target_canonical everywhere (action column + Concept.jsx Item). Bridge intermediary's algoScoreFirst row reverts to the placeholder spacer it had before; cascade children retain their Map action since they're in target_repo.
Table view broken for all candidates FixedrowsForTable now spreads the conceptForMapping legacy projection AND the rowView fields. Table cells read legacy fields (names, descriptions, source, etc.); card view renderer reads the tuple. Click-lookup still works (id/url/version_url set explicitly at the end).

Things still pending diagnostics

Re-test request

When you have a minute, would you re-run your previous flows and confirm:

  1. Bridge cascade in Algorithm view — bridge intermediary visible, no Map button on it, cascade children below with their Map buttons.
  2. Quality view — only target-repo concepts (no CIEL intermediaries).
  3. Open a row that hasn't been matched yet → candidates render (previously empty).
  4. Open a previously-matched row → no extra rerank API call fires (previously fired every time).
  5. Map a row, then re-open the Mapping Decisions panel — score chip shows a number (previously NaN%).
  6. Switch to table view — full columns populated (previously blank).

🤖 Generated with Claude Code

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.

2 participants