Review state: Closed. Dispositioned in ADR-A51-agent-review-disposition.md and ADR-A51-2nd-agent-review.md. ADR-A51 was then superseded by ADR-A82 before ratification, so no ratification is pending against this review. Current state: phase-0-status.md, P0.1.3.
The core of ADR-A51 is sound. It separates a stable lineage identity from an immutable, content-addressed revision. It keeps versions out of entity identifiers. It offers a small set of minting strategies with named risks, bans PII, and gates surrogate use. All of this matches good RDF and OWL practice and closely mirrors established models:
owl:versionIRIspecializationOfisVersionOfIt is not fundamentally in conflict with RDF or OWL. Two rules as written do conflict with normal expectations, though, and several claims are overstated. I would not ratify it unchanged. The problems fall into five groups:
derived-hash does not “avoid PII” for low-entropy keys.The rest of this review details each point, then gives a suggested amended policy.
| Decision | Why it is good |
|---|---|
| Lineage versus revision split | This is the right primitive. It matches OWL 2’s ontology IRI / version IRI split, which is widely understood by tools (Protégé, OWL API, ROBOT, OBO release practice). It gives you a stable handle for references and an immutable handle for evidence and provenance. |
| Content-addressed revisions | Registration becomes idempotent: the same content yields the same IRI, and replays are no-ops. It is cache-friendly and verifiable. It is exactly the P0 pattern (“make uniqueness structural”) from Chapter 5 of the guide, applied to graphs. |
| Alias for “current” | Separating “which revision is live” from revision identity is correct. It keeps the only mutable state in one pointer, which the guide can then protect with CAS (§30.1). |
| Minting strategy taxonomy with a risk column | Explicit and teachable. The “surrogate is never idempotent” warning is exactly the K4 failure mode the guide calls “the one that bites in production.” |
| No PII in IRIs | Correct and important. IRIs are copied into logs, dumps, URLs, caches and third-party systems, and cannot practically be erased. |
| No versions in entity IRIs | Correct for entities and lineages (“Cool URIs don’t change”). Versioning belongs in fnd:Version nodes. |
| Surrogate use requires declaration (G9) | Good governance. It forces the “will re-ingestion converge?” question at plan-review time. |
| Validator and grammar tests before any dataset (P0.3.7) | Correct sequencing. Identity mistakes are the most expensive kind to fix later. |
Severity scale: Critical means ratifying as-is will cause data-integrity or identity bugs. Major means a significant design flaw or contradiction. Minor means a gap that needs clarifying.
In RDF, an IRI is a global name. The same IRI should denote the same resource in every dataset in which it appears. Making the environment part of identity and rewriting it on clone causes six problems.
sh:sparql query strings, sh:prefixes/sh:declare namespace literals, JSON or YAML payloads in literals, rdfs:seeAlso strings, and documentation;{base} are environment-scoped, then dev and prod have different ontologies. Shapes, queries, mappings and tests become environment-specific. OWL axioms linking the two are meaningless.
https://www.nebularis.org/...) is clearly not environment-scoped. The policy must say that TBox and vocabulary IRIs never are.urn:lattice:{tenant}:{scope}:{family}:{localName} has no environment segment. So either lineage IRIs are identical across environments (contradicting Rule 3) or the environment is hidden inside {tenant} (undocumented).Recommendation: Keep the environment out of identity. Environments should be separate datasets or stores with separate access control, and the same IRIs should be used everywhere. If synthetic test fixtures need distinguishing, give them a distinct tenant (e.g., test-fixtures), not a rewritten base. If environment scoping is genuinely required, define it once in the lineage grammar, exclude it from content hashes via a base-relative canonical form, and exempt all TBox IRIs.
semanticHash[0:16] is, if hex (the encoding is unspecified), 64 bits.
§5.2 check prevented.Recommendation:
revisionHash verification field the ADR already keeps) and reject on mismatch. This is O(1) and costs nothing.The old rule rejects registration when (tenantId, projectId, graphIri) collides on hash, family or owner.
{family} is in the lineage.{localName} is chosen by someone. Two producers choosing the same lineage localName for different logical artefacts is a K2/K3 uniqueness problem. It needs the guide’s P1/P2 claim registry, or deterministic derivation of localName.projectId from the old key doesn’t obviously map to {scope}. Is scope the project? A domain? Say so.Recommendation: State that lineage IRIs are allocated through a key claim (P1 + P2) keyed on (tenant, scope, family, localName) with an owner. That is where the owner and family checks now live. Rewrite the consequence as “replaces the runtime content-conflict check with structural identity plus a lineage-allocation claim.” This is still a genuine simplification.
The revision IRI is {lineageIri}/rev/{profileVersion}-{hash}. It contains a version number, which Rule 2 forbids.
More broadly, version-bearing IRIs are standard in OWL. Consider owl:versionIRI values like http://purl.obolibrary.org/obo/go/releases/2024-01-17/go.owl, which contain a date. The rule as written would forbid OWL-conformant version IRIs.
The patterns guide also mints version and time-bearing IRIs:
v1 salt in urn:key:person-email:v1:…urn:g:txlog/2026-09urn:rev:orders/1/0000000000000042, urn:g:delta/orders/1/…/add and snapshot graphs urn:g:orders/1/…42Recommendation: Scope Rule 2 to entity IRIs and lineage IRIs. Explicitly permit version, profile or position components in:
A ULID’s first 48 bits are a millisecond Unix timestamp. So do UUIDv7s. {base}/{class}/s/{ULID} therefore contains a timestamp, violating Rule 2.
This is not only pedantry. It can leak personal data (Rule 1). The creation time of a patient record, a complaint, or an extraction candidate from a specific document can be sensitive, and it is trivially decodable from the IRI.
Recommendation: Use UUIDv4, or 128 random bits base32-encoded, for surrogates. Alternatively, keep ULIDs for their index locality and add an explicit, justified carve-out in Rules 1 and 2. The same applies to urn:txn:{ULID} in the guide, though infrastructure identifiers are less of a concern.
derived-hash does not “avoid PII in IRIs”sha256(canonical(keyTuple)) is unkeyed. For low-entropy keys (emails, national IDs, phone numbers, MRNs, names plus date of birth) it is reversible by dictionary or enumeration.
The patterns guide says this itself (Chapter 5, and the claim_iri docstring in §6.1), and uses HMAC for exactly that reason. Under GDPR, an unkeyed hash of an identifier is pseudonymous data, which is still personal data (Recital 26; EDPB guidance). So the strategy’s “use when” column is wrong for the very case it advertises.
A keyed hash (HMAC) fixes reversibility but creates a new tension: the key can never be rotated without re-minting every entity IRI, which contradicts “stable forever.”
Recommendation:
derived-hash is for non-sensitive composite keys only, unless keyed.Both can’t be the default. The ADR’s statement is true only of unindexed surrogates. A surrogate plus a claim registry is idempotent at the claim level, and it is the only option that satisfies all four of:
Recommendation: Add a fourth strategy, e.g. surrogate-claimed (random IRI plus a P1 claim on the natural key), as the recommended default for entities with mutable or sensitive keys. Then reserve Rule 4’s justification gate for unclaimed surrogates.
Both natural-key and derived-hash bind the IRI to the key. The ADR says “use when source has a stable business key,” but “stable” needs to mean immutable forever. Business keys change: SKUs get renamed, emails change, companies merge, account numbers get reissued. When they do, “stable forever” breaks, and every reference must migrate.
Relatedly, the mitigation “add a source-system discriminator” prevents cross-source collision but also prevents convergence. The same person from CRM and billing gets two IRIs by design. That is the K4 problem, and the ADR is silent on how it is resolved.
Recommendation:
fnd:replacedBy, or owl:deprecated plus a pointer) and references are rewritten.owl:sameAs as the operational mechanism. Under reasoning it causes sameAs-clique explosion, and it can’t be retracted cleanly (see §1.4 of the guide).A content-addressed revision identifies a content state, not an event. If a graph goes A → B → A (a revert), the third registration yields the existing revision IRI for A.
If fnd:Version nodes are chained with fnd:supersededBy, you get A supersededBy B, and B supersededBy A: a cycle. Any “latest version” or “history” traversal loops or gives nonsense. The alias /current repointing back to A is fine; the version graph is not.
The guide’s receipts (urn:rev:{aggregate}/{seq}) are event-identified, which is the other half of the model.
Recommendation: Make the distinction explicit.
fnd:Version, a snapshot).pat:Revision, or a prov:Activity).supersededBy, prevRev) chains events, and each event points at a content revision.A revert is then a new event pointing at an old content revision. Also decide whether profileVersion is inside the hash input; if not, identical content under two profiles is two IRIs with one hash.
{class} (and {family}) in “stable forever” IRIs is brittle under OWLOWL individuals routinely:
Customer becomes a Party plus a role).An IRI of the form {base}/{class}/… bakes one classification into permanent identity. When the class hierarchy changes, you either keep IRIs that lie about the type or re-mint them. The same applies to {family} in lineage IRIs, if a graph can ever be re-homed.
Recommendation: Replace {class} with a minting-namespace token from a registry (e.g., person, order). Make it explicitly not an rdf:type claim, and never rename it once used. Readers and tools must never infer types from IRI structure.
”{lineageIri}/current — repointed atomically at promotion” assumes graph aliasing. RDF 1.1 datasets and SPARQL 1.1 have no aliases. A named graph is a (name, graph) pair. You must pick a mechanism:
| Mechanism | Pros | Cons |
|---|---|---|
(a) Materialized copy into …/current |
Plain FROM/GRAPH queries work |
Copy cost; “atomic” only within one transaction; double storage |
(b) Pointer triple (pat:current, as in §20.3 of the guide) |
No copying | …/current is then not a graph but a resource; queries need a rewrite or resolution layer |
| (c) Store-specific feature | — | Not portable across the SPI |
Recommendation: Choose (b) as normative, protected by the guide’s CAS on the version row, with an optional (a) projection for tools that need a literal graph. State what FROM <…/current> means for readers during a promotion.
RDF compares IRIs by exact character string. %2F and %2f are different IRIs, as are Acme and acme, and NFC and NFD forms. The ADR needs rules for each of the following.
{localName} or {tenant} can contain :, /, ? or #, the grammar is ambiguous. urn:lattice:a:b:c:d:e doesn’t parse uniquely.urlsafe(keyTuple) and canonical(keyTuple).
("a|b","c") and ("a","b|c") collide.v1|. The ADR’s derived-hash has no salt, so a change of canonicalization silently splits identity.This is exactly what the P0.3.7 validator should enforce, but the ADR must define the rules it validates.
urn:lattice is an unregistered URN namespace, and relative resolution misbehavesurn-N), and lattice is not registered. Practically, every triple store and OWL tool accepts it, so this is a hygiene issue.
URN:LATTICE:x and urn:lattice:x are the same URN but different RDF terms. So mandate the lowercase form.{lineage}/rev/… looks hierarchical, which tempts authors to use @base and relative IRIs in Turtle or JSON-LD. RFC 3986 resolution against a rootless URN path gives surprising results.
@base <urn:lattice:acme:p:F:x>, the reference <rev/abc> resolves to urn:rev/abc. That is silently wrong.@base and relative references for URN-based IRIs.owl:imports of URN ontology IRIs needs a catalog (OWL API/Protégé XML catalogs). Anything published as Linked Data, LDP or Solid (which the guide discusses in §15.4) expects http(s) IRIs.
https://id.<domain>/… later is possible but is a second identity.https under a controlled domain from the start (w3id.org or your own), with URNs reserved for purely internal infrastructure.“semanticHash” is undefined. It needs:
owl:versionIRI in an ontology header, or provenance triples), the hash is circular. Hash with the self IRI replaced by a placeholder, or exclude the header."1"^^xsd:integer and "01"^^xsd:integer, or two OWL-equivalent axiomatizations, hash differently. Call it contentHash, or define literal canonicalization if you genuinely want value-level equivalence.The tenant segment must be an immutable opaque ID, never a name. Tenants rename, merge and split. For single-person tenants, a name would be PII.
The ADR should also define a reserved tenant for shared reference data (country codes, common vocabularies), or cross-tenant references will mint duplicates.
The guide requires skolemization (§5, §14.3). The ADR should specify:
/.well-known/genid/ for http bases; there is no equivalent convention for URNs, so define one.The “reified span / extraction candidate” surrogate case may also be better modeled with RDF 1.2 triple terms or annotations, if the target stores support them.
derived-hash“Requires an index from key to IRI” is backwards. Key → IRI is recomputable. What derived-hash needs is an index from IRI to key, for debugging and support, and that index is itself PII and must be access-controlled.
| Expectation | Status | Notes |
|---|---|---|
| IRIs are opaque global names; one IRI denotes one thing everywhere | ⚠ Conflict | Rule 3 (F-1). Fix by keeping the environment out of identity. |
| IRI equality is exact string equality (RDF 1.1 Concepts §3.2) | ⚠ Gap | Needs canonical-form rules (F-12, F-13). |
| No Unique Name Assumption in OWL | ✅ Compatible | “Structural uniqueness” is about IRI strings, not denotation. Say explicitly that distinct IRIs are not asserted owl:differentFrom, and define the merge policy (F-8). |
| OWL 2 ontology IRI / version IRI | ✅ Strong alignment | Lineage maps to ontology IRI and revision to version IRI. Rule 2 must allow version-bearing version IRIs (F-4). Define how in-graph owl:Ontology headers relate to the registry’s revision IRI (F-14 self-reference). |
owl:imports resolution |
⚠ Needs a catalog | URNs don’t resolve; ship XML catalogs or a resolver (F-13). |
| Types are asserted or inferred, can be many, and can change | ⚠ Conflict | {class} in IRIs (F-10). |
| Punning and graph-name semantics | ✅ Compatible | Using a revision IRI as both a graph name and a fnd:Version individual is fine. RDF 1.1 leaves graph-name denotation open, and OWL 2 DL tolerates it. Document the convention. |
| Named graphs have no aliasing | ⚠ Gap | Alias mechanism undefined (F-11). |
| Linked Data / Cool URIs / dereferenceability | ◑ Partial | “Never change” and “no versions in entity IRIs” are aligned. URNs and environment rewriting are not. Acceptable for internal-only identity; decide on public identity (F-13). |
| Skolemization (RDF 1.1 §3.5) | ⚠ Gap | Not specified (F-16). |
| Triple stores (TDB2, RDF4J, GraphDB, Neptune, Stardog, Virtuoso) | ✅ No blockers | All accept URNs of this length. Random-hash IRIs have poor B-tree locality in stores that index raw strings, but dictionary-encoded stores (most of them) don’t care. Prefix-based STRSTARTS queries over {lineage}/rev/ work but are scans. Use explicit link triples for “all revisions of lineage X.” |
| JSON-LD / Turtle tooling | ◑ Minor | Turtle prefixed names need \/ escapes for / in local names. Avoid @base with URNs. Beware JSON-LD contexts that define a term named urn. |
| GDPR (pseudonymous data is personal data) | ⚠ Conflict | Unkeyed derived-hash (F-6); ULID timestamps (F-5). |
The guide is labeled “authoritative,” and ADR-A54 and ADR-A63 depend on A51. Right now the two documents describe different identity models.
| Topic | ADR-A51 | Patterns guide | Resolution |
|---|---|---|---|
| Entity IRI default | natural-key or derived-hash; surrogate gated | opaque UUIDs plus P1 key claims (§8.4) | Add surrogate-claimed (F-7) |
| Revision identity | content-addressed {lineage}/rev/{pv}-{hash} |
position-addressed urn:rev:{agg}/{seq} |
Both are needed: content versus event (F-9). Name them differently. |
| Versions and times in IRIs | forbidden | v1 salts, txlog/2026-09, seq-bearing receipt, delta and snapshot graph IRIs |
Scope Rule 2 (F-4) |
| Hash for sensitive keys | plain SHA-256 | HMAC (and it explains why plain is unsafe) | Adopt the guide’s position (F-6) |
| IRI grammar | urn:lattice:{tenant}:… |
urn:g:, urn:rev:, urn:key:, urn:txn:, urn:person: |
Guide examples are illustrative, but the guide should include a crosswalk to A51/A54 grammar |
| Snapshot-per-revision graphs (§20.3) | revision IRIs under the lineage | urn:g:orders/1/0000000000000042 |
Align on one form |
A bug in the guide’s identity model: the corrected receipt IRI urn:rev:orders/1/0000000000000042 omits the epoch. The guide’s own F3 lists “receipt IRIs are reused” as a consequence of restore, and fixes it by adding the epoch to the guard, row, receipt and ETag, but not to the IRI.
After a restore that rewinds seq, a new revision 42 under epoch 4 mints the same IRI as the old revision 42 under epoch 3. The old one can still exist in:
pat:prevRev references held elsewhereThat is precisely the identity reuse the ADR’s philosophy forbids. Fix: urn:rev:{aggregate}/e{epoch}/{seq:019}. Also, the guide’s zero-padding width is inconsistent: 16 in examples, 19 “in production” (REV_WIDTH). Identity-bearing padding must be fixed in the grammar, because changing it later re-mints every IRI.
You asked for pros and cons of the material generally, so here is a condensed assessment beyond identity.
ASK plus the mandatory resolve(txnId) and an explicit Unknown outcome is the most valuable design element.min_level fails fast at startup. It forbids silent downgrades of multi-aggregate atomicity. This is mature SPI design.promote().ShaclSail and SERIALIZABLE.ConcurrentModificationException and Streams’ (commitNum, opNum).pat:retracts ?g is compared against the asserted graph ?g. In the patch-log model of §20.2, asserts and retracts point at different delta graphs (…/add versus …/del), so the FILTER NOT EXISTS never matches. Retracted triples are therefore returned.?r2 pattern also doesn’t constrain pat:target or require ?s2 > ?seq.str.strip() doesn’t strip it ('\u200b'.isspace() is False). Either the pipeline needs a “remove default-ignorable code points” step, or the test documents a false guarantee. Either way, this is a good illustration of why normalization pipelines need their own corpus (K-6).deterministic_iri takes 20 bytes, which is 32 base32 characters, but the example output shows 24. It’s minor, but in an “authoritative” guide these get copied into tests.sh:prefixes ex: / sh:prefixes pat: requires those IRIs to carry sh:declare blocks, which are not shown, so validators will fail to resolve prefixes.pat:VersionRowShape references pat:etag, which isn’t in the Appendix A vocabulary. That’s harmless, but should be declared deprecated.NoForkShape caveat is understated. sh:sparql cross-node constraints are exactly what the guide says incremental validators handle poorly (§7.3), so commit-time fork detection may force a full revalidation on every commit. That is worth stating alongside the §15.3 “SHACL trick.”MAX − MIN + 1 doesn’t detect a missing prefix: a stream whose first retained receipt is 5 when retention hasn’t expired. Compare MIN against the expected retention low-water mark.iri-policy.md does for A51, so they can be versioned and tested.Scope. This policy applies to LATTICE-minted IRIs only. External vocabulary IRIs are never rewritten. TBox and vocabulary IRIs are never tenant- or environment-scoped.
Grammar (ASCII-only, lowercase, components drawn from [a-z0-9-], validated by P0.3.7):
| Kind | Form | Denotes |
|---|---|---|
| Lineage | urn:lattice:{tenantId}:{scope}:{family}:{localName} |
fnd:PersistentIdentity (allocated via a P1 claim, with owner) |
| Content revision | {lineage}/rev/{schemeVer}-{hash128} |
fnd:Version (content state; hash covers canonical content with the self-IRI placeholdered, plus profileVersion; full hash verified on re-registration) |
| Promotion or receipt | {lineage}/evt/e{epoch}/{seq:019} |
event; chained by prevRev; points at a content revision |
| Current | pat:current pointer on the version row (normative); optional materialized …/current projection |
— |
Entity strategies:
| Strategy | Form | Notes |
|---|---|---|
natural-key |
{base}/{ns}/{enc(tuple)} |
Immutable, non-PII keys only; length-prefixed tuple encoding |
derived-hash |
{base}/{ns}/h/{hex(sha256(v1‖ns‖enc(tuple)))[0:32]} |
Non-sensitive keys only |
surrogate-claimed |
{base}/{ns}/s/{uuid4} plus a keyed-hash P1 claim |
Recommended default for mutable or sensitive keys |
surrogate |
{base}/{ns}/s/{uuid4} |
Identity-less nodes; declared and justified |
Rules:
{ns} is a registered minting namespace, not an rdf:type. It is never renamed.@base or relative references with URN bases. Only the canonical form is stored.fnd:replacedBy, rather than using owl:sameAs.https mapping) is decided and recorded before any external publication.With these amendments, the policy would be sound, consistent with the patterns guide, and well aligned with RDF and OWL practice.