You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bootstrapping the real Equippers instance (eqrm/ct-structure) showed the authored config is hard to read and maintain:
ct.config.ts is repetitive: 35 near-identical ct.group({...}) calls; the Mainz Kids subtree alone is ~20 hand-written declarations differing only in key + name.
Numeric ids leak: all 35 group declarations carry a raw groupStatusId (33× 1, 2× 4). Note: the DSL already supportsstatus: "<name>" — ID_SUGAR in src/config/context.ts and the resolver's group-status → /group/memberstatus catalog exist since feat: portable configs — logical references instead of numeric CT ids (shared resolver) #20. The gap is that ct adopt emits numeric ids and the real config was never migrated.
Dynamic membership is manual boilerplate: every auto-group repeats dynamic: { status: "active", ruleset: { ref: "./rulesets/<key>.json" } }.
ct-state.json churns: ~604 lines for ~48 resources; upsertResource (src/state/state.ts) bumps updatedAt unconditionally, so unrelated lines change on every apply and the file is unreviewable.
ct adopt emits raw one-liners:configSnippet() (src/resources/registry.ts:158) is a single-line JSON.stringify with numeric ids — not paste-ready config.
Goal: author ChurchTools structure as naturally as Terraform HCL — declarative, DRY, readable — with machine state out of the human's way.
Decided approach — ordered work items
A. ct adopt emits idiomatic config. Rework configSnippet(): multi-line, prettier-compatible formatting; reverse-resolve campusId/groupTypeId/groupStatusId to the logical campus:/groupType:/status: sugar via the shared resolver catalogs; when adopting several resources (bulk adopt, feat(adopt): bulk/filtered adoption + auto-capture dynamic rulesets + warn on unknown declaration fields #51), group output by type with a comment header. Acceptance: adopt a live group, paste the snippet verbatim, ct plan is a no-op — zero hand edits.
B. Dynamic sugar.dynamic: true desugars to { status: "active", ruleset: { ref: "./rulesets/<key>.json" } }; dynamic: "<path>.json" desugars with an explicit path. Pure eval-time desugaring in src/config/context.ts; engine untouched.
C. Located validation errors. Capture the call site (new Error().stack, first user-config frame) in each ConfigContext declaration; eval-time validation errors and unknown-field warnings report file:line + resource key + field (e.g. ct.config.ts:42 — group "kids_planeten": unknown field "shortName"). The unknown-field warning itself is feat(adopt): bulk/filtered adoption + auto-capture dynamic rulesets + warn on unknown declaration fields #51's item 3; this issue supplies the location machinery.
D. State is machine-only (decision: adopted). (1) upsertResource bumps updatedAt only when fields actually changed (deep-equal guard); adoptedAt never changes. (2) In ct-structure, mark ct-state*.json with linguist-generated=true in .gitattributes so GitHub collapses it; humans review ct plan output (PR comment, feat: GitOps loop — plan on PR, gated apply, scheduled drift detection #24), never the state file.
E. Blueprints become the default authoring pattern (lands with chore: bootstrap eqrm/ct-structure — adopt the real Equippers scaffold #23 in ct-structure). Rewrite the real config as a per-campus blueprint function (mechanism exists — examples/campus-blueprint.config.ts): Mainz Kids subtree 20 ct.group calls → 1 blueprint call + params, with shared fields (campus, status, groupType) supplied by the blueprint, not repeated per declaration. Migrate all 35 groupStatusId: N → status: "<name>". One-time live verification on eqrm-dev that /group/memberstatus rows carry name (documented assumption, src/resolve/resolver.ts:53). README gets an authoring guide (declare → plan → PR).
Cut: no ct fmt. Configs are TypeScript; prettier is the formatter (idempotent by construction). ct-structure enforces prettier --check in CI. No defaults engine either — blueprints are the DRY mechanism; a field omitted from a declaration stays unmanaged (existing semantics).
Suggested order: D (tiny) → C → B → A tool-side, then E with #23.
Locked targets (definition of done)
Numeric ids in hand-authored config: 0 (measure: grep -cE '[A-Za-z]+Id: [0-9]+' ct.config.ts; currently 35).
Fields per plain ct.group() call: median 3 (key, name, groupType); shared fields live in the blueprint (currently median 5).
State diff for a one-resource change touches only that resource's block — no unrelated updatedAt bumps; state file collapsed on GitHub (machine-only decision replaces the X-lines target).
Adopt round-trip: paste emitted config verbatim → ct plan no-op, 0 hand edits.
A new contributor adds a correct resource in ≤ 10 min using only the README (timed dry-run, T=10).
Invalid config / unknown field fails with file + line + resource key + field.
Bootstrapping the real Equippers instance (
eqrm/ct-structure) showed the authored config is hard to read and maintain:ct.config.tsis repetitive: 35 near-identicalct.group({...})calls; the Mainz Kids subtree alone is ~20 hand-written declarations differing only inkey+name.groupStatusId(33×1, 2×4). Note: the DSL already supportsstatus: "<name>"—ID_SUGARinsrc/config/context.tsand the resolver'sgroup-status → /group/memberstatuscatalog exist since feat: portable configs — logical references instead of numeric CT ids (shared resolver) #20. The gap is thatct adoptemits numeric ids and the real config was never migrated.dynamic: { status: "active", ruleset: { ref: "./rulesets/<key>.json" } }.ct-state.jsonchurns: ~604 lines for ~48 resources;upsertResource(src/state/state.ts) bumpsupdatedAtunconditionally, so unrelated lines change on every apply and the file is unreviewable.ct adoptemits raw one-liners:configSnippet()(src/resources/registry.ts:158) is a single-lineJSON.stringifywith numeric ids — not paste-ready config.Goal: author ChurchTools structure as naturally as Terraform HCL — declarative, DRY, readable — with machine state out of the human's way.
Decided approach — ordered work items
ct adoptemits idiomatic config. ReworkconfigSnippet(): multi-line, prettier-compatible formatting; reverse-resolvecampusId/groupTypeId/groupStatusIdto the logicalcampus:/groupType:/status:sugar via the shared resolver catalogs; when adopting several resources (bulk adopt, feat(adopt): bulk/filtered adoption + auto-capture dynamic rulesets + warn on unknown declaration fields #51), group output by type with a comment header. Acceptance: adopt a live group, paste the snippet verbatim,ct planis a no-op — zero hand edits.dynamic: truedesugars to{ status: "active", ruleset: { ref: "./rulesets/<key>.json" } };dynamic: "<path>.json"desugars with an explicit path. Pure eval-time desugaring insrc/config/context.ts; engine untouched.new Error().stack, first user-config frame) in eachConfigContextdeclaration; eval-time validation errors and unknown-field warnings reportfile:line+ resource key + field (e.g.ct.config.ts:42 — group "kids_planeten": unknown field "shortName"). The unknown-field warning itself is feat(adopt): bulk/filtered adoption + auto-capture dynamic rulesets + warn on unknown declaration fields #51's item 3; this issue supplies the location machinery.upsertResourcebumpsupdatedAtonly whenfieldsactually changed (deep-equal guard);adoptedAtnever changes. (2) In ct-structure, markct-state*.jsonwithlinguist-generated=truein.gitattributesso GitHub collapses it; humans reviewct planoutput (PR comment, feat: GitOps loop — plan on PR, gated apply, scheduled drift detection #24), never the state file.examples/campus-blueprint.config.ts): Mainz Kids subtree 20ct.groupcalls → 1 blueprint call + params, with shared fields (campus,status,groupType) supplied by the blueprint, not repeated per declaration. Migrate all 35groupStatusId: N→status: "<name>". One-time live verification on eqrm-dev that/group/memberstatusrows carryname(documented assumption,src/resolve/resolver.ts:53). README gets an authoring guide (declare → plan → PR).ct fmt. Configs are TypeScript; prettier is the formatter (idempotent by construction). ct-structure enforcesprettier --checkin CI. No defaults engine either — blueprints are the DRY mechanism; a field omitted from a declaration stays unmanaged (existing semantics).Suggested order: D (tiny) → C → B → A tool-side, then E with #23.
Locked targets (definition of done)
grep -cE '[A-Za-z]+Id: [0-9]+' ct.config.ts; currently 35).ct.group()call: median 3 (key,name,groupType); shared fields live in the blueprint (currently median 5).updatedAtbumps; state file collapsed on GitHub (machine-only decision replaces the X-lines target).ct planno-op, 0 hand edits.Relates
ct planPR comment is the human review surface once state is machine-only