Files
vscode-cowriting-plugin-con…/specs/coauthoring-cross-rung-format.md
T

28 KiB
Raw Blame History

status
status
graduated

Solution Design: Cross-Rung Git-Native Persistence Format + Gitea Round-Trip (F5)

Author(s) Ben Stull (with Claude)
Reviewers / approvers Ben Stull
Status draft
Version v0.1.0
Source artifacts Epic benstull/vscode-cowriting-plugin#1 · Feature #14 (F5) · Parent specs: coauthoring-inner-loop.md (§9 OPEN→F5: "the inner-loop shape, not yet the cross-rung contract"), coauthoring-attribution.md (§9 OPEN→F5), coauthoring-propose-accept.md (§9 OPEN→F5) · Ladder lineage: ben.stull/rfc-app#48 (the three rungs) and ben.stull/rfc-app#46 (the Gitea-substrate rung: "continuity of substrate — no migration cliff") · F4 shipped: #12 (PR #13, session 0012)

Change log

Date Version Change By
2026-06-10 v0.1.0 Initial draft — brainstorming session 0014 Ben Stull + Claude

1. Business Context

1.1 Executive Summary

F5 graduates the .threads/ sidecar from "the editor's private shape" into the cross-rung contract: a published, versioned, machine-validatable format any rung of the ladder (editor → Gitea substrate → rfc-app) can read and write without re-homing. It adds the three things a second writer needs — a contract (prose + JSON Schema, with compatibility rules including unknown-field preservation), a cross-rung identity extension on Provenance (git's own email model, plus onBehalfOf for agents), and a deterministic merge (mergeArtifacts) — and proves the round-trip: a record written in the editor is modified out-of-editor by a Gitea-rung stand-in and renders correctly on pull. Everything is additive; schemaVersion stays 1.

1.2 Background

Epic #1's outcome statement promises the record is "portable up to the Gitea rung later without re-homing", and rfc-app#46 names the strategic prize: continuity of substrate — no migration cliff ("git is truth; the surfaces are clients"). Every prior design deferred the contract to exactly this session: F2 called its format "the inner-loop shape, not yet the cross-rung contract"; F3 and F4 each routed "may re-home attributions[] / proposals[]" here. Meanwhile the shipped format was built for this moment — plain pretty JSON, stable key order, trailing newline, state-not-history sections, shared anchors — all chosen in F2 for mergeability. Issue #14 carried six design forks to this session; all six are resolved here (§6.7, §9). One shipped fact forces real work: serializeArtifact rebuilds a canonical object from known fields only, so today's writer would silently destroy another rung's data on rewrite — the forward-compatibility gap F5 closes (INV-15).

1.3 Business Actors / Roles

  • Coauthor (human / machine) — as in F2F4; unchanged in-editor.
  • The second writer — new: a teammate's editor on another clone, or a Gitea-rung surface appending to the same record per the contract.
  • The downstream rungs — the Gitea substrate (rfc-app#46) and rfc-app: consumers of the contract this Feature publishes.

1.4 Problem Statement

Portability has never been exercised. There is no published contract, no compatibility/versioning rules for readers that aren't this codebase, no forge-resolvable identity (Provenance.id is a local $USER), no defined behavior when a second writer touches the record, and a writer that drops fields it doesn't recognize. Until F5, "git is truth; the surfaces are clients" is an assertion, not a property.

1.5 Pain Points

  • Clone the repo on a second machine and the record probably works — nothing guarantees it, and any other tool that rewrites the sidecar loses data.
  • No rung can validate a sidecar without reading this extension's TypeScript.
  • kind:"human", id:"benstull" means nothing to a forge; agents don't record who they acted for.
  • Two writers = undefined behavior; git will textually conflict pretty JSON and nothing documents the semantic resolution.

1.6 Targeted Business Outcomes

A contract any rung can implement against; identity a forge can resolve; a merge story a human (or tool) can execute deterministically; and a demonstrated out-of-editor round-trip — converting the epic's portability promise into a tested property and unblocking the rfc-app#46 substrate brainstorming with a concrete input.

1.7 Scope (business)

In scope: the cross-rung contract (prose, normative) + JSON Schema + rung-neutral validator; unknown-field preservation in the writer; the Provenance identity extension (email, agent.onBehalfOf); newer-major fail-safe (read-only) behavior; mergeArtifacts (deterministic union-by-id, documented + unit-tested); the Gitea-rung stand-in writer script + round-trip host E2E; byte-fidelity tests.

Out of scope (deferred, not forgotten): the Gitea-side substrate primitives themselves (rfc-app#46's build — discussions UI, LLM participants, native proposal layers); any rfc-app work; a sync daemon/server (git push/pull IS the transport); a git merge driver installation (the pure function is the semantics; wiring it into .gitattributes is a later convenience); file rename/move tracking; ratifiedBy / multi-party provenance (team-rung concern); rejected-proposal history trails.

1.8 Assumptions · Constraints · Dependencies

  • Parent: Epic #1 (F5 is its final decomposition item; the three acceptance pillars shipped in F2 #4, F3 #6, F4 #12). Blocked by: #12 (shipped). Feeds: the rfc-app#46 brainstorming and the rfc-app rung.
  • Established format facts (the contract ratifies what shipped): per-document .threads/<path>.json; shared anchors + Fingerprint (text/before/after/lineHint, INV-3); Provenance (human|agent); threads[] / attributions[] (state, not history) / pending-only proposals[] (INV-13); SCHEMA_VERSION = 1; stable key order + trailing newline (INV-2). The extension stops being the assumed sole writer.
  • All changes are additive — existing sidecars remain valid v1 documents unmodified; the serializer's optional-field-omission pattern (F4 precedent) carries the new fields.
  • No new auth/credential surface; no server.

1.9 Business Use Cases

  • BUC-1 A coauthor clones the repo on another machine (or a teammate does) and the full record — threads, attribution, pending proposals — arrives intact and keeps working.
  • BUC-2 A Gitea-rung surface (today: the stand-in; tomorrow: rfc-app#46's primitives) appends a thread reply per the contract; on pull, the reply appears in the editor's comment thread.
  • BUC-3 Two writers diverge; the documented merge yields one record with no silent data loss, and anything genuinely conflicting is surfaced.

2. Solution Proposal

Publish the contract in two halves: a normative prose document (coauthoring-sidecar-contract.md, a standing spec in the content repo) and a JSON Schema (schemas/coauthoring-sidecar.schema.json, code-resident in the plugin repo next to the reference implementation, wired into the unit suite and runnable by any rung via a small validator script). Extend Provenance additively with git's own identity model — email on humans, agent.onBehalfOf {id, email?} on agents — and make the writer round-trip-safe: unknown fields are preserved on rewrite (serialized after known keys, sorted), and a sidecar with a newer major schemaVersion is read-only (fail-safe). Ship the merge semantics as a pure, vscode-free mergeArtifacts(ours, theirs): sections union by id, thread messages union by id ordered by createdAt (the forge-appends-a-reply case merges cleanly by construction), same-id divergence resolved by updatedAt where present else a deterministic tie-break — and every such resolution surfaced in the result, never silent. Prove the loop with a Gitea-rung stand-in writer (scripts/crossrung-reply.mjs: validate → append a reply → stable-serialize) driven by host E2E through the existing external-change machinery.


3. Product Personas

  • PP-1 Inner-loop coauthor — unchanged in-editor experience; gains second-machine/teammate safety.
  • PP-3 Rung implementer — new: the developer building the Gitea-substrate or rfc-app surface against the contract (today impersonated by the stand-in script and the validator).

4. Product Use Cases

  • PUC-1 (fidelity) A sidecar written by the editor survives serialize → git → reload byte-stable, and survives a conforming foreign writer's rewrite with all fields intact (including ones that writer doesn't know).
  • PUC-2 (out-of-editor reply) The stand-in appends a thread reply per the contract → the editor (on external change / pull) renders the new message in the right thread at the right anchor.
  • PUC-3 (merge) Two divergent copies merge via mergeArtifacts: disjoint changes union; the reply-append case is clean; same-id divergence resolves deterministically and is reported.
  • PUC-4 (fail-safe) The editor encounters a sidecar with a newer major schemaVersion: it surfaces a warning and does not write — never destroys what it can't fully read.
  • PUC-5 (validation) Any rung validates any sidecar against the schema (scripts/validate-sidecar.mjs <file> or any standard JSON Schema validator); the plugin's unit suite validates its own outputs.

5. UX Layout

No new UI surface (Claude Code, not Claude Design — same §5 call as F3/F4). Out-of-editor replies render through the existing F2 thread machinery (external-change → re-render). The only new user-visible element is a warning notification + read-only behavior on a newer-major sidecar (PUC-4), reusing the established notification patterns. The contract, schema, validator, and merge function are developer-facing artifacts.


6. Technical Design

6.1 Invariants

Parent invariants INV-1..INV-13 carry over unchanged. F5 adds:

  • INV-14 (contract-canonical): the contract document is the format's source of truth. Format changes land in the contract first, then in code — documentation-leads-automation (handbook §4.1) applied to the format. The JSON Schema and the TypeScript model conform to it; on disagreement the contract wins.
  • INV-15 (round-trip preservation): a conforming writer preserves fields it does not recognize when rewriting a sidecar. No rung's writer may destroy another rung's data. (Unknown keys serialize after known keys, sorted lexicographically, so output stays deterministic — INV-2.)
  • INV-16 (versioning): within a major schemaVersion, changes are additive-only (new optional fields); readers ignore unknown optionals. A reader encountering a newer major version goes fail-safe read-only: render what it understands, write nothing. Breaking changes bump the major and are recorded in the contract's changelog.
  • INV-17 (merge honesty): merging is deterministic union-by-id per the contract's documented rules; any same-id divergence is resolved by those rules and surfaced to the caller — never silently guessed (INV-1's spirit at the artifact level).

6.2 High-level architecture

Four small units — two artifacts, two vscode-free modules — plus a serializer change and one fail-safe check; controllers and the editor UX are otherwise untouched:

  • The contract (vscode-cowriting-plugin-content: specs/coauthoring-sidecar-contract.md) — normative prose: every section and field, its semantics and invariants, the identity model, the compatibility rules (INV-15/16), and the merge rules (INV-17), with a changelog. Status-stamped and graduated like a spec; versioned with the schema.
  • The schema + validator (plugin repo: schemas/coauthoring-sidecar.schema.json + scripts/validate-sidecar.mjs, ajv as a devDependency) — machine-checkable conformance; wired into the vitest suite (every serialized fixture validates) and runnable standalone by any rung.
  • model.ts extensionsProvenance gains optional email (human and agent alike); the agent payload gains optional onBehalfOf: { id: string; email?: string } (the operator the machine acted for — rfc-app#46 §6.5's On-behalf-of: made data). The serializer gains unknown-field preservation (INV-15) for the artifact root and every record type. currentAuthor() starts populating email from the workspace git config when available.
  • mergeArtifacts (new vscode-free src/mergeArtifacts.ts) — mergeArtifacts(ours, theirs): { merged, conflicts }: anchors map union; threads/attributions/proposals union by id; thread messages union by id ordered by (createdAt, id); same-id record divergence → newer updatedAt wins where the type has one, else the lexicographically larger canonical serialization (deterministic), and the id is reported in conflicts. Refuses (throws) on differing majors (INV-16).
  • Fail-safe checkCoauthorStore.load surfaces the artifact's schemaVersion; controllers (via one shared helper) treat a newer major as read-only: render best-effort, skip every store.update path, warn once (PUC-4).
  • Stand-in writer (scripts/crossrung-reply.mjs) — the Gitea-rung impersonator: read sidecar → validate → append a Message to a named thread (new id, createdAt, author with email) → stable-serialize → validate again. Doubles as executable documentation of "how a foreign writer behaves."
flowchart LR
  subgraph editor rung
    ext["extension writers\n(Thread/Attribution/Proposal controllers)"] --> ser["serializeArtifact\n+ unknown-field preservation (INV-15)"]
  end
  ser --> sc[".threads/&lt;doc&gt;.json\n(canonical carrier)"]
  sc <--> git["git push / pull\n(the transport)"]
  git <--> forge["Gitea rung\n(today: crossrung-reply.mjs stand-in)"]
  contract["contract doc (content repo)\n+ JSON Schema (plugin repo)"] -. governs (INV-14) .-> ser
  contract -. governs .-> forge
  sc --> merge["mergeArtifacts\n(union-by-id, surfaced conflicts, INV-17)"]
  sc --> guard{"schemaVersion major\n&gt; known?"}
  guard -- yes --> ro["read-only + warn (INV-16)"]
  guard -- no --> render["existing render/re-anchor paths\n(F2/F3/F4, unchanged)"]

6.3 Data model & ownership

Additive only — schemaVersion stays 1; every existing sidecar is already a valid v1 document:

// Provenance — human (new optional identity)
{ "kind": "human", "id": "benstull", "email": "ben@wiggleverse.org" }

// Provenance — agent (new optional on-behalf-of)
{ "kind": "agent", "id": "claude",
  "agent": { "sdk": "@cline/sdk", "model": "sonnet", "sessionId": "run_…",
             "onBehalfOf": { "id": "benstull", "email": "ben@wiggleverse.org" } } }
  • Identity is git's model: email is the cross-rung join key (forges already map email → account); id remains the display/short name. No signatures in v1 (provenance, not authentication — the git history is the integrity layer).
  • Ownership changes: the extension is no longer the assumed sole writer. Any conforming writer may append/modify per the contract; the self-write-suppression machinery is untouched (foreign writes should arrive as external changes and re-render — that is the F2 path).
  • Unknown fields: preserved on rewrite at every level (artifact root, anchor, thread, message, attribution, proposal), serialized after known keys, sorted (INV-15, INV-2).

6.4 Interfaces & contracts

  • Contract doc — normative; field tables per section + invariants + compatibility (INV-15/16) + merge rules (INV-17) + changelog. The F2/F3/F4 spec §6.3s are its inputs; it supersedes them as the format reference (INV-14).
  • Schema/validatorvalidate-sidecar.mjs <file> exits 0/1 with per-error paths; vitest helper expectValidSidecar(artifact) used by the model/store/merge suites.
  • mergeArtifacts(ours: Artifact, theirs: Artifact): { merged: Artifact; conflicts: string[] } — pure, vscode-free, documented as the reference implementation of the contract's merge rules. Not yet wired into any UI; rungs and humans invoke it (a .gitattributes merge driver is a later convenience, §1.7).
  • CoauthorStore.load — unchanged signature; a shared isNewerMajor(artifact) helper gates every write path (controllers skip update, warn once per doc).
  • scripts/crossrung-reply.mjs <sidecar> <threadId> <body> [--author-id --author-email] — the stand-in; validates before and after its write.

6.5 PerProduct-Use-Case design

  • PUC-1 (fidelity): unit tests pin byte-stable serialize→parse→serialize; a "foreign writer" test parses a fixture containing unknown fields (root + per-record), rewrites via serializeArtifact, and asserts the unknowns survive in sorted-after-known position (INV-15).
  • PUC-2 (out-of-editor reply): host E2E — create a thread in the editor (F2 path) → save → run the stand-in against the sidecar (append reply) → trigger the external-change path (the F2/F3/F4 externalWriteAndReload/watcher pattern) → assert the thread renders the new message at the re-resolved anchor. No LLM; no real network — the stand-in is a local process, exactly what a forge's write looks like to git.
  • PUC-3 (merge): unit tests of the documented cases: reply-append vs. disjoint editor edit (clean union, no conflicts); both sides append to the same thread (messages interleave by createdAt, no conflicts); same attribution id with divergent extents (newer updatedAt wins, id reported); divergent same-id proposals (deterministic tie-break, id reported); differing majors → throws.
  • PUC-4 (fail-safe): unit: a v2 fixture loads, isNewerMajor true; host E2E: open a doc whose sidecar says schemaVersion: 2 → warning surfaced, edits/saves do NOT rewrite the sidecar (file bytes unchanged).
  • PUC-5 (validation): the schema validates all shipped fixtures + every serialized artifact in the unit suite; the validator script exits non-zero on a deliberately broken fixture.

6.6 Non-functional requirements & cross-cutting concerns

All additions are O(artifact) pure functions; no runtime dependencies added to the extension bundle (ajv is dev/scripts-only). Unknown-field preservation slightly grows the serializer but keeps determinism (INV-2). Identity adds PII-class data (emails) to a repo-committed file — acceptable: it is exactly what git commits already record; the contract notes it so adopters know. Fail-safe read-only can mean reduced function on version skew — by design (INV-16); the warning names the version. No secrets, no server, no new auth.

6.7 Key decisions & alternatives considered

Decision Chosen Alternatives rejected
Substrate carrier (fork a) .threads/ sidecar stays the canonical cross-rung carrier git notes (no default push/fetch — breaks "travels with ordinary git"; forges don't render notes; bind to commits while the record is document-scoped); commit trailers (only fit commit-shaped data); a forge reads/renders/diffs JSON files natively — the F2 decision gets stronger cross-rung
Contract form + home (fork b) Prose contract = standing spec in the content repo; JSON Schema + validator = plugin repo (code-resident by the reference implementation, like the engineering repo's central-schema discipline applied at app scope) schema in the content repo (test suite would read cross-repo; drifts from the model it validates); engineering schemas/ now (org-central before a second rung exists — graduate it there when the Gitea rung lands, noted §9); prose-only (not machine-checkable); schema-only (semantics/invariants don't fit JSON Schema)
Cross-rung identity (fork c) Optional email on Provenance + optional agent.onBehalfOf {id, email?} — git's own identity model; forges map email→account forge usernames (couples the record to one forge — the ladder spans surfaces); signed identities (authentication, not provenance; git history is the integrity layer; YAGNI v1); a global actor registry (a server by the back door)
Merge semantics (fork d) Pure 2-way mergeArtifacts: union-by-id; messages ordered by createdAt; updatedAt-else-deterministic tie-break; conflicts surfaced (INV-17) 3-way with base (git gives no cheap base for sidecars in conflict resolution; union-by-id with fresh uuids makes 2-way sufficient at this scale); whole-file last-writer (silently loses the other side); mandated git merge driver (installation burden; the pure function IS the semantics — driver is later sugar)
Round-trip proof shape (fork e) Local stand-in writer script (validate → append reply → stable-serialize) + the existing external-change E2E path; byte-stability covers transport real Gitea API round-trip in CI (infra for nothing — git moves bytes verbatim; the forge's write behavior is exactly the stand-in); building any Gitea-side primitive now (rfc-app#46's tracker, not this Feature)
Versioning (fork f) Everything additive → schemaVersion stays 1; INV-16 rules (ignore unknown optionals; preserve unknown fields; newer-major → read-only) bump to 2 (nothing breaks — a bump would force every reader to handle two majors for no gain); no formal rules (the status quo — exactly the gap F5 closes)
Writer forward-compat Preserve unknown fields on rewrite (INV-15), sorted after known keys drop unknowns (today's behavior — a conforming v1.1 writer would destroy a v1.2 rung's data); refuse to write on unknowns (turns every additive change into a flag day)

6.8 Testing strategy

  • Unit (vitest, vscode-free): schema validation of every serialized artifact (PUC-5); byte-stability + unknown-field preservation round-trips (PUC-1); identity serialization (email/onBehalfOf present/absent — F4's optional-omission pattern); mergeArtifacts documented cases incl. the conflict reports and the differing-major throw (PUC-3); isNewerMajor + v2-fixture load (PUC-4).
  • Host E2E (@vscode/test-electron, F2F4 pattern, no LLM): the round-trip — editor-created thread → save → stand-in appends a reply → external-change → new message renders in the thread at the re-resolved anchor (PUC-2); newer-major sidecar → warning + no rewrite on save (PUC-4).
  • Scripted checks: validate-sidecar.mjs against good + broken fixtures; the stand-in's own before/after validation. No manual smoke needed — F5 has no LLM leg.

6.9 Failure modes, rollback & flags

A non-conforming foreign write → schema validation pinpoints it; the editor's existing resolve-or-orphan ladder already handles semantic damage (anchors that no longer resolve orphan, never guess). Version skew → INV-16 read-only, recoverable by upgrading the older rung. Merge conflicts → surfaced ids + documented rules; worst case the file is plain JSON a human resolves (the F2 stance, now with rules). Email absence (no git config) → fields omitted, everything still valid (additive). No feature flag: additive schema + a contract doc; old extensions keep working against new sidecars (they drop unknown fields — which is precisely why INV-15 ships now, before any second writer exists).


7. Delivery Plan

7.1 Approach / strategy

One planning-and-executing session (F5 = #14), plan written just-in-time from this spec, per the F2F4 precedent.

7.2 Slicing plan

  • SLICE-1 The contract: coauthoring-sidecar-contract.md (content repo, normative, with changelog) + schemas/coauthoring-sidecar.schema.json + scripts/validate-sidecar.mjs (+ ajv devDependency), schema wired into the unit suite (every serialized fixture validates).
  • SLICE-2 Writer conformance: unknown-field preservation in serializeArtifact (INV-15) + Provenance identity extension (email, agent.onBehalfOf) + currentAuthor() email population + newer-major fail-safe (isNewerMajor gating all store.update paths, PUC-4 warning) — with compat tests (existing sidecars load/serialize unchanged).
  • SLICE-3 mergeArtifacts + unit tests of every documented merge case (INV-17).
  • SLICE-4 The stand-in (scripts/crossrung-reply.mjs) + host E2E round-trip (PUC-2) + newer-major E2E (PUC-4).
  • SLICE-5 Docs: README cross-rung section; plugin-repo pointers to the contract; full gate.

E2E are first-class plan tasks (handbook §9 / §4; this app's required tier is host E2E — a VS Code extension, no browser surface, no deploy stage — the F2F4 precedent).

7.3 Rollout / launch plan

Still non-shippable (no marketplace publish). "Done" = issue #14 acceptance met: contract + schema published, unit + host E2E green (round-trip, merge, fail-safe, preservation all exercised). No manual smoke — no LLM leg.

7.4 Risks & mitigations

Risk Mitigation
Contract drifts from code INV-14 (contract first) + the schema runs in the unit suite — drift fails CI
Unknown-field preservation breaks byte-stability deterministic placement rule (after known keys, sorted) + round-trip tests
Merge rules too naive for real concurrency scale honesty: union-by-id + fresh uuids makes overlap rare at inner-loop scale; conflicts are surfaced, file stays human-resolvable; revisit with the team rung
Email-as-identity is wrong for some forge it is git's own join key and the lowest common denominator; forge-specific ids can be added additively later (INV-16)
Premature contract (Gitea rung may want changes) everything additive + a changelog + the rfc-app#46 brainstorming consumes this as INPUT — cheaper to amend a contract than to reverse-engineer an implementation
Read-only fail-safe surprises a user on version skew warning names the version and the action; recoverable by upgrading; tested in E2E

8. Traceability matrix

Requirement (issue #14 acceptance) Use case Design Slice
Contract document with compatibility rules PUC-5 §6.2, INV-14/15/16 SLICE-1
Identity crosses rungs (Provenance) PUC-2 §6.3, fork (c) SLICE-2
Round-trip proven, no re-homing PUC-1/2 stand-in + INV-15, §6.5 SLICE-2/4
Concurrent-writer story defined and exercised PUC-3 mergeArtifacts, INV-17 SLICE-3
Validation runnable outside the extension PUC-5 schema + validator SLICE-1
Unit + host E2E coverage §6.8 SLICE-14

9. Open Questions & Decisions log

  • RESOLVED (this session — the six forks routed by #14): (a) carrier = the sidecar, ratified cross-rung; (b) contract = prose (content repo standing spec) + JSON Schema (plugin repo, code-resident); (c) identity = email + agent.onBehalfOf, git's model; (d) merge = pure 2-way union-by-id with surfaced conflicts (INV-17); (e) proof = local stand-in writer + external-change E2E + byte-stability; (f) versioning = additive, schemaVersion stays 1, INV-16 rules (incl. writer unknown-field preservation, INV-15).
  • OPEN → Gitea rung (rfc-app#46): graduate the schema to the engineering central schemas/ when a second rung actually implements the contract; whether the substrate wants a .gitattributes merge driver shipped; forge-specific identity fields beyond email.
  • OPEN → later: ratifiedBy / multi-party provenance (team rung); file rename/move tracking (path is still the key); rejected-proposal history trails; signed provenance if integrity-beyond-git is ever required.

10. Glossary & References

  • Cross-rung contract — the published format definition all ladder surfaces implement against. Foreign/conforming writer — any non-editor process writing a sidecar per the contract. Stand-in — the local script impersonating the Gitea rung's write behavior. Fail-safe read-only — INV-16's posture on newer-major sidecars: render, never write.
  • Epic #1 · Feature #14 (F5) · F2 #4 · F3 #6 · F4 #12 (PR #13) · parent specs coauthoring-inner-loop.md / coauthoring-attribution.md / coauthoring-propose-accept.md · rfc-app#48 (ladder) · rfc-app#46 (substrate rung) · wiggleverse/engineering#19 (adjacent metadata discussion).