Skip to content

feat(dynamic): auto-portablize captured ruleset ids on adopt (#76) - #86

Merged
2000game merged 1 commit into
mainfrom
feat/portable-rulesets-76
Jul 11, 2026
Merged

2000game merged 1 commit into
mainfrom
feat/portable-rulesets-76

Conversation

@2000game

Copy link
Copy Markdown
Member

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 → RefKind catalog (src/config/query-refs.ts)
VAR_REF_KINDS maps each ChurchQuery var to a canonical RefKind (reusing the exact strings from src/resolve/refs.ts — no new kinds invented). Verified against the real captured prod rulesets in ct-structure/rulesets/*.json; the full set of entity-bearing vars there is exactly:

var kind
ctgroup.id group
ctgroup.campusId / person.campusId campus
ctgroup.groupTypeId group-type
role.id role-def

Unknown 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 var position, 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 via deepMapRefs → equals the normalized original.

Stage 3 — --portable-rulesets flag on ct adopt group --with-dynamic (default OFF)
Builds per-kind id→key maps from the ReverseResolver already instantiated in the command (campus/group-type/role-def catalogs, via a new idToKeyByKind method) plus managed state (for group, which has no catalog), runs Stage 2, and writes markers instead of raw ids. Emits the warnings via the existing warn(...) logger:

! left N unmanaged id(s) numeric in <key>.json — operational/unmanaged refs, not portable (escape hatch)

Default-off is deliberate: auto-rewriting an id you thought was managed would silently change query semantics, so you opt in per invocation.

role.id kind decision: role-def

role.id in a ChurchQuery is a single numeric id from the global role catalog /group/roles — exactly what the resolver's role-def kind reads (CATALOG_PATH in src/resolve/resolver.ts). group-role is a compound (group, role) permission domain addressed by a pair — a different currency that a lone role.id number cannot express. So role-def is the only kind a bare role.id can portablize to. Documented in a code comment on VAR_REF_KINDS.

Files changed

  • src/config/query-refs.ts (new) — Stage 1 catalog + Stage 2 helper
  • src/resolve/reverse.ts — idToKeyByKind(kind) exposing catalog id→key maps
  • src/commands/adopt-group.ts — --portable-rulesets flag + wiring
  • docs/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 rationale
  • tests/query-refs.test.ts (new), tests/adopt-group-command.test.ts, tests/fixtures/dynamic/portablize-sintegrationmeeting.json (new)

Verification

  • npm run lint — clean
  • npm run typecheck — clean
  • npm test — 549 passed | 5 skipped (pre-existing)

Remaining

  • Stage 4 — live-dev adopt verification (needs eqrm-dev credentials). Out of scope here: adopt a real dynamic group with --portable-rulesets and confirm ct plan --env dev is 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.

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).
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