Skip to content

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

@flenter

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions