Skip to content

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 Create actions, 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 Holochain Update actions.
  • Removed the immutable-native-field rule. ImmutableNativeFieldChanged and MAP-PVL-1302 are retained but reserved and unused (Section 10.2).
  • Renamed InvalidUpdateTarget's fields to expected_target_kind / actual_target_kind, matching InvalidDeleteTarget, 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), the PvlField enum and the normative Tag v1 codec-error mapping (Section 10.3), the actual_bytes field-width convention (Section 10.2), the property-value violations EmptyEnumValue and MalformedPropertyValue with the 1116/1117 reallocation (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::DeleteLink resolves its exact target action with must_get_action without re-validating the target tag (Section 9).
  • Added InvalidLinkDeleteTarget and MAP-PVL-2004 so 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 into PvlField by 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, and MAP-PVL-3001 from the active v1 contract. The 3000–3099 range 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, and LocalHolonSpace are 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 Update actions.

2. Validation Guarantee

Descriptor-independent PVL proves:

A submitted HolonNode or 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_types data (HolonNode, PropertyMap, BaseValue, LocalId, and the decoded SmartLink storage types)
  • re-exports the PvlViolation contract and error-code registry owned by integrity_core_types
  • validates hash-shaped identifiers structurally, by role and byte shape, without parsing Holochain types
  • has no hdi, hdk, or holo_hash dependency

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:

  • ActionHash
  • EntryHash
  • AgentPubKey
  • 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

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,
}

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_BYTES and 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 OccurrenceId and have distinct SmartLinkId values
  • for non-duplicate relationships, the semantic identity is source + target + relationship + absent occurrence ID
  • SmartLinkId is 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 OccurrenceId bytes
  • reserved payload-flag bits must be zero
  • whether an occurrence ID is required or forbidden for a given relationship depends on its AllowsDuplicates policy, 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.


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:

  • HolonNodeUpdates is 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.
  • AllHolonNodes remains 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.
  • LocalHolonSpace remains 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_action returns 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_record returns the full record — action plus entry, up to MAX_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 deleteRegisterDeleteLink already carries the original CreateLink and resolves no dependency. StoreRecord::DeleteLink uses must_get_action to establish that the exact named target is a CreateLink; 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

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.

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_address does not reference a Create action — update-to-update chains are invalid in MAP's root-addressed lineage topology (KEA; storage plan SL2)
  • the referenced action does not carry the HolonNode entry 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 Create nor an Update
  • 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.

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.

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-21102119 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:

  • InvalidHashLength is decode-reachable only for the supplied Holochain link target, not for a tag-payload field. It is an endpoint-identity check and surfaces as the InvalidSmartLinkEndpoint shape violation (Section 8.3), not a MalformedSmartLink reason. The tag's own OutboundProxyId is read at a fixed width, so a short proxy is an UnexpectedEnd (→ MissingField(OutboundProxyId)), never an InvalidHashLength.

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:

  1. minimal valid HolonNode
  2. typical 20-property HolonNode
  3. 256-property boundary HolonNode
  4. 256 KiB boundary HolonNode
  5. 128 KiB bytes property
  6. 1,024-item collection
  7. SmartLink tag at MAP_SMARTLINK_V1_MAX_BYTES
  8. one-dependency lifecycle path
  9. malformed input at maximum size
  10. 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_BYTES is 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 HolonNode immediately before create_entry or update_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 Invalid result
  • no payload logging
  • no routine warn! or error! call

Optional diagnostic behavior:

  • emit debug! or trace! 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:

  • ValidationResult holons
  • 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):

  • 11161118 were freed when the former key-property rules were removed (superseded by the Section 8.4 canonical-key bound). 1116 and 1117 are now reallocated to EmptyEnumValue and MalformedPropertyValue (Section 10.2); 1118 remains free.
  • 21102119 reserved for forward-reference provenance validation if a future tag field introduces it (Section 8.5).

Registry changes in v0.5, also before any release:

  • 1302 ImmutableNativeFieldChanged is 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.
  • 1301 InvalidUpdateTarget keeps its code; only its field names changed.

Registry changes in v0.6, also before any release:

  • 2004 is allocated to InvalidLinkDeleteTarget. The previous SmartLink decoding and endpoint assignments remain unchanged.

Registry changes in v0.7, also before any release:

  • Removed 3001 / ValidationDependencyLimitExceeded from the active contract when v1 adopted a structurally fixed zero-or-one dependency call graph instead of runtime accounting. The 3000–3099 range 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:

  1. 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.
  2. Confirm whether property collections currently exist in PropertyValue. Resolved. They do not: PropertyValue = BaseValue has five scalar variants. Section 6.6 is satisfied by construction.
  3. Confirm whether nested values are representable at all. Resolved. They are not. Section 6.7 is satisfied by construction.
  4. 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.
  5. 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.
  6. 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.
  7. 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).
  8. Confirm which native fields are immutable across HolonNode updates. Resolved: there are none. Storage SL2 removed original_id from the persisted HolonNode entry shape, leaving property_map alone — and no native field carrying a cross-version invariant. Lineage is carried by Holochain record metadata (Update.original_action_address referencing the lineage-root Create), so lifecycle validation enforces the root-addressed update contract instead: an update is valid only against a Create carrying the HolonNode entry type, and update-to-update chains are invalid (Section 10.2). ImmutableNativeFieldChanged is 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 contractPvlViolation, 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.

  1. Confirm SmartLink envelope mapping, endpoint forms, and delete-target behavior. Resolved. The codec separates encode and decode errors and owns typed structural positions; shared_validation owns 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. RegisterDeleteLink carries a resolved CreateLink; StoreRecord::DeleteLink uses must_get_action to 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).

  2. 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 3001 code 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).

  3. 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.