Skip to content

Embedded parent identity vs. parent reference: no consistency invariant #11

Open

Description

@sinkog

Problem

A hierarchical child entity can express its parent's identity in two independent places, with no mechanism tying them together.

Found in `cic-schema-registry`'s `KubernetesNode` (PR #152 review, `theads/thead16.txt`):

  1. `binding_surface.addresses[].key_fields`: `backend`/`provider`/`location`/`cluster_id` — the node's address is hierarchically nested under the cluster's address (RESTCONF path: `/clusters/cluster={backend},{provider},{location},{cluster_id}/nodes/node={id}`).
  2. `config_surface.cluster_ref`: a `cic-reference` field whose value is the parent cluster's own full `logical_id` (`cic:kubernetes:{backend}:{provider}:{location}:{id}`).

These are validated completely independently. Nothing enforces:

```
dereference(cluster_ref).logical_id == compose(backend, provider, location, cluster_id)
```

A node's embedded address could name one cluster while `cluster_ref` names a different one — both schema-valid, mutually inconsistent.

Why this matters

This is the third occurrence of the same pattern class the corpus keeps surfacing: field-level validation alone is insufficient, some claims can only be checked as a relationship between two positions, not within either position alone.

  • `cic-primitives#8` (closed, PR feat: item_choice, collection item discriminated union #9): a collection item's key field couldn't vary by variant — solved with `item_choice`, a structural fix.
  • `thead12` (`cic-schema-registry`): a key_field's valid value set is conditional on a sibling field — documented as an accepted, unaddressed language gap (no Contract mechanism exists for "this field's contract depends on that field's value").
  • This one: an embedded parent identity and a separate reference to the same parent can silently diverge — no Contract or grammar rule spans two field POSITIONS (one in `binding_surface`, one in `config_surface`) to check they resolve to the same entity.

Scope

Not proposing a fix here — this is a scoping issue, like #6 was for `derivation_chain`. Before any primitive/grammar design: is this worth solving at all (given it's the only known occurrence so far — `cic-schema-registry`'s `Subnet`/`NetworkSpace` pair does NOT have this problem, since `Subnet`'s `binding_surface` doesn't embed the network space's key at all, only a `network_space_ref`), and if so, at what layer:

  • a new Contract type that can reference another field's resolved value (something like `type: must` but crossing from `binding_surface` into `config_surface`, which today's `must` expressions with relative paths like `../config_surface/x` might already partially support syntactically, but nothing evaluates cross-position identity/reference equality specifically)
  • a dedicated "parent consistency" annotation on the reference field itself
  • accept it as a documented, unenforced invariant (like several AdapterContract correspondences already are) and leave it to adapter-level validation

Evidence / references

  • `cic-schema-registry` `general/kubernetes/kubernetes-node/kubernetes-node.yaml` — `binding_surface.addresses[].key_fields` (backend/provider/location/cluster_id) vs. `config_surface.cluster_ref`
  • `cic-schema-registry` `theads/thead16.txt` — full writeup, including why `Subnet`/`NetworkSpace` does NOT exhibit this pattern
  • `cic-schema-registry` PR #152 — where this was found (did not introduce it; the `cluster_id`+`cluster_ref` duplication predates it, PR #152 just made the full parent key visible in the child address)

Activity

  1. sinkog commented on Sep 27, 2026

    @sinkog
    MemberAuthor

    Proposal (recorded, not implemented — see rationale below)

    Surveyed the corpus first (cic-schema-registry, general/, working copies): this pattern — a child's binding_surface embedding the full parent key AND a separate cic-reference field to the same parent — occurs exactly once: KubernetesNode/KubernetesCluster. Every other cic-reference on the corpus (storage-resource.attached_to, compute-resource.network, subnet.network_space_ref, dhcp-options.network_space_ref, kubernetes-cluster.network_ref, kubernetes-node.compute_ref/network_ref) points to a sibling entity whose key is not embedded in the referrer's own address.

    The mechanism

    A new, optional annotation on a cic-reference field: parent_key_fields, naming the referrer's own key_fields (not the target's) whose combination composes the same address the reference field's value must resolve to.

    - name: cluster_ref
      shape_type: scalar
      scalar_type: string
      semantic_type: cic-reference
      reference_target: "cic:kubernetes:KubernetesCluster"
      parent_key_fields: [backend, provider, location, cluster_id]
      role: config
      mandatory: true

    Two layers, deliberately not conflated

    1. Schema level (check_grammar.py, a new C18): statically checkable — every name in parent_key_fields must resolve to an existing role: key entry in a key_fields list somewhere in the same document (not necessarily the same surface — cluster_ref lives on config_surface, the embedded key on binding_surface). This requires document-level pre-collection of all key_fields names before per-node checks run, unlike item_choice's purely local walk (feat: item_choice, collection item discriminated union #9). Real, but non-trivial, implementation work.
    2. Value level (runtime, the Relay/adapter's job): whether a filled-in instance's cluster_ref value actually equals the composed address from its own backend/provider/location/cluster_id values. check_grammar.py validates schema definitions, never instance data — it can never check this, full stop. parent_key_fields would only be a structured signal for whoever does validate instances (or auto-derives cluster_ref at write time instead of trusting client input), the same status today's type: must expressions already have (documented expectation, not grammar-evaluated).

    Why not implementing this now

    Introducing a new grammar primitive from a single occurrence is exactly the mistake #6/#134 (derivation_chain, ProviderCorrespondence) were decided against — a schema designed without a repeated, real usage pattern to generalize from. #8/item_choice was different: that fixed an active defect already live in the corpus (ref+vlan_id redundancy). This one isn't active — kubernetes-node.yaml doesn't currently violate the invariant, it's just not statically checked.

    Recorded here as a ready-to-implement plan. If a second occurrence of this pattern shows up anywhere in the corpus, this comment is the starting point — not a from-scratch design exercise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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