feat: portable configs — logical references via a shared per-host resolver (#20) - #46
Conversation
#20) Add Ref sentinels + ref.* helper (src/resolve/refs.ts) and a per-host Resolver (src/resolve/resolver.ts) sourcing ids from managed desired∪state (pending for same-run targets) then live catalogs (campus/group-type/group-status/role-def), throwing on unknown/ambiguous. DSL sugars campus/groupType/status into Ref-valued id fields and accepts groupType (group_type_role) + gated group+role (group_role). Wire the resolver into buildPlan (resolution pass before computePlan), permission domainId resolution, apply-time pending re-resolution, and pending plan rendering.
refs.ts (helper/guards/deepMapRefs/collectRefs), resolver.ts (state/catalog/ pending/ambiguous/unknown/gated group_role + resolveValue caching + apply-time re-resolution), DSL permission logical forms, and the two-host acceptance test.
…#20) A same-run reference embedded in a dynamic ruleset's var value was applied via applySyntheticFields, bypassing body re-resolution — the pending sentinel would leak into the ruleset PUT. Re-resolve the whole change set up front so both the write body and synthetic-field writes see real ids. Lock it with a test.
…r referencers A pending ref names a same-run resource, but tier ordering alone doesn't put the target first when referencer and target share a tier (a group's ruleset ref.group()-ing another group applied in declaration order and could never converge). buildPlan now injects a dependsOn edge from the referencer to each pending target so orderKeys sequences them; the pending-unresolved apply error no longer misdirects to tier ordering. Flagged by the PR #46 review with an empirical repro; test locks the create-before-ruleset-PUT order and the resolved id.
|
Review verdict was REQUEST_CHANGES with one MEDIUM finding (everything else verified clean empirically): same-tier pending refs had no ordering guarantee — a group whose ruleset Fixed in 30b59de: 343 → 344 tests passing; typecheck + lint clean. Merging. |
Implements #20 per the architect plan: configs reference master data by name/key instead of numeric CT ids, making one config portable across instances.
Authoring surface
ct.group({ key, groupType: "ministry_team", status: "active", campus: "mainz" })— sugared at eval into Ref-valuedgroupTypeId/groupStatusId/campusId. Declaring both forms throws.ref.*helper for inline positions:ref.campus("mainz")in dynamic-rulesetvarvalues;groupType: "<name>"forgroup_type_roledomains.campus:rejection guard superseded by real support.Resolution model
/campuses,/group/grouptypes,/group/memberstatus,/group/roles) by slug/exact-name with ambiguity detection; unknown/ambiguous refs throw (config error — no partial apply).<campus:mainz (created this apply)>, re-resolved at apply time against post-execute state — including refs inside dynamic rulesets routed via synthetic-field writes.buildPlan+buildPermissionPlanunder theirPromise.all.group_role(group, role)→pairing-id lookup has no confirmed API source — DSL accepts the form but resolver rejects with a clear "pass a numeric id (see feat: permissions ergonomics — domainId by reference, grant adoption, catalog lifecycle #25)" error, behind a pluggable seam.Acceptance
examples/portable.config.ts: zero numeric ids./group/memberstatusrows carryname(no fixture/schema available); mis-shape falls through to a hard error, never mis-resolves.Rebased onto main post-#45; all 14 grant-adoption round-trip tests pass against the new
desiredTuplespath.Verification: 343 passed / 4 skipped, typecheck + lint clean.
Closes #20.