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:
-
Node as source, not just target — a data node usable on both sides of a binding.
-
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 parity —
config.example.json ↔ config.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
-
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).
-
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.
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.jsonis the canonical example — docs constantly assert things that live in it:@typescript/native-preview, pinned inpackage.json" → bind the sentence topackage.json#/devDependencies/@typescript~1native-preview; a version bump flags the prose.package.json#/engines/node.package.json#/scripts.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:
Edit the English value for
auth.mode.signInand thenl/plvalues 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/signInfollowsen/translation.json#/auth/mode/signIn.This needs two things Direction A doesn't:
Node as source, not just target — a data node usable on both sides of a binding.
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: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.example.json↔config.schema.json; per-env configs.tokens.jsonvalue bound to docs or CSS that consume it.Proposed shape
Addressing. Extend the
#-fragment to a structured-data pointer. JSON Pointer (RFC 6901) fits and round-trips cleanly (note~1escaping for/in keys,~0for~— relevant for scoped npm names like@typescript/native-preview):Same scheme generalizes to YAML/TOML (parse to a node tree, address by path).
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
#: JSON Pointer (/a/b) vs dotted (a.b) vs JSONPath — and key escaping.drift.lock.Rough acceptance criteria
package.json#/engines/node;drift checkflags the doc when that value changes and does not flag on formatting/key-order changes.