Skip to content

Support binding to structured-data nodes (JSON/YAML/TOML) at the key/value level #36

Description

@flenter

Summary

drift can bind a doc to a code symbol (file.ts#Symbol) or a doc heading (doc.md#heading), and fingerprints that target so a change flags the binding for review. There's no way to bind to a value inside a structured-data file — a single key/value in a JSON (or YAML/TOML) document.

This proposes addressing a sub-document node by path and fingerprinting that node's value, so the existing "signature drift → flag for review" machinery applies to data files. It splits into two directions, easiest first.

Direction A — reference a JSON value as a target (the common case)

Bind a doc (or code) to a value in a JSON file. This is drift's existing doc → target flow, with the target being a JSON node instead of a symbol. package.json is the canonical example — docs constantly assert things that live in it:

  • "Rosie uses tsgo from @typescript/native-preview, pinned in package.json" → bind the sentence to package.json#/devDependencies/@typescript~1native-preview; a version bump flags the prose.
  • "Node ≥ 20"package.json#/engines/node.
  • A README listing available scripts → package.json#/scripts.
  • Any doc quoting an infra/config value → infra/limits.json#/maxConnections.

This needs only: (1) a way to address a JSON node, and (2) a value fingerprint. No new binding direction — it slots straight into today's model. It's the natural v1.

Direction B — bind a JSON value to another JSON value (the mirror case)

The richer case: one value should follow another, value → value. Motivating example, i18n catalogs with an identical key tree per locale:

locales/en/translation.json   ← source of truth
locales/nl/translation.json
locales/pl/translation.json

Edit the English value for auth.mode.signIn and the nl/pl values are now stale — they translate the old English. Key-coverage linting misses it (the key still exists); only a value-level binding catches it. And a maintainer who doesn't read Polish cannot see the staleness by eye — "the source moved and the translation didn't follow" is the only signal available. Binding: pl/translation.json#/auth/mode/signIn follows en/translation.json#/auth/mode/signIn.

This needs two things Direction A doesn't:

  1. Node as source, not just target — a data node usable on both sides of a binding.

  2. Bulk / "mirror" bindings (the real scaling problem) — a 146-key catalog × 2 target locales = ~292 bindings; nobody hand-maintains that in drift.lock. Needs a pattern binding that pairs leaves by matching pointer between two files:

    # conceptual
    mirror = "locales/{nl,pl}/translation.json"
    source = "locales/en/translation.json"
    match  = "by-pointer"   # each leaf follows the same JSON Pointer in source
    

    so one declaration covers every key, new keys are picked up automatically, and the lockfile stores a compact per-file signature map rather than N flat entries.

Other use cases

  • Config parityconfig.example.jsonconfig.schema.json; per-env configs.
  • Schema ↔ fixtures/types — an OpenAPI/JSON-Schema field bound to fixtures or types.
  • Design tokens — a tokens.json value bound to docs or CSS that consume it.
  • Version/identifier consistency — the same value duplicated across manifests.

Proposed shape

  1. Addressing. Extend the #-fragment to a structured-data pointer. JSON Pointer (RFC 6901) fits and round-trips cleanly (note ~1 escaping for / in keys, ~0 for ~ — relevant for scoped npm names like @typescript/native-preview):

    package.json#/devDependencies/@typescript~1native-preview
    locales/pl/translation.json#/auth/mode/signIn
    

    Same scheme generalizes to YAML/TOML (parse to a node tree, address by path).

  2. Signature = canonical value hash. Fingerprint the value at that path, canonicalized so formatting and sibling/key ordering don't false-trigger — mirroring drift's existing syntax-aware code comparison. A scalar hashes its value; an object/array subtree hashes its canonical form, so editing a different key in the same file doesn't churn unrelated bindings.

Open questions

  • Pointer syntax after #: JSON Pointer (/a/b) vs dotted (a.b) vs JSONPath — and key escaping.
  • Lockfile representation for mirror bindings without bloating drift.lock.
  • Is missing-key coverage in scope, or only changed-value staleness? (Coverage is often a domain linter's job; drift may want to stay focused on staleness.)
  • v1 format scope — JSON only, or JSON + YAML + TOML, since they share the "parse → address by path → canonical-hash the node" model.

Rough acceptance criteria

  • Direction A: bind a doc to package.json#/engines/node; drift check flags the doc when that value changes and does not flag on formatting/key-order changes.
  • Direction B: declare a mirror binding so every leaf in file(s) B follows the same-pointer value in file A, with one entry, surviving added/removed keys.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions