json-ld infrastructure category-theory pflow activitypub

The pflow ecosystem currently uses three JSON-LD vocabularies. Petri net models reference pflow.xyz/schema. Blog posts carry schema.org metadata. Federation speaks ActivityStreams. Three different contexts, authored by three different communities, consumed by different software — yet they all share the same envelope. That convergence is not accidental. JSON-LD earns its place as infrastructure because it is purely declarative and monotonically expansive: two properties that matter more than any feature list.

Three Contexts, One Discipline

Here is a trimmed snippet from each context as it appears in production.

Petri net model (pflow.xyz editor):

{
  "@context": "https://pflow.xyz/schema",
  "@type": "PetriNet",
  "places": {
    "Idle": { "@type": "Place", "initial": [1], "capacity": [1] }
  },
  "transitions": {
    "Brew": { "@type": "Transition" }
  },
  "arcs": [
    { "@type": "Arrow", "source": "Idle", "target": "Brew", "weight": [1] }
  ]
}

Blog metadata (auto-generated by tens-city):

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "JSON-LD as Declarative Infrastructure",
  "author": { "@type": "Person", "name": "stackdump" },
  "datePublished": "2026-02-15T00:00:00Z"
}

ActivityPub actor (federation profile):

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1"
  ],
  "type": "Person",
  "preferredUsername": "myork",
  "inbox": "https://blog.stackdump.com/users/myork/inbox",
  "publicKey": { "id": "...#main-key", "publicKeyPem": "..." }
}

Three JSON-LD Contexts

Each snippet is self-describing. Each links to a vocabulary that defines its terms. And none of them contain instructions — they are pure assertions.

Purely Declarative

A JSON-LD document is a serialized RDF graph: a set of subject-predicate-object triples. It makes statements about the world but never tells you what to do with them. There are no callbacks, no event handlers, no conditionally-included fields.

This matters for interoperability. The same .jsonld Petri net file is consumed by at least three independent interpreters:

  1. JavaScript — the pflow.xyz browser editor loads it, renders the net, and runs simulations.
  2. Go parsergo-pflow reads the same file for ODE-based analysis and validation.
  3. Go code generatorpetri-pilot compiles it into executable service modules.

None of these consumers coordinate with each other. They do not need to. The file is assertions, not a protocol. Each consumer extracts the triples it understands and ignores the rest. A code generator does not care about x/y layout coordinates; the visual editor does not care about Seal metadata. Declarative data degrades gracefully by design.

Contrast this with imperative serialization — formats where the order of fields implies a processing sequence, or where consumers must execute embedded logic to reconstruct the data. JSON-LD sidesteps all of that. The @context resolves local terms to global IRIs. The consumer interprets the graph. Nothing in between.

Monotonic Expansion

The pflow.xyz schema has grown three times since its introduction:

Each expansion introduced new terms. No existing term was removed or redefined. A model authored in 2024 using only PetriNet, Place, Transition, and Arrow remains valid under the 2026 schema — not because we tested backwards compatibility, but because the schema only grows.

Monotonic Schema Expansion

This is monotonic expansion: new facts can be added, but existing facts are never retracted. It is the same discipline that makes append-only logs reliable and RDF graphs composable. In a monotonic system, learning more never invalidates what we already know.

Practically, this means old models never break. A Petri net saved before Seal existed still loads in an editor that understands seals. The editor simply sees a net without seal metadata — a valid state. No migration scripts, no version negotiation, no "this file was created with an older version" warnings. Backwards compatibility emerges from structure, not policy.

Content Addressing via Canonicalization

If JSON-LD is declarative, its identity should derive from what it says, not from how it was serialized. Two documents that make the same assertions — regardless of key order, whitespace, or field arrangement — should hash to the same value.

The URDNA2015 algorithm (RDF Dataset Normalization) makes this possible. It converts any JSON-LD document into a canonical set of N-Quads — sorted, deterministic, order-independent. From there, a standard hash produces a content identifier.

Canonicalization Pipeline

In pflow-xyz, the sealing pipeline implements this directly:

// From seal.go — simplified
proc := ld.NewJsonLdProcessor()
opts := ld.NewJsonLdOptions("")
opts.Format = "application/n-quads"
opts.Algorithm = "URDNA2015"

normalized, _ := proc.Normalize(doc, opts)     // canonical N-Quads
multihash, _ := mh.Sum([]byte(normalized.(string)), mh.SHA2_256, -1)
cid := cid.NewCidV1(cid.DagJSON, multihash)    // CIDv1 with base58btc

The resulting CID becomes the model's @id — a self-certifying identifier. If the model changes, the CID changes. If two independently-created models happen to describe the same graph, they get the same CID. Identity follows from content, not from a registry or a counter.

This property is what makes seals in Categorical Net Types trustworthy: the seal is a commitment to a specific graph, and any party can verify it by re-canonicalizing and re-hashing.

The Categorical View

There is a clean categorical reading of what @context does. A JSON-LD context is a mapping from local terms (short names like Place, Transition) to global IRIs (like https://pflow.xyz/schema#Place). This is a functor:

@context : LocalTerms → GlobalIRIs

It maps the local category of terms used in a document to the global category of well-defined identifiers. Extending a context — adding new term mappings while preserving existing ones — is a natural transformation between functors: the old mapping still holds, and new mappings are layered on top.

The three vocabularies we use (pflow.xyz, schema.org, ActivityStreams) compose as a coproduct. Each vocabulary contributes its terms to the combined graph without collision, because the IRIs are namespace-disjoint. A document can reference all three contexts simultaneously, and the terms resolve unambiguously.

Content addressing then gives us identity in this category. Two objects (JSON-LD documents) are equal if and only if their canonical forms are equal — which is exactly what a content-addressed identifier witnesses.

JSON-LD, categorically: @context is a functor from local names to global identifiers. Schema extension is a natural transformation. Independent vocabularies compose as coproducts. Canonicalization provides identity.

Key Takeaways

  1. Declarative means safe to share. A JSON-LD document contains assertions, never instructions. Multiple consumers extract what they need and ignore the rest.

  2. Monotonic means safe to extend. Adding terms never breaks existing documents. Backwards compatibility is structural, not a testing burden.

  3. Canonicalization means content-addressable. URDNA2015 makes graph identity independent of serialization order, enabling self-certifying identifiers.

  4. @context is a functor. It maps local terms to global IRIs, and context extension preserves existing mappings — a natural transformation.

  5. Three vocabularies, zero coordination. pflow.xyz/schema, schema.org, and ActivityStreams coexist in one ecosystem because JSON-LD's design makes vocabulary composition the default, not a special case.

JSON-LD is not a feature. It is infrastructure — the kind that works precisely because it does less, not more. It asserts, it expands, it canonicalizes. Everything else is the consumer's problem, and that division of responsibility is exactly what makes composable systems possible.

×

Follow on Mastodon