// A profile as a plain text menu: entities, fields, code lists. Code lists up // to INLINE_VALUES_MAX are inlined (and cached), so the LLM never has to // search the short ones. import { codeList, INLINE_VALUES_MAX } from './lookup.js'; export async function describeProfile(profile) { const lines = []; lines.push('A spec is Turtle, in exactly this shape (copy these prefixes):'); lines.push('@prefix rep: .'); lines.push('@prefix sh: .'); lines.push('@prefix rdfs: .'); lines.push('@prefix dct: .'); lines.push(''); lines.push('> a rep:ReportSpec , sh:NodeShape ;'); lines.push(' dct:title "..." ;'); lines.push(` rep:profile <${profile.uri}> ;`); lines.push(' sh:targetClass ;'); lines.push(' sh:property [ ... ] ; # filters, optional'); lines.push(' rep:columns ( [ sh:path ( ... ) ; rdfs:label "..." ] ... ) .'); lines.push('Every column ends on a value field. Use rep:self as sh:path for the subject URI.'); lines.push('For the URI of a linked node, end the column on that link and add sh:nodeKind sh:IRI to the column.'); lines.push(''); lines.push('sh:targetClass is the full URI in brackets after the entity below.'); lines.push('In sh:path, use the predicate URI shown after each field name, not the field name itself.'); lines.push('An inverse hop (the field shows "(inverse)") goes in the list as [ sh:inversePath ] .'); lines.push('When two entities share one sh:targetClass, pick one with rep:entity .'); lines.push('A date filter is sh:minInclusive or sh:maxInclusive with "yyyy-mm-dd"^^xsd:dateTime'); lines.push('(the xsd prefix above is not copied: add @prefix xsd: ).'); lines.push(''); for (const shape of profile.shapes) { lines.push(`${shape.label || shape.targetClass} — sh:targetClass <${shape.targetClass}> ; rep:entity <${shape.uri}>`); for (const f of shape.fields) { const kind = f.datatype ? datatypeName(f.datatype) : f.class ? `→ ${conceptLabel(f.class, await codeList(f.class))}` : f.node ? `→ ${linkedLabel(profile, f.node)}` : '?'; const dir = f.inverse ? ' (inverse)' : ''; const pathShown = f.path || '?'; lines.push(` ${f.name || pathShown} sh:path <${pathShown}>${dir} ${kind}`); } lines.push(''); } return lines.join('\n'); } function conceptLabel(type, values) { if (!values.length) return short(type); const names = values.map(v => v.label).join(', '); const more = values.length >= INLINE_VALUES_MAX ? ', …' : ''; return `${short(type)} — ${values.length} waarden: ${names}${more}`; } function linkedLabel(profile, nodeUri) { const s = profile.shapes.find(x => x.uri === nodeUri); return s ? `${s.label || s.targetClass} (veel)` : short(nodeUri); } function datatypeName(dt) { return dt.split('#').pop(); } function short(uri) { return uri.replace(/^https?:\/\/[^/]+\//, '…/').slice(-60); }