Appearance
Hierarchy position is referenced by a bare parent field
Context
ADR#4761776210 attaches every resource to exactly one node of its tenant's hierarchy. Each service now needs a field on its resources that references that node, and without a shared convention each service will invent its own (folder, scope, container, parentId), which makes cross-service reading and tooling needlessly hard.
Two worries shaped the naming:
Ambiguity with kinship. Systems also have parent-child relationships between resources of the same kind (a session spawned by a session, an agent delegated by an agent). Could parent mean two things?
Collisions. container collides with OCI containers on any platform that runs workloads; scope and location carry established meanings in cloud APIs (auth scopes, regions).
Industry evidence, verified against primary sources (linked below):
- Google Cloud: the Project resource has a field named
parent, documented as "A reference to a parent Resource. eg.,organizations/123orfolders/876", plus aprojects.movemethod ("Move a project to another place in your resource hierarchy, under a new resource parent"). - AWS: Organizations speaks Parent throughout:
ListParents("Lists the root or organizational units (OUs) that serve as the immediate parent of the specified child"),MoveAccount(AccountId, SourceParentId, DestinationParentId). Its docs also state the single canonical parent outright: "In the current release, a child can have only a single parent." - Azure: the ManagementGroup resource carries
properties.details.parent(ParentGroupInfo: "The ID of the parent management group", withid,name,displayName). - Kubernetes HNC: "The parent is defined by the
.spec.parentfield" of each namespace'sHierarchyConfigurationobject.
An honest caveat about this evidence. The four attestations above sit on org-structure nodes (OUs, folders, management groups, projects), not on arbitrary resources attached inside the structure, which is our case. For placed leaf resources the picture differs: GCP leaves carry no stored parent field (the position is encoded in the resource name), AWS leaves have no hierarchy (tags), and Kubernetes, the one platform with a stored position field on every object, calls it namespace. The convention still transfers to our case on three narrower grounds:
- Movability is the real dividing line, not org-versus-resource: GCP's stored
parentfield appears exactly on things that move (Project, Folder) and not on things that don't. Our resources are movable, so the Project precedent is a case match. parentis Google's universal request vocabulary for placing any resource, not just org nodes: every AIP-133 create isCreate<Thing>(parent: ...)for books and topics as much as folders.- The honest counter-candidate from the leaf-resource world,
namespace, names a flat concept; importing it onto a recursive tenant-drawn tree would mislead the engineers it is meant to help.
GCP's and Azure's parent values are typed references, so the value states what kind of node the parent is.
Compound alternatives were considered and each carries a wart: parent_id does not say which parent; parent_container reintroduces the OCI collision; parent_folder introduces vocabulary the hierarchy ADR does not use; and parent_node, though it now uses the right word, says in the field name what the field's type already says. The ambiguity a compound would prevent does not occur in practice: ambiguity lives inside a single schema, and no schema carries both a hierarchy position and unqualified kinship.
Resolution
Chosen option: a bare parent field, because it matches the verified industry convention for movable resources and the feared ambiguity cannot arise under the qualification rule below.
- On any resource, the field
parentrefers to position: the node of the tenant's hierarchy the resource is attached to. It has no other meaning, on any resource, in any service. - Kinship between resources is always qualified, never bare:
parentSessionId,parentAgentId,sourceParentId. A bareparentholding kinship is a review defect. - Parent values are self-describing, so a value read in isolation says what it points at. Two mechanisms deliver that and both apply: the field's type,
trogon.hierarchy.v1alpha1.NodeId, and the resource id prefix carried inside the value,node_01h9x. Our prefix convention (ADR#4860595695) is what GCP achieves with a collection segment (folders/876); it is the same goal reached by the spelling this ecosystem already uses, so parent values take the prefix form and not the path form. The value names one node and never encodes ancestry. - The spellings
parentIdandparent_idare disallowed, for a reason internal to our own naming pattern rather than imported taste. Every id-suffixed reference field we write is<Type>Id: the word before the suffix names a resource type (agentId,sessionId,rubricId). "Parent" is not a type; it is a role, and a position may point at a node of any kind, which is its entire point. So the two valid shapes are: role-named bareparentfor position, and role-plus-typeparent<Type>Idfor kinship (parentSessionId, where Session is the type).parentIdis a role plus suffix with no type, fitting neither pattern, so it cannot be written. This mirrors actual Google practice, which mixes bareparentwith type-namedbook_idin one request without contradiction. Weight note: the load-bearing rule of this ADR is kinship qualification (rule 2); this spelling rule is a consistency rule on top of it. - When ids are wrapper messages rather than bare strings, the
Idsuffix belongs to the type and leaves the field name. Rule 4's two shapes then read asNodeId parentfor position andSessionId parent_sessionfor kinship, rather thanparent_session_id. The principle is unchanged, a field states its role and the type states what the role points at; only the spelling moves, because repeating the suffix in a typed field is stutter.
Consequences
- Engineers arriving from GCP, AWS, Azure, or Kubernetes read the field correctly on sight.
- Grep for
parent[^A-Z_]finds every position reference across services; qualified kinship never pollutes the result. parentis where accountability is anchored: attaching to a node is an authorized write, so who answers for the node answers for what hangs off it. Resources do not carry anownerfield to restate that, per ADR#4761776210. The field still grants nothing on its own; policy evaluation reads it as input.- Moving a resource is a change of
parent, which keeps move APIs uniform (sourceParent,destinationParent) across services. - The rule is enforced in schema tooling, not review vigilance: the protobuf lint pipeline accepts exactly two shapes and rejects the rest. A field named exactly
parentmust reference a hierarchy node; a field matchingparent<Type>Idis kinship and must carry the qualifier; every otherparent*spelling (includingparentIdandparent_id) is rejected (the machine-checkable analog of Google'sgoogle.api.resource_referenceannotations on parent fields). - Domains that own a tree of same-kind resources qualify with their own type (
parentTaskId,parentCommentId). The hierarchy service's own events are the one near-bare use (parentIdon a node), which is consistent: there the parent is the position. - Foreign protocols keep their names at the boundary. If an external standard uses bare
parentfor kinship, adapters translate at the edge; this ADR governs our stored schemas and events only.
Links
- ADR#4761776210: Resource hierarchy via untyped recursive nodes
- ADR#4860595695: Human-Readable IDs
- ADR#1394819661: Resource Is the Generic Noun, Object Is a Runtime Term
- GCP Project resource:
parentfield andprojects.move - AWS Organizations ListParents ("a child can have only a single parent")
- AWS Organizations MoveAccount (
SourceParentId,DestinationParentId) - Azure Management Groups Get (
properties.details.parent) - Kubernetes HNC concepts (
.spec.parent)