Skip to content

feat(irfacts): derive a correlation key for every entity - #1354

Open
OmarAlJarrah wants to merge 8 commits into
docs/ir-consumer-contractfrom
feat/irfacts-correlation-keys
Open

OmarAlJarrah wants to merge 8 commits into
docs/ir-consumer-contractfrom
feat/irfacts-correlation-keys

Conversation

@OmarAlJarrah

@OmarAlJarrah OmarAlJarrah commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Summary

An ID is a function of where its entity sits in the source. Across two revisions it therefore holds only while that position holds, and an array index can hand an old ID to a different entity. The golden spec's own history has a case. At revision b7e65ddd, t/anon/paths/~1organizations~1{organization_id}~1accounts/get/parameters/10/schema is the direction parameter's schema; at 319dc756 it is the status parameter's. A consumer keying state by ID, such as an emitter's prior manifest, would give one the other's name.

This builds the correlation key that docs/ir/derived-facts.md#correlation-keys designs (the design lands in #1351, this PR's base):

  • irfacts.Index.Correlation(id) returns what an entity of any ID class is called across revisions. Correlated(key) returns the reverse, so matching two revisions takes one call on each side.
  • A named entity is keyed by its name. An operation by its operationId, else its method and route with variables erased (a webhook by its event name). A type by its namespace and name, a primitive by its kind, and a service or auth scheme by its name. A group is keyed by its ID, which is never built from a position.
  • Everything else is keyed by a position that names it. That is its holder's key extended by one segment: a parameter by where it travels, a property or header by its wire name, a server by the scope naming it, and an anonymous type by the position holding it. An entity several positions name takes the least of their keys, settled in key order the way a shortest-path search settles the nearest vertex, so the key is the same whatever order the document declares things in.
  • The anonymous models a model composes in place are part of it. Their properties are keyed as the composing model's own, and each wire name keys the declaration the flattening takes, not the ones it overrides.
  • A key two entities share keys neither. Nothing beneath them is keyed through it either, because a wrong match is the failure the key exists to prevent.
  • Derived, never stored, as every irfacts fact is.

Measured on the golden spec (spec @319dc756, IR from morphic @b206dc99):

  • Every operation, parameter, group, server and auth scheme gets a key, as do 37,971 of 41,697 types, 47,367 of 49,390 properties and 29,633 of 32,291 enum members.
  • All but two of the entities left without a key lie beneath a union variant nothing in the document names. The two are anonymous models that one model composes in place, which share a key and so take none.
  • Against revision b7e65ddd, 41 entities whose IDs moved are matched by their keys, and the one ID that stays while naming another entity is told apart.
  • Deriving every key takes about 200 ms; the compile takes about 8 s.

Building against the golden spec corrected three rows of the design, which went back onto #1351. Its servers share URL templates, so the scope keys a server too. A header carries a property ID of its own. And a wire name declared twice in one composed model keys the declaration the flattening takes, rather than neither.

The docs that cited #679 now link the section: architecture.md, prior-art.md, reference-learnings.md. So do the consumers page (now "carried"), the package lists in CLAUDE.md and README.md, and the emitter contract's Facts comment.

Closes #679

The second commit replaces a local wire-name helper with ir.Property.EffectiveWireName; its subject is 78 characters with this PR's number appended. A later commit spelling the test fixtures with promoted fields is 74 with it. The branch has several commits, so the PR lands under its title, which fits.

Stack

Base: #1351, itself stacked on the emitter-design stack ending at #1339. Merge in order. GitHub retargets each PR after its base lands, and "Update branch" merges cleanly.

Test plan

  • go test ./ir/irfacts -run TestCorrelation: each named class, every position class, the depth bound on groups, callbacks under named and unnamed parents, in-place conjuncts, all three variant naming rules, a shared key, and the least-position rule in both declaration orders.
  • Mutants, each reddening a test: a shared key taken by one entity, the greatest key in place of the least, a header kept in its case, an overridden declaration keyed, an unnamed callback seeded with its own route, the group bound loosened, the greatest discriminator tag, no expansion, an in-place conjunct expanding its own members, and route variables kept. Two of these first survived, because the fixtures happened to sort the same either way. The fixtures now separate them.
  • Golden-spec probe and the two-revision differential above, by a throwaway test over morphic compile output. The probe also caught headers being keyed only through their types.
  • Push checklist: no artifact hits, no conflict with main or the base, skills_lint clean; the two subjects past 72 characters with the number are named above.
  • CI: build, vet, lint, archtest (apiRoots names the two methods), and coverage.

An ID is a function of where an entity sits in the source, so it holds
across revisions only while that position does, and an array index can
hand an old ID to a different entity. The golden spec's own history has
one: the organization accounts listing's tenth parameter schema is the
direction parameter's at one revision and the status parameter's at
the next.

irfacts.Index.Correlation now answers what an entity of any class is
called across revisions, and Correlated the reverse, as
docs/ir/derived-facts.md designs it. A named entity is keyed by its
name. Everything else is keyed by the least key among the positions
naming it, settled in key order as a shortest-path search settles the
nearest vertex. A key two entities share keys neither and nothing
through them, since a wrong match is what the key exists to prevent.

On the golden spec every operation, parameter, group, server and auth
scheme gets a key, as do 91% of types, 96% of properties and 92% of enum
members. All but two of the rest sit beneath a union variant nothing
names. Deriving every key takes about 200 ms.
Naming correlation keys took the package comment to 106 words.
Promoted fields in the test literals, preallocated slices and a
slices.Backward loop, as modernize and prealloc ask.
The modernize linter's embedlit check flags each type literal that names
its embedded TypeCommon; the fixtures now set ID, Name and Anonymous
directly, as the rest of the module does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pass: compute a key for matching entities across spec revisions

1 participant