camunda / camunda/api-test-generator
Ontology under-represents composite identity for tenant-scoped resources (`TenantClusterVariable`)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 3
- Avg merge
- 13h 41m
- Merged PRs (30d)
- 23
Description
Summary
TenantClusterVariable is the only OCA entity kind whose canonical identifier is a 2-tuple where one element (TenantId) is owned by a different entity kind (Tenant). The spec captures the composite identity correctly; the ontology cannot, because EntityKind.identifiers[] is constrained by a single-owner rule. The tenant half of the identity is reified as a requires: Tenant relationship instead.
This is a deliberate workaround documented in the entity's description. It is not currently a bug — the planner has everything it needs from the two signals combined — but it creates an asymmetry with the Edge shape that's worth tracking before more resources of this kind appear.
Findings (full sweep of x-semantic-establishes.identifiedBy[] with ≥2 entries)
13 operations in the OCA spec declare composite identifiers. They split into two structural patterns:
Pattern A — Tenant-scoped resource (1 op)
| Operation | Establishes | Identifiers in spec | Ontology shape | Ontology identifiers |
|---|---|---|---|---|
createTenantClusterVariable |
TenantClusterVariable |
TenantId + ClusterVariableName |
EntityKind | ClusterVariableName only |
Spec (spec/camunda-oca/bundled/rest-api.bundle.json, POST /cluster-variables/tenants/{tenantId}):
"x-semantic-establishes": {
"kind": "TenantClusterVariable",
"identifiedBy": [
{ "in": "path", "name": "tenantId", "semanticType": "TenantId" },
{ "in": "body", "name": "name", "semanticType": "ClusterVariableName" }
]
},
"x-semantic-requires": { "kind": "Tenant", "bind": { "tenantId": { … } } }
Ontology (configs/camunda-oca/ontology/entity-kinds.json):
{
"@type": "EntityKind",
"name": "TenantClusterVariable",
"shape": "entity",
"identifiers": ["ClusterVariableName"],
…
"description": "… Only ClusterVariableName is listed in 'identifiers' here because TenantId is owned by the Tenant entity and must remain single-owner for the edge-derivation rule; the Tenant requirement of tenant-scoped operations is expressed via the tenantId path parameter."
}
Pattern B — Membership edges (12 ops, all clean)
All assign*To* operations. These are correctly modelled as Edge records in configs/camunda-oca/ontology/edges.json, and Edge.identifiedBy[] accepts a full composite:
{
"@type": "Edge",
"name": "RoleUserMembership",
"endpoints": { "from": "Role", "to": "User" },
"identifiedBy": ["RoleId", "Username"],
…
}
Full list (no gap on any of these):
Spec kind |
Identifiers |
|---|---|
RoleUserMembership |
RoleId + Username |
RoleClientMembership |
RoleId + ClientId |
RoleGroupMembership |
RoleId + GroupId |
RoleMappingRuleMembership |
RoleId + MappingRuleId |
GroupUserMembership |
GroupId + Username |
GroupClientMembership |
GroupId + ClientId |
GroupMappingRuleMembership |
GroupId + MappingRuleId |
TenantUserMembership |
TenantId + Username |
TenantClientMembership |
TenantId + ClientId |
TenantGroupMembership |
TenantId + GroupId |
TenantRoleMembership |
TenantId + RoleId |
TenantMappingRuleMembership |
TenantId + MappingRuleId |
The asymmetry
| Shape | Supports composite identifiedBy[]? |
Why |
|---|---|---|
Edge |
✅ | Edges are inherently between two owned entities; the composite is the edge's primary key. |
EntityKind |
❌ | Single-owner rule on semantic types: each SemanticType may be listed in at most one EntityKind.identifiers[], so edge-derivation can unambiguously look up the owning kind. |
TenantClusterVariable is structurally an EntityKind (it has its own lifecycle: create/get/update/delete with a body carrying value) and not an Edge (it's not just an associative link between two pre-existing entities). So neither shape fits it cleanly today.
Why this matters
For the planner today: not at all. It uses x-semantic-requires: Tenant to chain the prereq and ClusterVariableName to mint the name, and the verification trace in PR #287 shows the emitted URL correctly populates both path segments.
For anyone reading the ontology to reconstruct identity:
- A uniform query like "list the canonical identifiers of
<kind>" gives the wrong answer forTenantClusterVariable(returns 1 of 2). - The composite identity has to be reassembled by joining
entity-kinds.identifiers∪entity-kinds.<this>.description-parsed∪edges.* where endpoints.from == 'Tenant'— i.e. there's no machine-readable single source of truth.
For future spec growth: if OCA adds another tenant-scoped resource (likely candidates: tenant-scoped roles, tenant-scoped client secrets, tenant-scoped form/decision artifacts), each will hit the same asymmetry and the workaround scales linearly.
Options (no recommendation yet)
- Relax single-owner rule on
EntityKind.identifiers. Allow a semantic type to appear in multipleEntityKind.identifiers[]. Edge-derivation would need a tie-breaker (e.g. "the kind that establishes the type wins" or explicitowns: truemarker). Largest change; touchespath-analyser/src/ontology/edgeSchema.tsand the derivation logic. - Add a non-authoritative
compositeIdentityfield onEntityKind. Purely informational; edge-derivation continues to useidentifiers[]only. Smallest change; cleanly captures the composite for any downstream consumer that wants it.TenantClusterVariablebecomes:"identifiers": ["ClusterVariableName"], "compositeIdentity": ["TenantId", "ClusterVariableName"] - Introduce a
ScopedEntityshape (third shape alongsideentityandedge).TenantClusterVariabledeclaresshape: "scopedEntity",scopedBy: "Tenant",localIdentifier: "ClusterVariableName". Most expressive but introduces a new structural concept. - Materialise the composite at export time. Ontology bundle (
generated/<config>/ontology-bundle.json) gets a derivedcanonicalIdentifiersfield on each kind, computed fromidentifiersplus any singlerequireschain. Source files stay as-is; only the bundle changes.
Option 2 has the cleanest cost/benefit for the current scope (one entry). Option 4 is attractive if the goal is purely "let SPARQL/GraphDB consumers see the full identity" without changing the source contract.
Acceptance criteria (if/when we act)
- Decide between options 1–4 (or another approach) with a single-line rationale.
- Update
path-analyser/src/ontology/entityKindSchema.tsandconfigs/camunda-oca/ontology/entity-kinds.jsonaccordingly. -
npm run build:ontologyregeneratesontology/vocabulary/entity-kinds.schema.json. - L3 invariant in
configs/camunda-oca/regression-invariants.test.ts: every operation whosex-semantic-establishes.identifiedBy[]has ≥2 entries is either (a) modelled as anEdgewith matchingidentifiedBy, or (b) modelled as anEntityKindwhose composite-identity representation (whatever shape we pick) covers all spec identifiers. - Class-scoped: the invariant must reject reintroduction of the asymmetry for any future kind, not just
TenantClusterVariable. - No planner output drift expected (
generated/camunda-oca/**byte-identical after regen) — this is an ontology-shape change, not a planning change. If drift occurs, justify in PR.
Out of scope
- The 12 membership edges. They are already correctly modelled.
- Changing how
createTenantClusterVariableis planned, sequenced, or emitted. - Any change to the upstream spec (
x-semantic-establishesalready carries the full composite identity — the gap is in our ontology shape, not in the spec).
Refs
- Spec:
spec/camunda-oca/bundled/rest-api.bundle.jsonPOST /cluster-variables/tenants/{tenantId} - Ontology entity-kinds:
configs/camunda-oca/ontology/entity-kinds.json#L91-L101 - Ontology edges (pattern B reference):
configs/camunda-oca/ontology/edges.json - Single-owner rule:
path-analyser/src/ontology/edgeSchema.ts(TBox source) /ontology/vocabulary/edge.schema.json(committed JSON Schema)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the TenantClusterVariable entry in configs/camunda-oca/ontology/entity-kinds.json and the ontology schemas in path-analyser/src/ontology/entityKindSchema.ts and edgeSchema.ts. Compare the four proposed identity representations, then run npm run build:ontology and configs/camunda-oca/regression-invariants.test.ts. Done means a decided representation covers all composite identifiers without planner output drift.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100