Skip to content

Type-aware label resolution in useIriLabels — eliminate fallback 404 noise #202

Description

@JohnRDOrazio

Problem

useIriLabels (lib/hooks/useIriLabels.ts) is type-agnostic: it resolves labels for arbitrary IRIs without knowing whether each one is a class, property, or individual. Its current strategy (after #200 / PR #104):

  1. Skip well-known external-vocabulary IRIs (skos, rdfs, owl, dcterms, …)
  2. Primary: searchEntities by local name → returns label and type for any project entity
  3. Fallback: getClassDetail → fires only when search returns no match

That fallback is the last source of console 404 noise. It fires for internal IRIs whose local name doesn't surface in the search index — typically opaque ID-style local names like RCBqIJm4IPngJgyvh49kP62. When such an IRI is in fact a property or individual, the class endpoint correctly returns 404; we catch the error silently, but Chrome still logs the 404.

The user-visible symptom (verified live):

GET /api/v1/projects/<id>/ontology/classes/https%3A%2F%2Fontology.catholicos.catholic%2FRCBqIJm4IPngJgyvh49kP62?branch=main
→ 404 (Not Found)

…repeating for every opaque-IRI'd property or individual referenced from the open panel. Functionally harmless; cosmetically bad.

Approach options

Option A — Per-IRI type hints from the caller (frontend-only)

Callsites already know the role of each IRI:

  • PropertyDetailPanel resolving parentIris → those are properties
  • PropertyDetailPanel resolving domainIris / rangeIris → those are classes
  • IndividualDetailPanel resolving typeIris → classes, sameAsIris → individuals, etc.

Change useIriLabels's API from (iris: string[], opts) to (entries: { iri: string; type?: SelectableEntityType }[], opts) (or three keyed lists). The hook routes each IRI to its specific endpoint when the type is known, falling through to search only when no hint is provided.

  • ✅ Eliminates 404 noise entirely for typed entries
  • ✅ Pure frontend change, no backend dependency
  • ✅ The SelectableEntityType type alias added in PR feat: preserve entity selection across viewer/editor modes #104 (lib/utils/selectionUrl.ts) is the natural shape
  • ⚠ Every caller of useIriLabels needs updating (currently PropertyDetailPanel, IndividualDetailPanel, plus indirect callers)
  • ⚠ "Mixed" IRI lists (e.g., a free-form annotation target) still need the search fallback

Option B — Parallel multi-type probes (frontend-only, no caller change)

When search misses, fire getClassDetail + getPropertyDetail + getIndividualDetail in parallel via Promise.allSettled, take the resolution that succeeds.

  • ✅ No caller changes
  • ❌ Doesn't reduce 404 noise — it just spreads it across three URL prefixes instead of one. Each parallel call to a non-matching endpoint still hits the network and logs 404.
  • Worth noting only to rule out.

Option C — Backend: single multi-type endpoint

Add GET /api/v1/projects/<id>/ontology/entities/<iri> returning { iri, type, labels, … } regardless of entity type. useIriLabels calls it as the only fallback.

  • ✅ One call per IRI, zero 404s
  • ✅ Lets us deprecate the search-by-local-name probe entirely if the endpoint is fast enough
  • ⚠ Backend change required (ontokit-api), needs API design review and a migration plan
  • ⚠ Fast-path performance vs. the existing per-type endpoints is unknown — a multi-type lookup may be slower than a typed one

Recommendation

A first, C eventually. Option A is contained, doesn't depend on the backend team, and gives correct routing immediately. The 1-3 callsites that pass mixed/free-form IRIs can keep the search fallback. If/when the API gains a multi-type endpoint (Option C), useIriLabels simplifies further but the caller-side type hints from Option A remain useful for documentation and short-circuit performance.

Plan (Option A)

1. Reshape useIriLabels's input

// lib/utils/selectionUrl.ts (existing)
export type SelectableEntityType = "class" | "property" | "individual";

// lib/hooks/useIriLabels.ts (new shape)
export interface IriToResolve {
  iri: string;
  type?: SelectableEntityType;
}

export function useIriLabels(
  entries: IriToResolve[],
  opts: { projectId: string; accessToken?: string; branch?: string; labelHints?: Record<string, string> },
): Record<string, string>;

Internally:

  • For each entry, if type is set: call the corresponding getClassDetail / getPropertyDetail / getIndividualDetail directly. No fallback.
  • If type is unset: keep today's search-first strategy (search → class fallback).

2. Migrate callsites

  • components/editor/PropertyDetailPanel.tsx — pass typed entries for parentIris (property), domainIris/rangeIris (class), inverseOf (property), relationship targets (varies — keep untyped).
  • components/editor/IndividualDetailPanel.tsx — typeIris (class), sameAsIris/differentFromIris (individual), object-property assertion subjects (typically class), data-property assertion targets (literal — skip).
  • Any other callers of useIriLabels audited.

3. Audit projectOntologyApi

Confirm getPropertyDetail and getIndividualDetail exist with signatures parallel to getClassDetail. If they don't, add them (small backend-aware addition, but already needed by the detail panels themselves).

4. Tests

  • __tests__/lib/hooks/useIriLabels.test.ts — extend with cases:
    • Typed-property entry calls getPropertyDetail, never getClassDetail.
    • Typed-individual entry calls getIndividualDetail, never getClassDetail.
    • Untyped entry falls back to today's search-first behavior.
    • Mixed entry list (some typed, some untyped) routes correctly per entry.
  • PropertyDetailPanel.test.tsx and IndividualDetailPanel.test.tsx — assert that the right API methods are called when the panel resolves related-entity labels.

Done criteria

  • Opening any panel with internal property or individual IRIs in its rendered annotations / domain / range / parent / etc. produces zero 404s in the dev console.
  • Existing label resolution for class IRIs unaffected (regression coverage).
  • useIriLabels's untyped path still works for free-form annotation targets where the type is unknown.

Out of scope

  • Backend multi-type endpoint (Option C). Worth a separate ticket once Option A lands.
  • Reducing the search-first fallback further (already minimal noise with external-vocabulary skip + search-first ordering).

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions