Skip to content

Commit 217e81e

Browse files
committed
Fix: redundancy
1 parent 8fe6120 commit 217e81e

2 files changed

Lines changed: 162 additions & 12 deletions

File tree

src/tools/group-g.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,16 @@ server.registerTool(
8787
- inheritedRange: range values from super-properties, each annotated with ancestor URI and label
8888
- effectiveDomain: deduplicated union of assertedDomain + inheritedDomain
8989
- effectiveRange: deduplicated union of assertedRange + inheritedRange
90+
- redundancy_analysis: diagnostic view of each asserted value:
91+
- "redundant": identical to an inherited value — the axiom can be dropped without semantic loss
92+
- "specialization": a rdfs:subClassOf of an inherited value — genuinely narrows the domain/range
93+
- "new": not present in any inherited value — adds information not implied by the super-property chain
94+
- summary counts per category for quick overview
9095
9196
**Interpreting the output:**
9297
- If assertedDomain is empty but effectiveDomain is not → domain is inherited; no need to re-assert it on this property
9398
- If assertedDomain equals effectiveDomain → the domain is fully explicit, not relying on inheritance
94-
- If a property is asserted in the subproperty chain and the local TTL already re-asserts the same domain → that axiom is redundant
99+
- Use redundancy_analysis.summary to immediately see if the local TTL has redundant axioms or genuine specializations
95100
- owl:equivalentProperty and owl:equivalentClass expansions are not included (use query_sparql for those)`,
96101
inputSchema: {
97102
propertyUri: z.string().describe("URI of the property to inspect"),
@@ -194,6 +199,73 @@ server.registerTool(
194199
hasRangeLocally: info.ranges.length > 0,
195200
}));
196201

202+
// Redundancy analysis
203+
type AnalysisEntry = {
204+
value: string;
205+
status: "redundant" | "specialization" | "new";
206+
inherited_match?: string;
207+
specializes?: string[];
208+
};
209+
210+
const inheritedDomainValues = new Set(inheritedDomain.map(x => x.domain));
211+
const inheritedRangeValues = new Set(inheritedRange.map(x => x.range));
212+
213+
const classifyInitial = (asserted: string[], inheritedValues: Set<string>): AnalysisEntry[] =>
214+
asserted.map(v => inheritedValues.has(v)
215+
? { value: v, status: "redundant" as const, inherited_match: v }
216+
: { value: v, status: "new" as const }
217+
);
218+
219+
let domainAnalysis = classifyInitial(assertedDomain, inheritedDomainValues);
220+
let rangeAnalysis = classifyInitial(assertedRange, inheritedRangeValues);
221+
222+
// Subclass check for "new" candidates → may be specializations
223+
const domainCandidates = domainAnalysis.filter(e => e.status === "new").map(e => e.value);
224+
const rangeCandidates = rangeAnalysis.filter(e => e.status === "new").map(e => e.value);
225+
const allCandidates = [...new Set([...domainCandidates, ...rangeCandidates])];
226+
const allInherited = [...new Set([...inheritedDomainValues, ...inheritedRangeValues])];
227+
228+
if (allCandidates.length > 0 && allInherited.length > 0) {
229+
const vSub = allCandidates.map(u => `<${u}>`).join(" ");
230+
const vSup = allInherited.map(u => `<${u}>`).join(" ");
231+
const subResult = await executeSparql(`
232+
SELECT ?sub ?sup WHERE {
233+
VALUES ?sub { ${vSub} }
234+
VALUES ?sup { ${vSup} }
235+
?sub rdfs:subClassOf+ ?sup .
236+
}
237+
`);
238+
const subMap = new Map<string, string[]>();
239+
for (const b of subResult.results?.bindings ?? []) {
240+
const sub = b.sub?.value ?? "", sup = b.sup?.value ?? "";
241+
if (!sub || !sup) continue;
242+
if (!subMap.has(sub)) subMap.set(sub, []);
243+
subMap.get(sub)!.push(sup);
244+
}
245+
const upgrade = (entries: AnalysisEntry[], inheritedValues: Set<string>): AnalysisEntry[] =>
246+
entries.map(e => {
247+
if (e.status !== "new") return e;
248+
const supers = (subMap.get(e.value) ?? []).filter(s => inheritedValues.has(s));
249+
return supers.length > 0 ? { value: e.value, status: "specialization" as const, specializes: supers } : e;
250+
});
251+
domainAnalysis = upgrade(domainAnalysis, inheritedDomainValues);
252+
rangeAnalysis = upgrade(rangeAnalysis, inheritedRangeValues);
253+
}
254+
255+
const count = (entries: AnalysisEntry[], s: string) => entries.filter(e => e.status === s).length;
256+
const redundancy_analysis = {
257+
domain: domainAnalysis,
258+
range: rangeAnalysis,
259+
summary: {
260+
domain_redundant: count(domainAnalysis, "redundant"),
261+
domain_specialization: count(domainAnalysis, "specialization"),
262+
domain_new: count(domainAnalysis, "new"),
263+
range_redundant: count(rangeAnalysis, "redundant"),
264+
range_specialization: count(rangeAnalysis, "specialization"),
265+
range_new: count(rangeAnalysis, "new"),
266+
},
267+
};
268+
197269
const totalRows =
198270
(defResult.results?.bindings?.length ?? 0) +
199271
(superResult.results?.bindings?.length ?? 0);
@@ -210,6 +282,7 @@ server.registerTool(
210282
inheritedRange,
211283
effectiveDomain,
212284
effectiveRange,
285+
redundancy_analysis,
213286
},
214287
rowCount: totalRows,
215288
};

src/tools/group-k.ts

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,14 @@ server.registerTool(
150150
title: "Inspect Concept in Local / Uploaded Ontology",
151151
description: `Get a full profile of a class or concept from a local or uploaded ontology.
152152
153-
**Args (provide exactly one of file_path or upload_id):**
153+
**CRITICAL — file access workflow:**
154+
1. Try \`file_path\` first (works only if the file is readable by the MCP server process).
155+
2. If file_path fails for any reason → call \`get_upload_instructions\` with the local path, execute the returned curl command via Bash tool, parse the \`id\` from the JSON response, then call this tool again with \`upload_id\`.
156+
3. NEVER read the file content and pass it through the conversation. NEVER attempt to parse or analyse the TTL text manually. The upload workflow sends raw bytes directly from disk to server without the AI ever seeing the content — it is always faster, cheaper, and more reliable.
157+
158+
**Args:**
154159
- uri: URI of the class/concept to inspect
155-
- file_path: Absolute path on the MCP server filesystem
156-
- upload_id: UUID returned by POST /upload (HTTP/remote mode)
160+
- file_path / upload_id: exactly one (see workflow above)
157161
- mode: "raw" | "effective" (default: "effective")
158162
159163
**mode: "raw"** — only triples explicitly present in the local file:
@@ -175,8 +179,8 @@ server.registerTool(
175179
**Unicode SPARQL note:** oxigraph rejects prefixed names with non-ASCII local parts (e.g. \`myont:modalità_cup\`). Always use full URIs in angle brackets (\`<https://...#modalità_cup>\`) for properties or classes with Unicode in the local name.`,
176180
inputSchema: {
177181
uri: z.string().describe("URI of the class or concept to inspect"),
178-
file_path: z.string().optional().describe("Absolute path to the ontology file on the server filesystem"),
179-
upload_id: z.string().optional().describe("Upload UUID returned by POST /upload (HTTP mode)"),
182+
file_path: z.string().optional().describe("Absolute path readable by the MCP server process. If this fails, do NOT read the file — use get_upload_instructions + Bash curl + upload_id instead."),
183+
upload_id: z.string().optional().describe("UUID from POST /upload. Preferred when the file is on the client machine or file_path failed."),
180184
mode: z.enum(["raw", "effective"]).optional().default("effective").describe(
181185
'"raw": only asserted triples (own_properties, no ancestor traversal). ' +
182186
'"effective" (default): adds inherited_properties via rdfs:subClassOf+/skos:broader+ and data-level incoming/outgoing.'
@@ -290,10 +294,10 @@ server.registerTool(
290294
title: "Inspect Property in Local / Uploaded Ontology",
291295
description: `Get the full semantic profile of a property from a local or uploaded ontology, resolving inherited domain and range via rdfs:subPropertyOf+.
292296
293-
**Args (provide exactly one of file_path or upload_id):**
294-
- uri: URI of the property to inspect
295-
- file_path: Absolute path on the MCP server filesystem
296-
- upload_id: UUID returned by POST /upload (HTTP mode)
297+
**CRITICAL — file access workflow:**
298+
1. Try \`file_path\` first (works only if the file is readable by the MCP server process).
299+
2. If file_path fails for any reason → call \`get_upload_instructions\` with the local path, execute the returned curl command via Bash tool, parse the \`id\` from the JSON response, then call this tool again with \`upload_id\`.
300+
3. NEVER read the file content and pass it through the conversation. NEVER attempt to parse or analyse the TTL text manually. The upload workflow sends raw bytes directly from disk to server without the AI ever seeing the content — it is always faster, cheaper, and more reliable.
297301
298302
**Returns:**
299303
- definition: direct attributes from the local store (type, label, comment, subPropertyOf, inverseOf, functional flags)
@@ -307,6 +311,11 @@ server.registerTool(
307311
- inheritedRange: range values collected from super-properties, each annotated with ancestor URI and source
308312
- effectiveDomain: deduplicated union of assertedDomain + inheritedDomain
309313
- effectiveRange: deduplicated union of assertedRange + inheritedRange
314+
- redundancy_analysis: diagnostic view of each asserted value:
315+
- "redundant": identical to an inherited value — the axiom can be dropped without semantic loss
316+
- "specialization": a rdfs:subClassOf of an inherited value — genuinely narrows the domain/range
317+
- "new": not present in any inherited value — adds information not implied by the super-property chain
318+
- summary counts per category for quick overview
310319
- warnings: super-properties not resolved, remote lookup failures
311320
312321
**owl:imports handling:** The local store typically does NOT contain imported ontologies (owl:imports declarations are not followed automatically). Super-properties from external namespaces (e.g. l0:name, l0:description from OntoPiA) are resolved against schema.gov.it automatically, making the effective semantics complete without requiring the full import chain to be loaded.
@@ -316,8 +325,8 @@ server.registerTool(
316325
**Unicode SPARQL note:** oxigraph rejects prefixed names with non-ASCII local parts. For properties with Unicode in the local name (e.g. \`myont:modalità_cup\`), always pass the full URI in angle brackets (\`<https://...#modalità_cup>\`).`,
317326
inputSchema: {
318327
uri: z.string().describe("URI of the property to inspect"),
319-
file_path: z.string().optional().describe("Absolute path to the ontology file on the server filesystem"),
320-
upload_id: z.string().optional().describe("Upload UUID returned by POST /upload (HTTP mode)"),
328+
file_path: z.string().optional().describe("Absolute path readable by the MCP server process. If this fails, do NOT read the file — use get_upload_instructions + Bash curl + upload_id instead."),
329+
upload_id: z.string().optional().describe("UUID from POST /upload. Preferred when the file is on the client machine or file_path failed."),
321330
},
322331
annotations: {
323332
readOnlyHint: true,
@@ -448,6 +457,73 @@ server.registerTool(
448457
hasRangeLocally: info.localRanges.length > 0,
449458
}));
450459

460+
// 7. Redundancy analysis
461+
type AnalysisEntry = {
462+
value: string;
463+
status: "redundant" | "specialization" | "new";
464+
inherited_match?: string;
465+
specializes?: string[];
466+
};
467+
468+
const inheritedDomainValues = new Set(inheritedDomain.map(x => x.domain));
469+
const inheritedRangeValues = new Set(inheritedRange.map(x => x.range));
470+
471+
const classifyInitial = (asserted: string[], inheritedValues: Set<string>): AnalysisEntry[] =>
472+
asserted.map(v => inheritedValues.has(v)
473+
? { value: v, status: "redundant" as const, inherited_match: v }
474+
: { value: v, status: "new" as const }
475+
);
476+
477+
let domainAnalysis = classifyInitial(assertedDomain, inheritedDomainValues);
478+
let rangeAnalysis = classifyInitial(assertedRange, inheritedRangeValues);
479+
480+
// Subclass check for "new" candidates → may be specializations
481+
const domainCandidates = domainAnalysis.filter(e => e.status === "new").map(e => e.value);
482+
const rangeCandidates = rangeAnalysis.filter(e => e.status === "new").map(e => e.value);
483+
const allCandidates = [...new Set([...domainCandidates, ...rangeCandidates])];
484+
const allInherited = [...new Set([...inheritedDomainValues, ...inheritedRangeValues])];
485+
486+
if (allCandidates.length > 0 && allInherited.length > 0) {
487+
const vSub = allCandidates.map(u => `<${u}>`).join(" ");
488+
const vSup = allInherited.map(u => `<${u}>`).join(" ");
489+
const subResult = runLocalSparql(store, `
490+
SELECT ?sub ?sup WHERE {
491+
VALUES ?sub { ${vSub} }
492+
VALUES ?sup { ${vSup} }
493+
?sub rdfs:subClassOf+ ?sup .
494+
}
495+
`, true);
496+
const subMap = new Map<string, string[]>();
497+
for (const b of subResult.results.bindings) {
498+
const sub = b.sub?.value ?? "", sup = b.sup?.value ?? "";
499+
if (!sub || !sup) continue;
500+
if (!subMap.has(sub)) subMap.set(sub, []);
501+
subMap.get(sub)!.push(sup);
502+
}
503+
const upgrade = (entries: AnalysisEntry[], inheritedValues: Set<string>): AnalysisEntry[] =>
504+
entries.map(e => {
505+
if (e.status !== "new") return e;
506+
const supers = (subMap.get(e.value) ?? []).filter(s => inheritedValues.has(s));
507+
return supers.length > 0 ? { value: e.value, status: "specialization" as const, specializes: supers } : e;
508+
});
509+
domainAnalysis = upgrade(domainAnalysis, inheritedDomainValues);
510+
rangeAnalysis = upgrade(rangeAnalysis, inheritedRangeValues);
511+
}
512+
513+
const count = (entries: AnalysisEntry[], s: string) => entries.filter(e => e.status === s).length;
514+
const redundancy_analysis = {
515+
domain: domainAnalysis,
516+
range: rangeAnalysis,
517+
summary: {
518+
domain_redundant: count(domainAnalysis, "redundant"),
519+
domain_specialization: count(domainAnalysis, "specialization"),
520+
domain_new: count(domainAnalysis, "new"),
521+
range_redundant: count(rangeAnalysis, "redundant"),
522+
range_specialization: count(rangeAnalysis, "specialization"),
523+
range_new: count(rangeAnalysis, "new"),
524+
},
525+
};
526+
451527
return {
452528
success: true,
453529
data: {
@@ -459,6 +535,7 @@ server.registerTool(
459535
inheritedRange,
460536
effectiveDomain,
461537
effectiveRange,
538+
redundancy_analysis,
462539
...(warnings.length > 0 ? { warnings } : {}),
463540
},
464541
rowCount: defResult.results.bindings.length + superResult.results.bindings.length,

0 commit comments

Comments
 (0)