From e5ad4a53001c840501bfe4ead00792c1bdcf55ff Mon Sep 17 00:00:00 2001 From: felix Date: Sat, 26 Sep 2026 22:14:26 +0200 Subject: [PATCH] allow to return URI in intermediate hop --- src/llm/describe.js | 1 + src/llm/loop.js | 4 +++- src/runner/check.js | 14 ++++++++++---- src/runner/spec.js | 2 ++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/llm/describe.js b/src/llm/describe.js index e948a33..5d09847 100644 --- a/src/llm/describe.js +++ b/src/llm/describe.js @@ -68,6 +68,7 @@ export function describeProfile(profile, codeLists = {}) { 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.'); diff --git a/src/llm/loop.js b/src/llm/loop.js index 24ead70..4a35cb5 100644 --- a/src/llm/loop.js +++ b/src/llm/loop.js @@ -27,7 +27,9 @@ const REFINE_PROMPT = `You refine report specs with the user. You never write SP The profile lists what can be asked. Compose paths by chaining fields. Rules: -- A column must end on a value, not on a link. Chain one more hop. +- A column must end on a value, not on a link. Chain one more hop. Only when + the user asks for the URI of a linked node itself, end the column on that + link and add sh:nodeKind sh:IRI to the column. - When two entities share one sh:targetClass, the spec also needs rep:entity with that entity's URI; describe_profile shows it after each entity. - Filters go in sh:property. Columns go in rep:columns, in the order you want. diff --git a/src/runner/check.js b/src/runner/check.js index 95f1aa8..fd573d2 100644 --- a/src/runner/check.js +++ b/src/runner/check.js @@ -115,7 +115,7 @@ function pathsResolve(spec, profile, maxPathDepth) { const startShapeUri = startShapeOf(spec, profile); for (const col of spec.columns) { const err = walk(profile, startShapeUri, col.path, maxPathDepth, - `column "${col.label}"`, true); + `column "${col.label}"`, true, col.nodeKind === SH + 'IRI'); if (err) return err; } for (const [i, filter] of spec.filters.entries()) { @@ -166,6 +166,9 @@ function columnPure(spec) { const which = Object.keys(col.constraints)[0]; return `a column carries ${which}. Constraints belong in sh:property, or in rep:where for a condition on a step of this column.`; } + if (col.nodeKind && col.nodeKind !== SH + 'IRI') { + return `column "${col.label}" has sh:nodeKind <${col.nodeKind}>. A column takes only sh:nodeKind sh:IRI, to show the URI of the node its path ends on.`; + } if (col.collect && ![SH + 'groupConcat', SH + 'min', SH + 'max', REP + 'row'].includes(col.collect)) { return `column "${col.label}" has an unknown rep:collect <${col.collect}>. Use sh:groupConcat (joins the values), rep:row or nothing (each value gets its own row), sh:min or sh:max.`; } @@ -365,7 +368,7 @@ function shapeAfter(profile, startShapeUri, hops) { return shapeUri; } -function walk(profile, startShapeUri, hops, maxPathDepth, what, isColumn) { +function walk(profile, startShapeUri, hops, maxPathDepth, what, isColumn, wantsIri = false) { if (hops.length > maxPathDepth) { return `${what} walks ${hops.length} hops. The limit is ${maxPathDepth}.`; } @@ -386,11 +389,14 @@ function walk(profile, startShapeUri, hops, maxPathDepth, what, isColumn) { } shapeUri = f.node; } else if (isColumn) { - if (!f.datatype && !f.class) { + if (wantsIri && f.datatype) { + return `column "${colLabel(what)}" has sh:nodeKind sh:IRI but ends on the value "${f.name}", which is no URI. Drop sh:nodeKind.`; + } + if (!wantsIri && !f.datatype && !f.class) { const valueFields = fieldsOf(profile, f.node) .filter(x => x.datatype || x.class) .map(x => `${x.name} (${x.path})`); - return `column "${colLabel(what)}" ends on a link. Add a hop: ${valueFields.join(' or ')}.`; + return `column "${colLabel(what)}" ends on a link. Add a hop: ${valueFields.join(' or ')}. Only if the user asked for the URI of that node itself, add sh:nodeKind sh:IRI to the column instead.`; } } } diff --git a/src/runner/spec.js b/src/runner/spec.js index fae545d..178f8d4 100644 --- a/src/runner/spec.js +++ b/src/runner/spec.js @@ -87,6 +87,8 @@ function parseColumn(store, col) { label: store.getQuads(col, RDFS_LABEL, null)[0]?.object.value || null, collect: store.getQuads(col, REP + 'collect', null)[0]?.object.value || null, separator: store.getQuads(col, SH + 'separator', null)[0]?.object.value || null, + // sh:nodeKind sh:IRI: the column shows the URI of the node it ends on + nodeKind: store.getQuads(col, SH + 'nodeKind', null)[0]?.object.value || null, constraints: readConstraints(store, col), // kept so check.js can refuse it where: readWhere(store, col), };