Implementer-facing notes for the Linked-Data Interchange domain. Pairs with PRD-006, ADR-012, and DDD-004.
management-api/middleware/linked-data/
├── index.js # createEncoder / createLinter entry points
├── encoder.js # LinkedDataEncoder (DDD-004 §EncodingPipeline)
├── context-resolver.js # ContextResolver (DDD-004 §ContextCatalogue)
├── lion-linter.js # LION rule enforcement
├── jcs.js # RFC 8785 canonicalisation
├── round-trip.js # PRD-006 §8.1 conformance helper
└── surfaces/
├── s01-pods.js … s11-http-meta.js
lib/linked-data-contexts.nix # build-time-pinned context catalogue
docs/reference/_vocab/
├── agbx.md # agbx: term registry
└── agentbox-v1.context.jsonld # in-tree first-party @context document
tests/contract/linked-data/
├── invariants.spec.js # L01–L12
├── jcs.spec.js # RFC 8785 vectors
└── surfaces.spec.js # one smoke test per surface
Every surface module exports the metadata fields the encoder needs and an encode(payload, ctx) function. Optional decode(jsonld) for bidirectional surfaces.
module.exports = {
id: 'S5', // SurfaceId
slot: 'events', // adapter slot or null for build-time-only
gateKey: 'provenance', // [linked_data].<gateKey>
prerequisiteAdapter: 'adapters.events',
form: 'compacted', // compacted | expanded | flattened | framed
direction: 'emit', // emit | consume | both
operations: ['emit', 'append'],
canonicalisation: 'none', // 'jcs' for S3, S8 only (DDD-004 §L06)
vocabularyBinding: ['prov:', 'agbx:'],
contextIri: 'http://www.w3.org/ns/prov-o#',
async encode(payload, { resolver, manifest, agentDid, operation }) {
return { document: jsonldDoc, contextIri };
},
// Optional — bidirectional surfaces only
decode(jsonldDoc) { return internalShape; },
};The encoder picks the surface for a given dispatch by matching slot + operation + enabled gate. Multiple surfaces may share a slot (e.g. S5 PROV-O is parallel emit alongside the regular events JSONL adapter).
- Create
surfaces/sNN-<name>.jswith the shape above. - Pin its
@contextIRI inlib/linked-data-contexts.nix(one new entry per vocabulary it uses). - Run
./scripts/prefetch-hashes.sh --linked-data. - Add a per-surface gate to the schema under
[linked_data].<gateKey>(schema/agentbox.toml.schema.json). - Add a manifest gate to
agentbox.toml. - Add a validation rule to
scripts/agentbox-config-validate.js(next freeE04xcode). - Register the module in
index.js'ssurfaceModulesarray. - Add a smoke test to
tests/contract/linked-data/surfaces.spec.js. - Add a fixture to
tests/contract/linked-data/round-trip.spec.jsso CI catches regressions. - Update PRD-006 §3 and the user doc table.
Per DDD-004 §L08, the dispatch order is:
observability.before() (ADR-005)
└─> privacy_filter.redact() (ADR-008)
└─> linked_data.encode() (ADR-012, this layer)
└─> adapter.<impl>.write()
The encoder receives post-redaction bytes and never sees raw user input. The manifest's [linked_data.privacy_handoff].order is documentation only — the validator's E048 rejects any other value.
- L01 — Every ContextDocument's SHA-256 is verified at boot when the index declares one. Mismatches abort startup.
- L02 — No mutation API. The encoder reads the same parsed object reference every time
resolve(iri)is called. - L03 — Bijective IRI → ContextDocument map. Duplicate IRIs in the index abort boot.
- L09 —
documentLoader()returns a function compatible with jsonld.js that never reaches the network. Tests monkey-patchfetchto throw, run the full encoder suite, and assert nofetchcalls.
new LIONLinter({ resolver, surface, baseIRI, inheritedContextIRIs }) enforces five rules. Construct with the same ContextResolver the encoder uses; the linter looks up inherited terms through the resolver's catalogue.
The CI gate is node management-api/middleware/linked-data/lion-linter.js <files…>. It returns non-zero on any rule violation. Wired into the standard npm test jest run via tests/contract/linked-data/invariants.spec.js.
canonicalise(value) returns the RFC 8785 canonical string. Used by surfaces S3 and S8 before signing. NaN, Infinity, undefined, function, symbol, and bigint are rejected. Object keys are sorted by Unicode code-point order.
Adding a new term to the agentbox extension namespace requires:
- A localname under
https://agentbox.dreamlab-ai.systems/ns/v1#. - A datatype (or
@type: @idfor IRI references). - A documented rationale in
docs/reference/_vocab/agbx.md. - A search confirming no upstream W3C / IETF / Schema.org equivalent.
- A round-trip test in
tests/contract/linked-data/surfaces.spec.js.
The published context document at docs/reference/_vocab/agentbox-v1.context.jsonld ships with the image.
Every surface has at least:
- An invariants test row in
invariants.spec.js. - A smoke test in
surfaces.spec.js.
To run only the linked-data tests:
cd management-api && npm test -- --testPathPattern='contract/linked-data'W3C JSON-LD 1.1 Test Suite integration is a Phase 1 follow-up — the harness location and runner script will live at tests/contract/linked-data/w3c-test-suite.spec.js once the suite has been vendored as a git submodule under tests/vendor/json-ld-api/.
| Phase | Target | Surfaces wired |
|---|---|---|
| 1 | 2026-05 | mechanism + S7 + S10 |
| 2 | 2026-06 | + S1 + S2 + S4 + S5 |
| 3 | 2026-07 | + S3 + S6 + S8 + S9 + S11 |
The encoder accepts payloads for every surface today; the per-phase gating happens at the manifest level so later surfaces can be flipped on without code changes.