Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

预备形状产物

在查看任何一个焦点节点之前,形状图就已经过解析、链接与分析,而这项工作在同一文档的每次验证中都完全相同。预备形状产物(prepared shapes product)只做一次这项工作,并把结果以字节形式写出:一个确定性的、带版本的、经过认证的工件,携带编译后的 SHACL 模型及其可复用的分析结果,从而使进程可以恢复一个预备好的验证器,而无须重新解析 Turtle。

purrdf shacl pack 写出产物。purrdf validate --shapes-product 恢复产物,而不是解析形状文档。purrdf shacl verify 与 purrdf shacl explain 是这些字节的准入接口。

从磁盘读回的产物是不受信任的,对其解码是准入(admission),而不是解析。 产物中的任何内容都不能证明其自身的溯源。阶段 id、profile 以及完整的输入绑定都会在其中任何部分到达验证器之前受到检查,不匹配的情况会在一个具名维度上被拒绝,而不是在错误的配置下进行验证。这一设计所要防止的故障是静默的:在某一前缀映射、基准、词汇表或函数注册表下预备的产物,在不同的前缀映射、基准、词汇表或函数注册表下恢复时并不会崩溃——它会进行验证,并返回一份良构的报告,而报告所针对的形状图无人请求过。

这一接口背后的设计记录见 docs/design/purrdf-prepared-products.md。

生成产物

purrdf shacl pack \
  --shapes shapes.ttl \
  --base https://example.org/shapes \
  --out shapes.purrshp

--shapes 与 --out 是必需的;--out - 把原始字节写到 stdout。回执在工件_之后_写到 stderr,因此绝不会污染经管道传出的产物:

$ purrdf shacl pack --shapes shapes.ttl --out shapes.purrshp
shacl product bytes 4213

--shapes 指定一个路径;显式提供 --base 时,也可用 - 从标准输入读取。产物会记录其形状图解析时所用的基准,而标准输入没有可从中推导基准的检索 IRI;未提供 --base 时传入 --shapes - 属于用法错误。对于未显式提供 --base 的路径,会推导出文档自身的 file:// 检索 IRI——即 validate --shapes 解析该文档时所用的同一基准,因此打包与直接验证在相对引用解析为何物这一问题上不可能出现分歧。

除非 --shapes-from 指明另一种 RDF 语法,否则形状文档按 Turtle 读取。其编解码器所报告的前缀映射(Turtle 与 TriG 的 @prefix/PREFIX,RDF/XML 的 xmlns)是每个 SHACL-SPARQL 与 SHACL-AF 查询据以解析的后备前缀环境,产物会记录这一映射。同一个形状图无论以上述哪种语法写成,打包后都得到相同的裁决。

owl:imports 在打包时折叠,方式与 validate --shapes 的折叠方式相同

shacl pack 接受与 validate --shapes 完全相同的 --import IRI=FILE 表,解析同一个传递闭包,并打包合并后的图——闭包语义参见形状图中的 owl:imports,二者使用的是共享代码,而不是一份平行的重新实现:

purrdf shacl pack \
  --shapes root.ttl \
  --import https://example.org/lib=lib.ttl \
  --out shapes.purrshp

只有位于形状文档自身 IRI 上、owl:Ontology 头上、sh:ShapesGraph(包括 sh:RulesGraph 及其子类)上,或以其中之一作为其 owl:versionIRI 的节点上的 owl:imports 才是导入;在其他任何节点上(包括仅为 sh:DataGraph 的节点),它都是数据。对形状文档自身 IRI(--base、其 file:// 获取 IRI,或文档内的 @base)的导入,或对形状图中已有的图(<X> a owl:Ontology、<X> a sh:ShapesGraph,或 owl:versionIRI 为 <X> 的本体)的导入,都无需配对。其他任何没有配对能够解析的 owl:imports 都会被点名拒绝,与 validate --shapes 完全一致;导入闭包从未到达的配对会作为未使用而被拒绝。产物绝不会从比所指名者更小的形状图打包而成。

过去并非总是如此。shacl pack 曾经通过一条既无导入表、也完全没有诊断通道的路径读取形状文档,因此未解析的 owl:imports 会在没有任何输出的情况下被丢弃,所得产物携带的形状比其打包来源文档更少——每次恢复时都会给出一个已判定的、良构的、错误的裁决。如今,用 --import 打包与用同一 --import 验证该文档,按构造就会得到逐字节相同的报告:两条命令通过同一个函数读取并折叠闭包。

Python、C-ABI 与 WebAssembly 绑定以各自的导入表经由同一解析过程打包——Python 中为 imports=,JavaScript 中为 importIris / importDocuments,C 中为 import_iris / import_documents / import_count——导入闭包未备齐时,会以与其验证函数所抛出的相同的类型化导入错误被拒绝。参见每个宿主上的同一规则。

--shapes-graph 在打包时解析并记录,方式与 validate --shapes 的解析方式相同

SHACL-SPARQL 把形状图本身公开为一个命名图——sh:select 主体可以读取 GRAPH $shapesGraph { … }——而 --shapes-graph IRI 指定该图的名称,覆盖文档所声明的 sh:shapesGraph:

purrdf shacl pack \
  --shapes shapes.ttl \
  --shapes-graph https://example.org/shapes \
  --out shapes.purrshp

相对值依据形状文档解析时所用的同一基准进行解析——即 --base 指定的基准,或文档自身的 file:// 检索 IRI——这与 validate --shapes --shapes-graph 使用该标志时的推导完全相同。这种一致正是关键所在:用 --shapes-graph IRI 打包的产物与用 --shapes --shapes-graph IRI 验证的文档,会在同一个绝对 IRI 下公开 $shapesGraph,并得到逐字节相同的报告。在这个标志出现之前,shacl pack 根本无法记录任何覆盖,因此一个 SHACL-SPARQL 主体依赖 $shapesGraph 的形状图,经由 --shapes 验证得到一个答案,经由恢复的产物验证却得到另一个答案,而命令行上没有任何手段能够弥合这一差距。

省略 --shapes-graph 时,打包行为与以往完全相同:产物记录形状图自身声明的 sh:shapesGraph,若无声明则不记录。

其他每个宿主都以同样的方式记录它:Python 的 pack_product(..., shapes_graph=IRI) 与 Shapes(..., shapes_graph=IRI).prepare().to_product(),WebAssembly 的 shaclPackProduct(..., shapesGraph),以及 C 的 purrdf_shapes_product_encode(..., shapes_graph_iri, ...)。每一个都与命令行一样,相对于形状文档的基准解析相对值。

--box-role-vocab 在打包时解析并记录,方式与 validate --shapes 的解析方式相同

图盒角色注解(graph-box role annotation)功能采用调用方提供的词汇表——由一个命名空间通过拼接推导出的六个术语 IRI——而 PurRDF 不生成任何自有的词汇表 IRI,因此不存在默认值:没有 --box-role-vocab 时,该功能只是处于未激活状态,与以往完全一样。

purrdf shacl pack \
  --shapes shapes.ttl \
  --box-role-vocab https://example.org/meta/ \
  --out shapes.purrshp

--box-role-vocab NS 推导出 NSgraphBoxRole、NSboxABox、NSboxTBox、NSboxRBox、NSboxCBox 与 NSboxConfigBox,把该命名空间记录到产物的身份中(即 box-role-vocab 组成项——参见拒绝维度),并且与 validate --shapes --box-role-vocab NS 使用该标志时的推导完全相同:用该标志打包的产物与用同一标志验证的文档,会解析出相同的 box_role_vocab,因此会在完全相同的 vocabulary 组成项下恢复或运行。省略该标志时,打包行为与以往完全相同:该组成项记录为缺失,不收集也不标记任何角色注解。

写出器是逐字节确定性的。挂钟时间、随机性与哈希迭代顺序都不会影响它,因此对同一文档、基准 IRI 与导入表的两次运行会产生完全相同的字节,而基于产物内容寻址的缓存键是稳定的。

在 Rust 中:

use purrdf::shapes::engine::{parse_shapes, PreparedShapes};
use purrdf::shapes::product::ShapesProfile;
use std::sync::Arc;

let shapes = parse_shapes(shapes_ttl, Some("https://example.org/shapes"))?;
let prepared = PreparedShapes::new(Arc::new(shapes));
let product: Vec<u8> = prepared.to_product(&ShapesProfile::CORE)?;

ShapesProfile::CORE 需要显式指定,不存在默认值。它是本构建实现的唯一一个 profile——SHACL Core 加 SHACL-SPARQL 加 SHACL-AF,且没有任何宿主注入的依赖——调用方无法生成自己的 profile,因为调用方能够拼写出来的 profile 将是关于产物的一项声称,而不是关于产物的一项事实。

「零宿主注入依赖」是关于产物_需要_什么的陈述,而不是对宿主接线的禁止。注入原生 SPARQL 函数或自定义聚合的 Rust 宿主,通过宿主注入的实现中描述的入口点进行打包与恢复;其他所有调用方,以及所有非 Rust 绑定,都使用上面的普通路径。

产物携带什么,不携带什么

产物携带三个部分,总是三者齐全:

部分携带内容
identity(身份)预备阶段 id、profile id,以及解析输入(基准、前缀映射、sh:shapesGraph IRI、盒角色词汇表)
dataset(数据集)形状数据集本身,采用 PurRDF 的简洁(succinct)打包容器
model(模型)声明式 SHACL 模型——节点形状、约束、路径、节点表达式、SHACL-AF 规则、sh:SPARQLTargetType 声明——以及作为其最后一个字段的、由该模型推导出的可复用类分析

每个部分都携带自己的 SHA-256;封套另外在一个封存的尾部中添加身份区域摘要与整个容器的摘要,该尾部还会重申总长度,因此在任何位置被截断或在任何位置被追加内容的产物都会被拒绝。

形状数据集随产物一起传递,出于两个彼此独立的原因。SHACL-SPARQL 把形状图公开为一个命名图——每个 sh:select 主体都绑定了 $shapesGraph——因此丢弃它的产物会恢复出一个对 GRAPH $shapesGraph { … } 返回零行、并将其报告为成功的验证器。此外,当产物由另一个构建写出时,rebuild 正是从它重新推导的;参见恢复产物。

目标不被携带,也没有为目标设置的部分。 目标解析是_数据图_的属性:缓存了已解析目标的产物,会针对旧快照的焦点节点验证新快照,产生一份每项发现都正确、而产物写出之后新增的一切都根本不存在的报告。目标总是按每个绑定的数据集逐次解析。产物从恢复路径中去掉的是解析、模型构建与形状分析——可复用的预备工作与按数据集的绑定是预算中不同的两行。

形状分析确实被去掉了,而不仅仅是被检查。 类分析——一次对循环安全的遍历,收集每个可到达的 sh:class、sh:targetClass 与 shnex:instancesOf IRI,并为每个 IRI 分配一个绑定行位置——作为模型部分的最后一个字段被携带,admit 读取它,而不是再次遍历形状树。它是被绑定的,而不是被信任的:身份中的 class-catalog 组成项正是这些配对(包括位置)的摘要,因此所携带的分析若不是产物打包时的那一份,就会被拒绝。它也不会在不同构建之间过时,因为预备阶段 id 是从类遍历自身的源码摘要得出的——可达性规则不同的构建不可能与打包该产物的构建共享同一阶段 id,因此 admit 会拒绝它,而 rebuild 会改为重新推导。

它与模型共享同一部分,而不是单独占用第四个部分,这样在它开始随产物传递时,部分目录与容器格式版本都无须变动;第四个部分会让此前打包的每个产物都无法_打开_,而这正是 rebuild 无法挽救的唯一一种状态。

出于同类原因,恢复的溯源也不被携带。 产物记录的是决定其形状_含义_的输入;某个特定预备结果是如何_获得_的,是关于一个进程中一次恢复的事实,因此对其编码的产物所携带的值,对除写出者之外的每个读取者都是假的。它还会为一个完全不改变含义的变更而改动预备阶段 id——一个针对模型含义的摘要——从而使此前写出的每个产物都失效。答案改为存放在恢复后的预备结果上,在那里它是真的:参见哪些形状产生了这一裁决。

RDF 1.2 的项身份在往返中完整保留,包括引用三元组、具体化节点(reifier)绑定、陈述注解,以及基础方向构成其身份一部分的 rdf:dirLangString 字面量("x"@en--ltr 与 "x"@en--rtl 是两个不同的项)。

形状图声明的 SPARQL 函数会被携带,并且由数据集携带。 SHACL-AF §5 的 sh:SPARQLFunction 由一个 IRI、一个有序的 sh:parameter 列表、一个 sh:select/sh:ask 主体和一个 sh:returnType 组成——全部由形状图陈述——因此恢复时会从产物已经携带的形状数据集重新推导该声明,而不是从模型中对它的第二份转录推导。这些声明只有一个解析器,恢复时会运行它,这正是恢复出的函数与解析出的函数保持一致的原因。SHACL 1.2 以表达式为主体的声明(sh:ListParameterExpressionFunction)同样会被携带,来源是模型,因为它们的主体_就是_节点表达式。

有一种能力会在打包时被拒绝,而不是在恢复时丢失,报告为 unsupported-capability:

  • 模型中没有任何部分到达的已声明函数——例如,一个仅从 sh:sparql 查询文本中调用的、以表达式为主体的声明。携带这些声明的是模型,因此模型从未到达的声明不在其中,据此写出的产物会把该调用点解析为空,并给出通过(绿色)的验证结果。从形状模型中调用该函数,它就可以打包。

不完整的 owl:imports 导入闭包不是一个准入维度:它被拒绝时尚不存在任何产物,因此每个宿主都把它报告为与其验证函数所抛出的相同的类型化导入错误,而产物只会从合并后的导入闭包打包而成——参见形状图中的 owl:imports。

数据图的 sh:shapesGraph 链接同样不是准入维度,而且产物无法纳入被链接的图:它在数据图存在之前就已预备好。用产物验证时,会依据产物所持有的内容检查每个链接——它的基准、其 owl:imports 导入闭包所吸收的每份文档(产物在其身份区段中记录这些文档的 IRI,因此自身没有本体头、由导入表提供的文档同样被持有),或其合并图所声明的某个形状图、本体或版本 IRI——其他任何链接都会以类型化导入错误被拒绝,kind 为 unheld-shapes-graph-link,并点名该链接。若要验证一个链接了更多形状的数据图,请把被链接的图打包进来(从形状图导入它),或从形状文档出发进行验证,并把该链接对应的文档放入导入表——参见数据图指名其形状图。

分发产物

产物是写出它的那个 PurRDF 构建的工件,而不是稳定的交换格式。应将其与将要恢复它的二进制文件一同分发——在同一个容器镜像、同一个发布包、同一个构建缓存条目中——并把它的字节视为缓存键,而不是文档。

恢复能否成功,取决于两项版本事实:

  • 容器格式版本不是本构建所解码的版本时,会被直接拒绝。应使用将要执行该产物的构建重新打包。
  • 预备阶段 id 是一个由内容推导出的摘要,涵盖声明式模型及其含义所依赖的各个表。它从不手动递增,因此不可能与模型的含义脱节。携带本构建所不认识的阶段 id 的产物_并非_无法使用:rebuild 可以恢复它。

由于写出器是确定性的,构建系统可以按内容寻址产物:打包、对字节求哈希,并在该哈希已存在时跳过这项工作。purrdf shacl explain 会在不准入任何内容的情况下读出产物自身的身份摘要,这是在决定恢复之前询问「这是否就是对应此配置的产物?」的低成本方式。

恢复产物

purrdf validate --shapes-product shapes.purrshp --format sarif data.ttl

--shapes 与 --shapes-product 互斥,且必须恰好提供其中之一。其裁决与 --shapes shapes.ttl 得出的裁决完全相同:运行的是同一个引擎入口点,作用于同样恢复出的形状。

描述形状解析的四个标志在与产物同用时会被点名拒绝,而不是被接受后忽略,因为这里不会发生该解析:

标志拒绝原因应当使用之处
--shapes-from指定形状文档读取时所用的语法,而产物不是文档不适用
--shapes-graph产物记录其自身的 sh:shapesGraph IRI把 --shapes-graph IRI 传给 purrdf shacl pack 并重新打包
--importowl:imports 闭包在打包时已折叠把 --import IRI=FILE 传给 purrdf shacl pack
--box-role-vocab产物记录其自身的盒角色词汇表(或对其有意的缺省)把 --box-role-vocab NS 传给 purrdf shacl pack 并重新打包

描述数据图的一切仍然有效:--from、--base、--format、governor(执行调控器)标志(--fuel、--deadline、--max-intermediate-cells、--max-scratch-bytes、--max-remote-requests),以及位置参数 IN/OUT。

哪些形状产生了这一裁决

每次 validate 运行都会在裁决之前向 stderr 写出一行 shacl shapes-provenance,指明其形状的来源:

$ purrdf validate --shapes-product shapes.purrshp data.ttl
shacl shapes-provenance restored-admitted 9f2c…64 lowercase hex…1b
shacl conforms false
shacl results 2
$ purrdf validate --shapes shapes.ttl data.ttl
shacl shapes-provenance parsed
shacl conforms false
shacl results 2

该标记是完全的(total)——总会有一个,且其中没有任何一个表示「未知」。恢复的产物会呈现该工件自身的输入绑定,即 shacl explain 在其 identity-digest 行上打印的同样 64 个十六进制数字,因此报告与其背后的工件是按同一种拼写进行比较的:

purrdf validate --shapes-product shapes.purrshp data.ttl 2> receipt
awk '/^shacl shapes-provenance restored-/{print $3}' receipt   # 该工件

三个标记分别是 parsed、restored-admitted <digest> 与 restored-rebuilt <digest>。两个恢复标记保持区分,因为摘要在二者上的含义不同:admitted 表示在任何内容到达验证器之前,产物的绑定已针对本进程进行了检查;而 rebuilt 表示该摘要是从工件上读出的,并且有意_未_加检查——关于为何在那里检查它会恰好拒绝该路径所要挽救的那些产物,参见rebuild,前向兼容路径。

这回答了认证所不回答的那一半问题。准入问的是_本进程可以执行这些字节吗?;溯源行回答的是_这份报告出自哪个工件?,这正是使用方事后在日志中查看某个裁决时会提出的问题。

把恢复绑定到本意所指的产物

上面的每项检查问的都是本进程——这是否是写出该备忘(memo)的构建,这些是否是产物预备时所针对的注册表,产物携带的类分析是否就是其自身身份所固定的那一份。其中没有一项会问命令行上指定的文件是否就是所需的产物,因为产物中没有任何内容表明本意所指的是哪个产物。因此指定了错误的产物并不是错误:

$ purrdf validate --shapes-product WRONG.purrshp data.ttl
shacl conforms true

这是关于一个无人问及的形状图的、已判定的、良构的、经过认证的裁决,并且以 0 退出。--expect-identity HEX 就是用来说明本意所指产物的方式:

$ purrdf validate --shapes-product WRONG.purrshp \
    --expect-identity 9f2c…64 lowercase hex…1b data.ttl
shacl dimension shapes-graph
purrdf: --shapes-product WRONG.purrshp: shapes-graph: this product was prepared
from the shapes graph whose input binding is 4b81…, and the caller required the
product bound to 9f2c…; …

HEX 是产物的输入绑定:其身份的 64 个十六进制数字,该身份同时涵盖形状数据集、sh:shapesGraph IRI、前缀映射、基准、profile、盒角色词汇表、全部三个注册表以及类目录。可通过任一会打印它的动词从目标产物上读出——shacl explain 的 identity-digest 行,或 shacl verify 的 stdout——并原样传回:

WANT=$(purrdf shacl explain shapes.purrshp | awk '/^identity-digest /{print $2}')
purrdf validate --shapes-product shapes.purrshp --expect-identity "$WANT" data.ttl

该检查是恢复所做的第一件事,先于 profile 与阶段 id:指定了错误工件的调用方会被如实告知这一点,而不是被引导去重新打包一个从来就不是其所需的产物。其代价是一次 32 字节的比较,因为在字节打开之时,身份摘要就已经解码完毕。

一个得到满足的期望不会改变其他任何东西。以产物自身的摘要使用 --expect-identity,产生的报告与不带该标志运行时逐字节相同——该标志改变的是入口,而不是答案。

有两种写法会作为用法错误被拒绝(退出码 2),而不是被接受:对 --shapes 使用 --expect-identity,因为形状文档没有可供要求的预备绑定;以及不是 64 个十六进制数字的值,这种情况不指明任何维度,因为根本没有检查过任何产物。输入时不区分大小写,因此经由清单或 CI 变量以大写形式传递的选择器仍然指向其产物。

写出器是逐字节确定性的,因此绑定是形状图及其解析输入的属性,而不是某次特定打包运行的属性:在相同的基准与前缀下重新打包同一形状图会得到相同的摘要,这正是该选择器值得与其所指产物一起写入部署清单的原因。

在底层,产物重新成为验证器有两种方式,它们是同一边界上的两个入口点,而不是某一个入口点上的标志:

  • admit——常用路径。它不重新推导任何开销大的内容:验证 profile,验证阶段 id,检查宿主提供的注册表,恢复数据集,解码模型及随之携带的类分析,对其进行链接,并检查完整的输入绑定——包括所携带的分析。
  • rebuild——前向兼容路径,在阶段 id 为本构建所不认识的 id 时采用。它完全忽略模型部分——连同所携带的类分析——并在产物自身记录的解析输入下,从产物携带的数据集重新推导形状及其分析。这正是它能作为补救手段的原因:按定义,它所必须挽救的产物,其携带的分析来自不同的可达性规则。

每种绑定都能到达这两条路径,而不仅仅是 Rust:CLI 的 validate --shapes-product --rebuild、Python 的 ShapesProduct.rebuild()、C ABI 的 purrdf_shapes_product_rebuild,以及 WebAssembly 的 shaclProductValidateToSarifRebuild,都是此处描述的同一个双入口点扩展点,每一个恢复的都是 admit 会恢复其备忘的那个同一携带数据集——因此重建一个当前产物(其阶段 id 为本构建所认识)时,在每个接口上都会得到与 admit 逐字节相同的报告。rebuild 也可以与「本意所指是哪个产物」的期望组合使用:--rebuild --expect-identity、rebuild_expecting,以及完全相同的 Rust ShapesProductView::rebuild_expecting,都会首先检查该期望,与 admit_expecting 完全一样,因此从数据集重新推导绝不会绕过调用方所要求的绑定。

rebuild 不是 Turtle 后备。 不解析任何 RDF 文本,也不读取任何文件;数据集在封套摘要的保护下随产物传递。另一种做法——只携带编译后的模型,并告诉调用方为格式变更的那一天保留源文档——会使前向兼容性依赖于一个产物并不拥有、也无法认证的文件。

use purrdf::shapes::product::{HostBindings, ShapesProduct, ShapesProfile, STAGE_ID};

// 询问这些字节是什么,而不准入它们。
let known = ShapesProduct::open(&product)?.stage_id() == &STAGE_ID;

// 两个恢复扩展点都会消耗该视图,因此需重新打开才能使用其中一个。
let view = ShapesProduct::open(&product)?;
let prepared = if known {
    view.admit(&ShapesProfile::CORE, &HostBindings::empty())?
} else {
    view.rebuild(&ShapesProfile::CORE, &HostBindings::empty())?
};

// 或者说明本意所指的究竟是哪个产物,并在不是该产物时按失败即关闭(fail closed)处理。
// `wanted` 是 `declared_identity().digest()` 为目标产物返回的摘要
// ——与 `shacl explain` 打印的值相同。`admit_expecting` 与
// `rebuild_expecting` 会最先检查它,先于任一恢复扩展点所检查的
// 其他一切,因此修复策略的选择绝不会绕过该绑定。
let prepared = ShapesProduct::open(&product)?
    .admit_expecting(&ShapesProfile::CORE, &HostBindings::empty(), &wanted)?;
let prepared = ShapesProduct::open(&product)?
    .rebuild_expecting(&ShapesProfile::CORE, &HostBindings::empty(), &wanted)?;

ShapesProduct::open 运行容器的低成本完整性层级——魔数、格式版本、部分目录、规范化偏移量、零填充、每个部分的 SHA-256、身份区域、尾部、整个容器的摘要——并且不准入任何内容。持有所得的视图只是关于封装结构(framing)的陈述,从不是关于适用性的陈述,而这正是读取被拒绝产物所声明的身份之所以有用的原因。

宿主注入的实现

上文的一切都传入 HostBindings::empty(),这正是 ShapesProfile::CORE 产物所需要的:此类产物所使用的每项能力都由形状图自身声明。Rust 宿主仍然可以接入自己的原生 SPARQL 函数或自定义聚合,而当它这样做时,产物会绑定关于该宿主的两项事实,而不是一项:

  • 注册表的声明——每个注入条目的 IRI、元数与易变性——这是在另一个进程中可复现的指纹所能涵盖的全部内容;以及
  • 一个实现身份:调用方用来指明这些声明所解析到的构建的不透明字节串,例如发布版本或提交摘要。

第二项并非多此一举。同一宿主的两个构建可以把同一个 IRI 注册到两个不同的闭包上,二者声明完全相同的元数与易变性,却计算出不同的答案——从注册表所能陈述的关于自身的每项事实来看都无法区分。仅基于声明的绑定会在另一个构建下准入针对某一构建预备的产物,并给出通过(绿色)的验证结果,而这正是该编解码器所要排除的静默错误答案。

因此,注入集合非空的预备结果完全不带身份时无法打包:写出器会以 unsupported-capability 拒绝,而不是输出一个其宿主那一半无法被任何东西检查的产物。而在不同身份下提供相同声明的恢复,会在承载该身份的注册表维度上被拒绝。

use purrdf::validate::{
    admit_shapes_product_with_implementations, prepared_to_product_with_implementations,
};

// 用于把宿主原生实现的这一构建与其所有其他构建区分开来的
// 字节串。PurRDF 从不解释它。
const BUILD: &[u8] = b"example.org/host@1";

// `prepared` 在 `Shapes::functions` 中携带宿主的原生实现。
let product = prepared_to_product_with_implementations(&prepared, BUILD)?;

// ……而恢复在其所接入的注册表旁指明同一个构建。
let restored = admit_shapes_product_with_implementations(
    &product,
    &functions,
    &aggregates,
    &property_functions,
    BUILD,
)?;

该机制不做的是验证身份。PurRDF 无法读取宿主的机器码并确认这些字节指明的就是它;身份是调用方关于其自身构建的声称,编解码器所强制的是:声称_不同_身份的恢复会在边界处停止。用同一个身份拼写两个不同构建的宿主,等于告诉了绑定它们是同一个构建。

缺失的身份是空字节串,并且编码后不占任何内容:三个宿主行仍然只是单纯的声明指纹。正因如此,未注入任何内容的产物——也就是上述路径写出的每个产物——与一个从未听说过实现身份的构建所产生的结果逐字节相同。

佐证产物

$ purrdf shacl verify shapes.purrshp
9f2c…64 lowercase hex…1b

verify 是编解码器的冷路径,并且有意设计为无法从恢复中到达。它对形状图的空节点重新规范化,并将结果与产物身份所声称的摘要进行比较——这是一种图同构计算,其开销可能超过产物本来要消除的形状解析开销。应在构建步骤、发布检查或一致性测试框架中调用它;绝不要在每次验证之前调用。

admit 就数据集所确立的是:该部分正是容器所封存的那一个——按部分计算的 SHA-256 与整个容器的摘要都覆盖其字节,因此被替换的部分会被拒绝。只有 verify 才能确立的是:这些字节_规范化_后得到身份所记录的摘要。一个存储的规范化摘要被篡改过的产物能够打开,能够准入,而会在 verify 中失败;这种划分由测试固定,并非分层带来的偶然结果。

它在 stdout 上打印产物的 64 个字符的小写十六进制身份摘要,并以 0 退出。拒绝会在 stderr 上指明其维度,并以 1 退出。

读取产物而不准入它

$ purrdf shacl explain shapes.purrshp
format-version 1
stage-id 7416f78d…
stage-known true
identity-digest 9f2c…
identity-components 13
identity source-dataset 0x1f0a…
identity shapes-graph "(present) https://example.org/shapes"
identity doc-prefixes 0x02657808…
identity base "(present) https://example.org/shapes"
identity profile "purrdf-shacl-core-v1"
identity box-role-vocab 0x00
identity user-functions-declared 0x4c1d…
identity user-functions-injected 0x4c1d…
identity aggregate-registry 0xb7e0…
identity property-function-registry 0xb7e0…
identity class-catalog 0x63aa…
identity parse-configuration 0x17657874…
identity included-graphs 0x0800000000000000…
parse-base https://example.org/shapes
parse-shapes-graph https://example.org/shapes
parse-prefixes 3
parse-prefix ex https://example.org/
parse-prefix sh http://www.w3.org/ns/shacl#
parse-prefix xsd http://www.w3.org/2001/XMLSchema#

按固定顺序输出的确定性 key value 行,以换行符结尾,因此使用方无须解析器即可按空白切分。身份组成项的值在其为可打印 UTF-8 时呈现为 "text",否则呈现为 0x… 小写十六进制。

stage-known 是据以行动的事实:false 表示本构建的 admit 会拒绝这些字节,而 rebuild 是仍能恢复它们的路径。

上面的 identity box-role-vocab 0x00 是缺失的编码——该示例打包的产物没有使用 --box-role-vocab。使用了该标志打包的产物会呈现一段更长的 0x…(六个推导出的术语 IRI,带长度前缀并按键排序),绝不会是 0x00;二者有意设计为可区分的,这样恢复就绝不会把「该功能未激活」与「该功能在一个由空 IRI 组成的词汇表上激活」相混淆。

正是这一点使具名拒绝成为可据以行动的信息,而不只是一行日志。在 prefixes 上被拒绝的恢复,应通过读取产物实际携带的是哪个前缀映射并修正配置来应对——而不是靠猜测,也不是盲目地重新打包。在两个产物之间——手头的那个与本意所指的那个——比较这一答案,用的是 shacl diff,而不是运行两次 explain 再用肉眼比较呈现结果。

比较两个产物

$ purrdf shacl diff a.purrshp b.purrshp
diff-count 1
diff box-role-vocab 0x00 0x0108626f78…

shacl diff 打开两个产物,但不准入其中任何一个——与 shacl explain 读取时所用的同一个仅限封装结构与完整性的层级——并打印每个值不同的身份组成项,每个组成项一行 diff <label> <value-in-a> <value-in-b>,按 A 自身的组成项顺序排列。diff-count 0 且没有 diff 行,表示两个产物声明了完全相同的身份。

正是这一点使具名拒绝在两个工件之间、而不只是在一个工件上成为可据以行动的信息。shacl explain 回答的是「这个产物声称自己是由什么编译而来的」;在 diff 出现之前,恢复在某个具名维度上被拒绝的操作人员必须运行两次 explain——一次针对手头的产物,一次针对本想恢复的产物——并用肉眼比较呈现结果,以找出是哪项声明发生了变动。diff 就是这一比较,一次完成,作用于 explain 所呈现的完全相同的已解码组成项,因此二者在某个组成项的值是什么这一点上永远不会出现分歧。

退出码沿用 shacl verify 的已认证/已拒绝划分,而不是 validate 的符合/不符合划分,因为发现差异的 diff 是一个关于两个工件的已判定答案,而不是关于数据的验证裁决:两个身份完全相同时以 0 退出,不同时以 1 退出(无论哪种情况,差异都打印在 stdout 上),用法错误时以 2 退出。

diff 从不准入任何一方,因此它也适用于预备阶段 id 不为本构建所识别的产物——阶段 id 存放在产物单独的预备备忘中,而不在 diff 所比较的身份中,而这恰恰是操作人员求助于 diff 来弄清楚的情形:「validate --shapes-product 直接拒绝了这个文件;如果重新打包,具体会有什么变化?」

拒绝维度

每次拒绝都带有二十二个稳定的 kebab-case 标签之一,以 shacl dimension <label> 的形式打印到 stderr。据以分支的是维度;供人阅读的是消息。 不支持按消息文本进行匹配。

各维度由容器外层向内依次检查,第一个失败的维度就是被报告的维度——因此标签总是指明最外层未满足的前提条件,而不是其下游症状。

这二十二个维度按哪个动词能够报告它们分为两组,而这种划分并非分类学:它是该编解码器所围绕的成本决策。恢复是常用路径,不得对形状图的空节点进行规范化;认证是会进行规范化的冷路径。只有两者之一能够到达的维度,并不是另一方悄悄跳过的东西——而是表明该动词从不作出这一陈述。

准入时维度

由 validate --shapes-product 在每次恢复时报告;由 shacl explain 报告其在打开字节时所到达的结构性各行;由 shacl verify 报告,因为它在认证任何内容之前会先打开产物。

维度含义处理方法
magic这些字节从来就不是预备形状产物向加载器提供由 purrdf shacl pack 写出的文件
format-version容器格式不是本构建所解码的格式使用将要执行它的 PurRDF 构建重新打包
stage-id模型备忘是针对本构建已不再具有的模型写出的用 rebuild 恢复,或重新打包
profile在不同的预备 profile 下预备使用将要执行它的构建重新打包
truncated字节在其所声明的某个结构内部就结束了重新打包——这是一次被中断的写入,而不是原地损坏
trailer尾部缺失,或与其之前的字节不一致丢弃并重新打包
section-digest某个部分已不再与其记录的摘要相符丢弃并重新打包——产物已原地损坏
container-digest整个容器的摘要与字节不符丢弃并重新打包——部分主体之外的某些内容发生了变动
shapes-graph在不同的 sh:shapesGraph IRI 下预备——或者,在 --expect-identity 下,所携带的输入绑定不是所要求的那一个在同一个命名图下公开这些形状,并以该 IRI 重新打包,或者让期望指向本意所指的产物
prefixes针对不同的前缀映射预备在相同的前缀下打包与执行——前缀映射决定了前缀名表示哪个 IRI
base针对不同的基准 IRI 预备在相同的基准下打包与执行——相对引用依据它进行解析
vocabulary在不同的盒角色词汇表下预备向 purrdf shacl pack 与执行方传入相同的 --box-role-vocab NS(或都不传);PurRDF 不生成任何词汇表 IRI,也没有可供回退的默认值
function-registry针对不同的 SPARQL 函数注册表预备,或针对其背后宿主实现的不同构建预备在执行宿主中接入相同的函数,并使用相同的实现身份
aggregate-registry针对不同的自定义聚合注册表预备,或针对其背后宿主实现的不同构建预备在执行宿主中接入相同的聚合,并使用相同的实现身份
property-function-registry针对不同的属性函数注册表预备,或针对其背后宿主实现的不同构建预备在执行宿主中接入相同的关系,并使用相同的实现身份
class-catalog产物所携带的类分析不是其自身身份所固定的那一份丢弃并从形状图重新打包——该部分与基于它的绑定已不再描述同一份分析
parse-configuration在不同的已声明扩展函数或关系命名空间下预备在执行端宿主上声明相同的命名空间
included-graphs产物的形状图所由组装的命名图集合(其 --import 文档与已解析的 sh:shapesGraph 链接)与其绑定所固定的集合不同以相同的导入表重新打包——所记录的图决定产物持有哪些数据图链接
unsupported-capability良构的字节要求本构建无法兑现的东西,或者是本构建不会为之写出产物的预备结果参见消息——模型中没有任何部分到达的已声明函数、超出当前生效的暂存(scratch)上限的解码,或打包器未获得身份的宿主实现
depth-limit某个结构的嵌套超出了解码器的固定上限从本构建能够解析的形状图重新打包;该上限是栈保护,而不是语义限制
malformed以其他任何维度都未指明的方式在结构上无效丢弃并重新打包

其中三个对应三种真正不同的处理方式,而这正是为它们命名的意义所在:malformed 是需要丢弃的损坏缓存,format-version 是需要重新编译的过时工件,而 function-registry 是调用方自身进程中的配置错误,重新打包无法修复。

认证时维度

维度含义处理方法
dataset-identity产物所携带的形状数据集_规范化_后得不到其自身绑定所声称的摘要丢弃并从形状图重新打包——该部分与基于它的绑定已不再描述同一个数据集

只有 shacl verify 报告这一维度。恢复不会对形状数据集重新规范化:它从产物自身的绑定中取得该组成项,因为这项计算是针对形状图空节点的图同构,其开销很可能超过产物本来要消除的形状解析开销。在每条路径上,封套按部分计算的 SHA-256 与整个容器的摘要仍然覆盖这些字节,因此被替换的部分会以 section-digest 或 container-digest 被拒绝;只有认证才能确立的是,这些字节规范化后得到绑定所声称的结果。

因此,一个存储的规范化摘要被篡改过的产物能够打开,能够准入,而会在 shacl verify 中失败。 这是一个由测试固定、有文档记录的划分,而不是漏洞:如果需要这一陈述,就运行 verify——在构建步骤、发布检查或一致性测试框架中运行,绝不要在每次验证之前运行。

有一种失败不带有任何维度:无法解析的形状_文档_或数据_文档_从未到达准入边界,因此没有检查任何内容,为它指明一个维度就等于声称并非如此。在 CLI 上不会打印 shacl dimension 行;在 Python 中,异常的 .dimension 为 None;在 JavaScript 中,dimension 为 undefined。

命令行动词

命令作用退出码 0退出码 1退出码 2
purrdf shacl pack --shapes FILE --out OUT [--base IRI] [--shapes-graph IRI] [--import IRI=FILE] [--box-role-vocab NS]解析、预备、写出产物产物已写出形状未能解析、图中声明了产物无法携带的内容,或某条 owl:imports 未解析错误的标志、未提供 --base 时使用 --shapes -,或形状图无法解析的 --shapes-graph/--import
purrdf shacl verify [IN]依据所声称的身份佐证所携带的数据集打印身份摘要被拒绝,并在 stderr 上输出 shacl dimension <label>错误的标志
purrdf shacl explain [IN]打印产物声称自己编译自什么打印 key value 呈现这些字节不是良构的产物错误的标志
purrdf shacl diff A B比较两个产物声明的身份,而不准入其中任何一个两个身份完全相同两个身份不同(diff 行仍会打印在 stdout 上)错误的标志,或 A 与 B 都指定为标准输入
purrdf validate --shapes-product FILE [--rebuild] [--expect-identity HEX] [IN]恢复并验证——--rebuild 从所携带的数据集重新推导,而不是准入备忘验证已运行产物被拒绝错误的标志、对产物传入了解析标志、在没有产物时使用 --rebuild 或 --expect-identity,或 --expect-identity 的值不是 64 个十六进制数字

对于 verify 与 explain,IN 默认为 -(标准输入)。对于 validate,--shapes-product - 会与数据图自身的默认值 - 冲突,因而被点名拒绝:标准输入只有一个。diff 的两个产物 A 与 B 都是必需的位置路径,没有默认值,原因完全相同:把两者都指定为标准输入,会使每个产物各得到同一字节流的一部分。

validate --shapes FILE [--box-role-vocab NS] 在解析通道上接受完全相同的标志——参见上文的生成产物——并在与 --shapes-product 同用时点名拒绝它,见上面的四标志表。

validate 在 governor(执行调控器)预算触发时还会以 3 退出,并且总是在运行之前写出 shacl shapes-provenance <token>,在运行之后写出 shacl conforms true 或 shacl conforms false 以及 shacl results <N>,全部输出到 stderr——包括形状来自产物的情况。

从 Python 调用

from purrdf import shapes

prepared = shapes.Shapes(shapes_ttl, base="https://example.org/shapes").prepare()
product = prepared.to_product()           # 字节,逐字节确定性

view = shapes.ShapesProduct.open(product) # 仅限封装结构与完整性
print(view.format_version())             # 1
print(view.stage_known())                # True
print(view.identity_digest())            # 64 个小写十六进制字符
for label, value in view.identity_components():
    print(label, value)

restored = view.admit() if view.stage_known() else view.rebuild()
report = restored.validate_nt(data_nt)
print(report.conforms)

# 该裁决出自哪个工件?完全的(total)——总有答案,绝不会是
# "unknown":"parsed"、"restored-admitted <digest>" 或 "restored-rebuilt <digest>"。
print(restored.provenance())             # restored-admitted 9f2c…1b

# ……或者说明本意所指的是哪个产物,并在不是该产物时按失败即关闭(fail closed)处理。
restored = view.admit_expecting("9f2c…64 lowercase hex…1b")

# 同一绑定,经由重建扩展点:期望会最先检查,
# 先于重新推导,因此选择重建绝不会绕过它。
restored = view.rebuild_expecting("9f2c…64 lowercase hex…1b")

PreparedShapes.provenance() 是 CLI 的 shacl shapes-provenance 回执在 Python 中的写法,呈现完全相同的标记——其中的摘要就是 identity_digest() 所报告的值,因此可以直接回传给 admit_expecting()。由 Shapes(...).prepare() 构建的预备结果回答 parsed,因为它从未从工件中恢复过,也不得为它虚构任何工件。

ShapesProduct.certify() 是冷路径;应从构建步骤或测试中调用它。拒绝会抛出 shapes.ShapesProductError,其 .dimension 携带标签字符串(从未检查过任何产物时为 None)。

子模块是 purrdf.shapes。purrdf.shacl 是同一对象的向后兼容别名,仍然可用,但新代码应写规范名称:该别名早于本页所描述的接口,而两个名称解析到同一个模块,在学习者看来就像是两套 API。

admit_expecting(expected_identity) 是绑定到本意所指产物的 admit():它接受 identity_digest() 所报告的 64 个十六进制数字,当产物携带不同的绑定时,抛出 .dimension == "shapes-graph" 的 ShapesProductError。不是摘要的选择器则会抛出普通的 ValueError——没有打开任何产物,因此不能把责任归到某个产物上。

rebuild() 从 admit() 恢复其备忘的那个同一携带数据集重新推导,因此在当前产物(stage_known() == True)上调用它会得到逐字节相同的报告——它是通往同一产物的第二道门,绝不是第二个、有分歧的答案。rebuild_expecting(expected_identity) 是它的绑定孪生版本,接受与 admit_expecting 完全相同的 expected_identity:期望会最先检查,先于重新推导,与 admit_expecting 上完全一样,因此求助于前向兼容挽救手段的调用方绝不会失去绑定。

在 JavaScript / WebAssembly 中

import {
  ready,
  shaclPackProduct,
  shaclProductExplain,
  shaclProductCertify,
  shaclProductValidateToSarif,
  shaclProductValidateToSarifRebuild,
  shaclProductValidateToSarifExpecting,
  shaclProductValidateToSarifRebuildExpecting,
} from "@blackcatinformatics/purrdf";

await ready(); // 一次性的异步 wasm 实例化

const product = shaclPackProduct(shapesTtl, "https://example.org/shapes");
console.log(shaclProductExplain(product));

try {
  const sarif = shaclProductValidateToSarif(product, dataNt);

  // ……或者,对于本客体(guest)不认识的阶段 id,从所携带的
  // 数据集重新推导,而不是准入备忘——即上文 `stage-id` 拒绝
  // 所会指明的补救手段。
  const rebuilt = shaclProductValidateToSarifRebuild(product, dataNt);

  // ……或者说明本意所指的是哪个产物,并在不是该产物时按失败即关闭(fail closed)处理。
  const bound = shaclProductValidateToSarifExpecting(product, dataNt, wantHex);

  // ……或者两者兼用:重建,并且在不是本意所指的产物时按失败即关闭处理。
  // 期望会最先检查,先于重新推导,因此
  // 挽救手段绝不会绕过绑定。
  const boundRebuilt = shaclProductValidateToSarifRebuildExpecting(
    product,
    dataNt,
    wantHex,
  );
} catch (refusal) {
  console.error(refusal.dimension, refusal.message);
  refusal.free();
}

wasm 客体(guest)没有可从中推导基准的检索 IRI,因此由宿主显式提供 shapesBase;它会记录在产物中,恢复时无须该文档即可解析同样的相对引用。

shaclProductValidateToSarifRebuild 从 shaclProductValidateToSarif 恢复其备忘的那个同一携带数据集重新推导,因此在当前产物上调用它会得到逐字节相同的报告——它是通往同一产物的第二道门,绝不是第二个、有分歧的答案。shaclProductValidateToSarifRebuildExpecting 是它的绑定孪生版本,接受与 shaclProductValidateToSarifExpecting 完全相同的 expectIdentity:期望会最先检查,先于重新推导,与 shaclProductValidateToSarifExpecting 上完全一样,因此求助于前向兼容挽救手段的宿主绝不会失去绑定。

shaclProductValidateToSarifExpecting 接受 shaclProductExplain 在其 identity-digest 行上打印的 64 个十六进制数字。携带不同绑定的产物会以 dimension === "shapes-graph" 拒绝;不是摘要的选择器会以 dimension === undefined 拒绝,因为没有检查任何产物。shaclProductValidateToSarifRebuildExpecting 采用完全相同的选择器与完全相同的拒绝形式。

在 C 中

C ABI 公开了 purrdf_shapes_product_encode、purrdf_shapes_product_open、purrdf_shapes_product_admit、purrdf_shapes_product_admit_expecting、purrdf_shapes_product_rebuild、purrdf_shapes_product_rebuild_expecting 与 purrdf_shapes_product_certify,并由 purrdf_shapes_product_error_dimension 返回拒绝的稳定标签(形状文档只是未能解析时返回 NULL)。

purrdf_shapes_product_rebuild 是 purrdf_shapes_product_admit 的前向兼容孪生版本:它从产物所携带的形状数据集重新推导预备结果,而不是准入其备忘,这正是 purrdf_shapes_product_admit 的 stage-id 拒绝所指明的补救手段。purrdf_shapes_product_rebuild_expecting 是它的绑定孪生版本,接受与 purrdf_shapes_product_admit_expecting 完全相同的 expect_identity:期望会最先检查,先于重新推导,与 purrdf_shapes_product_admit_expecting 上完全一样,因此求助于前向兼容挽救手段的 C 宿主绝不会失去绑定。

purrdf_shapes_product_admit_expecting 是绑定到本意所指产物的 purrdf_shapes_product_admit:它接受 purrdf_shapes_product_open 所呈现的 identity-digest 值(以 NUL 结尾的 C 字符串),当产物携带不同的绑定时,返回 PURRDF_STATUS_SHAPES_PRODUCT_ERROR 以及维度 shapes-graph。不是 64 个十六进制数字的选择器返回 PURRDF_STATUS_INVALID_ARGUMENT,且不带维度。purrdf_shapes_product_rebuild_expecting 采用完全相同的选择器与完全相同的拒绝形式。参见 crates/rdf-capi/include/purrdf.h 与入门:C。

相关内容

  • SHACL——验证器本身、其覆盖范围与报告模型。
  • docs/design/purrdf-prepared-products.md——设计记录:为何是准入而非解析、为何是三个扩展点、推导出的阶段身份、普查闭包(census closure),以及有界的保证。
  • 编解码器与确定性——每个 PurRDF 写出器(包括这一个)都遵循的逐字节确定性规则。
  • 基准 IRI 与相对引用——--base 记录什么,以及产物为何拒绝猜测它。
  • 性能——分解打包与恢复阶段的仅报告型基准测试。