allow to return URI in intermediate hop

This commit is contained in:
felix
2026-09-26 22:14:26 +02:00
parent 2e998a0a4e
commit e5ad4a5300
4 changed files with 16 additions and 5 deletions

View File

@@ -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.');

View File

@@ -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.

View File

@@ -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.`;
}
}
}

View File

@@ -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),
};