Skip to content

fix(dynamic): key portable ruleset roles by (group-type, role-name), not role-def (#76) - #87

Merged
2000game merged 1 commit into
mainfrom
fix/portable-role-refs-76
Jul 11, 2026
Merged

2000game merged 1 commit into
mainfrom
fix/portable-role-refs-76

Conversation

@2000game

Copy link
Copy Markdown
Member

The bug in #86

PR #86 (portable rulesets, #76) rewrote a captured dynamic: true ruleset's ChurchQuery role.id operands into logical { __ctRef } role-def markers, resolved via /group/roles by slug(name) alone.

That decision is broken. A ruleset's role.id is a groupTypeRoleId — a role scoped to a group type — and role NAMES are not globally unique.

Live evidence (prod /api/group/roles, 46 roles, 2026-07-11)

  • 3 roles named "Leiter", 6 "Organisator", 6 "Mitglied" — each belonging to a different group type.
  • The role-def resolver (src/resolve/resolver.ts) throws "ambiguous" when a slug matches multiple catalog rows, so these markers are unresolvable.
  • Each /group/roles row carries a groupTypeId, and (groupTypeId, name) is unique — 0 collisions across all 46 roles.

The fix

A new compound ref kind group-type-role, paralleling the existing group-role:

{ "__ctRef": true, "kind": "group-type-role", "groupType": "<group-type-key>", "role": "<role-name>" }

Chosen over reusing role-def (bare name — the ambiguous currency this replaces) or group-role (a group-instance permission-pairing domainId via /groups/{id}/roles, a different id). The compound (group-type, role-name) pair is the minimal self-contained key that resolves uniquely per environment. Justified in a code comment on GroupTypeRoleRef.

  • refs.ts — GroupTypeRoleRef, ref.groupTypeRole(groupType, role); refKey/refLabel and the pending-collection guards handle the new kind.
  • resolver.ts — resolves the group-type key to this host's id (managed state ∪ /group/grouptypes catalog), then picks the one /group/roles row whose groupTypeId matches AND slug(name) === slug(role). 0 or >1 is a hard error listing candidates (mirrors the existing ambiguous/not-found style). Reuses the cached catalog fetch.
  • query-refs.ts — removes the wrong role.id → role-def entry from VAR_REF_KINDS; portablizes role.id via a caller-supplied role catalog (id → {groupTypeId, name}) + group-type id→key map, emitting the compound marker (unmanaged group type → numeric + warning, the escape hatch). Also rewrites the out-of-query process.*.handleMembership.groupTypeRoleId field via the same mechanism. Stays pure (no network).
  • reverse.ts — adds roleGroupTypeCatalog() returning id → {groupTypeId, name}.
  • adopt-group.ts — --portable-rulesets now fetches the role catalog + group-type id→key map and passes them into portablizeRuleset.

Tests

  • Real fixture tests/fixtures/dynamic/portablize-skidscheckinopsmz.json (copied from ct-structure): ctgroup.id ["112","8","1246"], role.id ["84","85","17","16"], process...groupTypeRoleId: 66.
  • Byte-faithful round-trip: portablize → markers → deepMapRefs back with the same maps → equals the normalized original (including the process field).
  • Same-named-role disambiguation, unmanaged group left numeric + warning, focused handleMembership.groupTypeRoleId test, resolver unit tests (incl. not-found / ambiguous).

lint, typecheck, test (560 passed | 5 skipped) all green.

Out of scope

Live re-localization of the actual ct-structure rulesets/*.json files is handled separately by a human — this PR only fixes the portablize/resolve mechanism.

…not role-def (#76)

PR #86 rewrote a captured `dynamic: true` ruleset's ChurchQuery `role.id`
operands into `{ __ctRef }` `role-def` markers, resolving them via `/group/roles`
by `slug(name)` alone. That is broken: a ruleset's `role.id` is a
**groupTypeRoleId** (a role scoped to a group TYPE), and role names are NOT
globally unique. Live prod `/api/group/roles` (46 roles, 2026-07-11) has 3
"Leiter", 6 "Organisator", 6 "Mitglied" — each on a different group type — so the
resolver throws "ambiguous" and the marker is unresolvable.

The (groupTypeId, name) pair IS unique (0 collisions across all 46 roles). This
introduces a new compound ref kind `group-type-role`
(`{ __ctRef, kind: "group-type-role", groupType, role }`), paralleling
`group-role`, and:

- refs.ts: new `GroupTypeRoleRef` + `ref.groupTypeRole`; refKey/refLabel and the
  pending-collection guards handle it.
- resolver.ts: resolves the group-type key to this host's id (managed state ∪
  `/group/grouptypes`), then picks the one `/group/roles` row matching that
  groupTypeId AND `slug(name)`; 0 / >1 is a hard error listing candidates.
- query-refs.ts: removes the wrong `role.id → role-def` entry from
  `VAR_REF_KINDS`; portablizes `role.id` via a supplied role catalog (id →
  {groupTypeId, name}) + group-type id→key map, and also rewrites the
  out-of-query `process.*.handleMembership.groupTypeRoleId` field. Stays pure.
- reverse.ts: adds `roleGroupTypeCatalog()` (id → {groupTypeId, name}).
- adopt-group.ts: `--portable-rulesets` fetches the role catalog + group-type
  id→key map and passes them through.

Byte-faithful round-trip covered with the real ct-structure fixture
skidscheckinopsmz.json. Live re-localization of the ct-structure rulesets is
handled separately by a human.
@2000game
2000game merged commit 7384bb8 into main Jul 11, 2026
1 check passed
@2000game
2000game deleted the fix/portable-role-refs-76 branch July 11, 2026 09:43
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.

1 participant