@entryscape/inspec
v0.6.0
Published
Validation, Conversion, and Enrichment per the INSPEC specification
Keywords
Readme
@entryscape/inspec
JavaScript/Node.js library implementing Validation, Conversion, and Enrichment per INSPEC specification v1.1.1.
INSPEC describes how application profiles and vocabularies are packaged as PROF, SHACL, RDFS, and SKOS artifacts. This library provides tools to:
- Validate those artifacts against the INSPEC rule sets (PROF-INSPEC, SHACL-INSPEC, RDFS-INSPEC, SKOS-INSPEC).
- Convert between related representations: UML (OSLO JSON) → SHACL, RDForms ↔ SHACL.
- Enrich a PROF specification by deriving
dcterms:hasPart,inspec:introduces,inspec:reuses, andprof:isProfileOftriples from its SHACL and RDFS artifacts.
The library is not a full SHACL implementation — it focuses on the INSPEC-relevant subset.
Install
pnpm add @entryscape/inspec
# or
npm install @entryscape/inspecRequires Node.js 20+.
CLI
inspec validate <file> [--type prof|rdfs|skos|shacl] [--strict] [--indexation]
[--json] [--quiet]
inspec convert <converter> <file> [-o <output>]
[--format turtle|ntriples|rdfjson]
[--base-uri <uri>] [--profile-uri <uri>]
inspec enrich <prof-file> [--shacl <file>] [--rdfs <file>...] [--rdfs-owned]
[--overwrite] [-o <output>] [--format ...]Converters: uml2shacl, rdforms2shacl, shacl2rdforms.
enrich's --shacl is required for a profile specification (it drives
hasPart and the introduces/reuses split) but rejected for a foundational
specification, which has no application profile and is enriched from its
--rdfs vocabularies alone.
RDF graph inputs are parsed by file extension: .ttl (Turtle), .rdf/.xml
(RDF/XML), and .json/.rdfjson (RDF/JSON — the native @entryscape/rdfjson
form). This applies wherever a graph is loaded (validate, enrich's
<prof-file>/--shacl/--rdfs, and convert shacl2rdforms). The
rdforms2shacl and uml2shacl converters instead take their RDForms/OSLO JSON
bundle directly. For these two, --base-uri sets the base for minted URIs —
with an idMapper bundle it applies to the ids the mapper does not cover. When
no base is configured, URIs are minted under the placeholder
http://example.com/ and a warning is emitted.
For validate, the resource type is auto-detected from the graph when --type
is omitted. --indexation switches missing-dependency reports (DEP-1) from
warnings to errors — meant for when the dependency index is complete enough to
definitively confirm a dependency does not exist. It currently has no effect
from the CLI (see Known gaps).
Exit codes: 0 success, 1 validation failures or command misuse (including
running inspec without arguments), 2 runtime error. Errors report a
message only; set INSPEC_DEBUG=1 to include stack traces (internal errors
such as a TypeError always include one).
Examples
# Validate a PROF specification, emit JSON
inspec validate spec.ttl --type prof --json
# Convert an RDForms template bundle to SHACL
inspec convert rdforms2shacl template.json -o shapes.ttl
# Round-trip SHACL back into an RDForms bundle
inspec convert shacl2rdforms shapes.ttl -o template.json
# Enrich a PROF graph with hasPart / introduces / reuses
inspec enrich spec.ttl --shacl shapes.ttl --rdfs vocab.ttl -o enriched.ttlintroduces vs reuses is normally decided from the PROF resource descriptors
(a non-inherited prof:ResourceDescriptor with dcterms:conformsTo inspec:RDFS
and a dcterms:subject naming the owned ontology). During local development a
PROF file may list its resources as bare URIs, thus ontology ownership cannot
be established — pass --rdfs-owned to treat every ontology declared in the
--rdfs files as owned by the spec, classifying their terms as introduces.
Do not combine it with reused vocabularies passed as --rdfs, or their terms
will be misclassified as introduced.
Library
import {
validatePROF,
validateRDFS,
validateSKOS,
validateSHACL,
convertUMLToSHACL,
convertRDFormsToSHACL,
convertSHACLToRDForms,
enrich,
collectPartSubjects,
findSpecificationResource,
isFoundationalSpecification,
isInspecPartType,
isProfileSpecification,
listResourceDescriptors,
loadGraph,
parseTurtle,
parseRdfXml,
parseRdfJson,
URIManager,
IdMapperURIManager,
ValidationResult,
buildPrefixMap,
} from '@entryscape/inspec';Validation
Each validator returns a ValidationResult with a uniform shape:
const graph = await loadGraph('spec.ttl');
const result = validatePROF(graph, { strict: false, indexation: false });
result.valid; // boolean — true if no errors
result.violations; // [{ rule, severity, message, subject?, hint? }]
result.summary; // { errors, warnings }Rule coverage (rule identifiers as emitted in violations):
| Validator | Rules |
| --------------- | -------------------------------------------------------- |
| validatePROF | PROF-1, PROF-2, PROF-7–PROF-10, ENRICH-1–ENRICH-4, DEP-1 |
| validateSHACL | AP-2–AP-4, AP-6–AP-15 |
| validateRDFS | DV-2–DV-6, DEP-1 |
| validateSKOS | TE-2–TE-5 |
inspec:refines and inspec:variant targets must be URIs, reported as an
error under the rule whose precondition they break — AP-7/AP-8 (a public
property shape, which AP-3 requires to have a URI), AP-9/AP-10 (a public node
shape, AP-4) and AP-11/AP-12 (an application profile resource, AP-2). A literal
or blank-node target can be none of those.
Two limits: a relation carried by a shape that is not a typed sh:NodeShape
or sh:PropertyShape with a URI is not reached, and only the first target per
shape is inspected. In both cases the bad value is still kept out of the
enrichment and coverage sets — only the diagnostic is missing.
AP-1, AP-5, DV-1, and TE-1 are not checked. PROF-3/4/5 are realized by delegating each part's graph to the matching sub-validator rather than as separately emitted rules; PROF-6 is not implemented (see Known gaps).
DEP-1 is a repo-local rule identifier, not a rule of the INSPEC
specification.
PROF-2 also covers a resource descriptor that declares more than one INSPEC
part type — dcterms:conformsTo inspec:RDFS, inspec:SHACL — since a part
"MAY be a data vocabulary, a terminology, an application profile or a diagram",
one of them, and declaring two asserts two of PROF-3/4/5/6 over a single
artifact. enrich() throws on the same input rather than guess which applies.
AP-3's blank-node rule exempts any property shape whose sh:path is
rdf:type: it constrains the class of the enclosing node shape's instances
rather than describing a property of the resource, so it may stay anonymous.
sh:hasValue <C> and sh:in ( … ) are the two ways such a shape pins the
class down. The AP-3 target-class hygiene warnings read the same construct, so
a node shape re-using its class's URI as its own shape URI is reported as a
warning. rdforms2shacl emits the idiom and shacl2rdforms reads it back.
AP-3 likewise exempts the blank property shapes of a node shape that is itself
private, since a private shape's constraint scaffolding is not part of the
published specification. The exemption reaches that node shape's own
sh:property children and no further: a nested node shape, or a named property
shape, is judged on its own severity whatever links it — see Known gaps. A
blank property shape that a public node shape also links is still reported,
against that public node shape.
In the library, strict: true affects only validatePROF: it promotes the
ENRICH-1/2/4 advisories and PROF-10 to errors. The CLI's --strict is a
broader policy applied after validation — it promotes every remaining warning
to an error. indexation: true reports missing dependencies (DEP-1) as errors
instead of warnings; it only matters when the dependency inputs are supplied
(artifactGraphs for validatePROF, providedOntologyURIs for
validateRDFS).
PROF introspection
validatePROF sub-validates a specification's parts only for the artifacts it
is handed, and resolving those is the caller's job (see Known gaps). These
functions expose the traversal that finds them:
const specificationURI = findSpecificationResource(profGraph);
const parts = listResourceDescriptors(profGraph, specificationURI);
// [{ descriptor, artifactURI, conformsTo, kind, subject, format, title,
// isInheritedFrom }]
// Diagram parts are listed too, but an SVG is not a graph — fetch only the
// kinds that carry RDF, and only once per artifact.
const RDF_PART_KINDS = new Set(['rdfs', 'skos', 'shacl']);
const artifactURIs = [
...new Set(
parts
.filter((part) => RDF_PART_KINDS.has(part.kind) && part.artifactURI)
.map((part) => part.artifactURI)
),
];
const artifactGraphs = Object.fromEntries(
await Promise.all(
artifactURIs.map(async (uri) => [uri, await fetchGraph(uri)])
)
);
const result = validatePROF(profGraph, { artifactGraphs });findSpecificationResource returns the first non-blank-node subject of
dcterms:conformsTo inspec:PROF, or undefined. Blank-node candidates are
ignored, since PROF-1 requires the specification resource to have a URI.
listResourceDescriptors returns one record per prof:hasResource statement,
in graph order:
kindis'rdfs','skos','shacl'or'svg'— the part types PROF-3 to PROF-6 prescribe — andundefinedfor anything else, soif (part.kind)tests whether a part is INSPEC-controlled. The key is always present; only its value isundefined.conformsTois an array of every declared value, in graph order — a part may conform to something outside INSPEC as well — whilekindresolves the INSPEC one among them. Every other predicate exceptdcterms:titleis read single-valued. A descriptor declaring two INSPEC part types is contradictory and reported as PROF-2.descriptoris a URI or, where a specification inlines its parts, a blank node id.- Every field except
descriptorandconformsTomay beundefined;conformsTois[]when the descriptor declares none.titlecarries every language variant keyed by lower-cased tag (''for an untagged literal), one value per tag — a repeated tag keeps the last statement. - Passing no specification URI returns
[], so a graph with no specification resource yields nothing rather than every descriptor it happens to contain.
The remaining helpers answer the questions that follow from a specification URI and its parts:
isProfileSpecification(profGraph, specificationURI)— whether the specification is typedprof:Profile.isFoundationalSpecification(profGraph, specificationURI)— whether it is typeddcterms:Standard; further types do not affect the answer.isInspecPartType(conformance)— whether adcterms:conformsTovalue names an INSPEC part type. Filterpart.conformsTowith it rather than counting its length.collectPartSubjects(parts)— theSetofdcterms:subjectvalues of the given parts, i.e. the data vocabulary and terminology resources they describe (DV-6, TE-5). Parts without one are skipped.
The two type predicates are separate on purpose: PROF-1 types a specification
as prof:Profile or dcterms:Standard, so true to both — or neither — is
itself a PROF-1 violation for the caller to report, which a single
kind-returning helper would hide.
Conversion
import rdforms from '@entryscape/rdforms';
// RDForms → SHACL
const itemstore = new rdforms.ItemStore();
itemstore.registerBundle({ source: rdformsTemplate });
const shapes = convertRDFormsToSHACL({
itemstore,
uriManager: new URIManager(baseURI, { profileURI }),
});
// SHACL → RDForms
const target = new rdforms.ItemStore();
convertSHACLToRDForms({ itemstore: target, graph: shaclGraph });
const bundle = target.getBundles()[0].getSource();
// UML (OSLO JSON) → SHACL
const umlShapes = convertUMLToSHACL({
osloJSON: umlJson,
uriManager: new URIManager(baseURI, { profileURI }),
});All three converters accept a logger option ({ warn }, default console).
Malformed or unconvertible input is never silently dropped: every skipped or
degraded construct produces a warning through that logger — e.g. unsupported
RDForms item types and anonymous (id-less) items in convertRDFormsToSHACL,
implicitly-typed named shapes and unmappable sh:nodeKind values in
convertSHACLToRDForms, and OSLO entries without an assignedURI in
convertUMLToSHACL. Pass a custom logger to capture or silence them.
For RDForms bundles whose ids are minted through an idMapper, use
IdMapperURIManager in place of URIManager:
new IdMapperURIManager(idMapper, { baseURI, profileURI }) — the longest
matching mapper prefix wins, and baseURI covers ids no prefix matches.
Enrichment
Single entrypoint, derives PROF structure triples from the supplied SHACL (and optionally RDFS) graphs:
const { graph, added, removed } = enrich(profGraph, {
shaclGraph,
rdfsGraphs: [rdfsGraph1, rdfsGraph2],
specificationURI,
});enrich also accepts a logger ({ warn }, default console). It reports
input that could not be used — a non-URI inspec:refines target is skipped
rather than coerced into a URI — and never comments on the result itself.
A caller that already knows which terms the specification introduces hands that
answer over as introducedTerms, skipping the derivation from the RDFS graphs:
const { graph } = enrich(profGraph, {
shaclGraph,
specificationURI,
introducedTerms: new Set(knownIntroducedTerms),
});Members must be absolute http, https or urn URIs carrying no character a
URI cannot hold; a relative id or a CURIE is refused. The same goes for
specificationURI, which is the subject of every derived triple. ENRICH-3 emits every member, in insertion order. ENRICH-2 instead
tests the set against the terms its shapes refer to, so for a profile the URIs
must be written as the shapes write them — a term the set omits is reused, and a
member no shape refers to is not emitted but reported through logger. An empty
set is an answer, a profile that reuses everything, not an omission.
The set replaces the RDFS derivation rather than adding to it, so a non-empty
rdfsGraphs, or rdfsOwned: true, alongside it throws.
introduces and reuses cover what ENRICH-2 asks for: the classes and
properties referred to via the specification's public shapes. Public is
AP-3/AP-4's sense, but reached through private scaffolding rather than applied
shape by shape. The specification's worked example settles that: ex:ns-person
declares no sh:targetClass, so foaf:Person is named only by a blank
rdf:type constraint shape AP-3 counts as private — and the published
enrichment block still reuses it.
Reachability is settled per shape, by whether the specification publishes it.
A shape explicitly below sh:Violation publishes nothing. An own named
shape — one declaring rdfs:isDefinedBy <specURI> — otherwise always does,
whatever encloses it, being addressable and reusable in its own right. A blank
node publishes only while a shape of this specification that uses it does,
where uses means the object of sh:property, sh:node, sh:not or
sh:qualifiedValueShape, or a member of an sh:and/sh:or/sh:xone list.
So a terminology binding written as
sh:node [ sh:severity sh:Info ; sh:property [ … ] ] does not announce its
class as reused, while a named shape nested in the same place still does.
A term named only in a negative constraint is reused like any other:
sh:not [ sh:class ex:Draft ] announces ex:Draft, even though instances must
not carry it. The specification depends on the term either way, and a consumer
resolving inspec:reuses needs to find it.
Only this specification's own shapes vouch for a blank node: a blank node has no
URI and cannot be reused across specifications, so a foreign shape sharing a file
with it must not publish it. The ENRICH-2 advisory in validatePROF reads the
same set from each SHACL artifact, so the two agree for a specification whose
shapes all live in the single SHACL artifact enrich() is given. They can
differ on a split one: a shape whose rdfs:isDefinedBy sits in a second
artifact is not own in the first, so the advisory judges it — and anything it
holds — unpublished where enrich() on the merged graph does not.
graph is a copy of profGraph with the derived triples added — the
specification's original statements (titles, descriptions, resource
descriptors, …) are preserved. added holds the derived triples on their own,
and removed those a preceding overwrite cleared; all three are Graph
instances, so the delta can be merged elsewhere or exported without a
conversion step.
added is legitimately empty when no rule matches — a foundational
specification with no data vocabularies, or a profile whose SHACL carries no
inspec:refines statement and no shape the specification owns. An empty
added is a result, not a failure. removed is empty unless overwrite
actually cleared something.
The two are not a net diff: they report what this run derived and what it
cleared. Re-enriching an already-enriched graph with overwrite from the same
SHACL and RDFS inputs is idempotent, so every triple then appears in both —
after the inputs change, the two differ and the difference is the point. A
caller wanting a true diff subtracts them itself.
removed is for reporting, not replay. Clearing an arc that pointed at a blank
node leaves that node's own statements behind in graph, orphaned but still
reachable by that id; removed captures only the arc, with nothing describing
the node. Merging removed back would therefore mint a fresh, unconnected node
rather than restore the original link.
For a profile (prof:Profile), shapes whose sh:targetClass / sh:path refer
to terms defined by an ontology the spec declares as its own (via a
non-inherited RDFS descriptor) are classified as introduces; the rest as
reuses. For a foundational specification (dcterms:Standard), everything
defined in the spec's own ontology is treated as introduces. Either way,
introducedTerms replaces that derivation when the caller supplies it.
If the specification already carries enrichment triples (dcterms:hasPart,
inspec:introduces, inspec:reuses, prof:isProfileOf), enrich throws (the
CLI prints the error and exits non-zero) rather than merging stale and fresh
values. Pass overwrite: true (CLI --overwrite) to clear those four
predicates on the specification before adding the freshly derived ones.
Status & roadmap
The current release covers the MVP scope. Known gaps:
UML2RDFS — not implemented (UML2SHACL is). Reuse-vs-introduce handling for OSLO JSON is still an open question.
CSVW converters (CSVW2SHACL, CSVW2PROF) — not implemented.
Implicit ResourceDescriptor detection — not implemented.
Specification / RDFS index integration (e.g. dataportal.se lookups) — not implemented; the
ArtifactCachecurrently has no population mechanism.CLI dependency checking — validator-level dependency checking (DEP-1, DV-5) is implemented and usable via the library API by passing
artifactGraphs;listResourceDescriptorsprovides the part list to build it from, so what remains is fetching and caching the artifacts. The CLI'svalidate --indexationflag is forwarded to the validators but has no effect from the CLI, because nothing there buildsartifactGraphs(no--cache-dirwiring to read anArtifactCache), and DEP-1 is only emitted whenartifactGraphsis supplied. Complementary to the index-integration gap above — both are needed for end-to-end dependency checking from the command line.PROF-6 (SVG diagram parts) — sub-validation is not implemented; SVG parts are reported as
kind: 'svg'bylistResourceDescriptorsbut otherwise accepted without being checked.URI inputs in the CLI — commands accept local file paths only; resolving remote URIs (via cache or direct fetch) is deferred.
RDForms
propertygroupitems — not convertible to SHACL: a propertygroup's predicate is itself variable (its first child is a choice of predicates), which has nosh:pathequivalent.rdforms2shaclskips such items with a warning.RDForms items without an id — anonymous inline items in a group's
itemsarray cannot become INSPEC shapes (no stable URI to mint);rdforms2shaclskips them with a warning nudging toward assigning an id.Foreign shapes in merged graphs — both the ENRICH-1/2 validation advisories and
enrich()'s output are scoped to the spec's own shapes: a named shape counts as own only if it declaresrdfs:isDefinedBy <specURI>(blank-node shapes always count). In a merged multi-AP graph the shapes of a refined/variant parent AP are neither flagged nor enriched; shapes missingrdfs:isDefinedByare likewise skipped — AP-13 reports those.URI-bearing private shapes — public/private classification follows the AP-3/AP-4 severity rule: a shape is public only at severity
sh:Violation(SHACL's default when the severity is absent), and a property shape must additionally carry at least one validating constraint predicate. A shape meant as private that carries a URI but no below-sh:Violationseverity is treated as public and is therefore subject to AP-3/AP-6/AP-13. Publicness here marks a shape as meant for publication, which is why AP-3/AP-4 then require a URI for it — a blank node shape carrying no below-sh:Violationseverity is an error to fix, not a published shape.Privacy is mostly not inherited: a nested node shape that omits a below-
sh:Violationseverity is public whatever links it, so AP-4 reports it when it is a blank node and AP-6/AP-13 apply when it has a URI, and a named property shape is public even when the only node shape linking it is private, so AP-13 and ENRICH-1 still expect it to be covered. AP-3 is the exception: it skips a private node shape's property children outright rather than demanding URIs for them.enrich()'sintroduces/reusesreach through a public shape into the private scaffolding it holds, judging a blank node by what uses it, as ENRICH-2 requires (see Enrichment);dcterms:hasPartreads only URIs and so follows the severity rule above.
Development
pnpm install
pnpm test
pnpm lint
pnpm buildLicense
LGPL-3.0-only
