Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

SHACL

purrdf-shapes(重新导出为 purrdf::shapes)是 PurRDF 的原生 SHACL 验证器与规则引擎。它实现了 SHACL 1.2:Core、SPARQL 扩展、节点表达式、推理规则以及 SPARQL 1.2 RL 规则语言,外加被这些文档取代的 SHACL-AF 1.0 拼写。它完全运行在 PurRDF 自己的驻留 IR 与原生 SPARQL 引擎之上(不含 PyO3)。

它依据 SHACL 形状图验证 RDF 1.2 数据图,不做任何推理(与 pySHACL inference="none" 一致);若要验证物化后的推理闭包,可与蕴涵组合使用,或声明 sh:entailment sh:RulesEntailment,先运行形状图自身的规则,或将形状图中的某个 sh:RuleSet 作为 sh:entailment 的值,只运行该规则集的规则(包括其所含规则集的规则);若给出多个这样的值,则合并为一个规则集一起运行。其他任何 sh:entailment 值都会被拒绝,这与 SHACL 对不支持该值的处理器的要求一致。把规则集作为 sh:entailment 的值,是 2026-10-16 的 SHACL 1.2 编辑草案首次定义的,因此默认法则和 shacl-ed-2026-10-16 会读取它,而 shacl-wd-2026-09-18 和 shacl-rec-2017-07-20 会拒绝它。

覆盖范围

  • SHACL 1.2 Core——SHACL 1.2 词汇表声明的每个约束组件都以原生方式求值:完整的属性路径、限定值形状、列表组件(sh:minListLength、sh:maxListLength、sh:uniqueMembers、sh:memberShape,带 sh:detail 结果)、三者均可取列表值的 sh:class、sh:datatype 与 sh:nodeKind(sh:nodeKind 还可取 sh:TripleTerm)、sh:singleLine、sh:rootClass、sh:someValue、属性对与 sh:subsetOf(二者均以路径为值)、sh:uniqueValuesFor、sh:closed sh:ByTypes、sh:reifierShape 与 sh:reificationRequired,以及按语言标签与基础方向判定的 sh:uniqueLang。属性形状的 sh:values 与 sh:defaultValue 会计算值节点。目标包括隐式类目标(sh:ShapeClass 亦然)、数据图中的 sh:shape、sh:targetWhere,以及以节点表达式给出的 sh:targetNode。约束三元组的具体化节点可以为这一条约束携带 sh:deactivated、sh:severity 或 sh:message。严重级别包括 sh:Debug 与 sh:Trace,一致性判定遵循请求中的 sh:conformanceDisallows 集合。每条 sh:message 都连同其语言标签与方向一起报告。
  • 精确的数值范围 —— sh:minInclusive、sh:maxInclusive、sh:minExclusive、sh:maxExclusive、sh:lessThan 与 sh:lessThanOrEquals 按 SHACL 的定义,用 SPARQL 运算符比较数值。任意大小的 xsd:integer 或 xsd:decimal 字面量都精确比较,从不经过 double,所以 9007199254740992 低于界限 9007199254740993。词法形式被其数据类型拒绝的字面量(例如 "1.5"^^xsd:integer)无法比较,因而违反约束。参见数值限制。
  • SHACL 1.2 SPARQL 扩展——基于 SPARQL 的约束与目标、具有预绑定语义的自定义约束组件、用户定义的 sh:SPARQLFunction 调用、sh:SPARQLTargetType、sh:sparqlExpr,以及按该文档规定解析的 sh:prefixes(见下文),全部运行在原生 SPARQL 引擎上。在默认法则和 shacl-ed-2026-10-16 下,基于 SPARQL 的约束的 sh:deactivated 是一个节点表达式;工作草案和 2017 年推荐标准在那里只允许 true 或 false。传给自定义 SPARQL 函数的空节点,在同样这两种法则下是错误,在工作草案下则是普通参数。
  • SHACL 1.2 节点表达式——shnex: 词汇表与较早的 SHACL-AF sh: 节点表达式拼写解析为同一种表示,并由同一个求值器执行。每种表达式都保持其求值条款所规定的顺序与重数:有序序列、多重集或集合。sh:if 仅当其条件为列表 ( true ) 时才取 sh:then。shnex-sparql.ttl 函数库为原生实现。sh:nodeByExpression 与 sh:ExpressionConstraintComponent 均参与验证。
  • SHACL 1.2 推理规则——sh:TripleRule 与 sh:SPARQLRule,支持 sh:condition、sh:layer、sh:order、sh:runOnce、sh:deactivated、规则集与 sh:RulesGraph、SPARQL 规则模板、临时三元组、sh:expectedPredicate 与 sh:ruleProcessor。各层按升序运行。在每一层中,只运行一次的规则先运行一次,随后迭代规则反复执行,直到某次迭代不再推出任何结果。在一次迭代内,规则按 sh:order 升序运行,相同次序的规则一起运行,每一组都能看到之前各组的推论,因此交换两条规则的次序可能改变推论。无法解析的 sh:condition 是加载错误,而不是一条静默地永不触发的规则。
  • SPARQL 1.2 RL——该规则语言的文本语法会被解析、检查良构性、分层、处理导入并求值(purrdf_shapes::srl)。SHACL 规则降为同一种规则集表示,二者都运行在同一个规则引擎 purrdf-datalog 上。

形状图要么被如实加载,要么被拒绝。未知的术语、类型不符的参数值,或引擎不求值的构造,都会引发点名指出该项的加载错误,绝不会成为一条静默消失的约束。同样的检查也覆盖 SHACL-SPARQL 的节点:基于 SPARQL 的约束、验证器与 sh:SPARQLTarget 只能携带其规范赋予它们的术语,因此约束的 sh:select 旁的 sh:ask,或拼错的 sh:mesage,都会被拒绝,而不是被忽略。

SHACL 词汇表定义、而引擎会点名拒绝的术语有:

  • SHACL JavaScript Extensions(sh:js、sh:JSConstraint 等),它们不属于 SHACL 1.2;本引擎没有可对其求值的 JavaScript 引擎。拒绝以 ShapesError::ShaclJs 给出,并且适用于形状触及 SHACL-JS 之处:形状上的 sh:js 或 SHACL-JS 类型,作为其目标的 sh:JSTarget 或某个 sh:JSTargetType 的实例,其规则中的 sh:JSRule,以及从节点表达式或该形状所运行的 SPARQL 中对 sh:JSFunction 的调用。只声明了无人调用的 sh:JSLibrary 与 sh:JSFunction 的库可以加载,这些声明保持闲置。挂接到约束组件上的 sh:JSValidator 则并非闲置:它既不是基于 ASK 的验证器,也不是基于 SELECT 的验证器,因此作为 sh:validator(validator-class)、sh:nodeValidator(nodeValidator-class)或 sh:propertyValidator(propertyValidator-class)的值时,它会使形状图成为格式错误的,无论是否有形状使用该组件,加载都会以 ShapesError::IllFormed 失败;
  • 出现在形状、节点表达式、基于 SPARQL 的约束、验证器或规则上的 sh:describe 与 sh:update。SHACL 1.2 词汇表把它们声明为 sh:SPARQLDescribeExecutable 与 sh:SPARQLUpdateExecutable 的查询,但没有任何 SHACL 规范执行这两个类,因此在这些位置上,它们否则会被静默忽略。若某个资源仅仅是此类可执行体,且没有任何形状读取它,则它可以加载。

引擎所求值的术语中,有两个需要说明:

  • SHACL-SPARQL 结果注解。 基于 SPARQL 的约束上、或基于 SPARQL 的约束组件的验证器上的 sh:resultAnnotation,会把其 sh:annotationProperty 添加到该查询产生的每一条结果中。其值是解中 sh:annotationVarName 所指变量的绑定;未给出变量名时,以该属性的本地名作为变量名。该变量未绑定时,使用 sh:annotationValue 给出的默认值。ASK 验证器的注解读取 this、value 与组件的参数。这些注解出现在 ValidationResult::annotations、报告图、SARIF 属性 shaclResultAnnotations、预备产物以及 Python 结果字典中。任何 SPARQL 查询都无法绑定的变量名会在加载时被拒绝;把 sh:focusNode 之类的 SHACL 报告属性用作注解属性,同样会被拒绝。
  • sh:minus。 SHACL Advanced Features 1.1 的 minus 表达式 [ sh:nodes N ; sh:minus M ],其求值与 SHACL 1.2 的 [ shnex:nodes N ; shnex:remove M ] 完全相同:给出 N 中不在 M 里的节点,并保持 N 的顺序。其 sh:nodes 是必需的。

引擎实现的每个 IRI 都由某份 W3C 文档定义;PurRDF 一个也不生成。

W3C SHACL 1.0 data-shapes 套件按已批准的预期通过(129/129,撰写时入台账的缺口为 0);已批准的 W3C SHACL 1.2 套件同样按已批准的预期通过(590/591,唯一的例外是 hasLang-example,它与 SPARQL 1.2 语法的参数个数相矛盾);实时数字见docs/CONFORMANCE.md,复现方法见下文的 SHACL 1.2 一致性。

解析后的形状图还可以编译一次,写出为一个确定性的、经认证的字节工件,这样后续进程可以恢复一个预备好的验证器,而不必重新解析 Turtle——参见预备形状产物。

形状图中的 owl:imports

形状文档可以带有一个 owl:Ontology 头或一个 sh:ShapesGraph 声明,它们通过 owl:imports 导入其他文档,而它用来施加约束的形状可以完全位于这些导入中。PurRDF 会解析这一导入闭包——但绝不获取它。工作区中没有 HTTP 客户端,每个发布 crate 都为 wasm32-unknown-unknown 构建,而且依赖某个 URL 今天提供了什么内容的验证裁决是不可复现的。因此,导入闭包是调用方提供的配置,与 entails 和 shex 完全一样:

purrdf validate --shapes root.ttl \
  --import https://example.org/shapes-a=a.ttl \
  --import https://example.org/shapes-b=b.ttl \
  data.ttl -

该表会被传递地跟随——被导入文档自身的 owl:imports 也从同一张表中解析——遇到环时会终止而不会无限循环,因为 OWL 2 §3.4 把导入闭包定义为传递闭包,并明确允许 A 导入 B、后者再导入 A。每个被导入文档自身的 @prefix 声明随其一同携带,因此写在被导入文件中的 SHACL-AF sh:select 会依据该文件所声明的前缀解析。

形状文档在其自身的 file:// 获取 IRI 下解析,若给出 --shapes-base IRI 则在该 IRI 下解析。这个标志与 shacl pack --base 相同,并与 validate --base 相互独立——后者只设置数据图的基准 IRI。按 Turtle 的规定,形状文档内部的 @base 在该文档内仍然优先。

并非每条 owl:imports 三元组都是导入。OWL 2 从文档的本体头读取其导入(Mapping to RDF Graphs §3.1.2,表 4:x rdf:type owl:Ontology . x owl:imports y)。SHACL 1.2 Core 从形状图自身的 IRI 出发,沿 ^owl:versionIRI?/owl:imports 跟随这些导入,并指名了声明形状图的类:「The sh:ShapesGraph class MAY be used as an rdf:type of the IRI of a graph that typically acts in the role of a shapes graph.」PurRDF 以 OWL 2 作为这条规则的底线,并在其上加入 SHACL 的形状图。因此,只有当 owl:imports 的主语是以下之一时,它才是导入:

  • 形状文档自身的 IRI(其 file:// 获取 IRI、--shapes-base、shacl pack --base 或文档内的 @base;对于被导入的文档,则是导入它时所用的 IRI);
  • 文档将其类型声明为 owl:Ontology 的节点;
  • 文档将其类型声明为 sh:ShapesGraph 的节点。每个这样的节点都计入;sh:RulesGraph,以及某个被文档声明为 rdfs:subClassOf sh:ShapesGraph 的类的节点,同样计入;
  • 以上述之一作为其 owl:versionIRI 的节点。

在其他任何节点上,它都是数据:它按原样留在形状图中,也不会去查找任何文档。仅为 sh:DataGraph 的节点就是这样的节点。SHACL 1.2 Core §6.2 写道「owl:imports in the data graph is not enacted」,其注释要求:数据图若意在表示一个导入,则应同时将其类型声明为 owl:Ontology。数据图自身的 owl:imports 从不被读取。purrdf shapes lint 会把每条属于数据的 owl:imports 列在 unanchored-imports 下。蕴涵的导入由同一规则决定。导入锚点与下文隐式 SPARQL 前缀来源共用一个图角色分类器,但采用不同的选择条件:数据图角色提供前缀,却不会使其 owl:imports 成为导入。

SHACL-SPARQL 的前缀路径正是这样的数据。查询在形状图内沿 sh:prefixes/owl:imports*/sh:declare 收集其前缀,而 W3C 测试套件是这样写的:

<http://example.com/ns#> sh:declare [ sh:prefix "ex" ; sh:namespace "http://example.com/ns#"^^xsd:anyURI ] .
ex:TestPrefixes owl:imports <http://example.com/ns#> ;
  sh:declare [ sh:prefix "test" ; sh:namespace "http://test.com/ns#"^^xsd:anyURI ] .

ex:TestPrefixes 既不是文档的 IRI,也不是本体头,也不是形状图,因此那条 owl:imports 是一条前缀边,而不是导入,该测试按原样即可验证。sparql/component/validator-001 同样如此:其 owl:imports <http://datashapes.org/dash> 所在的节点三者皆非。

若某个导入的文档或本体已经加载,则无需提供配对。当它指向一份已读取的文档时即视为已解析:形状文档自身的基准(其 file:// 获取 IRI、--shapes-base 或 shacl pack --base)、文档内的 @base,或某个 --import 文档的 IRI。当图中含有 <X> a owl:Ontology、<X> a sh:ShapesGraph,或含有一个 owl:versionIRI 为 <X> 的本体时,导入同样视为已解析。

上述每条规则都适用于整个导入闭包,因此出现在某个被导入文档中的声明同样算数。合并了 W3C SHACL 1.2 词汇表的形状文档——shnex.ttl 导入 sh:,而与之并列的 shacl.ttl 声明了 sh:——按原样即是完整的。

其他任何没有配对能够解析的 owl:imports 都会被拒绝(退出码 1)。拒绝信息点名每个缺失的 IRI 以及可解析它的 --import IRI=FILE 配对,并对一份导入其自身已发布 IRI、而此处从本地文件读取的文档建议使用 --shapes-base IRI。在缺少被导入文档的情况下进行验证,得出的将是针对一个比所指名者更小的形状图的裁决,因此不存在「警告后继续」的路径。导入闭包从未到达的配对会作为未使用而被拒绝(退出码 2),而不是读入后忽略。

导入闭包还必须每个系列只含一个版本。SHACL 1.2 Core §1.3 规定:当形状图的导入闭包中含有两个图,它们「are different versions of the same series (i.e., they share the same shapes graph IRI but have different owl:versionIRI values), or one contains an owl:incompatibleWith annotation whose value is equal to either the shapes graph IRI or the owl:versionIRI of the other」时,该形状图是格式错误的;§6.1 则写道导入闭包「SHOULD NOT」含有它们。PurRDF 将其读作 MUST NOT。这样的导入闭包会在每个宿主上以类型化导入错误被拒绝,kind 为 incompatible-import-versions,并点名这两个图。蕴涵拒绝同样的导入闭包(OWL 2 §3.4 给出了同样的两个条件)。

数据图指名其形状图

SHACL 1.2 Core §6.4 允许由数据图一方建议形状图:

<http://example.com/myDataGraph> a sh:DataGraph ;
  sh:shapesGraph ex:graph-shapes1 ;
  sh:shapesGraph ex:graph-shapes2 .

「Every value of sh:shapesGraph is an IRI representing a graph that SHOULD be included into the shapes graph used to validate the data graph」,PurRDF 将这里的 SHOULD 读作 MUST。当一条 sh:shapesGraph 三元组的主语是数据图自身的 IRI(在命令行上为 --base,或数据文件的 file:// 获取 IRI),或是一个 sh:DataGraph 节点时,它就是这样的链接(交给绑定的 N-Triples 文本没有 IRI,因此在那里由类型充当锚点)。每个被链接的图都通过与 owl:imports 相同的导入表解析:一个表项、形状图已声明的图,或 owl:versionIRI 即为该链接的本体——「the same strategy of resolving a shapes graph IRI from a version IRI … applies here」——被链接图自身的 owl:imports 也会被跟随。被链接的图并入所提供的形状图;若形状文档为空,它们就构成整个形状图。只被某个链接指名的表项算作已使用,而不是未到达。

没有任何内容能够解析的链接会以 unresolved-shapes-graph-link 被拒绝,并点名该 IRI(在命令行上还会点名能解析它的 --import IRI=FILE 配对)。不是 IRI 的链接值为 invalid-shapes-graph-link。任何其他节点上的 sh:shapesGraph 都是数据,与其他三元组无异。数据图自身的 owl:imports 仍不执行(§6.2):链接是为形状图指名一个图,其中没有任何东西会把数据图的导入变成指令。

在数据图尚未可知之前就已预备好的形状图——一个针对多个图进行验证的已解析 Shapes、一个 PreparedShapes、一个预备好的产物——无法再纳入新的图。用它进行验证时,会检查每个链接是否都是它已持有的图:它的基准、其导入闭包或链接所并入的某份文档(产物会记录这些 IRI,因此恢复之后同样持有它们),或它所声明的某个形状图、本体或版本 IRI。其他任何链接都会以 unheld-shapes-graph-link 被拒绝,而不是针对一个比数据图所指名者更小的形状图进行验证。在增量变更路径上,添加或撤回链接的变更会改变形状图本身,因此该次运行会验证整个变更后的图,其范围(scope)会说明原因。

每个宿主上的同一规则

命令行是一条位于引擎内部的规则的调用方之一。形状图成为验证器的每一种途径——解析它、用它验证、运行它的规则、对它的某个节点表达式求值、对它执行 lint、把它打包成产物——都经由同一个辅助函数 purrdf_shapes::imports::resolve_shapes_imports 解析导入闭包,该函数建立在蕴涵同样应用的 purrdf_core::imports 规则之上。因此,同一个形状图在每个宿主上得到同一裁决,而每个宿主以各自的写法接受导入表:

宿主导入表拒绝
Rust传给 parse_shapes_with_config、from_dataset_with_base、validate_graphs_with_options、lint::lint 或 FreeExpression 的 ShapesImports(from_turtle、insert、declare_loaded、link_data_graph);purrdf-validate 边界接受 (IRI, Turtle) 配对ShapesError::Imports(ShapesImportError::Unresolved | Unreached | InvalidEntry | UnresolvedLink | UnheldLink | InvalidLink | IncompatibleVersions)
CLI--import IRI=FILE,可重复未解析的导入为退出码 1;没有任何导入指向的配对或格式错误的配对为退出码 2
Pythonvalidate、entail、apply_rules、eval_node_expr、lint_shapes、pack_product 与 Shapes(...) 上的 imports=[(iri, turtle), ...]purrdf.shapes.ShapesImportError(一个 ValueError),带有 .kind 与 .iris
WebAssembly每个接受形状图的 shacl* 函数末尾的 importIris、importDocuments 数组ShaclImportError,带有 kind、iris 与 message
C ABI位于输出参数之前的 import_iris、import_documents、import_countPURRDF_STATUS_SHAPES_IMPORT_ERROR,通过 purrdf_shapes_import_error_kind、_iri_count 与 _iri 读取

每个被导入的文档都是 Turtle,以其本体 IRI 作为基准进行语法解析。kind 为 unresolved-import、unreached-import(既没有任何导入、也没有任何数据图链接指名的表项——在每个宿主上都会被拒绝,因为其中的形状会被读入却永不应用)或 invalid-import(不是绝对 IRI 的键、被指名两次的键,或语法解析失败的文档)。版本冲突使用 incompatible-import-versions;上文三种 *-shapes-graph-link kind 属于数据图的链接。应依据 kind 分支,而不是依据消息文本。

import purrdf

report = purrdf.shapes.validate(
    shapes_ttl, data_nt, imports=[("https://example.org/lib", lib_ttl)]
)

lint 认证整个导入闭包:被导入文档中的形状会像导入方文档中的形状一样被加载,并依据 shacl-shacl.ttl 检查;其导入文档并未备齐的形状图会被拒绝,而不是被报告为干净。

SPARQL 1.2 RL 规则集中的 IMPORTS

SPARQL 1.2 RL 规则集可以用 IMPORTS 导入其他规则集。PurRDF 不会获取其中任何一个。规则工具从它为形状图所接受的同一张导入表中取得被导入的规则集,只是以规则集文本代替 Turtle:purrdf rules --srl 上的 --import IRI=FILE,Python 的 apply_rules(srl=...) 上的 imports=[(iri, text), ...],shaclApplyRules 上的 importIris / importDocuments,以及 purrdf_shacl_apply_rules 上的 import_iris / import_documents / import_count。每个宿主都经由 RuleSetDocument::resolve_import_table 解析该导入表。导入会被传递地跟随,每个 IRI 只读取一次。被导入的规则集以其 IRI 作为基准 IRI 进行解析,其规则在导入方的规则之后运行。

对于没有任何表项提供的导入,每个宿主都以同一条点名该 IRI 的消息拒绝。导入闭包从未提及的表项同样会被拒绝,因为那个规则集会被读取却从不被使用。对第一种情况,命令行以退出码 1 退出;对第二种情况,以退出码 2 退出。

import purrdf

inferred = purrdf.shapes.apply_rules(
    data_nt, srl=rules_srl, imports=[("https://example.org/more", more_srl)]
)["inferred"]

SHACL-SPARQL 查询中的前缀

形状图中的 SPARQL 查询(sh:select、sh:ask、sh:construct、sh:sparqlExpr、验证器、目标或函数体)在编译前会获得一个 PREFIX 头。该头遵循 SHACL 1.2 SPARQL 扩展中的「Prefix Declarations for SPARQL Queries」:

  • 带有 sh:prefixes 的查询使用所选法则的路径上的前缀声明,该路径从查询节点以及携带它的形状或组件读取。默认法则和 shacl-ed-2026-10-16 读取编辑草案的路径 sh:prefixes/(^owl:versionIRI)?/(owl:imports/(^owl:versionIRI)?)*/sh:declare。shacl-wd-2026-09-18 读取工作草案的路径 sh:prefixes/(^owl:versionIRI?/owl:imports)*/sh:declare,它不读取版本 IRI 所指向的图上的声明。shacl-rec-2017-07-20 读取 sh:prefixes/owl:imports*/sh:declare,没有 owl:versionIRI 步骤。
  • 不带 sh:prefixes 的查询使用形状图中 owl:Ontology、sh:DataGraph、sh:ShapesGraph 或 sh:RulesGraph 的每个 SHACL 实例的每个 sh:declare。这些类的子类同样计入。前缀收集与导入解析共用一个图角色分类器;仅具有 sh:DataGraph 角色的节点提供隐式前缀,但不是导入锚点。
  • 在查询所能到达的声明中,同一前缀对应两个不同的命名空间会使形状图不良构。加载会失败,错误信息点名该前缀与两个命名空间。同一前缀以相同命名空间声明两次则没有问题。没有任何查询到达的声明不会被检查。
  • 查询所到达的格式错误的声明同样会使加载失败。一条声明需要恰好一个 xsd:string 类型的 sh:prefix 与恰好一个 sh:namespace。

形状文档自身的 @prefix / PREFIX 指令是后备来源。它们只补充上述声明未绑定的前缀,绝不替换声明已绑定的前缀。它们取自 Turtle 解析器对文档的自身记录,因此查询字符串字面量中的 PREFIX 行绝不会被误认为指令。若文档把某个前缀声明了两次,后备来源采用最后一次声明。查询自身文本中的 PREFIX 只作用于该查询。

作为命名图的形状图:$shapesGraph

SHACL 1.0 在每个 SHACL-SPARQL 查询中把 $shapesGraph 预绑定到形状图的 IRI,因此查询可以用 GRAPH $shapesGraph { … } 读取形状图。SHACL 1.2 移除了这一预绑定,在那里 $shapesGraph 是一个普通变量。PurRDF 把 SHACL 1.0 的行为保留为调用方的选择。指名形状图的 IRI,$shapesGraph 就会预绑定到它,形状图也以该名称公开。不指名,$shapesGraph 就保持为普通的未绑定变量。相对 IRI 相对于形状文档的基准解析,因此它所指名的,就是文档中 sh:shapesGraph <that reference> 所指名的对象。作用域内没有基准时,相对 IRI 不指名任何图,会被拒绝(iri-relative-no-base)。

每个宿主都接受该 IRI:

宿主写法
CLIvalidate、shapes lint、shacl pack 与 rules 上的 --shapes-graph IRI
Pythonvalidate、Shapes(...)、pack_product、lint_shapes、apply_rules 与 entail 上的 shapes_graph=
WebAssemblyshaclValidateToSarif、shaclValidateChangesToSarif、shaclPackProduct、shaclLintShapes、shaclApplyRules 与 shaclEntail 末尾的 shapesGraph 参数
Cpurrdf_shacl_validate_to_sarif、purrdf_shacl_validate_changes_to_sarif、purrdf_shapes_product_encode、purrdf_shacl_lint_shapes、purrdf_shacl_apply_rules 与 purrdf_shacl_entail_to_ntriples 上紧随 shapes_base_iri 之后的 shapes_graph_iri
Rustengine::parse_shapes_with_graph、engine::validate_graphs_with_shapes_graph、engine::entail_graphs_with_shapes_graph、RulesRequest::shapes_graph,以及 purrdf-validate 的 *_with_shapes_graph 入口点

在该 IRI 下解析的 Shapes 会把它带入每一次验证、每一个 PreparedShapes 绑定以及每一个预备好的产物,产物的身份会绑定它。SHACL-AF 的 SPARQL 规则在同一形状图上下文中运行:若某次规则运行指名了 IRI,sh:SPARQLRule 的 $shapesGraph 就预绑定到该 IRI;若未指名,$shapesGraph 就是一个普通变量。SPARQL 1.2 RL 规则集没有形状图,因此与它一同指名的 IRI 会被拒绝,而不是被忽略。W3C SHACL 1.0 测试 sparql/pre-binding/shapesGraph-001 展示了其中的差别。指名 IRI 时,它报告那一条经批准的结果。不指名时,$shapesGraph 未绑定,查询什么也选不出,数据图符合约束。

保存在形状图中的类层次:subClassOfInShapesGraph

SHACL 类型——一个节点是否为某个类的 SHACL 实例,sh:targetClass、隐式类目标、sh:class、sh:rootClass 与 shnex:instancesOf 问的都是这个问题——从数据图的 rdf:type 与 rdfs:subClassOf 三元组中读取。类层次却往往与形状放在一起,因此 SHACL 1.2 Core §6.3 写道「SHACL processors SHOULD offer a parameter subClassOfInShapesGraph that, if set to true, should alter the definition of SHACL Type so that the rdfs:subClassOf triples are queried from the shapes graph in addition to the data graph.」PurRDF 在每个宿主上都提供该参数。它默认关闭,这也是规范的默认值。

开启后,形状图的 rdfs:subClassOf 三元组(其整个导入闭包中的)会在每一处判定 SHACL 类型的地方与数据图的三元组合并,且仅限于这些地方。它们不会变成数据图三元组,因此路径、sh:targetSubjectsOf rdfs:subClassOf 或 SPARQL 模式仍然只读取数据图。rdf:type 三元组始终从数据图读取。

宿主写法
CLIvalidate 上的 --subclass-of-in-shapes-graph(文档与产物两种路径)
Pythonvalidate 与 Shapes(...) 上的 subclass_of_in_shapes_graph=True
WebAssemblyshaclValidateToSarif 末尾的 subClassOfInShapesGraph 参数
Cpurrdf_shacl_validate_to_sarif 上紧随导入表之后的 bool subclass_of_in_shapes_graph
RustValidationOptions::with_subclass_of_in_shapes_graph

内置声明与 W3C 词汇表

SHACL 1.2 词汇表(shacl.ttl、shnex.ttl 与 shnex-sparql.ttl)声明了每个内置约束组件与节点表达式函数,例如 sh:SPARQLExprExpression a sh:NamedParameterExpressionFunction。这些声明都没有函数体或验证器,因为实现由引擎提供。形状图可以合并这些词汇表,加载时会把每条声明与引擎的「已实现项」表进行解析:

  • 对内置项的裸声明绑定到原生实现,不向自定义函数索引或组件注册表添加任何内容;
  • 若内置项被再次声明并带有函数体,即为重复定义;为内置组件另行给出其自身的 sh:ask 或 sh:select 同样如此;在错误的类下声明或带有相矛盾签名的内置项即为不匹配;以上所有情形都会使加载失败;
  • 为内置组件声明的验证器,是被原生实现取代的备选实现。SHACL 1.2 SPARQL Extensions 从组件的验证器中选取「one of the values」,因此每个验证器都是同一组件的一种实现,而实际运行的是引擎自身的实现。DASH 等词汇表为 SHACL Core 组件声明了这类验证器。SPARQL 备选实现必须是与其挂接方式相符的良构 SPARQL 验证器:sh:propertyValidator 下的 ASK 验证器(propertyValidator-class)、任何挂接方式下的 SHACL-JS sh:JSValidator(validator-class、nodeValidator-class、propertyValidator-class),或无法解析的查询(ask-sparql),都会使加载失败。它从不执行,因此其查询可以调用引擎所没有的函数,其带预绑定变量的查询中的 MINUS 也不会使加载失败:purrdf shapes lint 会把它列在 unexecuted 下;
  • 内置项声明上的任何其他 sh: 语句都会使加载失败,但 sh:message、sh:labelTemplate 以及不参与验证的特征(sh:name、sh:description 等)除外:例如,sh:MinCountConstraintComponent 上的 sh:severity 会要求原生实现做一件它并不做的事;
  • 引擎未实现的函数无论位于哪个命名空间都仍需函数体:没有函数体的 ex:f 会使加载失败,没有函数体的 sh:NotABuiltin 同样如此;
  • 若自定义函数的键参数与某个内置项的键参数相同,加载失败,因为所有节点表达式函数的键参数必须互不相交(任意两个函数都不得共用同一个键参数)。

因此,把 W3C 词汇表合并进形状图是一个空操作。一项测试在三个语料(第一方语料、W3C SHACL 1.0 套件以及 SHACL 1.2 的 sht:Validate 条目)中的每个形状图上验证这一点:无论是否合并,验证报告都逐字节相同。purrdf_shapes::spec 公开词汇表声明了什么、引擎实现了什么,一项测试把两者之差固定为零。purrdf shapes lint 按每个函数调用点报告该调用绑定到了原生实现、自定义函数体、SPARQL 注册项还是宿主扩展,并把为内置组件声明的每个验证器列为 superseded-by-native。这些行从不算作问题。

违反语法规则的 SHACL-SPARQL 或 SHACL-AF 声明,无论是否有形状触及它,都会使加载失败。SHACL 1.2 Core 写道「A SHACL processor SHOULD produce a failure in this case」,并没有把这一点限于形状所使用的内容。这些规则出自 SHACL 1.2 SPARQL Extensions 与 SHACL Advanced Features,拒绝会按 id 点名每一条规则。sh:nodeValidator 或 sh:propertyValidator 的值必须是 SELECT 验证器,sh:validator 的值必须是 ASK 验证器(nodeValidator-class、propertyValidator-class、validator-class)。组件参数不得命名为 this、path、PATH 或 value(parameter-name-not-in)。sh:SPARQLFunction 的参数既不得使用这些名称,也不得命名为 shapesGraph 或 currentShape。sh:SPARQLFunction 需要恰好一个 sh:ask 或 sh:select(SPARQLFunction-query)。sh:select 主体返回其唯一结果变量的绑定,SHACL Advanced Features 写道「such SELECT queries should only return at most one solution」;PurRDF 将其读作必须,因此若某次调用的主体返回了第二个解,验证会失败,而不是以恰好排在最前的那一行作答。拒绝以 ShapesError::IllFormed 给出,并列出图中的每一处违规,因此导入了带有格式错误声明的库(DASH 就有这类声明)的形状图会加载失败,并点名其中的全部违规。

预绑定违规在查询运行之处判定。SHACL 1.2 SPARQL Extensions 要求:一个「executed with pre-bound variables」的查询若含有 MINUS、提及预绑定变量的 VALUES,或针对预绑定变量的 AS ?var,则必须报告失败。sh:sparql 约束、组件验证器、sh:SPARQLTarget 的 sh:ask 以及 SPARQL 规则拒绝所有 VALUES,无论是否预绑定,因为随库固化的 W3C 用例 unsupported-sparql-002 要求拒绝 VALUES ?any { true };节点表达式、sh:SPARQLFunction 函数体和 sh:SPARQLTargetType 查询只拒绝提及预绑定名称的 VALUES。因此,当形状的 sh:sparql 约束违反其中一条,当自定义组件的某次使用选中了这样的验证器,当某个节点表达式或某个形状触及的查询调用了这样的 sh:SPARQLFunction,当某个形状实例化了这样的 sh:SPARQLTargetType,或当某个形状触及这样的 sh:select 节点表达式时,加载会以 ShapesError::Prebinding 失败。函数或目标类型的参数就是其可能预绑定的变量,可选参数也包括在内,因为提供它的调用会绑定它。节点表达式的预绑定变量是 $this 以及其上下文绑定的每个名称:sh:expression 中的 $value、自定义函数体中的函数参数,以及自由求值的作用域名称。每一个都与 $this 遵循同一规则;违反该规则的节点表达式查询会在某个形状触及该表达式时使加载失败。这比引擎自身的通道更严格:预备执行或请求的替换接受针对预绑定名称的 VALUES 和 MINUS(参见预绑定变量)。分组约束则更窄:在自定义函数体中,它只把每次调用都会绑定的参数(即必需参数)视为常量,因为调用可以省略可选参数。引擎自带组件的验证器从不运行;没有任何使用选中的验证器、没有任何调用的函数,以及没有任何形状实例化的目标类型,同样不会运行。它们可以加载,purrdf shapes lint 会把每一个列为一个问题。

同一附录写道「SPARQL queries SHOULD not contain a federated query (SERVICE)」,以及「Implementations that do not permit SERVICE MUST report a failure」。PurRDF 在任何 SHACL-SPARQL 查询中都不允许 SERVICE,包括 sh:SPARQLTarget 以及不带参数、因而什么也不预绑定的函数。一个取决于远程端点如何应答的裁决,就不再是关于数据图的裁决,而 PurRDF 不获取任何内容。这样的查询会在其运行之处以 ShapesError::Prebinding 使加载失败;在没有任何东西运行它的地方,则由 lint 列出。字符串字面量中的这个词不是 SERVICE。

若某个形状的 sh:target 是 PurRDF 无法计算的自定义目标,则会以 ShapesError::UnsupportedTarget 拒绝,并点名该形状与该目标。PurRDF 能计算 sh:SPARQLTarget,以及已声明的 sh:SPARQLTargetType 的实例。SHACL Advanced Features 写道,引擎若「cannot handle a given custom target SHOULD at least report a warning」。在一份关于无人计算过的焦点节点的报告旁附上一条警告,它仍然是那份报告,因此 PurRDF 选择拒绝,这更为严格。SHACL-JS 目标以 ShapesError::ShaclJs 被拒绝。

sh:SPARQLTarget 还可以携带一个 sh:ask。SHACL Advanced Features §3.1 写道「SPARQL-based targets have at most one value for the property sh:ask」,它面向无法改写为等价 ASK 的 SELECT:「A SHACL engine can then determine whether a given shape applies to a given node by executing the ASK query with the variable this pre-bound to the node.」凡是检查给定节点、而不枚举目标的地方,PurRDF 都正是这样做的:PreparedValidator::validate_focus_nodes 及其直接以内部 id 运作的对应版本。(对带有 SPARQL 目标的形状图进行变更路径验证时,其影响范围没有上界,因此它会验证整个图。)候选节点被预绑定到 $this,因此该 ASK 必须满足预绑定限制,数据图中并不存在的节点也同样会得到应答。该 ASK 并不单独作出决定:由于两者陈述的是同一个目标,它的应答会与该目标自身的 SELECT 在同一数据图上的结果相互核对。两者应答不同的节点会以 ShapesError::SparqlTargetDisagreement 被拒绝,错误中点名该形状、两个查询、该节点以及两个应答,因为无论依据哪一个应答来检查它,都无法得到完整验证所给出的判定。完整的验证仍用其 SELECT 枚举目标,并且从不运行该 ASK。第二个 sh:ask、不是 ASK 查询的值,或违反预绑定限制的 ASK,都会在加载时被拒绝。没有 sh:ask 的目标检查候选节点的方式,是在其 SELECT 的结果中查找它。

SHACL 1.2 一致性

完整的 W3C shacl12-test-suite 逐字节随库固化在 vectors/shacl12/ 下,并经由库 API 运行:共 592 个测试,涵盖 213 个 sht:Validate、143 个 sht:EvalNodeExpr 与 27 个 sht:Infer 测试,以及 209 个 SPARQL 1.2 RL 语法、良构性、分层与求值测试。其中 591 个由上游清单列出。在这 591 个中,590 个按已批准的预期通过,且预期失败台账为空。其余 1 个是没有任何清单收录的随库固化文件中的条目,单独报告。唯一未通过的已批准条目 node-expr/shnex-sparql/hasLang-example 连同其理由一并报告为失败:它以两个参数调用 sparql:hasLang,而 SPARQL 1.2 语法中的 hasLANG 恰好接受一个参数。SPARQL 1.2 RL 的负例测试必须在其测试类型所指明的阶段失败,而不能在更早的阶段失败。复现方式:

cargo test -p purrdf-shapes --test w3c12_conformance -- --nocapture

这 213 个 sht:Validate 测试也会经由构建出的 purrdf validate --shacl-profile shacl-ed-2026-10-16 二进制运行,并由同一个评分器对照已批准的结果评分,因此该结果在已发布的接口上同样成立:

cargo test -p purrdf-cli --test w3c12_validate_cli -- --nocapture

已批准套件记分板的最后一行是 W3C12 TOTAL: passed 590, spec-contradicted 1, xfailed 0, ledger 0,未收录文件自己的那一行是 W3C12 UNLISTED: passed 1, exact 1, with-delta 0, total 1。

有一个随库固化的文件带有没有任何上游清单收录的测试条目:inference-rules/rdfs/rdfs1.ttl。它不属于已批准的套件,因此从不计入该套件的通过数。另一个单独的测试 w3c_shacl12_unlisted_vendored_files 严格按原文对它判分,并以它自己的标签报告。

sh:reifierShape 或 sh:reificationRequired 的结果以值节点作为 sh:value。SHACL 1.2 Core 第 7.8.5 节的文字定义用名称 t 指代两样东西。它开头写道「Let t be the triple term (focus node, $path, value node)」,随后写道「For each reifier t that does not conform to $reifierShape, there is a validation result with t as sh:value」,并在缺失具体化时写道「there is a validation result with t as sh:value」。已批准的测试 core/property/reifierShape-001 与 -002 都期望值节点,PurRDF 遵循已批准测试套件的这一解读。针对不符合形状的具体化节点的结果,还以 sh:detail 携带该具体化节点自身的验证结果,每条都以该具体化节点为焦点节点,因此报告仍会点名该具体化节点并说明其失败原因。缺失的具体化没有具体化节点,因而不携带任何细节。

在六个节点表达式测试中,已批准结果将计算得到的 xsd:decimal 写作 "4.0"、"3.0"、"42.0" 与 "00"。规范把计算所得字面量的词法形式留给处理器决定,因此 PurRDF 计算十进制数时带小数点和一位小数(4.0),并将 SECONDS 计为其参数词法形式的秒部分(00)。这六个条目与其他所有条目一样,按 RDF 项逐一与已批准结果比较判分。

sparql/component/validator-001(本套件与 SHACL 1.0 套件中各有一份)在不提供任何导入的情况下按名称通过。其 owl:imports <http://datashapes.org/dash> 所在的节点既不是文档自身的 IRI,也不是 owl:Ontology 或 sh:ShapesGraph,也不以其中之一作为其 owl:versionIRI,因此按照 OWL 2 到 RDF 的映射(§3.1.2)与 SHACL 1.2 Core,这条三元组是数据,而不是导入(参见形状图中的 owl:imports)。测试工具以空的导入表加载该用例。

SPARQL 1.2 RL 语法规则 [2] RuleOrDataBlock ::= Prologue ( RuleOrData+ ( Prologue1 RuleOrData? )* )? 按其明显的意图实现:在起始序言之后,声明与规则块或数据块可以自由交错,因此每条声明之后都可以跟任意数量的规则块或数据块。若按字面理解,该规则会拒绝 RULE … PREFIX … RULE … RULE …,却接受在最后两条规则之间多一条声明的同一组规则。

JSON-LD 实例投影与 JSON Schema

json_schema::compile 与 instance::project_graph 是一对。模式描述投影所写出的 JSON-LD @graph 文档,一个被投影的节点当且仅当符合形状时才通过验证,下文列出的已记录损失除外。投影不丢弃任何三元组。每个被携带的项都保留约束所能判定的内容:

  • @id 是完整的 IRI,空节点则为 _:label。它从不是紧凑 IRI,因此词法约束直接判定 IRI 的 str()。键与 @type 值保持紧凑形式。
  • 良构的 RDF 列表是 JSON-LD 列表对象 {"@list": [...]},其单元不是 @graph 节点。rdf:nil 是 {"@list": []}。哪些列表会被转换,由 JSON-LD 1.1 Processing Algorithms and API 第 8.4.2 节的列表转换决定,并按严格解读,从而不丢弃任何三元组。分叉的、成环的、共享的、以 IRI 命名的或带注解的列表(含有类型为 rdf:List 的单元的列表也包括在内)保持节点图形式。作为另一个列表成员的列表,以其首单元的标签作为 @index。@index 不具有 RDF 意义,它使两个不同的成员列表保持区分。
  • 裸 JSON 标量只出现在它精确表示该字面量的位置(JSON-LD 1.1 第 8.6 节):64 位以内的规范 xsd:integer,以及 xsd:boolean 的 true 或 false。其他所有数值字面量都保留其 {"@value", "@type"} 对象。
  • rdf:dirLangString 字面量携带 @direction。

模式把 SHACL 1.2 列表组件投影到 @list 数组上。sh:minListLength 变为 minItems,sh:maxListLength 变为 maxItems,sh:uniqueMembers true 变为 uniqueItems,sh:memberShape 变为 items,并按单个值的模式编译。它们每一个都要求值是列表。在节点形状上,它们判定焦点节点的 rdf:first 与 rdf:rest。每个 sh:flags 字母(包括 i)都写入 ECMA-262 正则模式。i 遵循 XPath 的大小写变体,而不是简单大小写折叠。词法约束判定 IRI 的 @id。它们在编译时判定常量 rdf:nil、true 与 false,并把裸整数的数字串长度作为整数范围来判定。数值数据类型在其词法空间与值空间上加以区分。范围界对带类型的整数族或十进制字面量,按其词法形式上的次序正则模式进行比较,对 double 或 float 界则采用 SPARQL 的数值提升。xsd:dateTime、xsd:date 或 xsd:time 上的时间界,按词法形式上的次序正则模式,在 XSD 时间线上比较其数据类型的带类型字面量。时区使值成为一个时刻,24:00:00 是次日的午夜。带时区的值与不带时区的界(或反之)仅在相距超过 14 小时时才可比较。相距更近时它们不可比较,组件将此报告为违反。任何其他数据类型的值都违反该界。dateTime 或 time 界的正则模式逐一表述其 ±14:00 范围内的 1,681 个分钟,因此体积很大:约 100 KB,带小数秒的界则更大。节点形状的节点种类、sh:in、sh:hasValue 与词法约束判定焦点节点的 @id。

余下的情况都记录在正向台账上,每种情况附带其原因:

  • 以链接的 @graph 节点形式保留的列表不被判定。JSON Schema 关键字只判定它所作用的实例位置及其下方的位置,因此没有任何关键字能到达另一个节点的 rdf:first。
  • 节点形状的 sh:uniqueMembers 不会把焦点节点自身的成员与其尾部的成员相比较。它们是两个实例位置。
  • 裸整数数字串上的正则模式不被判定。数字串以 1 开头的整数,不是 minimum、maximum 与 multipleOf 所表述的区间与剩余类的任何有限并集。
  • xsd:double 或 xsd:float 词法形式上的范围界不被判定。位于某个界一侧的这些形式不构成正则语言。
  • 节点形状的类成员关系不被判定。它要经由其他节点上的 rdfs:subClassOf* 三元组。

Pydantic 包通过一个作用于原始 JSON 项的验证器,强制执行列表组件(包括唯一性)。TypeScript 在任意大小下都能精确表述长度界,并在成员是有限多个标量时表述元素互不相同。对于无约束成员的互不相同元素,或整数下界,它没有对应的类型。LinkML 在 @list 槽上表述全部四项。GraphQL 把列表值承载为一个由节点引用与列表对象组成的 @oneOf 输入对象,并把每个成员承载为一个由成员形状的备选项组成的 @oneOf 输入对象,因此成员类型会被检查。GraphQL 列表类型没有长度或唯一性约束,其数值类型也没有界,因此这些都在其所在位置被记录。每个输出器判定依据(oracle)都在真实数据的投影实例上运行这些组件。

依据形状判定其值的属性形状,会把每个形状投影为单个值的模式,其编译方式与 sh:memberShape 相同。sh:node 与 sh:and 变为 allOf,sh:or 变为 anyOf,sh:xone 变为 oneOf,sh:not 变为 not。sh:someValue 在由多个值组成的数组上变为 contains,在单个值上则变为值模式本身,此时它要求该值存在。形状的属性形状判定的是值自身的属性,这些属性位于值自身的 @graph 节点上,因此它们被记录。当某个成员模式接受的比其形状更多时,oneOf 与 not 会拒绝一个值。因此,只有当每个成员形状都是精确的——即由 sh:datatype、sh:nodeKind、sh:in、sh:hasValue 以及其他精确形状构建,且没有任何内容被记录——时,才会输出它们。否则,该 sh:xone 或 sh:not 会被记录并被省略。sh:class 不是精确的,因为值的类型位于它自己的节点上。当单个值不加包装地写出、与多个值组成的数组并存时,值模式也会拒绝数组,因此数组中的每个值都会被判定。

Pydantic 通过其作用于原始 JSON 输入的运行时检查,强制执行 allOf、oneOf、not 与 contains,因此在其中每一项上都与验证结果一致。LinkML 将它们表述为 all_of、any_of、exactly_one_of、none_of 与 has_member,其中 has_member 的表达式是完整的值模式(包括类的值域),因此没有任何内容需要记录。官方 LinkML 1.11.1 JSON Schema 生成器(LinkML 验证器也会运行它)只针对标量约束把 has_member 转换为 contains。因此,尽管 LinkML 模式声明了该约束,其输出仍会接受一个不含任何符合约束的值的数组。TypeScript 表述交集与并集,并记录它没有对应类型的 oneOf、not 与 contains。GraphQL 把组合委托给其自定义标量。每个输出器判定依据(oracle)都在真实数据的投影实例上运行这些约束。

每个输出器判定依据还在投影实例上运行时间界。在每个时间界上,Pydantic 与 LinkML 都与验证结果一致。TypeScript 与 GraphQL 在该界所在位置记录其否定。两者都没有补集类型。TypeScript 也无法正向写出被接纳的词法形式,因为仅四位数年份的日期就已超出其并集上限。

本体完整的开发者模式(schema)

公开的 compile_schema 边界接受一个 SchemaCompileRequest,它绑定解析后的形状、精确的本体数据集、调用方持有的 Namespaces,以及一个显式的 SchemaSurfaceMode。ShapedOnly 保留活动的 SHACL 目标类接口。OntologyComplete 额外加入调用方词汇中已有的类,以及可选的、由 OWL/RDFS 推导的属性。结果携带 JSON Schema draft 2020-12、OpenAPI 3.1、常规的正向损失台账、一份规范的属性覆盖报告,以及一个确定性的预编译缓存键。其 CompiledSchema 供 LinkML、TypeScript、GraphQL 与 Pydantic 输出器使用,无需第二遍模式发现。

有界理论只收录模式证据:直接 IRI 形式的 sh:path、RDF/OWL 属性声明、定义域/值域声明,以及子属性、等价属性与逆属性关系的两个端点。仅在实例上出现的谓词不会被提升。类的接纳同样是显式的,合成的定义仅限于调用方提供的命名空间;PurRDF 不会把内置的压缩前缀变成本体边界。

子类/等价类闭包决定定义域成员关系。多个定义域是合取关系;OWL 并集的成员是备选项,交集的成员是合取关系。子属性继承父属性的定义域、值域与正向函数性,等价属性双向传播,逆属性交换定义域与值域。强连通环以确定性方式收缩。多个值域在输出的 JSON Schema 中仍保持合取;并集与交集表达式分别映射为 anyOf 与 allOf。

匿名类表达式是该理论的一部分。命名类通过 rdfs:subClassOf 或 owl:equivalentClass 关联到的限制(owl:someValuesFrom、owl:allValuesFrom、owl:hasValue、owl:hasSelf,以及基于 owl:onClass 或 owl:onDataRange 的非限定与限定基数)或布尔形式(owl:unionOf、owl:intersectionOf、owl:complementOf、owl:oneOf)会连同其源公理一起被记录,并由每个子类继承;若断言于 owl:Thing,它对每个类都成立,而 owl:Thing 上的全称限制就是该属性的值域。它所命名的属性加入属性目录,并在承载该限制的类上输出,无论其声明的定义域为何;该公理作为溯源出现在其拥有者以及引用它的类的覆盖行上。若某属性的逆被命名,则针对该属性之逆的限制就是针对那个命名属性的限制。由 owl:onDatatype 与 owl:withRestrictions、owl:datatypeComplementOf 或字面量 owl:oneOf 构成的数据范围被读作填充项与值域;[ owl:inverseOf p ] 在任何可以出现属性表达式的位置都会被读取。OWL 2 数据类型映射以及 XSD 之外的 RDF 1.2 数据类型(owl:real、owl:rational、rdf:PlainLiteral、rdf:XMLLiteral、rdf:dirLangString、rdf:HTML、rdf:JSON)都是数据类型。IRI 与数据范围(或另一个数据类型)之间的等价定义了一个数据类型:此后,该数据类型的值要么满足其定义范围,要么以该数据类型的名称作为类型。类所等价的并集中的每个命名成员都成为它的子类,存在性限制则把类置于被限制属性的定义域之内,使该定义域的其他属性也能到达它。

开发者 schema 能够表述的部分会被投影到类定义上。owl:allValuesFrom 要求每个值都满足填充项;类填充项只作为节点引用来检查,这是一种近似,而 owl:Nothing 禁止该属性。存在性限制、owl:hasValue 或最小基数使该属性成为必需属性,并在填充项收窄取值时附加 contains 与 minContains。最大基数变为 maxItems;若限定项的值 schema 是精确的,则变为针对该限定项的 maxContains。数据类型限制的数值、时间、长度与正则(pattern)分面由 SHACL 值约束编译器编译,XSD 正则会被锚定;数据类型补集依据字面量的数据类型标签而非其值空间来判断,这是一种近似。由命名个体组成的 owl:oneOf 约束 @id,命名类的补集把该类排除在 @type 之外,限制的并集变为针对各成员属性约束的 anyOf。要求存在取值以及按不同的项计数,是以封闭世界的方式解读开放世界的公理,因此它们所涉及的覆盖行属于表示近似。

继承的限制以引用方式表示,而不是复制:拥有该限制的类把它放在 <Class>.Restriction.<property> 片段定义中,每个子类和每个下层片段都通过 allOf 引用最近拥有者的片段(析取同样通过 <Class>.Disjunctions 引用),因此 schema、溯源与清单随限制的数量增长,而不随层次结构的深度增长。IRI 上的类构造子,例如 ex:Day owl:oneOf ( … ) 或类型为 owl:Restriction 的 IRI,按 OWL 赋予它的等价关系来解读。

任何 schema 关键字都无法表述的部分会被报告,从不被丢弃:owl:hasSelf、针对未命名之逆或多个属性的限制、基于类限定项的最大基数、匿名表达式的补集、含有该类尚未蕴涵的命名成员的并集、owl:equivalentClass 的充分条件方向、子类为匿名表达式的一般类包含、出现在 owl:disjointWith、owl:AllDisjointClasses、owl:disjointUnionOf 与类断言中的匿名类,以及匿名类上的 owl:hasKey。SchemaCompileRequest::class_expression_report 与 compile_schema_with_class_expressions 返回类表达式清单:每条此类公理恰好出现一次,并在公理本身或拥有它的每个类上给出每个组成部分的结果(已投影、已近似、无法表示或被排除)及原因;拥有者之下的类只有在其结果不同时才有自己的行。类定义还会在 $comment 中列出其无法表示的组成部分。此前被接受的输入不会被拒绝:此前被跳过的公理中格式错误的匿名部分会被报告为无法表示。在一直会被读取的公理中,格式错误的表达式会被拒绝:没有属性的限制、不是非负整数的基数、限定项不匹配、取值不为 true 的 owl:hasSelf、没有构造的空节点或同时是类表达式与数据范围的空节点、格式错误的枚举、列表或数据类型限制、包含自身的表达式,或出现在需要类的位置上的数据范围。带有多种解读的空节点(多个分面、分面取值、属性、限定项或类构造)被读作这些解读的合取;定义数据类型上的分面约束的是其定义范围的值。空的、仅含一个成员的或含重复成员的并集与交集、空枚举、空分面列表以及超出 64 位的基数都按其含义读取,双关(punning)的属性则按基于 RDF 的语义读取。每个属性的值也都按这种方式读取,与其声明的种类无关:值可以是任意节点(IRI 可以指称一个数据值),也可以是任意类型正确的字面量(其词法形式属于其数据类型的词法空间;"abc"^^xsd:integer 不是)。没有值域约束的属性精确地表述这一点。数据范围按其值空间约束字面量,并不加判定地接受节点;类值域或类填充项不加判定地接受类型正确的字面量;二者都报告为近似,但与 rdfs:Literal 不相交的类精确地拒绝每个字面量。跨种类的 owl:hasValue 读作 ∃p.{v},数据类型属性上的 owl:hasSelf 读作自指限制;每个带有 owl:hasSelf 的单元格都报告为近似。owl:Nothing 以及按其形式为空的表达式(¬owl:Thing、空枚举)在任何值域或填充项位置都不接受任何值,并按精确报告:基于它的存在限制或限定最小基数使该类没有任何实例。一次请求最多展开 1,048,576 个表达式节点。

OWL 的值域、填充项、数据范围、枚举与 owl:hasValue 按字面量的值判断它,与 OWL 2 解读数据类型的方式一致,而 sh:datatype 判断的是其类型标签:xsd:decimal 接受整数 1,xsd:string 接受 xsd:token,1 的枚举匹配 "01"^^xsd:integer。十进制或整数值域对类型为 owl:rational 的字面量不加判断地接受,并报告为近似;长度或模式分面作用于每个字符串字面量的词法形式,即其值。在以井号结尾的命名空间下,schema 的 $id 去掉片段,这是 draft 2020-12 的要求。

直接的 SHACL 仍是权威。仅来自本体的字段是可选的,除非某个类限制要求它们;owl:FunctionalProperty 给出标量表示,并附带标明其为近似的溯源信息(approximation provenance),而逆函数性不会如此。封闭形状拒绝未被形状描述的字段,除非这些字段直接出现或被忽略。没有目标形状的类以开放载体的形式输出,绝不会被输出为捏造的封闭模型。这既不是 ABox 物化,也不是不受限的 OWL:属性链以及该片段之外的公理不会产生字段。

SchemaCoverageReport 对每个收录的属性恰好计入一次(包括被排除的属性),并附有排序后的逐类结果与源公理溯源。SchemaCompileRequest::coverage_report 可以在输出之前生成它。请求缓存键绑定形状图与本体图的 RDFC-1.0 标识、调用方命名空间、模式(mode)、值词汇标记、编译器/策略盐值,以及固定上限:65,536 个属性、65,536 个类、1,048,576 个关系或覆盖单元格、16,777,216 个类层次成员关系,以及表达式深度 64。格式错误的 OWL 列表、相互矛盾的属性种类/值域、键冲突以及上限耗尽,都是带类型的失败。

运行完整的双模式、四输出器示例:

cargo run -p purrdf-shapes --example ontology_schema_surface --locked

Schema → SHACL 导入

模式投影接口是双向的。SchemaImportConfig 要求调用方提供命名空间表,以及 JSON 标量到 RDF 数据类型的完整映射;不存在默认词汇表。五种生产用逆向转换是 JSON Schema draft 2020-12(import_json_schema)、原生 LinkML 1.11(import_linkml),以及经过校验的、由 PurRDF 输出的 Pydantic v2、TypeScript 7.0 与 GraphQL September 2025 包(import_*_package)。五者都经由同一个有序的 JSON-Schema 语义模型转换,返回带类型的形状,外加一份总会计算的、带位置的反向 LossLedger。

范围界读回时仍是其自身。次序正则模式表述的是位于某个界一侧的词法形式集合,而不是该界本身,而不同的界可以表述同一个集合:< 150 与 ≤ 149 接纳相同的整数。因此,编译器依据某个界写出的每条拒绝,都在其 $comment 中携带该界,形式为该分面(facet)的 SHACL 术语以及以 N-Triples 项表示的界,例如 sh:minInclusive "2020-01-01"^^<http://www.w3.org/2001/XMLSchema#date>。导入器只有在检查过注释之后才信任它。它重新生成所指名的界投影出的拒绝,并按值的 sh:datatype 收窄,这些拒绝必须恰好是携带该注释的那些。对于数值界,其旁边的 minimum 与 maximum 还必须正是该界所接纳的整数。任一项检查失败的注释都记录为 schema-applicator-dropped。因此,sh:maxExclusive 150、sh:minInclusive 1.5 以及 xsd:date、xsd:dateTime 或 xsd:time 上的界都能经受往返,重新输出的模式逐字节相同。

格式错误的值、开放或悬空的引用、标识冲突、生成的工件/映射漂移以及资源上限耗尽,都按失败即关闭(fail closed)处理。没有精确 SHACL 解释的有效源构造,会在其原生 JSON Pointer 处记入台账。任意的 Python、TypeScript 与 GraphQL SDL 被有意排除在逆向边界之外,因为它们都没有定义唯一的运行时 JSON 接受关系。LinkML 确实有原生读取器;因此,即使承载验证语义的 SHACL 逐字节重新编译一致,其模式标识与文档仍可能表现为损失。

可执行示例构造调用方持有的 example.org 配置,并覆盖全部五条路径:

cargo run -p purrdf-shapes --example schema_reverse --locked

Pydantic v2 投影

purrdf-shapes 可以完全在内存中把编译后的、由 SHACL 推导出的 JSON Schema 转写为一个确定性的、带类型的 Pydantic v2 包。公开函数 emit_pydantic 消费 CompiledSchema;PydanticConfig 要求调用方提供包名以及包/模块说明文字,因此库不会自行发明词汇表、命名空间或下游品牌。

每个 $defs 条目都获得一个稳定的导入路径,JSON 属性名通过 Pydantic 别名保持精确,生成的类通过 model_json_schema(by_alias=True) 公开其来源定义。Pydantic 运行时注解强制执行可表示的部分。没有精确运行时注解的 JSON Schema 断言在该模式接口上保持可见,并在总会计算的 json-schema → pydantic-v2 LossLedger 中产生一条带位置的条目;无损输入得到空台账。渲染器本身不依赖 Python,并保持 wasm-clean。一个仅用于开发的 Python 判定依据(oracle)执行生成的代码,并检查实际的反向/模式接口。import_pydantic_package 在导入 SHACL 之前,单独校验所保留的源模式、生成的文件、模型映射、方言以及正向台账。

JSON Schema 的 not 没有对应的注解。包改为在运行时检查它:一个 before 验证器在原始 JSON 输入上对被否定的模式求值。该检查遵循 JSON Schema 自身的语义。数字按精确值比较,字符串长度按码点计数,pattern 经由 Pydantic 自己的正则引擎运行。该检查覆盖一张封闭的关键字表,包括 $ref。使用任何其他关键字的被否定模式——例如 propertyNames、某种 format,或通用语法之外的 pattern——仍是一项已记录的损失。

可选的、由调用方持有的 PydanticPackageTopology 是把 $defs 条目划分到可移植的点分叶模块中的一个全划分。每条路由携带类的 docstring,以及一个排序后的、与词汇无关的 json_schema_extra 映射,适用于文档 URL、内容摘要以及其他由调用方定义的关联。可选的 PydanticVersionStamp 增加一个精确的 PEP 440 __version__ 导出。经路由的包共享模式/运行时支持模块,生成中间包的初始化文件,使用显式符号表完成一次根级重建,并通过可执行的运行时判定依据(oracle)以及严格模式的 mypy。精确的路由覆盖、可移植的路径/符号唯一性以及固定的输入/配置/输出上限,都按失败即关闭处理。拓扑与版本标记都省略时,原有的扁平包字节保持不变。扁平包的版本标记会增加 __about__.py 并更新 __init__.py 的导出。

LinkML 1.11 投影

同一个 CompiledSchema 载体可以用 emit_linkml 投影为规范的 LinkML 1.11。LinkmlConfig 要求调用方提供模式 IRI、名称、描述、默认前缀以及完整的前缀映射,因此 PurRDF 绝不会为使用方生成词汇表或标识。返回的 LinkmlPackage 包含带类型的文档、确定性的 YAML、一个可逆的 $defs 键映射,以及一份带位置的 json-schema → linkml-1.11 损失台账。它还携带有序的、经完整性检查的 slot 重命名报告与跳过诊断报告。

类以及精确的属性别名、类型、枚举、本地引用、内联对象、是否必需、同质数组、模式(pattern)、闭区间边界与 LinkML 布尔表达式都被直接表示。不安全的 LinkML slot 名默认使用 SanitizePolicy::Rename;Skip 只省略该 slot,并给出带位置的诊断/损失;Fail 返回带上下文的错误。Rename 在 slot_uri 中逐字节保留所声明的 CURIE 与绝对 IRI 标识;不安全的裸 token 或调用方精确指定的重新归属,会在调用方提供的默认前缀下获得一个被报告的标识。除非调用方标记了该确切 token,所有有效的 IRI scheme 都保持为绝对形式,因此不会根据拼写猜测自定义 scheme。安全名称优先保留,而基于哈希加序号的冲突分配是有界且确定性的。

每个不受支持的断言都由一张封闭的能力表分类;格式错误的输入、外部/动态/悬空引用、过时的重新归属提示、语义标识冲突以及超出固定上限,都按失败即关闭处理。parse_linkml 与 write_linkml 保留所有与 JSON 兼容的元模型字段,提供逐字节稳定的读/写往返(转换),同时拒绝仅限 YAML 的标签、重复键、非字符串键以及非有限数。

import_linkml 消费经过验证的原生文档;面向输出包的变体 import_linkml_package 会先校验规范 YAML 以及可逆的元素映射、slot 报告、策略损失、别名与输出的标识。两者使用同一个由调用方持有的 SHACL 导入配置。迁移适配器应原样传递 CompiledSchema,配置精确的重新归属,并消费 slot_renames;无需重写共享的属性键/必需键。

Rust 生产路径不依赖 LinkML 工具包。CI 仅把锁定版本的官方 LinkML 1.11.1 Python 包用作差分判定依据(oracle)。它通过 SchemaDefinition 与 SchemaView 加载安全、有损与重命名的测试夹具,重新生成 JSON Schema,并验证反向谓词:

make linkml-oracle

TypeScript 7.0 投影

emit_typescript 把同一个 CompiledSchema 投影为确定性的 TypeScript 7.0 声明。调用方通过 TypeScriptConfig 提供包名以及所有包/模块说明文字。返回的包包含一个 index.d.ts、一个从 $defs 键到导出类型的可逆映射,以及一份带位置的 json-schema → typescript-7.0 损失台账;PurRDF 不为使用方发明任何标识或词汇表。

固定的声明方言使用 strict 加 exactOptionalPropertyTypes。类型别名保留 JSON 原始类型与字面量、必需字段与可选字段之分、显式的 null、本地递归引用、并集、交集、数组以及元组。不存在运行时枚举、可合并的接口、带品牌的伪验证器或 any 逃生口。无效关键字、开放/悬空引用以及名称冲突都会在输出字节之前失败。

数组长度界在任意大小下都是精确的。元组元素表述每个特定位置的项。超出这些位置的长度以元素属性表述:上下文类型类似元组的数组字面量会被类型化为元组,而长度为 n 的元组恰好具有属性 "0" 到 "n-1"。因此,minItems: m 是一个必需属性 "m-1",maxItems: n 是一个可选的 never 属性 "n"。当各项只接纳有限多个 JSON 标量时,uniqueItems 同样是精确的。生成的 JsonDistinct 辅助类型把互不相同的序列枚举为元组类型的并集。枚举止于编译器的上限:实例化深度 100(TS2589),以及编译器将其展开为元组、或将交集分配于其上的并集中的 100,000 个成员(TS2590)。作用于有限 const 或 enum 的数值关键字,会排除不满足它们的数字。

TypeScript 结构可赋值性之外的运行时断言绝不会被静默抹去:整数、数值/字符串谓词、封闭性、模式属性、依赖、条件、否定、contains、唯一性以及求值状态等缺口,都会获得稳定的代码与 JSON Pointer 位置。作用于无限多个数的数值界,以及无限项类型的互不相同元素,都没有对应的类型。number 仅有的真子类型是字面量的并集,而元组类型对每个位置分别独立约束。判定依据(oracle)编译这些损失所依赖的事实:Exclude<number, -1> 仍接纳 -1,且每个枚举上限在其边缘处被接受、越过一步即被拒绝。CI 用一个 draft 2020-12 验证器独立地对实例分类,并用锁定版本的 TypeScript 7.0.2 编译器编译生成的声明,包括新鲜字面量与经由变量的探针:

make typescript-oracle

该投影有意不提供任意 TypeScript 的读取器。TypeScript 声明并未定义唯一的运行时 JSON 接受关系,而且该投影是多对一的。import_typescript_package 是权威的反向接口:它以确定性方式校验所保留的源模式、声明、可逆名称映射、方言以及正向台账。TypeScript 只是开发期的判定依据(oracle)依赖;Rust 输出器/导入器不访问文件系统,并保持 wasm-clean。

GraphQL September 2025 投影

emit_graphql 把 CompiledSchema 投影为确定性的 GraphQL September 2025 SDL。GraphqlConfig 没有默认值:调用方提供模式名称、包与模块说明文字,以及一个非内置的后备标量名称。返回的 GraphqlPackage 包含 schema.graphql、规范的 name-map.json、以带类型 Rust 数据表示的同一名称映射、一份带位置的 json-schema → graphql-september-2025 损失台账,以及生产用的值编解码器。

该 SDL 被有意设计为类型系统片段。PurRDF 输出成对的输出 type 对象与输入 input 对象,但不输出查询、变更或订阅根类型、解析器、分页规则、授权策略、联邦指令或其他应用行为。调用方把该片段与其自己的可执行模式组合起来。

精确语法包括 GraphQL 布尔值、字符串、数字、有符号 32 位 Int 值域、显式可空性、有限的 JSON const/enum 集合、封闭的对象字段、是否必需、同质列表、直接的本地 $defs 引用与别名、描述以及内联对象辅助类型。当每个值都只选中一个备选项时,anyOf 是精确的:依据其 JSON 种类,或者在对象备选项之间,依据一个其他备选项都未声明的必需键。它变为每个备选项各占一个字段的 @oneOf 输入对象,以及一个输出联合类型。不是对象类型的备选项,经由一个以其 value 字段承载它的包装类型成为联合成员。一个全局的、经冲突检查的命名空间覆盖类型、辅助类型与后备标量;字段与枚举符号在其 GraphQL 局部命名空间中检查。带类型/规范的名称映射保留源定义键、属性键以及有限的 JSON 值。

GraphqlPackage::encode_input 把源 JSON 键与有限值映射为输入字段名与枚举符号,并把联合值包装在其备选项的 @oneOf 字段中。若某个非对象值的种类不被任何备选项接纳,该值会原样通过,并由 GraphQL 强制转换拒绝。decode_input 是针对已强制转换参数的逆映射。encode_output 写出解析器(resolver)返回的值,其中联合值在 __typename 中指名其成员。decode_output 对 GraphQL 响应中出现的字段执行逆映射,不会凭空补出被省略的选择。未知或不兼容的值会失败。这个包编解码器是精确的值边界;import_graphql_package 是模式的反向边界,它校验 SDL、带类型/规范的映射、标识、所保留的源模式以及正向台账。任意的 GraphQL SDL 没有唯一的 JSON Schema 接受关系,不被接受为逆向格式。

GraphQL 变量强制转换与 JSON Schema 验证在以下封闭边界上存在差异:

边界带位置的损失类别
对象字段与名称附加属性、模式属性、属性名/属性数量
是否必需与递归可空存在性放宽,一处确定性的递归输入可空性放宽
列表单例强制转换、基数、contains、唯一性、元组、未求值项
标量断言整数值域委托、数值谓词、字符串谓词
应用器(applicator)条件、依赖、交集、重叠的并集与类型数组并集、oneOf、否定
运行时边界自定义标量与未知关键字的验证委托

由调用方命名的后备标量会被声明,但 PurRDF 不会自行发明其 parseValue、parseLiteral 或序列化语义。因此,每一处委托使用都会记入台账。损失条目携带稳定的代码与源 JSON Pointer 位置;精确的包台账为空。

对于无效的调用方配置、格式错误的模式关键字、$id 重定基准、外部/间接/悬空的 $ref、$dynamicRef/$recursiveRef、别名环、无法满足的封闭必需字段以及生成名称冲突,输出会在返回字节之前失败。输入模式没有固定的大小上限;每个工件的上限为每个输入字节四个字节,且至少 16 MiB。固定上限为:单个编解码值 16 MiB;定义数、每个对象的字段数或有限值数各 65,536;深度 128;每个 GraphQL 名称 255 字节。

独立的开发期判定依据(oracle)用 purrdf-jsonschema 对源值分类,用锁定版本的官方 GraphQL.js 16.14.0 构建 SDL,并执行真实的变量强制转换。它验证结果完全一致,验证每个封闭的损失类别及其位置、名称映射与生产用编解码器,以及刻意损坏时的失败。每个有效值还会经由其输出类型返回,且 GraphQL.js 必须原样序列化它:

make graphql-oracle

GraphQL.js 仅用于开发。输出与值转换仍是不访问文件系统、wasm-clean 的 Rust。

规则、节点表达式与形状图认证

验证之外还有四个工具。每个宿主都到达同一个库入口点,因此命令行、Python、WebAssembly 与 C 在答案上不可能出现分歧。

工具RustCLIPython(purrdf.shapes)WebAssemblyC ABI
运行规则,写出推理图purrdf_shapes::infer, srl::inferpurrdf rulesapply_rulesshaclApplyRulespurrdf_shacl_apply_rules
检查 SPARQL 1.2 RL 规则集,不运行任何规则srl::checkpurrdf rules --srl FILE --checkcheck_rulesshaclCheckRulespurrdf_shacl_check_rules
对单个节点表达式求值free_expression::evaluatepurrdf node-expreval_node_exprshaclEvalNodeExprpurrdf_shacl_eval_node_expr
认证形状图lint::lintpurrdf shapes lintlint_shapesshaclLintShapespurrdf_shacl_lint_shapes

规则。 规则来源要么是形状图(运行其 SHACL 1.2 规则,即其默认规则集),要么是 SPARQL 1.2 RL 规则集,但不能两者兼有。结果是推理图:只包含推出的三元组,绝不包含数据图,以 N-Triples 1.2 按唯一的规范顺序给出。应请求,每个宿主还会返回每个推出三元组的证明。证明对每个三元组给出一个块:derived S P O .,随后是 rule R,以及规则体所匹配的每个事实各一行 premise S P O .。SPARQL 1.2 RL 数据块中的三元组则改为 data-block。

每个宿主在其两个 SHACL 规则入口点上都接受同样的四个规则求值上限:上文的规则运行,以及 SHACL-AF 蕴涵(Python shapes.entail、WebAssembly shaclEntail、C purrdf_shacl_entail_to_ntriples),后者对自身运行施加的界限与规则运行完全相同。在 WebAssembly 中,两个入口点都在导入表与 shapesGraph 之后按以下顺序连续接受这些上限:shaclApplyRules(dataNt, shapesTtl?, srl?, shapesBase?, srlBase?, explain?, importIris?, importDocuments?, shapesGraph?, maxTermGeneratingRounds?, maxGeneratedTerms?, maxStoredFacts?, maxJoinSteps?) 与 shaclEntail(shapesTtl, dataNt, shapesBase?, importIris?, importDocuments?, shapesGraph?, maxTermGeneratingRounds?, maxGeneratedTerms?, maxStoredFacts?, maxJoinSteps?)。

  • 生成新项的轮数上限限制那些推出图中原本没有的项的求值轮数。默认值为 16384 轮。一个步进到 10,000 的计数器会完成。一个没有界的计数器每轮推出一个新项,同样受这一上限约束。
  • 生成项预算限制在输入所含的项之外推出的项。默认值为 max(65536, 4 × N),其中 N 是数据图(以及 SPARQL 1.2 RL 规则集的数据块)中不同项的数量。新项每次迭代都翻倍的规则集会在几次迭代之内达到该预算,远早于达到轮数上限。
  • 存储事实上限限制求值存储所持有的事实:数据图、规则集的数据以及每一条推出的三元组。默认值在原生环境下为 4,194,304 条事实,在 WebAssembly 中为 131,072 条,因为那里的存储位于同一块线性内存中。一条把 70,000 条 ex:p 三元组逐条复制到 ex:q 的规则,或一条千节点链的传递闭包(500,500 条三元组),都能在原生环境下完成。
  • 连接步数上限限制规则体所枚举的候选解。默认值在每个宿主上都是 1,048,576。它约束那些枚举的候选远多于其推出三元组的规则体:一个规则体在某一轮的候选会在检查上限之前就被全部持有,因此大得多的默认值会让笛卡尔积式的规则体先耗尽内存。需要更多工作量的规则集——千节点链的非线性传递闭包要枚举超过 6700 万个候选——需声明更大的上限。

越过任一上限的运行都会被拒绝。错误信息会点名该上限、相关数值、用于提高该上限的参数;对于两个与项相关的上限,还会点名在最后一次迭代中推出了新项的规则。参数采用调用方宿主的写法:命令行上为 --max-term-generating-rounds、--max-generated-terms、--max-stored-facts 与 --max-join-steps;Python 中为关键字参数 max_term_generating_rounds、max_generated_terms、max_stored_facts 与 max_join_steps;WebAssembly 中为 maxTermGeneratingRounds、maxGeneratedTerms、maxStoredFacts 与 maxJoinSteps;C 中为参数 max_term_generating_rounds、max_generated_terms、max_stored_facts 与 max_join_steps。蕴涵入口点点名的是它自己的参数:Python 中为 entail(max_stored_facts=...),WebAssembly 中为 shaclEntail's maxStoredFacts,C 中为 purrdf_shacl_entail_to_ntriples's max_stored_facts。一个不断推出新项的规则集最终是否会停止是不可判定的,因此错误信息从不称其为发散。声明的上限是精确的,而且上限只能拒绝:它所允许的运行,推出的内容与在任何更大上限下完全相同。

形状规则(即关联到某个形状、对每个焦点节点各运行一次的规则)在每次迭代中只会针对答案可能已改变的焦点节点重新运行:新出现的焦点节点,以及——按规则的三元组模式与路径的读取方式——上一次迭代新增的某条三元组所涉及的焦点节点。若规则读取的某条三元组与 $this 相隔一个连接步,则会针对每个焦点节点重新运行该规则;而答案不可重复的规则——即生成空节点、调用 RAND()、NOW()、UUID()、STRUUID() 或 PurRDF 无法解读的函数,或以 LIMIT、OFFSET、SAMPLE、GROUP_CONCAT 或 FOLD 挑选解的规则——会按规范的规定,在每次迭代中针对每个焦点节点运行。规则所读取的图会以每次迭代新增的内容进行扩展,而不是重新构建。推出的三元组、它们的空节点标签及其证明,都与每次迭代运行全部焦点节点时完全相同。改变的是代价:一条每次迭代生成一个新焦点节点的形状规则,每次迭代只需付出一次运行的代价,而不是按迄今为止的焦点节点数各运行一次,因此一条永不停止的规则会被某个项上限拒绝——新项保持较短时为轮数上限,每个新项都比上一个更长时为固定的 16 MiB 项存储区上限——而绝不会被连接步数上限拒绝。

查询为合取模式(三元组模式、FILTER 与 BIND,不含 OPTIONAL、UNION、MINUS、子查询或图访问)的全局 SPARQL 规则,会以规则元素的方式运行。于是每次迭代只需为新增的内容付出代价,而不必遍历整个求值图,且推出的三元组相同。BIND 变量必须出现在每个模板三元组中,且模板不得含有空节点。否则,该规则仍像以前一样,作为作用于整个图的生产者运行。

import purrdf

out = purrdf.shapes.apply_rules(data_nt, shapes_ttl, explain=True,
                                max_term_generating_rounds=1024)
print(out["inferred"])   # 推理图,N-Triples
print(out["proof"])      # 证明文本
out = purrdf.shapes.apply_rules(data_nt, srl=srl_text)  # SPARQL 1.2 RL

检查 SPARQL 1.2 RL 规则集。 规则集可以在不运行的情况下接受检查。检查会施加截至某一级别的静态检查,每一级都包含之前的各级。syntax 检查规则集及其 IMPORTS 闭包所读取的每份文档的 SPARQL 1.2 RL 语法。well-formed 还会检查每条规则(包括导入的规则)是否良构。stratified 还会检查合并后的规则集能否分层。它是默认级别,并且恰好就是规则运行在求值之前施加的那组检查:每个宿主的规则运行都会先经过同一检查。导入从运行所接受的同一导入表解析。不读取任何数据图,也不运行任何规则。通过检查的规则集会返回其规则、数据块三元组数量、导入的规则集、VERSION 标签和分层,以及一行每个宿主都以相同方式打印的摘要。被拒绝的规则集以拒绝它的那一阶段的错误失败。W3C SPARQL 1.2 RL 套件的语法、良构性与分层测试都通过这一检查评分。正向语法测试「regardless of well-formedness and stratification」都是有效的,因此以 syntax 级别评分。其中十五个在完整检查中失败,由后面的阶段拒绝。

syntax 阶段还会约束规则集宣告其版本的位置。SPARQL 1.2 RL §7.1 写道「The version announcement SHOULD be made early in the document」,PurRDF 将其读作必须。若规则集的第一条 VERSION 指令位于某个 RULE 或 DATA 块之后,则在 syntax 阶段被拒绝,并点名该句。规则集可以不宣告任何版本;在较早的 VERSION 之后出现的后续 VERSION,对文档中其后的部分而言仍是一条指令。

purrdf rules --srl closure.srl --check              # 全部静态检查
purrdf rules --srl closure.srl --check=syntax       # 仅检查语法

节点表达式。 形状图中的单个节点表达式针对数据图的某个焦点节点求值,即 SHACL 1.2 节点表达式中的 evalExpr(expr, focusGraph, focusNode, scope)。表达式由形状解析器本身解析,因此自定义函数、形状引用与 sh:prefixes 的绑定方式与在形状内部相同。作用域变量按名称绑定,并由 shnex:var 读取。输出节点以 N-Triples 项的形式返回,顺序由表达式的序列语义决定,与之并列返回的是形状图的强制诊断——求值会像其他每一次运行一样报告它们(参见报告是一个数据集):purrdf node-expr 在其 node-expr outputs N 行之后向 stderr 写出 shacl diagnostic RULE SHAPE 行,Python 的 eval_node_expr 返回 {"outputs", "diagnostics"},WebAssembly 的 shaclEvalNodeExpr 返回一个带有 outputs 与 diagnostics 的 ShaclNodeExprOutcome,C 的 purrdf_shacl_eval_node_expr 则把 diagnostic RULE SHAPE 行写入非 NULL 的 out_diagnostics。

每个宿主都以下列三种方式之一指名表达式:

  • 节点本身:一个 IRI,或以 _:label 表示形状文档所标注的空节点。不作为任何三元组主语的 IRI 是常量表达式,求值结果为其自身。
  • 从一个具名节点出发的游走:一个起始节点以及一个或多个谓词,按顺序沿其前进。每一步都必须恰好到达一个值;到达零个或多于一个值的步骤会被拒绝,错误信息会点名该步骤与计数。匿名的 [ … ] 表达式就是这样被指名的,而大多数节点表达式正是以匿名形式书写的。ex:Label 再接 sh:values,就是该属性形状的计算值表达式。W3C sht:EvalNodeExpr 测试条目的表达式是:该条目,然后 mf:action,然后 sht:nodeExpr。该套件全部 143 个节点表达式测试都以这种方式经由 purrdf node-expr 运行。
  • 内联 Turtle:一份在形状文档的前缀与基准下读取的 Turtle 文档,其自身的指令优先。它被合并进形状图,因此其中的形状引用与函数调用在那里绑定,而它的空节点与形状文档的空节点保持分离。表达式是该文档唯一的根:作为某个三元组的主语、且不作为任何三元组宾语的那个空节点。没有根或有多个根的文档会被拒绝。
宿主节点游走内联 Turtle
CLI--expr--expr-at NODE --expr-via PREDICATE…--expr-turtle, --expr-turtle-file
Pythonexprexpr_at=, expr_via=[…]expr_turtle=
WebAssemblyexprexprAt, exprViaexprTurtle
C ABIexprexpr_at, expr_via, expr_via_countexpr_turtle
Rust(purrdf_validate)ExprSelector::NodeExprSelector::AtExprSelector::Turtle

三者一个都不指名,或指名多于一个,都会被拒绝。

purrdf.shapes.eval_node_expr(shapes_ttl, data_nt, "_:suffix",
                             "http://example.org/a", scope={"suffix": '"!"'})
purrdf.shapes.eval_node_expr(shapes_ttl, data_nt, None, "http://example.org/a",
                             expr_at="http://example.org/Label",
                             expr_via=["http://www.w3.org/ns/shacl#values"])
purrdf.shapes.eval_node_expr(shapes_ttl, data_nt, None, "http://example.org/a",
                             expr_turtle="[ sh:path ex:name ] .")
purrdf node-expr --shapes shapes.ttl --expr-turtle '[ sh:path ex:name ] .' \
  --focus http://example.org/a data.ttl

形状图认证。 加载形状图是热路径:它拒绝遇到的第一个无法如实求值的构造,且不为依据 W3C shacl-shacl.ttl 验证该图付出代价。lint 按需付出这一代价一次,并报告七个部分:

  1. load:加载器的裁决,即接受,或它抛出的拒绝。
  2. shacl-shacl:依据随库固化的 shacl-shacl.ttl 验证形状图所得的每条结果。该文件早于 SHACL 1.2 Core 的部分放宽(取列表值的 sh:nodeKind、以路径为值的 sh:equals、以节点表达式给出的 sh:targetNode)。若某条结果所指出的构造在 SHACL 1.2 Core 中是良构的,该结果会被标记为 superseded,不算作问题,但仅在加载器接受了该图时如此。
  3. functions:每个节点表达式函数调用绑定到哪个实现——native、custom、sparql-registered 或 host-extension。对于合并了 W3C SHACL 1.2 词汇表的形状图,即使图中声明了 sh:SPARQLExprExpression,sh:sparqlExpr 调用仍绑定为 native。
  4. validators:形状图为内置约束组件声明的每个验证器,这些验证器被原生实现取代,从不运行。这些行从不算作问题。
  5. unexecuted:每个违反预绑定限制、且从不被运行的查询,即内置组件的验证器、其组件的任何使用都未选中的验证器、没有任何调用的 sh:SPARQLFunction,或没有任何形状实例化的 sh:SPARQLTargetType。加载会接受它们;它们在这里各计为一个问题。违反语法规则的声明从不列在这里,因为它会使加载失败。
  6. diagnostics:每一条强制诊断,每个 sh:in 或 sh:xone 列表为空的形状各占一行 diagnostic RULE SHAPE。RULE 是语法规则 id,即 in-minListLength 或 xone-minListLength(「Each such list SHOULD have at least one member」)。这样的形状图是良构的,也可以正常验证(见下文),但该规则向作者传达了需要知道的信息,因此无论加载是否成功,lint 都会报告它,且每一行都算作一个问题。shacl-shacl.ttl 会就同一个空列表发出警告;该结果列出时标记为 diagnosed RULE,不会被重复计数。
  7. unanchored-imports:导入闭包中每条因其主语不是所在文档的锚点而不属于导入的 owl:imports 三元组(参见形状图中的 owl:imports)。每一条都是数据,因此不会去查找任何文档。这些行从不算作问题。W3C 标记的情形改由 shacl-shacl 报告:其 shsh:DataGraphImportsShape 会以 sh:Info 严重级别,报告使用了 owl:imports 却不带 owl:Ontology 类型的 sh:DataGraph。

当加载器接受了该图、每条 shacl-shacl 结果都已被取代(sh:Info 结果与其他结果一样计入)、没有任何未执行的查询违反预绑定限制,且没有任何强制诊断适用时,报告即为干净。unanchored-imports 从不影响这一点。每个宿主渲染同一份确定性的文本;CLI 参考给出了示例。

从 Python 调用

from purrdf import shacl

report = shacl.validate(shapes_ttl="...", data_nt="...")
print(report["conforms"])  # True / False
print(report["results"])   # 违规字典列表

每个结果字典保留稳定的键 focus、path、value、severity、component、source_shape 与 messages(每条 sh:resultMessage,各为一个字典,含 text,存在时还含其 language / direction / datatype)。携带 SHACL-SPARQL 结果注解的结果还带有 annotations:一个元组列表,每个元组由一个属性 IRI 与一个采用 N-Triples 语法的值组成。

带日期的 SHACL 法则

未选择时,SHACL 验证按兼容法则运行。调用方按稳定标识符选择一种精确的、带日期的 SHACL 法则:shacl-rec-2017-07-20(2017 年推荐标准)、shacl-wd-2026-09-18(SHACL 1.2 工作草案)或 shacl-ed-2026-10-16(W3C SHACL 1.2 测试套件所取自的 data-shapes 编辑草案,也是 PurRDF 的 EARL 报告据以评分的法则)。每个宿主都以同样的方式选择同一种法则,并且每个宿主都通过 purrdf_validate::complete_profile 把该标识符精确地解码一次。

宿主写法
CLIvalidate 上的 --shacl-profile <ID>,可与 --shapes、--shapes-product(预备产物不存储 SHACL 法则,因此恢复时从请求中取得法则)或 --changes 搭配使用
Pythonshapes.validate 与 Shapes 构造函数上的 shacl_profile=
WebAssemblyshaclValidateToSarif 末尾的 shaclProfile 参数;其异步孪生入口则在选项对象中接受它
Cpurrdf_shacl_validate_to_sarif_law 上的 shacl_profile,它在 purrdf_shacl_validate_to_sarif_xpath_regex 的基础上增加该参数,且不改变任何现有签名
purrdf validate --shapes shapes.ttl --shacl-profile shacl-ed-2026-10-16 data.ttl

不对应任何法则的标识符(包括 shacl-ed-2026-10-17)会被拒绝,状态为 unsupported-profile,也不会替换为相近的版本。所选法则的文本所拒绝的形状图,会在加载时被拒绝,绝不会改用另一种法则验证。每个宿主上同一对用例都表明了这一点。兼容法则会在加载时拒绝 sparql/pre-binding/pre-binding-009,因为它在一个未预绑定的变量上使用了 VALUES,而编辑草案会验证它。工作草案会拒绝 sparql/node/prefixes-003(它通过版本 IRI 到达前缀声明),而编辑草案会验证它。

带日期的法则自带其 XPath 法则组合(见带日期的 XPath 正则法则)。在它旁边指名另一种 XPath 法则,例如 --shacl-profile shacl-ed-2026-10-16 --xpath-regex xpath-2.0-2010-12-14,会作为用法错误被拒绝;指名同一种法则则被接受。节点表达式与 SPARQL 1.2 RL 记录与法则无关,不接受 SHACL 法则。

带日期的 XPath 正则法则

sh:pattern 及其 sh:flags,以及 SHACL-SPARQL 目标、约束与 SHACL-AF 函数中的 REGEX 与 REPLACE,都运行在兼容正则表达式之上,除非调用方按名称选择一种带日期的原生 XPath 法则:xpath-2.0-2010-12-14(XPath and XQuery Functions and Operators 2.0,第二版)或 xpath-3.1-2017-03-21(Functions and Operators 3.1)。两者都接受反向引用,因此 sh:pattern "^(a)\\1$" 接受 "aa"。只有 3.1 接受非捕获组和 q 标志。求值模式的形状图工具同样接受该法则。对规则而言,它覆盖 SHACL 规则、SHACL-AF 函数、节点表达式以及 SPARQL 1.2 RL 过滤器与赋值中的 REGEX/REPLACE,以及规则条件中的 sh:pattern。对节点表达式而言,它覆盖过滤形状的 sh:pattern,以及函数调用与基于 SPARQL 的表达式中的 REGEX/REPLACE。lint 不编译也不匹配任何模式,因此不接受法则。

宿主写法
RustPreparedShapes::with_xpath_regex(profile, limits)(返回一个 XPathPreparedShapes)以及自由函数 purrdf_shapes::xpath::validate_dataset;purrdf-validate 的 *_with_xpath_regex 孪生函数(validate_to_sarif_string、validate_changes_to_sarif_string、validate_with_shapes_product、validate_with_rebuilt_shapes_product、entail_to_ntriples、apply_rules_to_ntriples 与 eval_node_expr)接受 xpath_regex::parse_profile 返回的 profile
CLIvalidate(配合 --shapes、--shapes-product 或 --changes)、rules 与 node-expr 上的 --xpath-regex <NAME>;rules --check 拒绝该标志,shapes lint 不提供该标志
Pythonshapes.validate、Shapes.validate_nt、Shapes.validate_store、PreparedShapes.validate_nt、PreparedShapes.validate_store_changes、shapes.entail、shapes.apply_rules 与 shapes.eval_node_expr 上的 xpath_regex=
WebAssemblyshaclValidateToSarif、shaclValidateChangesToSarif、四个 shaclProductValidateToSarif… 入口、shaclEntail、shaclApplyRules 与 shaclEvalNodeExpr 的尾随参数 xpathRegex;它们的异步孪生方法在选项对象中接受它
Cpurrdf_shacl_validate_to_sarif_xpath_regex、purrdf_shacl_validate_changes_to_sarif_xpath_regex、purrdf_shapes_product_admit_xpath_regex、purrdf_shapes_product_admit_expecting_xpath_regex、purrdf_shapes_product_rebuild_xpath_regex、purrdf_shapes_product_rebuild_expecting_xpath_regex、purrdf_shacl_entail_to_ntriples_xpath_regex、purrdf_shacl_apply_rules_xpath_regex 与 purrdf_shacl_eval_node_expr_xpath_regex 上的 regex_profile
import purrdf

report = purrdf.shapes.validate(
    shapes_ttl, data_nt, xpath_regex="xpath-3.1-2017-03-21"
)

名称须精确匹配,因此 xpath-3.1 会被拒绝,并附上可接受名称的列表。未作选择时(没有关键字参数、选项或标志,或 regex_profile 为 NULL),兼容正则表达式的行为与以往完全一样。预备形状产物无需重新打包,因为法则属于求值,而不属于预备好的形状。

所选法则未定义的模式或标志,按无效模式一贯的方式报告:作为声明它的形状的一条结果。所选法则还在有限的资源上限下运行,其中包括 64 KiB 的模式源文本。超出任一上限会使验证以该资源的 xpath-* 代码失败,并且不写出任何报告、数据集或输出。资源拒绝绝不会变成判定为符合约束的图。

报告是一个数据集

ValidationReport::to_dataset() 把 W3C 验证报告——sh:ValidationReport 节点及其各个 sh:ValidationResult——物化为一个冻结的 RdfDataset,直接由报告自身的项构建,而不是经过 to_ntriples() → parse_dataset() 的往返(转换)。这条直接路径携带报告所含的每一个 RDF 1.2 项(包括作为焦点节点的三元组项)。以任意语法渲染报告时,需要先处理 to_dataset() 的结果,再把返回的数据集交给 serialize_dataset。这正是 purrdf validate --format 所做的。

报告携带来自两个图的空节点:焦点节点、值与注解值来自数据图,源形状来自形状图。空节点标签局限于使用它们的那个图,因此两份文档常常用同一个标签表示不同的节点。于是,报告把每个图的空节点写在该图自己的标签空间中:数据图为 _:dg0、_:dg1、…,形状图为 _:sg0、_:sg1、…,按首次出现的顺序编号。两个不同的节点绝不共享同一标签,同一个节点在其出现的每一处都保持同一标签,SARIF 投影中也是如此。这些标签不取决于由哪个解析器读取文档,因此以 Turtle、TriG 或 RDF/XML 写出的同一个形状图会得到相同的报告字节。ValidationReport::with_report_blank_labels() 返回重新标记后的报告,以及一个 ReportBlankLabels 映射,把每个报告标签映射回其所属的图与原始标签。内存中的 results 保留原始标签。报告所_生成_的空节点(报告节点、每条结果各一个、复杂 sh:path 的内部节点)与这两个标签空间都不相同。

验证产生的每份报告还会给出 sh:shapesGraphWellFormed。SHACL 1.2 Core §6.7.1.4 写道,检查形状图的处理器「SHOULD use the property sh:shapesGraphWellFormed to inform the consumer of the validation report about this fact」,而 true 表示「the processor was certain that the shapes graph that was used for the validation process is well-formed」。PurRDF 在验证之前就会拒绝格式错误的形状图,因此该值始终为 true。空的 sh:in 或 sh:xone 列表并不改变这一点。附录 A 的 in-minListLength 与 xone-minListLength 写道「Each such list SHOULD have at least one member」,PurRDF 把它们作为强制诊断来执行:形状图是良构的,验证按经批准的 W3C 测试 core/node/in-002、in-003、xone-002 与 xone-003 的要求照常进行,而 purrdf shapes lint(以及每个宿主的 lint 入口点)总会把每个空列表报告为一个点名该规则 id 的问题。ValidationReport::shapes_graph_well_formed 携带该值,SARIF 运行则以 properties.shaclShapesGraphWellFormed 携带它。

报告这些诊断的不只是 lint:每一次运行都会报告同样的诊断。验证报告把它们放在 ValidationReport::diagnostics 中——每个空列表对应一条 MandatoryDiagnostic,含规则 id 与形状——与结果并列,却绝不混入结果之中:诊断不是 sh:ValidationResult,它既不改变 sh:conforms,也不改变任何结果,W3C 评分也不受影响。它也不会被写入报告图。SHACL 没有定义任何可用于陈述形状图的报告项,而 PurRDF 不生成任何词汇,因此 RDF 报告与没有该诊断时完全相同。各个宿主转而在各自的专属位置中携带它。SARIF 日志把每条诊断放在 invocations[0].toolExecutionNotifications 中,作为级别为 note 的通知(「The notification is purely informational. There is no required action」,SARIF 2.1.0 §3.58.6),其 descriptor 在 tool.driver.notifications 中点名该规则。命令行在判定行之后向 stderr 写出 shacl diagnostic RULE SHAPE。Python 的 validate 字典与 ValidationReport 对象都有一个由 {"rule", "shape"} 字典组成的 diagnostics 列表。规则运行、蕴涵运行或节点表达式运行也会报告同样的诊断:purrdf rules 与 purrdf node-expr 写出同样的 stderr 行,Python 的 apply_rules、entail 与 eval_node_expr 字典携带 "diagnostics",WebAssembly 的 ShaclRulesInference、ShaclEntailment 与 ShaclNodeExprOutcome 有一个由 ShaclDiagnostic 值(rule、shape)组成的 diagnostics 数组,C 的 purrdf_shacl_apply_rules、purrdf_shacl_entail_to_ntriples 与 purrdf_shacl_eval_node_expr 则把 diagnostic RULE SHAPE 行写入非 NULL 的 out_diagnostics。这种编码在各宿主间是统一的:规则与形状是两个独立字段——在 Python 中是字典键,在 WebAssembly 中是 getter——C 的输出行以相同顺序携带同样的两个字段,规则在前。

SARIF 输出

验证报告在引擎中保持结构化;SARIF 2.1.0 边界是独立的 purrdf-validate crate(purrdf::validate),它把报告——或解析器诊断——渲染为可追溯到源位置、逐字节确定性的 SARIF 日志,供编辑器、CI 与代码扫描仪表板使用:

use purrdf::validate::{validate_to_sarif_string, SarifOptions};

let shapes = r#"
    @prefix sh:  <http://www.w3.org/ns/shacl#> .
    @prefix ex:  <http://example.org/> .
    @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
    ex:PersonShape a sh:NodeShape ;
      sh:targetClass ex:Person ;
      sh:property [ sh:path ex:age ; sh:datatype xsd:integer ] .
"#;
let data = r#"<http://example.org/alice> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/Person> .
<http://example.org/alice> <http://example.org/age> "nope" .
"#;

let sarif = validate_to_sarif_string(shapes, None, data, &SarifOptions::default())
    .expect("sarif produced");
assert!(sarif.contains("\"version\": \"2.1.0\""));

更底层的入口点(build_report_sarif、build_diagnostics_sarif)构建的是 SarifLog 值而非字符串,因此宿主可以在序列化之前合并多个 run。

一致性

该验证器由随库固化的 W3C SHACL 1.0 data-shapes 套件、随库固化的 W3C SHACL 1.2 套件、随库固化的 DASH SHACL-AF/规则语料,以及一个含 73 个用例、预期报告逐字节冻结的第一方冻结语料把关;SHACL Rules 的输出按 RDFC-1.0 同构与预期推理图比较。解析器还受 SHACL 1.2 规范自带的 SHACL-for-SHACL 形状图约束:在每个第一方与 W3C 形状图以及一组生成的变异体上,除已点名的用例外,它恰好在 shacl-shacl.ttl 报告违规时拒绝该图。参见一致性与测试。