diff --git a/packages/api/src/channel/index.ts b/packages/api/src/channel/index.ts index 3e19a8885..588559ba5 100644 --- a/packages/api/src/channel/index.ts +++ b/packages/api/src/channel/index.ts @@ -1,4 +1,4 @@ -import { Ad4mModel, HasMany, HasManyMethods, Flag, Literal, LinkQuery, Model, Property, PerspectiveProxy, parseLit, parseSparqlCount, CountBinding } from '@coasys/ad4m'; +import { Ad4mModel, HasMany, HasManyMethods, Flag, Literal, LinkQuery, Model, Property, PerspectiveProxy, parseSparqlCount, CountBinding } from '@coasys/ad4m'; import { community } from '@coasys/flux-constants'; import { EntryType } from '@coasys/flux-types'; import { SynergyGroup, SynergyItem, ItemType, icons } from '@coasys/flux-utils'; @@ -122,21 +122,23 @@ export class Channel extends Ad4mModel { let type: ItemType = 'Message'; const itemType = binding.type; + // body/title/taskName/transcriptStart are stored as typed XSD + // literals; Oxigraph returns their lexical form directly, no decode. if (itemType === 'flux://has_message') { - text = parseLit(binding.body); + text = binding.body ?? ''; type = 'Message'; } else if (itemType === 'flux://has_post') { - text = parseLit(binding.title); + text = binding.title ?? ''; type = 'Post'; } else if (itemType === 'flux://has_task') { - text = parseLit(binding.taskName); + text = binding.taskName ?? ''; type = 'Task'; } return { id: binding.id, author: binding.author, - timestamp: new Date(parseLit(binding.transcriptStart) || binding.timestamp).toISOString(), + timestamp: new Date(binding.transcriptStart || binding.timestamp).toISOString(), text, type, icon: icons[type] || 'question', @@ -228,21 +230,22 @@ export class Channel extends Ad4mModel { let type: ItemType = 'Message'; const itemType = binding.type; + // Typed XSD literals — no parseLit() decode needed. if (itemType === 'flux://has_message') { - text = parseLit(binding.body); + text = binding.body ?? ''; type = 'Message'; } else if (itemType === 'flux://has_post') { - text = parseLit(binding.title); + text = binding.title ?? ''; type = 'Post'; } else if (itemType === 'flux://has_task') { - text = parseLit(binding.taskName); + text = binding.taskName ?? ''; type = 'Task'; } return { id: binding.id, author: binding.author, - timestamp: new Date(parseLit(binding.transcriptStart) || binding.timestamp).toISOString(), + timestamp: new Date(binding.transcriptStart || binding.timestamp).toISOString(), text, type, icon: icons[type] || 'question', @@ -310,8 +313,9 @@ export class Channel extends Ad4mModel { for (const r of results || []) { const cid = r.channelId; if (!cid || channelMap.has(cid)) continue; - const parsed = parseLit(r.isConv); - if (String(parsed) !== 'true') continue; + // CHANNEL_IS_CONVERSATION is a boolean typed literal; SPARQL binding + // returns its lexical form directly. + if (r.isConv !== 'true') continue; channelMap.set(cid, { channelId: cid, conversationId: r.conversationId || undefined, diff --git a/packages/api/src/conversation-subgroup/index.ts b/packages/api/src/conversation-subgroup/index.ts index ff71ab27e..cd684f925 100644 --- a/packages/api/src/conversation-subgroup/index.ts +++ b/packages/api/src/conversation-subgroup/index.ts @@ -1,4 +1,4 @@ -import { Model, Ad4mModel, Flag, HasMany, Property, Literal, parseLit } from '@coasys/ad4m'; +import { Model, Ad4mModel, Flag, HasMany, Property, Literal } from '@coasys/ad4m'; import Topic, { TopicWithRelevance } from '../topic'; import SemanticRelationship from '../semantic-relationship'; import { SynergyTopic, SynergyItem, ItemType, icons } from '@coasys/flux-utils'; @@ -102,7 +102,8 @@ export default class ConversationSubgroup extends Ad4mModel { if (topicBase && !uniqueTopics.has(topicBase)) { uniqueTopics.set(topicBase, { topicBase, - topicName: parseLit(binding.topicNameRaw), + // Typed XSD literal — SPARQL binding gives lexical form directly. + topicName: binding.topicNameRaw ?? '', }); } } @@ -155,24 +156,26 @@ export default class ConversationSubgroup extends Ad4mModel { if (!id) continue; // Coalesce OPTIONAL fields from multiple SPARQL rows for same id + // body/title/taskName/transcriptStart are typed XSD literals; SPARQL + // binding returns their lexical form directly, no decode needed. if (seen.has(id)) { // Merge optional fields from this binding into the existing item const existing = seen.get(id)!; - const transcriptStart = parseLit(binding.transcriptStart); + const transcriptStart = binding.transcriptStart ?? ''; const channelTs = binding.channelTs; const fallbackTs = binding.timestamp; if (!existing.channelTimestamp) { existing.channelTimestamp = transcriptStart || channelTs || fallbackTs; } - if (!existing.messageBody) existing.messageBody = parseLit(binding.body); - if (!existing.postTitle) existing.postTitle = parseLit(binding.title); - if (!existing.taskName) existing.taskName = parseLit(binding.taskName); + if (!existing.messageBody) existing.messageBody = binding.body ?? ''; + if (!existing.postTitle) existing.postTitle = binding.title ?? ''; + if (!existing.taskName) existing.taskName = binding.taskName ?? ''; if (!existing.type) existing.type = binding.type; if (!existing.author) existing.author = binding.author; continue; } - const transcriptStart = parseLit(binding.transcriptStart); + const transcriptStart = binding.transcriptStart ?? ''; const channelTs = binding.channelTs; const fallbackTs = binding.timestamp; const channelTimestamp = transcriptStart || channelTs || fallbackTs; @@ -182,9 +185,9 @@ export default class ConversationSubgroup extends Ad4mModel { type: binding.type, author: binding.author, channelTimestamp, - messageBody: parseLit(binding.body), - postTitle: parseLit(binding.title), - taskName: parseLit(binding.taskName), + messageBody: binding.body ?? '', + postTitle: binding.title ?? '', + taskName: binding.taskName ?? '', }; seen.set(id, item); items.push(item); @@ -249,8 +252,9 @@ export default class ConversationSubgroup extends Ad4mModel { if (topicBase && !uniqueTopics.has(topicBase)) { uniqueTopics.set(topicBase, { topicBase, - topicName: parseLit(binding.topicNameRaw), - relevance: parseLit(binding.relevanceRaw), + // Typed XSD literals — no decode needed. + topicName: binding.topicNameRaw ?? '', + relevance: binding.relevanceRaw ?? '', }); } } diff --git a/packages/api/src/conversation/index.ts b/packages/api/src/conversation/index.ts index 99c581c1b..8046cf1e1 100644 --- a/packages/api/src/conversation/index.ts +++ b/packages/api/src/conversation/index.ts @@ -1,4 +1,4 @@ -import { Ad4mModel, Ad4mClient, Flag, HasMany, HasManyMethods, Link, Literal, Model, Property, parseLit } from '@coasys/ad4m'; +import { Ad4mModel, Ad4mClient, Flag, HasMany, HasManyMethods, Link, Literal, Model, Property } from '@coasys/ad4m'; import { getProfile, Topic } from '@coasys/flux-api'; import { ProcessingState, Profile } from '@coasys/flux-types'; @@ -112,7 +112,8 @@ export class Conversation extends Ad4mModel { if (topicBase && !uniqueTopics.has(topicBase)) { uniqueTopics.set(topicBase, { topicBase, - topicName: parseLit(binding.topicNameRaw), + // Typed XSD literal — SPARQL binding gives lexical form directly. + topicName: binding.topicNameRaw ?? '', }); } } @@ -164,8 +165,9 @@ export class Conversation extends Ad4mModel { subgroupMap.set(id, { id, timestamp: binding.timestamp, - name: parseLit(binding.nameRaw), - summary: parseLit(binding.summaryRaw), + // has_name / has_summary are typed XSD literals — no decode. + name: binding.nameRaw ?? '', + summary: binding.summaryRaw ?? '', }); } } @@ -196,7 +198,8 @@ export class Conversation extends Ad4mModel { for (const r of batchResults || []) { const sgId = r.sg; if (!sgId) continue; - const ts = parseLit(r.transcriptStart) || r.channelTs; + // transcript_started_at is a typed XSD literal string. + const ts = r.transcriptStart || r.channelTs; if (ts == null || ts === '') continue; const time = new Date(ts).getTime(); if (isNaN(time)) continue; diff --git a/packages/api/src/semantic-relationship/index.ts b/packages/api/src/semantic-relationship/index.ts index 56aea3d79..e52ee55a4 100644 --- a/packages/api/src/semantic-relationship/index.ts +++ b/packages/api/src/semantic-relationship/index.ts @@ -1,5 +1,4 @@ import { Model, Ad4mModel, Flag, Property } from '@coasys/ad4m'; -import { parseLit } from '../utils/parseLit'; import { SynergyMatch } from '@coasys/flux-utils'; const TYPE_MAP: Record = { @@ -72,7 +71,8 @@ export default class SemanticRelationship extends Ad4mModel { type: 'Conversation', embedding: JSON.parse(embeddingExpression.data), channelId: binding.channelId, - channelName: parseLit(binding.channelName), + // has_channel_name is a typed XSD literal — no decode needed. + channelName: binding.channelName ?? '', }; }), ); @@ -110,7 +110,8 @@ export default class SemanticRelationship extends Ad4mModel { type: 'Subgroup', embedding: JSON.parse(embeddingExpression.data), channelId: binding.channelId, - channelName: parseLit(binding.channelName), + // has_channel_name is a typed XSD literal — no decode needed. + channelName: binding.channelName ?? '', }; }), ); @@ -153,7 +154,8 @@ export default class SemanticRelationship extends Ad4mModel { type: typeNameMap[binding.type] || binding.type, embedding: JSON.parse(embeddingExpression.data), channelId: binding.channelId, - channelName: parseLit(binding.channelName), + // Typed XSD literal. + channelName: binding.channelName ?? '', }; }), ); @@ -197,7 +199,8 @@ export default class SemanticRelationship extends Ad4mModel { type: itemType, embedding: JSON.parse(embeddingExpression.data), channelId: binding.channelId, - channelName: parseLit(binding.channelName), + // Typed XSD literal. + channelName: binding.channelName ?? '', }; }), ); diff --git a/packages/api/src/topic/index.ts b/packages/api/src/topic/index.ts index 694fd5228..a3d3677c2 100644 --- a/packages/api/src/topic/index.ts +++ b/packages/api/src/topic/index.ts @@ -1,5 +1,4 @@ import { Model, Ad4mModel, Flag, Property } from '@coasys/ad4m'; -import { parseLit } from '../utils/parseLit'; import { SynergyMatch } from '@coasys/flux-utils'; export class TopicWithRelevance { @@ -42,9 +41,10 @@ export default class Topic extends Ad4mModel { dedupMap.set(id, { id, type: 'Conversation', - relevance: parseInt(parseLit(binding.relevance), 10) || 0, + // Typed XSD literals — SPARQL bindings give lexical form directly. + relevance: parseInt(binding.relevance ?? '', 10) || 0, channelId: binding.channelId, - channelName: parseLit(binding.channelName), + channelName: binding.channelName ?? '', }); } } @@ -81,9 +81,10 @@ export default class Topic extends Ad4mModel { dedupMap.set(id, { id, type: 'ConversationSubgroup', - relevance: parseInt(parseLit(binding.relevance), 10) || 0, + // Typed XSD literals — no decode needed. + relevance: parseInt(binding.relevance ?? '', 10) || 0, channelId: binding.channelId, - channelName: parseLit(binding.channelName), + channelName: binding.channelName ?? '', }); } } diff --git a/packages/api/src/utils/parseLit.test.ts b/packages/api/src/utils/parseLit.test.ts index e87df2603..4cd9babc2 100644 --- a/packages/api/src/utils/parseLit.test.ts +++ b/packages/api/src/utils/parseLit.test.ts @@ -27,10 +27,16 @@ describe('parseLit', () => { expect(parseLit(url)).toBe('hello world'); }); - it('extracts .data from JSON literal objects', () => { + // Note: prior to the typed-RDF-literals refactor, Flux stored message + // bodies as `Literal.from({ data: text }).toUrl()` envelopes and this + // helper extracted `.data` for legacy read compatibility. With scalar + // properties now stored as deterministic typed literals, envelope-wrapped + // objects arriving through SPARQL are treated uniformly — the whole + // object is JSON-stringified (matches @coasys/ad4m's `parseLit`). + it('JSON-stringifies JSON literal objects (including {data: ...} envelopes)', () => { const { Literal } = require('@coasys/ad4m'); const url = Literal.from({ data: 'extracted' }).toUrl(); - expect(parseLit(url)).toBe('extracted'); + expect(parseLit(url)).toBe(JSON.stringify({ data: 'extracted' })); }); it('JSON-stringifies objects without .data field', () => {