Repository navigation
feat(dynamic): auto-portablize captured ruleset ids on adopt (#76) - #86
Merged
Merged
Conversation
Add the ergonomics layer for portable dynamic-group rulesets. The resolve/diff
engine already turns `{ __ctRef }` markers inside a ruleset into per-host ids
before the diff (byte-faithfully); this fills the missing piece — rewriting a
fresh capture's raw numeric ids into those markers.
Stage 1: `VAR_REF_KINDS` catalog (src/config/query-refs.ts) mapping each
ChurchQuery `var` to a canonical RefKind, verified against the real captured
prod rulesets. Unknown vars (groupStatusId, isArchived, dateOfDeath) are absent
→ left untouched (escape hatch).
Stage 2: pure `portablizeRuleset(ruleset, { idToKeyByKind })` — walks the query,
rewrites managed ids in known var positions to ref markers, leaves unmanaged ids
numeric and collects `{ var, id }` warnings. Deterministic, offline; round-trip
tested against a real fixture.
Stage 3: opt-in `--portable-rulesets` flag on `ct adopt group --with-dynamic`
(default OFF). Builds per-kind id→key maps from the existing ReverseResolver
(campus/group-type/role-def catalogs) plus managed state (group), then writes
markers and warns about unmanaged ids left numeric.
role.id → role-def (global /group/roles catalog id), not group-role (a compound
permission domain a lone numeric id cannot express).
This was referenced Jul 11, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the ergonomics gap in #76 (Stages 1–3). The resolve/diff engine already resolves logical
{ __ctRef }markers embedded anywhere in a ruleset to per-host ids before the diff, byte-faithfully (pinned by #82). The only thing missing was rewriting a freshly captured snapshot's raw instance-specific ids into those markers. This PR adds that, opt-in.What each stage does
Stage 1 —
var → RefKindcatalog (src/config/query-refs.ts)VAR_REF_KINDSmaps each ChurchQueryvarto a canonicalRefKind(reusing the exact strings fromsrc/resolve/refs.ts— no new kinds invented). Verified against the real captured prod rulesets inct-structure/rulesets/*.json; the full set of entity-bearing vars there is exactly:ctgroup.idgroupctgroup.campusId/person.campusIdcampusctgroup.groupTypeIdgroup-typerole.idrole-defUnknown vars (
ctgroup.groupStatusId— no REST catalog #67;person.isArchived/person.dateOfDeath— literals) are deliberately absent → left untouched (escape hatch).Stage 2 — pure
portablizeRuleset(ruleset, { idToKeyByKind })Walks the query; for each numeric id in a known
varposition, rewrites to the{ __ctRef }marker when the id maps to a managed logical key, else leaves it numeric and collects a{ var, id }warning. Deterministic, no network, no input mutation. Unit-tested with a real fixture (portablize-sintegrationmeeting.json, copied from ct-structure), including a byte-faithful round-trip: portablize → resolve markers back viadeepMapRefs→ equals the normalized original.Stage 3 —
--portable-rulesetsflag onct adopt group --with-dynamic(default OFF)Builds per-kind id→key maps from the
ReverseResolveralready instantiated in the command (campus/group-type/role-def catalogs, via a newidToKeyByKindmethod) plus managed state (forgroup, which has no catalog), runs Stage 2, and writes markers instead of raw ids. Emits the warnings via the existingwarn(...)logger:Default-off is deliberate: auto-rewriting an id you thought was managed would silently change query semantics, so you opt in per invocation.
role.idkind decision:role-defrole.idin a ChurchQuery is a single numeric id from the global role catalog/group/roles— exactly what the resolver'srole-defkind reads (CATALOG_PATHinsrc/resolve/resolver.ts).group-roleis a compound (group, role) permission domain addressed by a pair — a different currency that a lonerole.idnumber cannot express. Sorole-defis the only kind a barerole.idcan portablize to. Documented in a code comment onVAR_REF_KINDS.Files changed
src/config/query-refs.ts(new) — Stage 1 catalog + Stage 2 helpersrc/resolve/reverse.ts—idToKeyByKind(kind)exposing catalog id→key mapssrc/commands/adopt-group.ts—--portable-rulesetsflag + wiringdocs/dynamic-groups.md— extends the "Portable snapshot files" section (test+docs(dynamic): pin & document portable ruleset snapshots across environments (#76) #82) with the flag, the var→kind table, and the role-def rationaletests/query-refs.test.ts(new),tests/adopt-group-command.test.ts,tests/fixtures/dynamic/portablize-sintegrationmeeting.json(new)Verification
npm run lint— cleannpm run typecheck— cleannpm test— 549 passed | 5 skipped (pre-existing)Remaining
--portable-rulesetsand confirmct plan --env devis a no-op (markers resolve to the same ids on the capture host), plus a second-env hand-map check. Engine already handles markers, so this is a live-write rehearsal, not new code.