MAP Descriptor-Independent PVL Design Spec — v0.7¶
1. Purpose¶
This document specifies the descriptor-independent subset of the MAP Peer Validation Language (PVL).
This PVL layer defines the fixed structural and resource-safety rules enforced by the Holochain Integrity Zome without resolving or interpreting:
- authored Descriptor Graphs
TypeDescriptors- persisted or cached effective-surface artifacts
- descriptor caches
- type activation
- coordinator state
- TrustChannel agreements
- dynamic validation rules
The goal is to establish a small, deterministic, peer-reproducible validation floor that can be implemented immediately and remain valid regardless of whether MAP later adopts descriptor-dependent PVL.
The SmartLink structural rules in this specification validate the canonical storage contract defined in the Storage Layer and SmartLink Design Specification, which owns the version 1 tag byte format, the prepared-write boundary, and storage insertion identity. PVL validates that contract; it does not define a competing one.
This specification is intended to be prescriptive enough to support issue definition and implementation.
1.1 Trust Posture¶
This PVL layer is a deterministic hygiene floor, not an adversarial defense system.
MAP AgentSpaces are limited-membership communities. Every space is its own DHT, participation is governed by an Agreement (roles, accessible properties and relationships, Promise Weave entry), and misbehaving members are expected to be removed through governance rather than resisted indefinitely at the storage layer. Some space churn is an accepted cost of learning.
Descriptor-independent PVL therefore exists to:
- reject malformed or non-canonical data regardless of author intent
- bound resource consumption per entry, link, and validation call
- keep the committed graph structurally coherent across entry, link, and lifecycle shapes
- catch coordinator bugs before they pollute the DHT
It deliberately does not attempt:
- spam or flooding defense beyond per-object resource bounds
- per-agent rate limiting or write quotas
- role, agreement, or access enforcement
- semantic validation of any kind
Rate limiting and abuse response are governance and operational concerns. If experience shows that a storage-level control is needed, it can be added in a future DNA version as a deliberate migration.
1.2 Changes in v0.5¶
The normative rule changes concern lifecycle validation (Sections 9, 10.2, 11, and 15 decision 8); this revision also records implementation decisions confirmed during PRs 1–4. All changes precede any release, so the code-reuse rule of Section 13.2 does not apply.
- Corrected the lifecycle sequencing rationale as of v0.5. Before Storage SL2 landed, MAP version-producing writes emitted Holochain
Createactions, so activating the rule early rejected no MAP-authored update. The contract therefore landed as proactive hardening; Storage SL2 subsequently adopted it when version publication switched to root-addressed HolochainUpdateactions. - Removed the immutable-native-field rule.
ImmutableNativeFieldChangedandMAP-PVL-1302are retained but reserved and unused (Section 10.2). - Renamed
InvalidUpdateTarget's fields toexpected_target_kind/actual_target_kind, matchingInvalidDeleteTarget, because the variant's primary failure is an action-kind mismatch (Section 10.2). - Made the valid delete-target policy explicit (Section 10.2).
- Added the lifecycle pure-core/adapter seam (Section 3.3) and the dependency-resolution form (Section 9.2).
- Stated the unresolved-dependency obligation as the negative rule it actually is (Section 11).
- Recorded implementation decisions from PRs 1–4: the violation-contract placement in
integrity_core_types(Sections 10.1, 13.2, and 15 decision 10), thePvlFieldenum and the normative Tag v1 codec-error mapping (Section 10.3), theactual_bytesfield-width convention (Section 10.2), the property-value violationsEmptyEnumValueandMalformedPropertyValuewith the1116/1117reallocation (Sections 10.2 and 14), the shape and naming violations (Section 10.2), and the per-call dependency-budget clarification (Section 9.1).
1.3 Changes in v0.6¶
The normative changes concern the SmartLink envelope work in PR 6:
- Pinned ActionHash-only SmartLink endpoint forms and distinguished endpoint identity failures from Tag v1 grammar failures (Section 8.3).
- Made SmartLink create and delete dependency behavior explicit: creates resolve no dependencies, and a
StoreRecord::DeleteLinkresolves its exact target action withmust_get_actionwithout re-validating the target tag (Section 9). - Added
InvalidLinkDeleteTargetandMAP-PVL-2004so a link-delete target action-kind failure has a deterministic PVL result (Sections 10.2 and 14). - Corrected and hardened the Tag v1 codec-error mapping: invalid UTF-8 in a canonical key maps to
CanonicalKey, the codec separates encode and decode errors, and its typed structural positions are projected intoPvlFieldby one shared pure-core mapper (Section 10.3).
1.4 Changes in v0.7¶
The normative changes concern Milestone 6 integration and the dependency bound:
- Replaced the proposed runtime dependency counter with a structural guarantee. Every descriptor-independent PVL v1 operation requests zero or one DHT dependency, the closed adapter call graph establishes that bound, and exact-call-count tests pin it (Section 9).
- Removed
MAX_VALIDATION_DEPENDENCIES_PER_OP,ValidationDependencyLimitExceeded, andMAP-PVL-3001from the active v1 contract. The3000–3099range remains reserved for a future version that introduces composed or data-driven dependency resolution (Sections 9, 10.2, 13.2, and 14). - Defined coordinator preflight as reuse of the same pure rules over the canonical bytes and typed identities the coordinator intends to author, not as parity with Integrity's Holochain-aware adapter behavior (Sections 3.3 and 13.3).
- Clarified that
AllHolonNodes,HolonNodeUpdates, andLocalHolonSpaceare fixed infrastructure links outside the SmartLink Tag v1 PVL contract. Active infrastructure access paths retain explicit Integrity validation until they are retired (Section 8.6). - Updated lifecycle descriptions for the landed Storage SL2 write path: version-producing writes now author root-addressed Holochain
Updateactions.
2. Validation Guarantee¶
Descriptor-independent PVL proves:
A submitted
HolonNodeor SmartLink is structurally well formed, uses supported native representations, remains within fixed resource bounds, and obeys the descriptor-independent create, update, delete, and structural rules of the DNA.
Fixed infrastructure links that support bootstrap or legacy storage indexes are not SmartLinks and are outside the Tag v1 rules in this specification. Their Integrity obligations are stated separately in Section 8.6.
It does not prove:
- that a property is declared by the holon’s type
- that a descriptor-required property is present
- that a value satisfies descriptor-defined constraints
- that a holon satisfies its descriptor-defined key rule
- that a relationship is declared by the source type
- that a relationship target has the required type
- that cardinality, ordering, exclusivity, or uniqueness constraints hold
- that a descriptor is activated
- that a TrustChannel or agreement permits an operation
- that an agent has authority to create, delete, or repair a relationship link
Those rules remain outside this specification. In particular, relationship-write authority is defined over MAP agents, AgentSpaces, agreements, roles, and security policy — not over Holochain signing keys — and is never inferred from substrate action authorship (Section 8.5).
3. Normative Limit Definition¶
3.1 Location of Limit Constants¶
All descriptor-independent PVL limits must be defined in one Integrity-safe module shared by the Integrity Zome and coordinator-side preflight validation.
Recommended location:
shared_crates/shared_validation/src/pvl_limits.rs
or an equivalently low-gravity crate that:
- compiles to Integrity WASM
- has no coordinator dependencies
- has no ReferenceLayer dependencies
- has no descriptor dependencies
- exposes constants and pure validation helpers
The constants should be grouped into a versioned contract:
pub struct PvlLimitsV1;
or:
pub mod pvl_limits_v1;
The DNA must compile against one exact limit version.
Changing a limit changes Integrity semantics and therefore requires deliberate DNA versioning.
One constant is defined elsewhere and consumed here: the MAP SmartLink v1 tag ceiling MAP_SMARTLINK_V1_MAX_BYTES is part of version 1 wire-format validity and has its single authoritative definition alongside the Tag v1 codec (storage spec Section 9). pvl_limits_v1 re-exports that definition rather than declaring a second constant, so storage packing and PVL enforcement cannot drift.
3.2 Serialization Basis¶
Limits involving entry, tag, or payload size must be measured against the canonical serialized byte representation presented to Integrity.
Do not estimate size from:
- Rust heap allocation
- character count
- JSON text
- debug formatting
- decoded object size
Use serialized bytes.
For UTF-8 strings and names, limits are measured in UTF-8 bytes, not Unicode scalar values or displayed characters.
3.3 Layering: Pure Core and Substrate Adapter¶
MAP core crates must not depend on Holochain. Descriptor-independent PVL is therefore split into two layers:
holons_integrity (zome) — callback declaration, dispatch, and result projection only
-> holons_guest_integrity — substrate adapter (Holochain-aware)
-> shared_validation — pure PVL core (substrate-independent)
-> core_types -> integrity_core_types
The pure core (shared_validation):
- holds the limit contract and every check expressible over
integrity_core_typesdata (HolonNode,PropertyMap,BaseValue,LocalId, and the decoded SmartLink storage types) - re-exports the
PvlViolationcontract and error-code registry owned byintegrity_core_types - validates hash-shaped identifiers structurally, by role and byte shape, without parsing Holochain types
- has no
hdi,hdk, orholo_hashdependency
The canonical SmartLink Tag v1 codec and the storage-boundary types it operates on live in core_types: the SmartLink shape carries HolonId and external routing identity, which core_types owns, and core_types already depends on integrity_core_types, so a lower placement would create a dependency cycle. The storage implementation plan specifies the same core_types placement. PVL consumes that shared codec; it must not define a second decoder or a competing tag model.
The substrate adapter (holons_guest_integrity):
- maps Holochain ops and actions onto lifecycle checks
- parses exact Holochain hash types where they are available
- resolves the zero-or-one deterministic dependency permitted by each operation's closed call path
- passes resolved, decoded data into pure core functions
For Tag v1, the pure core owns the one mapping from codec decode errors to PvlViolation. Both the substrate adapter and coordinator preflight invoke that mapper through the SmartLink envelope validator; neither maintains a second mapping.
Lifecycle rules are the case where the pure core cannot see its own input. Whether an update's target is a lineage-root Create carrying a HolonNode is a fact only an adapter can resolve. The seam is a small substrate-free description of the resolved target — its action kind and its entry kind — owned by shared_validation and populated by the Integrity adapter. The pure core judges that description. It never receives a Holochain Action or Record, never receives a mirror of one, and never requires entry content to reach a lifecycle verdict. Keeping the input this narrow makes the rule exhaustively testable without a conductor and keeps Holochain facts out of the pure core.
An earlier sketch of a general substrate-free action model exists in integrity_core_types/src/hc_action.rs (PersistenceAction and its Persistence* structs). It mirrors Holochain's action shape rather than any rule's actual input, and the fields lifecycle validation needs are commented out in it. It is superseded. PVL lifecycle validation defines its own minimal facts type, and the Persistence* values still constructed in the Integrity zome are retired as each rule that would have consumed them lands.
Coordinator preflight invokes the pure core directly at the canonical persistence write boundaries. For a HolonNode, it validates the canonical serialized bytes and decoded model immediately before create_entry or update_entry. For a SmartLink, it validates the source identity, target identity, and once-encoded Tag v1 bytes immediately before create_link; the validated bytes are the bytes submitted to Holochain.
This is rule reuse, not validation parity. Preflight begins with typed coordinator-owned values and can prove that the intended canonical write satisfies the shared pure rules. Integrity additionally validates arbitrary peer-supplied raw encodings and Holochain operation facts. Exact Holochain hash-kind parsing, op-to-rule mapping, target-action classification, must_get_* resolution, and UnresolvedDependencies behavior remain Integrity-adapter responsibilities. Preflight cannot reproduce malformed raw encodings that its typed serializers cannot construct, and passing preflight does not predict that the conductor must accept the operation.
Do not build a coordinator clone of the HDI operation adapter merely to simulate parity. Coordinator lifecycle and storage checks may resolve facts needed for their own write contract, but those checks remain distinct from Integrity's peer-validation path. Both paths share pure functions wherever their inputs genuinely overlap.
4. PVL v1 Limits¶
These are the normative v1 values. The entry and value limits remain initially measured rather than production-ratified; the SmartLink v1 validity ceiling is ratified as a format constant.
The initially measured limits must be ratified through the fixture and benchmark process in Section 12 before they freeze into a production DNA.
| Constant | Value |
|---|---|
MAX_HOLON_NODE_BYTES |
262,144 bytes |
MAX_PROPERTY_COUNT |
256 |
MAX_PROPERTY_NAME_BYTES |
128 bytes |
MAX_STRING_VALUE_BYTES |
16,384 bytes |
MAX_ENUM_VALUE_BYTES |
256 bytes |
MAX_CANONICAL_KEY_BYTES |
256 bytes |
MAX_BYTES_VALUE_BYTES |
131,072 bytes |
MAX_COLLECTION_ITEMS |
1,024 |
MAX_VALUE_NESTING_DEPTH |
2 |
MAP_SMARTLINK_V1_MAX_BYTES |
512 bytes |
MAX_RELATIONSHIP_NAME_BYTES |
128 bytes |
MAX_REMOTE_OBJECT_ID_BYTES |
256 bytes |
The design intent behind each value is defined below.
5. HolonNode Limits¶
5.1 Total HolonNode Entry Size¶
MAX_HOLON_NODE_BYTES = 262_144
A serialized HolonNode must not exceed 256 KiB.
Rationale:
- ordinary MAP holons should contain structured semantic data, not bulk media
- large binary artifacts should be stored outside the DHT and referenced by digest or locator
- 256 KiB provides substantial room for rich property maps
- the limit bounds decoding, hashing, validation, gossip, storage, and traversal cost
- the limit is substantially below the substrate ceiling
Validation rule:
serialized_holon_node_len <= MAX_HOLON_NODE_BYTES
Violation:
PvlViolation::HolonNodeTooLarge {
actual_bytes,
max_bytes,
}
5.2 Property Count¶
MAX_PROPERTY_COUNT = 256
A HolonNode.property_map must contain no more than 256 entries.
Rationale:
- most domain holons should be far smaller
- unusually broad types remain possible
- validation effort is bounded independently of serialized size
- an attacker cannot create a pathological number of tiny properties
Validation rule:
property_map.len() <= MAX_PROPERTY_COUNT
Violation:
PvlViolation::TooManyProperties {
actual_count,
max_count,
}
5.3 Property Name Length¶
MAX_PROPERTY_NAME_BYTES = 128
Each property name must:
- be valid UTF-8
- be non-empty
- contain no leading or trailing whitespace
- contain no control characters
- encode to no more than 128 UTF-8 bytes
Property names should use the existing MAP canonical naming rules. PVL must not invent a competing normalization algorithm.
If the existing property-name type already guarantees part of this contract, PVL should rely on that constructor and separately enforce the byte limit.
Violations:
PvlViolation::EmptyPropertyName
PvlViolation::InvalidPropertyName {
reason,
}
PvlViolation::PropertyNameTooLong {
actual_bytes,
max_bytes,
}
6. Property Value Limits¶
6.1 String Values¶
MAX_STRING_VALUE_BYTES = 16_384
A scalar string value must not exceed 16 KiB of UTF-8 data.
This applies to ordinary string values, descriptions, labels, and other unrestricted native string representations unless a narrower native limit applies.
Violation:
PvlViolation::StringValueTooLarge {
property_name,
actual_bytes,
max_bytes,
}
Descriptor-defined string limits may be narrower, but they are outside descriptor-independent PVL.
6.2 Enum Values¶
MAX_ENUM_VALUE_BYTES = 256
A native enum token must:
- be valid UTF-8
- be non-empty
- encode to no more than 256 bytes
This rule validates only the native representation.
It does not verify that the value is a member of a descriptor-defined enum.
Violations:
PvlViolation::EmptyEnumValue {
property_name,
}
PvlViolation::EnumValueTooLarge {
property_name,
actual_bytes,
max_bytes,
}
6.3 Integer Values¶
Integer values must decode into the exact integer representation supported by PropertyValue.
PVL must reject:
- unsupported integer widths
- overflowing conversions
- alternate serialized representations that decode ambiguously
- floating-point values presented where an integer is required by the native enum
No additional magnitude limit is required if the Rust type is already a fixed-width signed integer.
Violation:
PvlViolation::UnsupportedNativeValue {
property_name,
value_kind,
}
or:
PvlViolation::MalformedPropertyValue {
property_name,
reason,
}
6.4 Boolean Values¶
Boolean values must use the one canonical native Boolean representation.
Numeric or string substitutes such as:
0
1
"true"
"false"
must not be accepted as native Boolean values.
Violation:
PvlViolation::MalformedPropertyValue {
property_name,
reason,
}
6.5 Byte Values¶
MAX_BYTES_VALUE_BYTES = 131_072
A single native bytes property must not exceed 128 KiB.
The total HolonNode limit still applies, so a holon cannot contain multiple maximum-sized byte values.
Bulk files, images, audio, video, model binaries, and other large artifacts must not be embedded directly in a HolonNode.
Violation:
PvlViolation::BytesValueTooLarge {
property_name,
actual_bytes,
max_bytes,
}
6.6 Collection Values¶
Where native property arrays or collections are supported:
MAX_COLLECTION_ITEMS = 1_024
A collection must not contain more than 1,024 elements.
Each element must independently satisfy its scalar or byte-value limit.
A collection must be homogeneous if the native PropertyValue variant declares a homogeneous array type.
PVL validates only native collection representation. It does not validate descriptor-defined minimum or maximum collection cardinality.
Violations:
PvlViolation::CollectionTooLarge {
property_name,
actual_items,
max_items,
}
PvlViolation::HeterogeneousCollection {
property_name,
expected_kind,
actual_kind,
item_index,
}
Grounding note (v0.2): the current PropertyValue (BaseValue) has no collection variant, so these rules are satisfied by construction and require only regression tests until a collection representation is introduced.
6.7 Nesting Depth¶
MAX_VALUE_NESTING_DEPTH = 2
PVL v1 permits only:
- depth 1: scalar value
- depth 2: collection of scalar values
Recursive arrays, arrays of arrays, arbitrary maps, and nested object values are not supported unless a future native PropertyValue representation explicitly adds them.
Violation:
PvlViolation::ValueNestingTooDeep {
property_name,
actual_depth,
max_depth,
}
If the current PropertyValue model cannot represent nested values, this limit is satisfied by construction and requires only a regression test.
Grounding note (v0.2): verified — BaseValue is scalar-only, so value depth is fixed at 1 today. The depth-2 allowance is reserved headroom for a future collection representation.
6.8 PropertyMap Absence and Null Values¶
This section applies only to the native PropertyMap representation of holon properties.
The canonical PropertyMap contract is:
BTreeMap<PropertyName, PropertyValue>
A property is either:
- absent from the map
- present with a concrete
PropertyValue
There is no third state for a present-but-null property value.
Descriptor-independent PVL must reject any native property-map encoding that represents a property as present with None, Null, or any other nullable value form.
Absence remains structurally valid. Descriptor-independent PVL does not enforce requiredness; descriptor-dependent validation later determines whether an absent property violates a descriptor-required field.
Violation:
PvlViolation::UnsupportedNativeValue {
property_name: Some(property_name),
value_kind,
}
7. Identifier Limits¶
7.1 Holochain Hash-Shaped Identifiers¶
Identifiers representing Holochain hashes must be validated using the existing canonical parser or validated wrapper type.
PVL must not rely only on a generic maximum-length check.
The validation should confirm:
- exact supported encoded length
- supported hash prefix/type
- checksum or internal hash encoding validity, where provided by the Holochain type
- correct role, where a role-specific wrapper exists
Examples include:
ActionHashEntryHashAgentPubKey- current ActionHash-shaped
LocalId
Violation:
PvlViolation::InvalidIdentifier {
field_name,
identifier_kind,
reason,
}
No separate arbitrary maximum should override the exact native hash shape.
Per §3.3, exact Holochain hash parsing happens in the substrate adapter. The pure core enforces role and byte shape — for example, the 39-byte ActionHash-shaped LocalId — without depending on holo_hash.
Grounding note (v0.2): LocalId is currently LocalId(pub Vec<u8>) with no validating constructor, so the shape check is new PVL logic in the pure core, not a delegation to an existing parser.
7.2 RemoteObjectId¶
MAX_REMOTE_OBJECT_ID_BYTES = 256
Where a native structure permits an opaque RemoteObjectId, it must:
- be non-empty
- contain no more than 256 bytes
Its internal semantics are steward-defined and are not interpreted by PVL.
Violations:
PvlViolation::EmptyIdentifier {
field_name,
identifier_kind,
}
PvlViolation::IdentifierTooLong {
field_name,
identifier_kind,
actual_bytes,
max_bytes,
}
If RemoteObjectId is not present in an Integrity-visible structure, this rule is not invoked in the initial implementation.
8. SmartLink Limits¶
8.1 SmartLink Tag or Payload Size¶
MAP_SMARTLINK_V1_MAX_BYTES = 512
The complete serialized version 1 SmartLink tag must not exceed 512 bytes.
The storage specification (Section 9) distinguishes three values. The Holochain LinkTag ceiling (1,024 bytes) is a substrate platform fact and is never duplicated as an independent MAP constant. The MAP SmartLink v1 ceiling (512 bytes) is normative wire-format validity: the shared codec rejects a larger version 1 tag as malformed, and PVL rejects it before persistence. The active packing budget (initially 512 bytes) is storage writer policy; it may be lowered without narrowing validity, so PVL accepts structurally valid version 1 tags up to the 512-byte ceiling regardless of the packing budget in force when a link was written.
The ceiling has one shared authoritative definition alongside the Tag v1 codec (Section 3.1); storage and PVL consume that definition rather than declaring separate limits. Changing the MAP ceiling is a format-evolution decision and a DNA-versioning event.
The limit includes all encoded Tag v1 fields:
- relationship-name and canonical-key prefix segments
- payload version and flags
- external routing identity (
OutboundProxyId) - occurrence identity
- the authoritative relationship-property section
- the cached target-property section
Violation:
PvlViolation::SmartLinkTagTooLarge {
actual_bytes,
max_bytes,
}
8.2 Relationship Identifier Length¶
MAX_RELATIONSHIP_NAME_BYTES = 128
A relationship identifier must:
- be valid UTF-8
- be non-empty
- contain no NUL byte (it is a NUL-delimited tag prefix segment)
- contain no control characters
- have no leading or trailing whitespace
- encode to no more than 128 bytes
This rule validates only the identifier representation.
It does not verify that the relationship is declared by a descriptor.
Violations:
PvlViolation::EmptyRelationshipName
PvlViolation::InvalidRelationshipName {
reason,
}
PvlViolation::RelationshipNameTooLong {
actual_bytes,
max_bytes,
}
8.3 SmartLink Base and Target¶
A SmartLink base and Holochain link target are ActionHash identities. The base is the local source action hash. The Holochain target is the local target action hash: with the external-target flag clear it is the whole target identity, and with the flag set it is paired with the Tag v1 OutboundProxyId as the external target's routing identity.
The pure core validates the role and 39-byte ActionHash shape of these values. The substrate adapter parses the base, link target, and present OutboundProxyId as exact ActionHash values where Holochain types are available.
PVL must reject endpoint identities with:
- unsupported hash kinds
- malformed encoded hashes
Payload reserved bits, external-flag/proxy framing, and occurrence framing are Tag v1 grammar concerns. They are rejected through MalformedSmartLink according to the codec mapping in Section 10.3, rather than as endpoint-identity violations.
Violations:
PvlViolation::InvalidSmartLinkEndpoint {
endpoint,
reason,
}
PvlViolation::UnsupportedSmartLinkEndpointKind {
endpoint,
endpoint_kind,
}
8.4 Canonical Key Values¶
MAX_CANONICAL_KEY_BYTES = 256
Every SmartLink tag carries a structurally mandatory CanonicalKey segment (storage spec Sections 3.3 and 7.2). The segment must:
- be valid UTF-8
- contain no NUL byte (it is a NUL-delimited tag prefix segment)
- encode to no more than 256 UTF-8 bytes
An empty canonical key is valid and represents a keyless target type.
The coordination/reference layer computes the canonical key from the target's descriptor semantics before storage insertion. Descriptor-independent PVL validates only the native representation and the byte bound; it does not verify that the key was computed correctly for the target's type.
Rationale:
- the canonical key participates in the tag prefix grammar, so exact-key and key-prefix retrieval depend on its structural validity
- an unbounded key would let a holon that passes every entry-level limit still push its relationship tags toward
MAP_SMARTLINK_V1_MAX_BYTESand squeeze out authoritative relationship properties - the largest key in the current core-schema corpus is 96 bytes, giving 2.6× headroom
Violation:
PvlViolation::CanonicalKeyTooLarge {
actual_bytes,
max_bytes,
}
A canonical key containing invalid UTF-8 or a NUL byte is a malformed tag and maps to MalformedSmartLink with the appropriate reason.
8.5 Occurrence Identity and Provenance¶
Descriptor-independent PVL validates SmartLink structure and provenance, not relationship write authority.
Inverse pairing in the canonical storage model is carried by occurrence identity, not by a forward-link hash (storage spec Sections 3.1, 5.1, and 6):
- a declared link and the inverse realization of the same semantic occurrence share an
OccurrenceIdand have distinctSmartLinkIdvalues - for non-duplicate relationships, the semantic identity is source + target + relationship + absent occurrence ID
SmartLinkIdis the Holochain create-link action hash and is never encoded in the tag
PVL v1 validates the structural form of occurrence identity:
- when the occurrence payload flag is set, the tag must carry exactly 16
OccurrenceIdbytes - reserved payload-flag bits must be zero
- whether an occurrence ID is required or forbidden for a given relationship depends on its
AllowsDuplicatespolicy, which is descriptor-defined and therefore outside descriptor-independent PVL
Tag version 1 carries no deterministic reference to the forward realization, and Integrity has no link-query surface, so cross-link correspondence between a declared link and its inverse is not verifiable in descriptor-independent PVL v1. If a future tag field deterministically references the forward realization, PVL may verify that the referenced action mirrors the inverse link's base, target, relationship identity, occurrence identity where present, and required structural fields.
PVL must not infer relationship-write authority from the Holochain action author of the source HolonNode. Holochain action authors are substrate-level signing agents, while MAP authority is defined over MAP agents, AgentSpaces, agreements, roles, and security policy. Whether a person, device-agent, organization, or delegated service may assert a relationship is not determined by CreateLink.author == source_holon_action.author.
Authorization to create, delete, or repair SmartLinks is outside descriptor-independent PVL unless a future DNA defines a fixed, integrity-local authorization rule.
8.6 Infrastructure Links¶
AllHolonNodes, HolonNodeUpdates, and LocalHolonSpace are fixed Holochain infrastructure link types. They are not relationship SmartLinks, do not carry Tag v1, and must not be routed through the SmartLink envelope validator.
Their treatment follows their active storage responsibility:
HolonNodeUpdatesis obsolete under native root-addressed Holochain updates. MAP authors no links of this type. Until the type and its remaining readers are retired together, new creates are rejected and deletes may be accepted for cleanup.AllHolonNodesremains an active legacy whole-space index until Storage SL5 replaces its remaining consumers. Its fixed path-base, empty-tag, and HolonNode-target contract retains explicit Integrity validation while the index is live; deletes remain invalid while the index is authoritative.LocalHolonSpaceremains an active bootstrap path. Its fixed path-base, empty-tag, and HolonNode-target contract retains explicit Integrity validation, and deletion remains valid because deleting a local-space holon removes the path link.
These small fixed validators belong in the Holochain-aware integrity adapter because they classify Holochain link endpoints and, where required, resolve a target record. They are infrastructure integrity rules, not new PvlViolation variants. The Integrity zome owns only link-type dispatch and callback-result projection.
Removing scaffold-generated validator functions is not permission to leave an active infrastructure access path unvalidated. A link type and its validator are removed together only after its writers and readers have been retired or migrated.
9. Validation Dependency Bound¶
9.1 Structural Maximum¶
Descriptor-independent PVL v1 has no runtime dependency counter. Its closed operation call graph structurally permits at most one DHT dependency request per validation call:
| Operation | Expected dependencies |
|---|---|
Create HolonNode |
0 |
Update HolonNode |
1 |
Delete HolonNode |
1 |
| Create SmartLink (declared or inverse) | 0 |
Delete SmartLink (RegisterDeleteLink / StoreRecord::DeleteLink) |
0 / 1 |
The bound is per validation call, not per logical write. One authored update reaches validation through several flattened operations, each validated independently and usually by a different authority; each call follows its own zero-or-one-dependency path.
The adapter establishes this bound through operation-specific entry points rather than a general validation context. Tests must pin the exact must_get_* call count for every adapter path, including zero-call paths, and callback routing tests must prove that every applicable flattened operation reaches the intended entry point.
PVL must not perform:
- open-ended relationship traversal
- recursive graph search
- “get all links”
- repeated retries inside validation
- dependency requests driven by unbounded user collections
If a required dependency is unavailable, validation returns UnresolvedDependencies.
That is not a HolonError and must not be reported as invalid data.
If a future PVL version introduces a rule that composes multiple dependencies or derives requests from submitted data, that version must define and enforce an explicit budget before the rule is activated. The 3000–3099 error-code range is reserved for that future contract; v1 has no dependency-limit violation because no reachable v1 operation can exceed the structural maximum.
9.2 Dependency Resolution Form¶
Both HDI dependency calls short-circuit the callback to UnresolvedDependencies when the target is unknown to the visible network, so either satisfies Section 11. They differ in what else they guarantee and what they cost:
must_get_actionreturns the action alone and does not guarantee the target was ever validated; an invalid action that was published can still be returned.must_get_valid_recordreturns the full record — action plus entry, up toMAX_HOLON_NODE_BYTES— and guarantees the visible network consistently reports it valid. That guarantee is what makes inductive validation of a graph structure possible.
Choose by whether the target is a structural parent:
- Update target — the lineage root is the structural parent of every version in its lineage, so use
must_get_valid_record. Root-addressing bounds the induction at exactly one hop, and a lineage must not be rooted at an entry the network reports invalid. - Delete target — a delete names one exact version, asserts no lineage, and carries no structure forward, so use
must_get_action. - SmartLink create — the complete tag and endpoint identities are carried by the operation. PVL resolves no endpoint existence dependencies; it validates only their exact native forms.
- SmartLink delete —
RegisterDeleteLinkalready carries the originalCreateLinkand resolves no dependency.StoreRecord::DeleteLinkusesmust_get_actionto establish that the exact named target is aCreateLink; the target's scoped link type then selects the applicable validator rather than constituting a second target-validity axis. Neither form deserializes a record or re-validates the target tag, whose structural validity was established at creation.
In both cases the rule reads the target's entry type from its action. Lifecycle validation must not deserialize the target entry. The entry's own validity was established when it was authored, and decoding it again pays up to MAX_HOLON_NODE_BYTES of work per validated operation to learn a fact the action already carries — which is exactly the multiplication the Section 12.2 acceptance rule forbids.
10. Structured PVL Violations¶
10.1 Recommended Error Model¶
Do not create a flat HolonError variant for every individual PVL rule.
Add one structured top-level variant:
pub enum HolonError {
...
PvlViolation(PvlViolation),
}
Define a dedicated Integrity-safe enum:
pub enum PvlViolation {
...
}
This keeps the main HolonError stable while allowing PVL violations to remain:
- typed
- testable
- serializable
- mapped to deterministic callback messages
- reusable by coordinator preflight checks
Placement: because HolonError is defined in integrity_core_types, the violation contract — this enum, PvlMalformedReason, the PvlField enum (Section 10.3), and the error-code registry (Section 13.2) — is defined beside it in integrity_core_types/src/pvl_error.rs and re-exported from shared_validation for the pure-core import path. Defining it in shared_validation would form a dependency cycle, since shared_validation already depends on integrity_core_types (Section 15, decision 10). The versioned limit constants and pure measurement helpers stay in shared_validation: they are not wrapped by HolonError and carry no cycle.
10.2 Required PvlViolation Variants¶
The variants below compose one PvlViolation enum carrying the same posture as the rest of the violation contract: #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)], externally tagged (serde default), matching the existing HolonError convention so the TypeScript mirror is mechanical. Some variants carry descriptive String fields (value_kind, field_name, identifier_kind, endpoint, endpoint_kind, expected_*_kind, and the naming/endpoint reason fields); these are local diagnostics only and are excluded from the deterministic Section 13.1 message summary — the consensus-visible content is the MAP-PVL-<code> prefix plus the variant's numeric fields. This is a different concern from the decode-reason enums of Section 10.3, which forbid String because they feed the deterministic malformed message directly. Grouped by category:
Malformed entry¶
MalformedHolonNode {
reason: PvlMalformedReason,
}
Use when the entry cannot be decoded or violates the fixed native entry shape.
Malformed link¶
MalformedSmartLink {
reason: PvlMalformedReason,
}
Use when the tag or payload cannot be decoded or violates the fixed native SmartLink shape.
Unsupported native representation¶
UnsupportedNativeValue {
property_name: Option<PropertyName>,
value_kind: String,
}
Use when data decodes into a representation not supported by PVL v1.
Malformed native property value¶
EmptyEnumValue {
property_name: PropertyName,
}
MalformedPropertyValue {
property_name: PropertyName,
reason: PvlMalformedReason,
}
EmptyEnumValue is the empty-token case of the Section 6.2 enum rule. MalformedPropertyValue covers a native value that decodes into a non-canonical or unsupported representation where a distinct malformed reason is more useful than UnsupportedNativeValue — for example a non-canonical Boolean (Section 6.4) or an ambiguous/alternate integer encoding (Section 6.3). Both validate only the native representation; neither consults descriptors.
Resource-limit violations¶
Use specific variants rather than one opaque limit error:
HolonNodeTooLarge {
actual_bytes: u32,
max_bytes: u32,
}
TooManyProperties {
actual_count: u16,
max_count: u16,
}
PropertyNameTooLong {
actual_bytes: u32,
max_bytes: u32,
}
StringValueTooLarge {
property_name: PropertyName,
actual_bytes: u32,
max_bytes: u32,
}
EnumValueTooLarge {
property_name: PropertyName,
actual_bytes: u32,
max_bytes: u32,
}
BytesValueTooLarge {
property_name: PropertyName,
actual_bytes: u32,
max_bytes: u32,
}
CanonicalKeyTooLarge {
actual_bytes: u16,
max_bytes: u16,
}
CollectionTooLarge {
property_name: PropertyName,
actual_items: u32,
max_items: u32,
}
ValueNestingTooDeep {
property_name: PropertyName,
actual_depth: u8,
max_depth: u8,
}
SmartLinkTagTooLarge {
actual_bytes: u16,
max_bytes: u16,
}
RelationshipNameTooLong {
actual_bytes: u16,
max_bytes: u16,
}
IdentifierTooLong {
field_name: String,
identifier_kind: String,
actual_bytes: u16,
max_bytes: u16,
}
Field-width convention for actual_bytes: this reports the size of the rejected input, which can be far larger than the limit, so the width must cover the largest input that can reach the check — not the limit. Free-standing entry values (HolonNodeTooLarge, StringValueTooLarge, BytesValueTooLarge, PropertyNameTooLong, EnumValueTooLarge) are bounded only by MAX_HOLON_NODE_BYTES (262,144) and therefore use u32; a narrower type would silently wrap an oversized input to a misleading value in the deterministic message. Values embedded in a SmartLink tag (CanonicalKeyTooLarge, RelationshipNameTooLong, SmartLinkTagTooLarge) are bounded by the ≤512/1024-byte tag ceiling and use u16, matching the codec's own width. These fields exist only on the transient PvlViolation error value and never enter a persisted entry or tag.
Shape and naming violations¶
These reject a value that decodes cleanly but violates a native shape or naming rule (Sections 6.1, 6.5, 7.1, 7.2, 8.2, 8.3). They consult no descriptor.
EmptyPropertyName
InvalidPropertyName {
reason: String,
}
EmptyRelationshipName
InvalidRelationshipName {
reason: String,
}
HeterogeneousCollection {
property_name: PropertyName,
expected_kind: String,
actual_kind: String,
item_index: u32,
}
InvalidIdentifier {
field_name: String,
identifier_kind: String,
reason: String,
}
EmptyIdentifier {
field_name: String,
identifier_kind: String,
}
InvalidSmartLinkEndpoint {
endpoint: String,
reason: String,
}
UnsupportedSmartLinkEndpointKind {
endpoint: String,
endpoint_kind: String,
}
The naming and endpoint reason fields are typed as String for parity with the other descriptive _kind fields, and are likewise diagnostic-only (excluded from the deterministic message). InvalidPropertyName / InvalidRelationshipName cover the non-empty, no-NUL, no-control-character, and no-leading/trailing-whitespace rules — the length rule is the separate *TooLong variant. InvalidSmartLinkEndpoint covers malformed exact ActionHash identities in the base, link target, or present outbound proxy; UnsupportedSmartLinkEndpointKind covers a base or target whose Holochain hash kind is not ActionHash. Tag flags and field framing are malformed-tag conditions (Section 8.3). HeterogeneousCollection is satisfied by construction until a collection PropertyValue representation exists (Section 6.5).
Invalid update target¶
InvalidUpdateTarget {
expected_target_kind: String,
actual_target_kind: String,
}
Use when:
- the update's
original_action_addressdoes not reference aCreateaction — update-to-update chains are invalid in MAP's root-addressed lineage topology (KEA; storage plan SL2) - the referenced action does not carry the
HolonNodeentry type
Field naming changed in v0.5. The variant's primary failure is an action-kind mismatch, not an entry-kind mismatch, so it now uses the same neutral *_target_kind fields as InvalidDeleteTarget. Both fields are diagnostic-only and excluded from the deterministic message (Section 13.1): the consensus-visible content is MAP-PVL-1301 whichever axis failed. Check the action kind first and report action tokens, then the entry kind and report entry tokens, so the diagnostic names the axis that actually failed.
This rule originally landed before Storage SL2 as proactive hardening. Storage SL2 has since landed: PublishVersion now calls update_entry against the resolved lineage-root Create, so normal MAP version publication exercises the rule. The persisted entry contains semantic content only; lineage is carried by Holochain record metadata, not by an in-entry field.
ImmutableNativeFieldChanged {
field_name: String,
}
Reserved, unused as of v0.7. Storage SL2 removed original_id, leaving no native field that carries a cross-version invariant to compare. The variant and its MAP-PVL-1302 code are retained for a future field that does.
Invalid delete target¶
InvalidDeleteTarget {
expected_target_kind: String,
actual_target_kind: String,
}
A delete target is valid when the action it names is a Create or an Update whose entry type is HolonNode. Both are accepted because each names one exact persisted version: under the landed root-addressed lineage model, a root is a Create, a later version is an Update, and exact-version identity is the enclosing record's action hash in both cases.
Use InvalidDeleteTarget when a delete targets:
- an action that is neither a
Createnor anUpdate - an action carrying no app entry, or an app entry type other than
HolonNode
Deleting a non-root version is structurally valid. Whether a given version should be deleted — lineage policy, head selection, tombstoning — is a coordination concern and is outside descriptor-independent PVL.
Invalid link delete target¶
InvalidLinkDeleteTarget {
expected_target_kind: String,
actual_target_kind: String,
}
Use when a DeleteLink names an action that is not a CreateLink. Both fields are diagnostic-only; the consensus-visible result is always MAP-PVL-2004.
The target's scoped link type selects the applicable validator; it is not a second failure axis for this violation. RegisterDeleteLink carries a resolved CreateLink, so the rule is satisfied by construction there. StoreRecord::DeleteLink is the enforcement point because it begins with an unclassified target action. The target tag is not decoded again during delete validation: its create operation already established structural validity.
Link authorship and provenance¶
Removed in v0.3. Relationship-write authority is not a descriptor-independent PVL concern, and Tag v1 carries no forward-link reference to verify (Section 8.5). The MAP-PVL-2110–2119 code block is reserved for forward-reference provenance validation if a future tag field introduces it.
10.3 Malformed-Reason Enum¶
PvlMalformedReason and PvlField are two orthogonal axes: the reason names what kind of failure occurred; the field names where in the grammar it occurred. Keep them independent — never fold a failure kind into a field name (there is no TruncatedPropertyName; that state is InvalidLength(PropertyName)). This orthogonality is what keeps both enums small and the codec-error mapping (below) total.
To keep messages stable, use typed malformed reasons:
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PvlMalformedReason {
DecodeFailed,
MissingField(PvlField),
InvalidDiscriminant(PvlField),
InvalidUtf8(PvlField),
InvalidLength(PvlField),
InvalidFieldCombination,
NonCanonicalEncoding,
}
PvlField is a small owned, serializable enum naming the structural position that was malformed. It replaces the earlier &'static str payload, which cannot participate in an owned deserialize/round-trip contract — and Section 13.3 requires PvlViolation to cross the WASM/preflight boundary as an owned value. A variant names a semantic role in the byte grammar, not a byte offset: a position's length-prefix and its payload collapse into one field, because the reason already distinguishes a truncation (InvalidLength) from a bad-UTF-8 payload (InvalidUtf8) at the same field. One variant serves both the native-entry and the Tag v1 grammars when the role is the same; the enclosing PvlViolation variant (MalformedHolonNode vs MalformedSmartLink) supplies which grammar. The set is exhaustive for the v1 native-entry and Tag v1 grammars:
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PvlField {
// Shared property structure (both grammars)
PropertyName,
PropertyValueDiscriminant,
PropertyValue,
// HolonNode native entry (implementation plan PR 3)
HolonNodeEntry,
PropertyMap,
// SmartLink Tag v1 grammar (implementation plan PR 6 / PR 8)
TagHeader,
RelationshipName,
CanonicalKey,
PayloadVersion,
PayloadFlags,
OutboundProxyId,
OccurrenceId,
PropertySectionType,
PropertySection,
}
PropertySection covers all section-level framing failures (declared length and boundary crossing), matching the codec's single SectionBoundaryCrossing error; it is not split into a separate length field.
The enumeration is stable and owned by the violation contract (integrity_core_types/src/pvl_error.rs, Section 10.1). It is exhaustive for the v1 grammars; it may grow when a grammar version does, but each addition is a deliberate, co-versioned contract change, and every value is a fixed deterministic token with no free-form text. Both enums are externally tagged (serde default) so unit PvlField variants serialize as plain strings and reason newtypes serialize as {"MissingField": "RelationshipName"}, mapping directly to the TypeScript SDK's discriminated union. Neither enum carries #[serde(other)] or any String catch-all: on a consensus-visible contract an unknown token must fail deserialization loudly, and forward compatibility is handled by a version bump, never by silent acceptance. The absence of any String payload is also what makes Eq derivable.
Codec-error mapping (normative)¶
The shared Tag v1 codec (core_types/src/smartlink/codec.rs) defines separate typed encode and decode errors (SmartLinkTagEncodeError and SmartLinkTagDecodeError). Their field-bearing variants use a codec-owned typed structural-position enum, never a free-form string. The codec remains independent of PVL; shared_validation owns the one pure mapping from decode positions into PvlField and from SmartLinkTagDecodeError into PvlViolation. The substrate adapter and coordinator preflight must invoke that shared mapper rather than reproduce it.
Every decode-reachable error maps onto exactly one (PvlMalformedReason, PvlField?) pair — or, for supplied link-target identity, the shape violation noted below — so the deterministic message for a given malformed tag is identical on the guest and in preflight. This total mapping is the obligation that fixes the PvlField set, and it is stated against that decoder.
Truncation is uniform: every UnexpectedEnd(x) — the byte stream ends before a fixed position is complete — maps to MissingField at that position's field. The fixed positions and their fields are TagHeader, PayloadVersion, PayloadFlags, OutboundProxyId, OccurrenceId, PropertySectionType, and PropertySection (a truncated section-length prefix).
SmartLinkTagDecodeError |
PvlMalformedReason |
PvlField |
|---|---|---|
UnexpectedEnd(x) (any fixed position) |
MissingField |
field for x (see truncation rule above) |
InvalidHeader (bytes present but wrong) |
InvalidDiscriminant |
TagHeader |
MissingDelimiter (relationship / key) |
MissingField |
RelationshipName / CanonicalKey |
InvalidUtf8 (relationship / canonical key / property name / string or enum property value) |
InvalidUtf8 |
RelationshipName / CanonicalKey / PropertyName / PropertyValue |
UnsupportedVersion |
InvalidDiscriminant |
PayloadVersion |
UnknownFlags |
InvalidDiscriminant |
PayloadFlags |
UnknownSectionType |
InvalidDiscriminant |
PropertySectionType |
UnknownValueType |
InvalidDiscriminant |
PropertyValueDiscriminant |
SectionBoundaryCrossing |
InvalidLength |
PropertySection |
InvalidIntegerLength |
InvalidLength |
PropertyValue |
InvalidBooleanValue |
NonCanonicalEncoding |
— |
EmptySection, DuplicateSection, NonCanonicalSectionOrder, NonCanonicalPropertyOrder |
NonCanonicalEncoding |
— |
InvalidBooleanValue maps to NonCanonicalEncoding, not InvalidLength: a one-byte boolean holding 0x02 has the correct length and a non-canonical value. Only InvalidIntegerLength (wrong byte count) is a length failure.
One decoder case is deliberately outside the malformed-reason mapping:
InvalidHashLengthis decode-reachable only for the supplied Holochain link target, not for a tag-payload field. It is an endpoint-identity check and surfaces as theInvalidSmartLinkEndpointshape violation (Section 8.3), not aMalformedSmartLinkreason. The tag's ownOutboundProxyIdis read at a fixed width, so a short proxy is anUnexpectedEnd(→MissingField(OutboundProxyId)), never anInvalidHashLength.
Encode errors do not participate in the PVL mapping. PackingBudgetTooLarge, MandatoryContentExceedsBudget, LengthOverflow, DuplicateCacheCandidate, and ContainsNul remain SmartLinkTagEncodeError variants and have no peer-visible mapping. On decode, the delimiter scan establishes that the canonical-key segment contains no NUL before it is constructed; the codec must preserve that invariant without routing a logically unreachable ContainsNul through the decode-error type. TagTooLarge is a decode resource-limit condition, not a malformed reason: it surfaces as the SmartLinkTagTooLarge { actual_bytes, max_bytes } violation (Section 10.2). This separation makes the decode-error mapping exhaustive without a panic or invented fallback violation.
Avoid embedding arbitrary low-level deserializer strings in the deterministic peer-facing result.
The underlying low-level cause may be included in local debug logs, but not in the consensus-visible validation message.
11. Unresolved Dependencies¶
An unresolved dependency is not a HolonError and not a PvlViolation.
It maps directly to:
ValidateCallbackResult::UnresolvedDependencies(...)
Examples:
- original action for an update is not yet available
- delete target is not yet available
The distinction is:
dependency absent
-> UnresolvedDependencies
dependency present but wrong
-> PvlViolation
This prevents temporarily unavailable valid data from being permanently rejected.
The substrate already provides this distinction: must_get_action and must_get_valid_record short-circuit the callback to UnresolvedDependencies when the target is unknown to the visible network. The obligation on PVL is therefore a negative one — a validator must never intercept that short-circuit and convert it into a violation. Mapping a dependency-resolution failure onto a PvlViolation, for example with .map_err(|_| PvlViolation::InvalidUpdateTarget { .. }), silently turns "not yet available" into permanent rejection of valid data.
Keep the two channels separate in the return type: the outer result carries host and callback-execution failures, and the inner result carries completed validation. A validator shaped as ExternResult<Result<T, PvlViolation>> cannot express the confusion in the first place.
12. Limit Ratification Process¶
Before the constants become part of a production DNA, run a one-time ratification process.
12.1 Fixture Corpus¶
Measure:
- complete core-schema import
- largest core
TypeDescriptor - largest current
HolonNode - representative content holons
- representative query/request holons
- representative projection holons
- largest current SmartLink tag
- inverse-link tags
- adversarial near-limit fixtures
Record:
- serialized size
- property count
- maximum property-name size
- largest scalar
- largest bytes property
- collection counts
- validation dependency count
- validation execution time
12.2 Acceptance Rule¶
A proposed limit is acceptable when:
- all legitimate fixtures fit with at least 2× headroom
- no common workflow approaches more than 50% of the limit
- near-limit validation remains comfortably within the Integrity WASM execution budget
- an attacker cannot multiply the limit into unbounded dependency work
- the limit remains below substrate maxima
If legitimate fixtures exceed the proposed limit, raise only the affected limit and document the reason.
Do not increase all limits proportionally.
12.3 Benchmark Scenarios¶
At minimum benchmark:
- minimal valid HolonNode
- typical 20-property HolonNode
- 256-property boundary HolonNode
- 256 KiB boundary HolonNode
- 128 KiB bytes property
- 1,024-item collection
- SmartLink tag at
MAP_SMARTLINK_V1_MAX_BYTES - one-dependency lifecycle path
- malformed input at maximum size
- repeated invalid-link validation
The benchmark report should be committed with the implementation issue or linked from it.
12.4 Initial Measurements (2026-07)¶
Status: initially measured, not ratified.
These measurements predate the canonical Tag v1 format defined by the storage-layer specification. The entry-level measurements (HolonNode size, property counts, names, strings, keys) remain valid; they were taken in the larger update form with the 39-byte original_id entry field that Storage SL2 later removed, so they slightly overstate current entry sizes. The tag measurements were taken against the former prolog encoding — their 512-byte budget column happens to match the since-ratified MAP v1 ceiling, but the byte counts understate Tag v1 sizes, which add a canonical-key prefix segment, optional 16-byte occurrence identity, TLV property sections, and cached target properties deliberately packed toward the budget.
Ratification requires re-measurement with the shared Tag v1 encoder, published as a committed, reproducible artifact: the measurement program, the corpus commit it ran against, and the generated report. The regression-suite milestone of the implementation plan owns that artifact.
A first measurement pass was run against the complete canonical core-schema import corpus (380 holons across 12 files), with each holon converted to its staged PropertyMap (including the key property) and serialized exactly as the Holochain HolonNode entry, in the larger update form (39-byte original_id present). SmartLink tag sizes were measured with a byte-exact replica of the then-current tag encoding across realistic shapes.
| Measurement | Corpus max | Proposed limit | Share of limit |
|---|---|---|---|
| HolonNode entry bytes | 1,204 | 262,144 | 0.5% |
| Property count | 14 | 256 | 5.5% |
| Property name bytes | 31 | 128 | 24% |
| String value bytes | 719 | 16,384 | 4.4% |
| Key bytes | 96 | 256 | 37.5% |
| Relationship name bytes | 23 | 128 | 18% |
| Tag: local target, no smart properties | 33 | 512 | 6.4% |
| Tag: local target + key smart property | 140 | 512 | 27% |
| Tag: external target + key smart property | 180 | 512 | 35% |
| Tag: external + key + 39-byte forward-link reference | 190 | 512 | 37% |
Findings:
- Every entry-level proposed limit measured against the current core-schema corpus satisfies the Section 12.2 acceptance rule. No measured HolonNode entry, property count, property name, string value, key value, or relationship name exceeds 50% of its limit.
- The SmartLink tag rows are historical only. They used the superseded prolog encoding and do not ratify the Tag v1 ceiling; PR 9 must re-measure tags with the shared Tag v1 encoder before tag-size limits are considered ratified.
- The key bound (now the Section 8.4
MAX_CANONICAL_KEY_BYTES) was added as a result of this pass: keys feed SmartLink tags, and without a key bound a holon passing every per-property limit could still bloat its relationship tags. MAX_HOLON_NODE_BYTESis set by policy, not by measurement; the largest real holon uses 0.5% of it. The headroom is reserved for future content holons.- The corpus contains schema descriptors only. Re-measurement must additionally cover representative content holons before the limits are frozen into a production DNA.
13. Reporting and Logging¶
13.1 Integrity Callback Result¶
A PVL violation must return:
ValidateCallbackResult::Invalid(message)
The message should use a stable code and concise deterministic summary.
Recommended format:
MAP-PVL-<code>: <summary>
Examples:
MAP-PVL-1003: HolonNode exceeds 262144-byte limit
MAP-PVL-1101: property count exceeds 256
MAP-PVL-2201: SmartLink tag exceeds 512-byte limit
MAP-PVL-2202: canonical key exceeds 256-byte limit
Do not include:
- entire property values
- full serialized entries
- secrets
- TrustChannel data
- arbitrary deserializer output
- nondeterministic debug representations
13.2 Error Code Registry¶
Define stable numeric or symbolic codes as part of the violation contract, co-located with PvlViolation:
shared_crates/type_system/integrity_core_types/src/pvl_error.rs
The code is a deterministic function of the PvlViolation variant (for example, a PvlViolation::code(&self) -> &'static str method), so the enum and its stable codes cannot drift apart across crates. shared_validation re-exports them for the pure-core import path (Section 10.1, Section 15 decision 10).
Suggested groups:
| Range | Category |
|---|---|
1000–1099 |
HolonNode decoding and shape |
1100–1199 |
Property and value limits |
1200–1299 |
Identifier validation |
1300–1399 |
Update and delete validation |
2000–2099 |
SmartLink decoding and shape |
2100–2199 |
Link identifiers and provenance |
2200–2299 |
Link resource limits |
3000–3099 |
Reserved for future dependency-budget violations |
Codes should not be reused after release.
13.3 Coordinator-Side Reporting¶
When the same pure validation helpers are invoked before commit, map:
PvlViolation
->
HolonError::PvlViolation(...)
The coordinator may return the structured error to:
- command callers
- Dance callers
- import processes
- DAHN diagnostics
- developer tooling
The structured response may include:
- stable PVL code
- affected field or property name
- actual value
- allowed maximum
- remediation hint
Remediation hints are coordinator-side only and are not included in Integrity consensus messages.
Preflight is attached to the canonical persistence write boundaries so every coordinator write path receives the same protection without creating another ingress API:
- serialize and validate a
HolonNodeimmediately beforecreate_entryorupdate_entry - encode and validate a SmartLink once immediately before
create_link, then submit those same bytes
Preflight validates coordinator-constructible canonical writes. It does not claim parity with Integrity's validation of arbitrary peer bytes, Holochain operation routing, exact target-action facts, or unresolved dependencies. Coordinator storage checks that happen to establish similar lifecycle facts remain storage/write-contract checks unless they explicitly project those facts through the shared pure lifecycle rule.
13.4 Integrity Logging¶
Integrity validation should not emit warning or error logs for every rejected remote op.
Repeated peer validation could otherwise create a log-amplification attack.
Default behavior:
- return deterministic
Invalidresult - no payload logging
- no routine
warn!orerror!call
Optional diagnostic behavior:
- emit
debug!ortrace!records behind a diagnostic feature or explicit runtime configuration - include only:
- PVL code
- operation kind
- action hash
- entry or link type
- do not include property contents or private identifiers unnecessarily
Example:
pvl_code=MAP-PVL-2104
op=RegisterCreateLink
action_hash=<hash>
result=invalid
13.5 Validation Receipts and Persistent Error Holons¶
Descriptor-independent PVL failures must not automatically create:
ValidationResultholons- validation receipts
- conflict holons
- audit entries in the DHT
An invalid operation is not integrated and cannot safely trigger another DHT write from Integrity.
Persistent reporting, metrics, or governance escalation belongs to coordinator or operational tooling and is outside PVL.
14. Initial Error Code Proposal¶
| Code | PvlViolation |
|---|---|
MAP-PVL-1001 |
MalformedHolonNode |
MAP-PVL-1002 |
UnsupportedNativeValue |
MAP-PVL-1003 |
HolonNodeTooLarge |
MAP-PVL-1101 |
TooManyProperties |
MAP-PVL-1102 |
EmptyPropertyName |
MAP-PVL-1103 |
InvalidPropertyName |
MAP-PVL-1104 |
PropertyNameTooLong |
MAP-PVL-1110 |
StringValueTooLarge |
MAP-PVL-1111 |
EnumValueTooLarge |
MAP-PVL-1112 |
BytesValueTooLarge |
MAP-PVL-1113 |
CollectionTooLarge |
MAP-PVL-1114 |
HeterogeneousCollection |
MAP-PVL-1115 |
ValueNestingTooDeep |
MAP-PVL-1116 |
EmptyEnumValue |
MAP-PVL-1117 |
MalformedPropertyValue |
MAP-PVL-1201 |
InvalidIdentifier |
MAP-PVL-1202 |
EmptyIdentifier |
MAP-PVL-1203 |
IdentifierTooLong |
MAP-PVL-1301 |
InvalidUpdateTarget |
MAP-PVL-1302 |
ImmutableNativeFieldChanged |
MAP-PVL-1303 |
InvalidDeleteTarget |
MAP-PVL-2001 |
MalformedSmartLink |
MAP-PVL-2002 |
InvalidSmartLinkEndpoint |
MAP-PVL-2003 |
UnsupportedSmartLinkEndpointKind |
MAP-PVL-2004 |
InvalidLinkDeleteTarget |
MAP-PVL-2101 |
EmptyRelationshipName |
MAP-PVL-2102 |
InvalidRelationshipName |
MAP-PVL-2103 |
RelationshipNameTooLong |
MAP-PVL-2201 |
SmartLinkTagTooLarge |
MAP-PVL-2202 |
CanonicalKeyTooLarge |
This registry may be refined before implementation, but the category boundaries should remain stable.
Registry changes in v0.3, all before any release (the no-reuse rule begins at release):
1116–1118were freed when the former key-property rules were removed (superseded by the Section 8.4 canonical-key bound).1116and1117are now reallocated toEmptyEnumValueandMalformedPropertyValue(Section 10.2);1118remains free.2110–2119reserved for forward-reference provenance validation if a future tag field introduces it (Section 8.5).
Registry changes in v0.5, also before any release:
1302ImmutableNativeFieldChangedis retained but reserved and unused: the current persisted entry shape carries no cross-version invariant to compare (Section 10.2). The code is not reallocated, so it remains available to the rule it names if a qualifying field is ever added.1301InvalidUpdateTargetkeeps its code; only its field names changed.
Registry changes in v0.6, also before any release:
2004is allocated toInvalidLinkDeleteTarget. The previous SmartLink decoding and endpoint assignments remain unchanged.
Registry changes in v0.7, also before any release:
- Removed
3001/ValidationDependencyLimitExceededfrom the active contract when v1 adopted a structurally fixed zero-or-one dependency call graph instead of runtime accounting. The3000–3099range remains reserved for a future version that introduces composed or data-driven dependency resolution.
15. Open Decisions Required Before Issue Generation¶
Status of the pre-implementation decisions, updated for v0.7 after integration refinement:
- Confirm the proposed numeric limits against real serialized fixtures. Initially measured (Section 12.4). Entry-level limits pass the acceptance rule against the core-schema corpus. Tag measurements used the superseded encoding; ratification requires re-measurement with the shared Tag v1 encoder, published as a committed reproducible artifact, and coverage of representative content holons.
- Confirm whether property collections currently exist in
PropertyValue. Resolved. They do not:PropertyValue = BaseValuehas five scalar variants. Section 6.6 is satisfied by construction. - Confirm whether nested values are representable at all. Resolved. They are not. Section 6.7 is satisfied by construction.
- Confirm the exact canonical property-name validation already provided by
PropertyName. Resolved.PropertyName(pub MapString)is a thin wrapper with no validating constructor; PVL implements the Section 5.3 rules itself. Fixtures must confirm that existing core-schema names pass them. - Confirm the current SmartLink tag fields and serialized size. Superseded. The tag format is now owned by the storage-layer specification (Tag v1, storage spec Sections 7–9). PVL validates that contract rather than documenting its own.
- Confirm the fixed SmartLink attachment-authority rule. Withdrawn (v0.3). Attachment authority is not a descriptor-independent PVL concern: Holochain action authors are substrate signing agents, not MAP authority holders (Section 8.5). Authorization belongs to agreements, roles, and security policy above storage.
- Confirm the current inverse-link provenance representation.
Resolved by the storage model. Inverse pairing is occurrence identity — a declared link and its inverse share an
OccurrenceId— not a forward-link reference. Cross-link correspondence verification is deferred unless a future tag field deterministically references the forward realization (Section 8.5). - Confirm which native fields are immutable across HolonNode updates.
Resolved: there are none. Storage SL2 removed
original_idfrom the persistedHolonNodeentry shape, leavingproperty_mapalone — and no native field carrying a cross-version invariant. Lineage is carried by Holochain record metadata (Update.original_action_addressreferencing the lineage-rootCreate), so lifecycle validation enforces the root-addressed update contract instead: an update is valid only against aCreatecarrying theHolonNodeentry type, and update-to-update chains are invalid (Section 10.2).ImmutableNativeFieldChangedis reserved and unused.
Enforcement was not sequenced against Storage SL2 (revised in v0.5). The rule landed first as proactive hardening. Storage SL2 has now landed and reuses the same rule: PublishVersion authors a root-addressed Holochain Update, so the normal version-publication path exercises it without a second lifecycle check.
9. Confirm the exact validated byte representation used by LocalId.
Resolved. LocalId(pub Vec<u8>), ActionHash-shaped (39 bytes), with no validating constructor today. Shape checking lives in the pure core; exact hash parsing lives in the substrate adapter (Sections 3.3 and 7.1).
10. Confirm the crate in which PvlViolation, limit constants, and error codes will live.
Resolved (revised). The split is a dependency-cycle constraint. HolonError is defined in integrity_core_types, so HolonError::PvlViolation(PvlViolation) cannot wrap a type defined in the higher-gravity shared_validation crate — shared_validation already depends on integrity_core_types, and defining PvlViolation there would form integrity_core_types -> shared_validation -> integrity_core_types. Therefore:
- The violation contract — PvlViolation, PvlMalformedReason, PvlField, and the error-code registry — lives in integrity_core_types/src/pvl_error.rs, beside HolonError, and is re-exported from shared_validation so PVL code imports it from the pure-core path.
- The versioned limit constants and pure measurement helpers (pvl_limits_v1) live in shared_validation as before: they are not wrapped by HolonError and carry no cycle.
- The Tag v1 codec and storage-boundary types live in core_types.
All are consumed by holons_guest_integrity (substrate adapter) and coordinator preflight (Section 3.3). Note: adding the HolonError::PvlViolation variant fans out to the existing exhaustive HolonError consumers — HolonErrorKind and its From<&HolonError> mapping, the From<HolonError> for ResponseStatusCode classification, and the hand-maintained HolonErrorWire TypeScript SDK mirror.
-
Confirm SmartLink envelope mapping, endpoint forms, and delete-target behavior. Resolved. The codec separates encode and decode errors and owns typed structural positions;
shared_validationowns the one exhaustive decode-error-to-PVL mapper. SmartLink bases, Holochain link targets, and present outbound proxies are ActionHash identities. Create validation resolves no dependencies.RegisterDeleteLinkcarries a resolvedCreateLink;StoreRecord::DeleteLinkusesmust_get_actionto establish the target action kind, after which scoped link type selects the validator. Neither form re-validates the original tag (Sections 8.3, 9.2, and 10.3). -
Confirm whether PVL v1 needs a runtime dependency counter. Resolved in v0.7: no. Every v1 operation has a closed zero-or-one dependency path, and exact-call-count adapter tests pin the structural bound. The former limit constant, violation variant, and
3001code are removed from the active pre-release contract. Any future composed or data-driven dependency rule must introduce an explicit budget before activation (Section 9.1). -
Confirm coordinator preflight parity and placement. Resolved in v0.7. Preflight runs at the canonical HolonNode and SmartLink persistence write boundaries and invokes the same pure rules over the exact canonical bytes and typed identities about to be authored. It maps violations to
HolonError::PvlViolation. It deliberately does not reproduce HDI operation routing, target dependency resolution, or arbitrary raw peer-input failure modes, and therefore makes no Integrity-parity claim (Sections 3.3 and 13.3).
The remaining open item is Tag v1 re-measurement (Section 12.4). The shared tag ceiling is ratified at 512 bytes (Section 8.1), and decision 8 is resolved by the root-addressed update contract with no remaining sequencing constraint. Implementation issues can now enumerate exact tasks rather than categories of possible checks.