Three-Tier Navigation Implementation Plan

Overview

Transform the CEDS Ontology Browser from a two-tier (domain → classes) to a three-tier navigation system (domain → middle tier → classes/option sets) using first-word grouping for the middle tier.

Important Context and Configuration

Database Location

Known Test Data

Use these actual refIds for testing:

Authentication Requirements

Existing Components to Verify

IMPORTANT: Test these existing components as if implementing fresh, verify they work correctly:

Success Metrics


PHASE 1: Database Setup and Data Population

Goal: Create entity lookup table and populate with all CEDS entities using first-word middle tier grouping

TQ SAYS (about the above):

1.1 Create Database Table

File: Manual SQL execution or add to rdf-tester initialization

CREATE TABLE IF NOT EXISTS CEDS_RDF_UI_SUPPORT_INDEX (
    refId TEXT PRIMARY KEY,
    entityType TEXT NOT NULL,
    code TEXT,
    uri TEXT,
    label TEXT,
    prefLabel TEXT,
    notation TEXT,
    domainRefId TEXT,
    functionalAreaRefId TEXT,
    parentRefId TEXT,
    isOptionSet INTEGER DEFAULT 0,
    displayOrder INTEGER,
    crossDomainList TEXT,
    createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
    updatedAt TEXT DEFAULT CURRENT_TIMESTAMP
);

-- Create indexes
CREATE INDEX IF NOT EXISTS idx_entity_type ON CEDS_RDF_UI_SUPPORT_INDEX(entityType);
CREATE INDEX IF NOT EXISTS idx_domain ON CEDS_RDF_UI_SUPPORT_INDEX(domainRefId);
CREATE INDEX IF NOT EXISTS idx_functional_area ON CEDS_RDF_UI_SUPPORT_INDEX(functionalAreaRefId);
CREATE INDEX IF NOT EXISTS idx_parent ON CEDS_RDF_UI_SUPPORT_INDEX(parentRefId);
CREATE INDEX IF NOT EXISTS idx_code ON CEDS_RDF_UI_SUPPORT_INDEX(code);

TQ SAYS (about the above): Add to rdf-tester. It has to be able to create the entire working database for this in an empty sqlite file (in fact, it has to be able to create the file, too.)

Also, I would like to call this table CEDS_RDF_UI_SUPPORT_INDEX

1.2 Implement Population Logic in rdf-tester

File: /cli/lib.d/rdf-tester/rdf-tester.js

Add new command: --buildEntityLookup

Implementation Steps:

  1. Add command line option handling

  2. Create buildEntityLookupTable() function

  3. Implement first-word extraction:

    function extractFirstWord(className) {
        // Handle special cases
        if (!className) return 'Uncategorized';
    
        // Simple extraction: everything before first camelCase boundary
        // This handles: StudentRecord → Student, LEAAccountability → LEA, 123ABC → 123ABC
        const match = className.match(/^[A-Z][a-z]+|^[A-Z]+(?=[A-Z][a-z])|^[a-z]+|^[0-9]+|^./);
        return match ? match[0] : 'Other';
    }
    

    Note: This is intentionally simple - just extract whatever comes before the first camelCase word boundary. We will refine categorization later. Edge cases:

  4. Process entities in order:

  5. For each class:

  6. For each property:

TQ SAYS (about the above): Shouldn't the list in #4 include code sets?

ANSWER: Code sets (option sets) are already included in Step C "Insert all classes" because option sets ARE classes, just marked with ConceptScheme. They're not subordinate to other classes - they're peer classes that get referenced by properties.

Also, make sure the README and the help text explain everything about how to use this for either updating or creating a new database from scratch.

1.2a Documentation Requirements

Update rdf-tester README and help text to include:

1.3 Verification Steps

# Run the population
./rdf-tester --buildEntityLookup

# Verify data in SQLite
sqlite3 /path/to/cedsIds.sqlite3

# 1. Verify exact counts by entity type
SELECT entityType, COUNT(*) FROM CEDS_RDF_UI_SUPPORT_INDEX GROUP BY entityType;
# Expected: 
#   domain: 9
#   functionalArea: 50-150 (depends on unique first words)
#   class: ~1344 (including option sets)
#   property: 748

# 2. Check first-word extraction edge cases
SELECT functionalAreaRefId, COUNT(*) as cnt, GROUP_CONCAT(label, ' | ') as examples
FROM CEDS_RDF_UI_SUPPORT_INDEX 
WHERE entityType IN ('class', 'optionSet')
GROUP BY functionalAreaRefId 
ORDER BY cnt DESC
LIMIT 10;
# Expected: Logical groupings like "Student" (30-50), "Organization" (20-40), etc.
# Watch for: Single-item groups that should be merged, overly large groups (>100)

# 3. Verify functional areas were created as entities
SELECT refId, label FROM CEDS_RDF_UI_SUPPORT_INDEX 
WHERE entityType = 'functionalArea'
ORDER BY label;
# Expected: Clean first-word entries like "Student", "Organization", "Course"

# 4. Verify option set detection accuracy
SELECT label, isOptionSet, jsonString LIKE '%ConceptScheme%' as hasConceptScheme
FROM CEDS_RDF_UI_SUPPORT_INDEX
WHERE entityType = 'class'
AND (isOptionSet = 1 OR jsonString LIKE '%ConceptScheme%')
LIMIT 10;
# Expected: isOptionSet and hasConceptScheme should always match

# 5. Check properties have proper metadata
SELECT COUNT(*) as total,
       COUNT(label) as has_label,
       COUNT(code) as has_code,
       COUNT(uri) as has_uri
FROM CEDS_RDF_UI_SUPPORT_INDEX 
WHERE entityType = 'property';
# Expected: All counts should be 748 (no missing metadata)

# 6. Verify cross-domain accuracy
SELECT c.refId, c.label, c.crossDomainList, COUNT(DISTINCT cd.domainRefId) as actual_domains
FROM CEDS_RDF_UI_SUPPORT_INDEX c
JOIN CEDS_ClassToDomain cd ON c.refId = cd.classRefId
WHERE c.entityType = 'class'
GROUP BY c.refId
HAVING actual_domains > 1
LIMIT 5;
# Expected: crossDomainList count matches actual_domains

# 7. Test rebuild capability
DROP TABLE CEDS_RDF_UI_SUPPORT_INDEX;
./rdf-tester --buildEntityLookup
SELECT COUNT(*) FROM CEDS_RDF_UI_SUPPORT_INDEX;
# Expected: Same counts as before, completes without errors

Success Criteria:


PHASE 2: API Endpoints

Goal: Create API endpoints to serve entity lookup data efficiently

TQ SAYS (about the above): I take your word that these comprise an adequate set to support the UI.

2.1 Create Entity Lookup Access Point

File: /server/data-model/access-points-dot-d/accessPoints.d/fetch-entity-lookup.js

Functions:

  1. fetchAllEntities() - Return all entities (lightweight)
  2. fetchEntitiesByDomain(domainRefId) - Return entities for specific domain
  3. fetchEntityDetails(refId) - Return full details for one entity
  4. fetchFunctionalAreasByDomain(domainRefId) - Return middle tier categories

TQ SAYS (about the above):

2.2 Create Entity Lookup Mapper

File: /server/data-model/data-mapping/mappers/ceds-entity-lookup.js

Define SQL queries:

TQ SAYS (about the above):

2.3 Create API Endpoints

File: /server/endpoints-dot-d/endpoints.d/ceds/cedsEntityLookup.js

Endpoints:

TQ SAYS (about the above):

2.4 Verification Steps

// Test in browser console or with Node.js script

// 1. Test full entity lookup (should include all types)
const response1 = await fetch('/api/ceds/fetchEntityLookup');
const allEntities = await response1.json();
console.log('Total entities:', allEntities.length);
console.log('Entity types:', [...new Set(allEntities.map(e => e.entityType))]);
// Expected: ~2100+ entities, types: ['domain', 'functionalArea', 'class', 'property']

// 2. Test domain filtering
const response2 = await fetch('/api/ceds/fetchEntityLookup?domain=DOM_Organizations___Institutions');
const domainEntities = await response2.json();
const classCount = domainEntities.filter(e => e.entityType === 'class').length;
console.log('Classes in Organizations domain:', classCount);
// Expected: 100-200 classes for this domain

// 3. Test functional areas with counts
const response3 = await fetch('/api/ceds/fetchFunctionalAreas/DOM_Organizations___Institutions');
const functionalAreas = await response3.json();
console.log('Functional areas:', functionalAreas.map(fa => `${fa.label}: ${fa.count}`));
// Expected: Array like [{label: 'Organization', count: 42}, {label: 'Student', count: 35}...]
// Verify: Total of counts should equal classCount from test 2

// 4. Test entity details for a class
const response4 = await fetch('/api/ceds/fetchEntityDetails/C200015');
const classDetails = await response4.json();
console.log('Class has required fields:', {
    hasLabel: !!classDetails.label,
    hasType: !!classDetails.entityType,
    hasOptionSetFlag: classDetails.isOptionSet !== undefined,
    hasCrossDomains: !!classDetails.crossDomainList
});
// Expected: All should be true

// 5. Test entity details for a property
const propResponse = await fetch('/api/ceds/fetchEntityDetails/P000748');
const propDetails = await propResponse.json();
console.log('Property details:', propDetails);
// Expected: entityType = 'property', has label, has uri

// 6. Performance test - measure full lookup time
console.time('FullEntityLookup');
const perfResponse = await fetch('/api/ceds/fetchEntityLookup');
const perfData = await perfResponse.json();
console.timeEnd('FullEntityLookup');
// Expected: < 500ms for ~2100 entities

// 7. Test non-existent entity
const response7 = await fetch('/api/ceds/fetchEntityDetails/FAKE_ID_12345');
const notFound = await response7.json();
console.log('Non-existent entity returns:', notFound);
// Expected: null or empty object, not an error

Success Criteria:


PHASE 3: Store Updates

Goal: Update Pinia store to manage entity lookup data and three-tier navigation state

TQ SAYS (about the above):

3.1 Extend Ontology Store

File: /html/stores/ontologyStore.js

New State Properties:

const entityLookup = ref(new Map());        // refId -> entity info
const functionalAreas = ref([]);            // Current domain's middle tier
const selectedFunctionalArea = ref(null);   // Current middle tier selection
const entitiesInArea = ref([]);            // Classes in selected functional area

New Actions:

// Load entity lookup on app init
async loadEntityLookup() {
    const response = await fetch('/api/ceds/fetchEntityLookup', {
        headers: authHeader
    });
    const entities = await response.json();

    // Build Map for O(1) lookups
    entities.forEach(entity => {
        entityLookup.value.set(entity.refId, entity);
    });
}

// Get entity name by refId (replaces inline lookups)
getEntityName(refId) {
    const entity = entityLookup.value.get(refId);
    return entity?.label || refId;
}

// Load functional areas when domain changes
async loadFunctionalAreas(domainRefId) {
    const response = await fetch(`/api/ceds/fetchFunctionalAreas/${domainRefId}`, {
        headers: authHeader
    });
    functionalAreas.value = await response.json();
}

// Select functional area and load its classes
selectFunctionalArea(areaId) {
    selectedFunctionalArea.value = areaId;
    // Filter loaded classes by functional area
    entitiesInArea.value = classes.value.filter(c => {
        const entity = entityLookup.value.get(c.refId);
        return entity?.functionalAreaRefId === areaId;
    });
}

TQ SAYS (about the above):

3.2 Update Domain Selection Logic

Modify selectDomain() to also load functional areas:

async selectDomain(domain) {
    currentDomain.value = domain;
    await Promise.all([
        loadClassesByDomain(domain.refId),
        loadFunctionalAreas(domain.refId)
    ]);
    // Auto-select first functional area if exists
    if (functionalAreas.value.length > 0) {
        selectFunctionalArea(functionalAreas.value[0].id);
    }
}

TQ SAYS (about the above):

3.3 Verification Steps

// In browser console after loading app
const store = useOntologyStore();

// 1. Verify entity lookup Map structure
console.log('Entity lookup size:', store.entityLookup.size);
console.log('Sample entity:', store.entityLookup.get('C200015'));
// Expected: ~2100+ entities, each with refId, label, entityType, etc.

// 2. Test name resolution for different entity types
const testIds = {
    class: 'C200015',
    property: 'P000748', 
    domain: 'DOM_Organizations___Institutions',
    fake: 'FAKE_ID_999'
};
for (const [type, id] of Object.entries(testIds)) {
    console.log(`${type}: ${id} -> ${store.getEntityName(id)}`);
}
// Expected: Actual names for real IDs, 'FAKE_ID_999' for non-existent

// 3. Test domain selection loads both classes and functional areas
console.time('DomainSwitch');
await store.selectDomain(store.domains[0]);
console.timeEnd('DomainSwitch');
console.log('Classes loaded:', store.classes.length);
console.log('Functional areas:', store.functionalAreas.map(fa => `${fa.label}(${fa.count})`));
// Expected: Both load in parallel, < 1s total time

// 4. Test functional area selection and filtering
const firstArea = store.functionalAreas[0];
store.selectFunctionalArea(firstArea.id);
console.log('Selected area:', store.selectedFunctionalArea);
console.log('Filtered classes:', store.entitiesInArea.length);
// Expected: entitiesInArea.length matches firstArea.count

// 5. Verify option sets are identifiable
const optionSets = store.entitiesInArea.filter(e => {
    const entity = store.entityLookup.get(e.refId);
    return entity?.isOptionSet;
});
console.log('Option sets in area:', optionSets.map(os => os.label));
// Expected: Some classes should be marked as option sets

// 6. Test cross-domain information availability
const crossDomainClass = [...store.entityLookup.values()].find(e => 
    e.crossDomainList && e.crossDomainList.includes(',')
);
console.log('Cross-domain class:', {
    label: crossDomainClass.label,
    domains: crossDomainClass.crossDomainList
});
// Expected: Should find classes that appear in multiple domains

// 7. Memory usage check
if (performance.memory) {
    console.log('Memory used by entity lookup:', 
        `${(performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`);
}
// Expected: Reasonable memory usage for ~2100 entities (< 10MB)

Success Criteria:


PHASE 4: UI Component Updates

Goal: Update components to display three-tier navigation

TQ SAYS (about the above):

4.1 Update ClassOutline Component

File: /html/components/tools/ontology/ClassOutline.vue

Changes:

  1. Add toolbar with expand/collapse controls

  2. Add functional area accordion/expansion panels

  3. Structure:

    [Expand All] [Collapse All]
    ─────────────────────────────
    Domain Tab (selected)
    └── Functional Areas (collapsible sections)
        ├── Organization (42 classes)
        │   ├── OrganizationIdentifier
        │   ├── OrganizationIndicator [Option Set]
        │   └── ...
        ├── Student (38 classes)
        │   ├── StudentAcademicRecord
        │   └── ...
    
  4. Visual distinctions:

TQ SAYS (about the above): Please add 'Collapse All' and 'Expand All' buttons at the top of the list.

ANSWER: Added toolbar with these buttons in step 1 above.

Also, what does Option Set mean in this: OrganizationIndicator [Option Set]? I still think option sets are subordinate to classes. Am I wrong?

ANSWER: Option sets ARE classes themselves (marked with ConceptScheme type). They appear at the same level as regular classes in the navigation. The [Option Set] label just helps users distinguish "this class contains predefined values" from "this class stores data". Classes reference option sets through their properties' range definitions.

4.2 Update ClassDetails Component

File: /html/components/tools/ontology/ClassDetails.vue

Changes:

  1. Replace inline getClassName() with store method:

    const getClassName = (refId) => {
        return ontologyStore.getEntityName(refId);
    };
    
  2. Add breadcrumb showing position:

    Organizations & Institutions > Student > StudentAcademicRecord
    
  3. For cross-domain classes, show badges for other domains

  4. NEW: Enhanced property display showing option set ranges:

    // In properties section, show which option sets are used
    Properties:
    - stateCode → RefStateCodeSet [Option Set] (click to view values)
    - organizationType → RefOrganizationType [Option Set]
    - identifier → String (primitive type)
    - startDate → Date (primitive type)
    

    Implementation:

TQ SAYS (about the above): Good idea. I want the breadcrumb.

4.3 Create FunctionalAreaPanel Component

File: /html/components/tools/ontology/FunctionalAreaPanel.vue

New component for middle tier section with:

TQ SAYS (about the above): Is this talking about the display when a collapsible item is opened?

ANSWER: Yes, exactly. This component renders the content that appears when you expand a functional area (middle tier category). It shows the sorted list of classes and option sets within that category, with appropriate icons and click handlers.

4.4 Verification Steps

Detailed UI Testing Checklist:

// Browser console tests during manual interaction

// 1. Three-tier navigation structure
console.log('Domain tabs visible:', document.querySelectorAll('.domain-tab').length === 9);
console.log('Functional areas visible:', document.querySelectorAll('.functional-area').length);
// Expected: 9 domain tabs, 10-50 functional areas per domain

// 2. Expand/Collapse All functionality
document.querySelector('[data-test="expand-all"]').click();
console.log('All expanded:', document.querySelectorAll('.functional-area.expanded').length);
document.querySelector('[data-test="collapse-all"]').click();
console.log('All collapsed:', document.querySelectorAll('.functional-area.expanded').length);
// Expected: All areas expand/collapse together

// 3. Option set visual indicators
const optionSetIcons = document.querySelectorAll('.mdi-format-list-bulleted-square');
console.log('Option sets with special icon:', optionSetIcons.length);
// Expected: ~200-300 option sets with different icon

// 4. Breadcrumb accuracy
// Click: Organizations domain → Student functional area → StudentAcademicRecord class
const breadcrumb = document.querySelector('.breadcrumb').textContent;
console.log('Breadcrumb:', breadcrumb);
// Expected: "Organizations & Institutions > Student > StudentAcademicRecord"

// 5. Property range display
const propertyRows = document.querySelectorAll('.property-row');
const withOptionSets = [...propertyRows].filter(row => 
    row.textContent.includes('[Option Set]')
);
console.log(`Properties with option sets: ${withOptionSets.length}/${propertyRows.length}`);
// Expected: Many properties should show their option set ranges

// 6. No IDs visible test
const allText = document.body.innerText;
const hasClassIds = /C\d{6}/.test(allText);  // Pattern for class IDs
const hasPropertyIds = /P\d{6}/.test(allText);  // Pattern for property IDs
console.log('Class IDs visible:', hasClassIds);
console.log('Property IDs visible:', hasPropertyIds);
// Expected: Both should be false (all resolved to names)

// 7. Cross-domain navigation
// Click a class that exists in multiple domains
const crossDomainBadges = document.querySelectorAll('.cross-domain-badge');
console.log('Cross-domain badges visible:', crossDomainBadges.length);
crossDomainBadges[0]?.click();
// Expected: Should switch to different domain and select the class

Performance Metrics:

// Measure UI responsiveness
console.time('ExpandFunctionalArea');
document.querySelector('.functional-area-header').click();
console.timeEnd('ExpandFunctionalArea');
// Expected: < 100ms to expand and show classes

console.time('SelectClass');
document.querySelector('.class-item').click();
console.timeEnd('SelectClass');
// Expected: < 200ms to load and display class details

console.time('NavigateToLinkedClass');
document.querySelector('.linked-class').click();
console.timeEnd('NavigateToLinkedClass');
// Expected: < 500ms even if switching domains

Accessibility Checks:

Success Criteria:


PHASE 5: Integration Testing & Optimization

Goal: Ensure system works end-to-end and performs well

TQ SAYS (about the above):

5.1 End-to-End Testing Scenarios

Scenario 1: Complete Navigation Flow

// Start at root, navigate to specific class via three tiers
1. Load app fresh (no deep link)
2. Click "Organizations & Institutions" domain tab
3. Wait for functional areas to load
4. Expand "Organization" functional area
5. Click "OrganizationIdentifier" class
6. Verify breadcrumb: "Organizations & Institutions > Organization > OrganizationIdentifier"
7. In properties, click linked option set "RefOrganizationIdentifierType"
8. Verify navigation to option set (should have [Option Set] indicator)
9. Check that option set values are displayed
// Expected: Each step < 500ms, total flow < 3s

Scenario 2: Cross-Domain Navigation via RDF Links

1. Navigate to a class with cross-domain relationships
2. In RDF relationships table, identify class from different domain
3. Click the linked class name
4. Verify:
   - Domain tab switches automatically
   - Functional area expands to show the class
   - Class is selected and highlighted
   - Breadcrumb updates correctly
// Expected: Seamless transition even across domains

Scenario 3: Deep Link Restoration

// Test URL: /ontology/DOM_Organizations___Institutions/C200015
1. Load app with deep link URL
2. Verify:
   - Correct domain tab is selected
   - Entity lookup is loaded (no IDs showing)
   - Functional area containing C200015 is expanded
   - C200015 class is selected
   - Details panel shows full information
// Expected: Direct navigation works, < 2s to fully load

Scenario 4: Edge Cases

// Test unusual data conditions

// A. Single-item functional area
1. Find functional area with only 1 class
2. Expand it
3. Verify it still works correctly

// B. Very large functional area (>100 items)
1. Find largest functional area
2. Expand it
3. Measure performance
// Expected: Still responsive, consider pagination if >100

// C. Class with no properties
1. Find class with no properties
2. Verify RDF relationships still show
3. Verify metadata relationships visible

// D. Property with no range
1. Find property without option set range
2. Verify shows primitive type or "unspecified"

Scenario 5: Search Integration

1. Open search (magnifying glass icon)
2. Search for "Student"
3. Verify results show:
   - Functional area "Student" 
   - All classes starting with "Student"
   - Domain context for each result
4. Click a search result
5. Verify:
   - Navigates to correct domain
   - Expands correct functional area
   - Selects the class
   - Search overlay closes
// Expected: Search-to-selection < 1s

TQ SAYS (about the above):

5.2 Performance Testing

// Comprehensive performance benchmarks

// 1. Initial app load (cold start)
performance.mark('AppLoadStart');
// ... app initialization ...
performance.mark('AppLoadEnd');
performance.measure('AppLoad', 'AppLoadStart', 'AppLoadEnd');
const appLoad = performance.getEntriesByName('AppLoad')[0];
console.log(`App load: ${appLoad.duration}ms`);
// Target: < 2000ms including entity lookup

// 2. Stress test functional area expansion
const allAreas = document.querySelectorAll('.functional-area-header');
console.time('ExpandAll');
allAreas.forEach(area => area.click());
console.timeEnd('ExpandAll');
// Target: < 1000ms for all areas

// 3. Rapid domain switching
console.time('Switch5Domains');
for (let i = 0; i < 5; i++) {
    await store.selectDomain(store.domains[i]);
}
console.timeEnd('Switch5Domains');
// Target: < 3000ms total (600ms average per switch)

// 4. Name resolution stress test
const allRefIds = [...store.entityLookup.keys()];
console.time('ResolveAllNames');
allRefIds.forEach(refId => store.getEntityName(refId));
console.timeEnd('ResolveAllNames');
console.log(`Resolved ${allRefIds.length} names`);
// Target: < 200ms for ~2100 names

// 5. Memory profiling
const beforeExpand = performance.memory?.usedJSHeapSize || 0;
document.querySelector('[data-test="expand-all"]').click();
const afterExpand = performance.memory?.usedJSHeapSize || 0;
console.log(`Memory increase: ${((afterExpand - beforeExpand) / 1024 / 1024).toFixed(2)} MB`);
// Target: < 5MB increase

// 6. Search performance
console.time('SearchStudent');
await searchStore.search('Student');
console.timeEnd('SearchStudent');
// Target: < 100ms for search results

TQ SAYS (about the above):

5.3 Data Integrity Validation

-- 1. Verify no orphaned classes (all have functional areas)
SELECT COUNT(*) as orphaned_classes
FROM CEDS_RDF_UI_SUPPORT_INDEX 
WHERE entityType = 'class' 
AND (functionalAreaRefId IS NULL OR functionalAreaRefId = '');
-- Expected: 0

-- 2. Check functional area consistency
SELECT fa.functionalAreaRefId, 
       COUNT(DISTINCT fa.domainRefId) as domain_count
FROM CEDS_RDF_UI_SUPPORT_INDEX fa
WHERE fa.entityType = 'class'
GROUP BY fa.functionalAreaRefId
HAVING domain_count > 1;
-- Expected: Some functional areas span domains (like "Student")

-- 3. Verify all original classes are indexed
SELECT 'Missing classes:', COUNT(*) 
FROM CEDS_Classes c
WHERE NOT EXISTS (
    SELECT 1 FROM CEDS_RDF_UI_SUPPORT_INDEX e 
    WHERE e.refId = c.refId
);
-- Expected: 0

-- 4. Verify all original properties are indexed
SELECT 'Missing properties:', COUNT(*)
FROM CEDS_Properties p
WHERE NOT EXISTS (
    SELECT 1 FROM CEDS_RDF_UI_SUPPORT_INDEX e
    WHERE e.refId = p.refId
);
-- Expected: 0

-- 5. Check option set marking accuracy
SELECT 'Mismarked option sets:', COUNT(*)
FROM CEDS_RDF_UI_SUPPORT_INDEX
WHERE entityType = 'class'
AND ((isOptionSet = 1 AND jsonString NOT LIKE '%ConceptScheme%')
OR (isOptionSet = 0 AND jsonString LIKE '%ConceptScheme%'));
-- Expected: 0

-- 6. Validate cross-domain lists
SELECT refId, label, 
       LENGTH(crossDomainList) - LENGTH(REPLACE(crossDomainList, ',', '')) + 1 as list_count,
       (SELECT COUNT(DISTINCT domainRefId) 
        FROM CEDS_ClassToDomain 
        WHERE classRefId = e.refId) as actual_count
FROM CEDS_RDF_UI_SUPPORT_INDEX e
WHERE entityType = 'class' AND crossDomainList IS NOT NULL
AND list_count != actual_count;
-- Expected: 0 (all counts match)

-- 7. Performance check on indexed columns
EXPLAIN QUERY PLAN
SELECT * FROM CEDS_RDF_UI_SUPPORT_INDEX
WHERE domainRefId = 'DOM_Organizations___Institutions'
AND functionalAreaRefId = 'Organization';
-- Expected: Uses indexes, not table scan

Rollback Plan

If issues arise at any phase:

  1. Database: Keep original tables unchanged, only ADD new table
  2. API: New endpoints don't affect existing ones
  3. Store: New properties/methods don't break existing functionality
  4. UI: Can revert to two-tier by hiding functional area panel

Future Improvements (Post-Implementation)

Once the three-tier system is working:

  1. Better Categorization: Replace first-word grouping with semantic analysis
  2. Smart Grouping: Combine small groups, split large ones
  3. User Preferences: Remember expanded/collapsed state
  4. Lazy Loading: Load functional area contents on demand
  5. Caching: Browser-side caching of entity lookup
  6. Search Enhancement: Full-text search across all entity fields

Schedule Estimate

Total: 9-14 hours


Definition of Done

The implementation is complete when:

  1. ✅ All 1,344 classes are accessible through three-tier navigation
  2. ✅ No class/property IDs visible in UI (all resolved to names)
  3. ✅ Option sets are visually distinguished
  4. ✅ Cross-domain navigation works seamlessly
  5. ✅ Performance targets are met
  6. ✅ No console errors during normal use
  7. ✅ Code is committed with appropriate commit message