Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ code, and reconcile it against the ChurchTools API with Terraform-style
**`plan` / `apply`**.

> **People are never managed.** This tool touches only the scaffold, and only
> resources that are _explicitly_ declared or adopted. Everything else is
> invisible: never shown, never changed, never proposed for deletion.
> ct-cli resources that are _explicitly_ declared or adopted. External
> bindings are read-only prerequisites: they may be resolved and validated,
> but are never changed or proposed for deletion.

## Why

Expand Down Expand Up @@ -213,9 +214,14 @@ ct auth status # who am I? (`--env <name>` asks on another instan

ct get groups # JSON to stdout — pipe into jq (every page, not just the first)
ct adopt campus 0 # bring ONE existing resource under management
ct unadopt campus mainz --env prod # stop managing it; keep the live object
ct use group 4711 --key shared_group # bind an existing object read-only
ct unuse group shared_group --env prod # remove the binding; keep the live object
ct coverage # what the instance has that the config does not manage
ct state list # what is managed
ct state rm campus mainz # un-adopt: drop it from state. Never touches ChurchTools.
ct state list # managed and external entries, explicitly labelled
ct state rm campus mainz # low-level repair escape hatch; typed confirmation required
ct state rekey group old new # rename a logical key; update every ref.* use too
ct ownership check .. --env prod # validate visible owner/consumer projects
ct plan # diff the config against ChurchTools (read-only)
ct plan --format markdown # plain-language review report (German by default)
ct apply # create + update in dependency order (confirm + backup first)
Expand All @@ -231,14 +237,19 @@ printed — only the login token and its host reach the Keychain. There is delib
Windows there is no Keychain to store anything in, so the prompt is not offered at all:
export `CT_HOST` and `CT_LOGINTOKEN` there.

`state rm` is the inverse of `adopt`, and only of `adopt`: it removes the entry
from the state file, makes no HTTP call, and leaves the resource in place in
ChurchTools, now unmanaged. It refuses a key the config still declares — that
would make the next plan propose creating a resource that already exists — so
delete the declaration first, or pass `--force` to do both in one change.
"Declares" covers permission declarations too, not only resources: a key named
by a `ct.groupRole` domain or a group scope is just as broken to remove, and the
refusal is what keeps that from surfacing one command later as a plan error.
Use `unuse` for external bindings and `unadopt` for managed ownership. Both
commands make no HTTP call, leave the ChurchTools object in place, fail closed
when the config cannot be inspected, and refuse a key that remains declared or
referenced. They show a preview and require the environment name to be typed;
automation must pass an exactly matching `--confirm-env <name>`. `--force`
overrides only the config-reference guard, never the typed confirmation.

`state rm` remains a low-level repair escape hatch for either state partition.
It performs the same typed confirmation and a best-effort config-reference
check, warning when a broken config cannot be inspected, and points at `unuse`
or `unadopt` as the normal lifecycle command. A project without a named
environment types the logical key instead (or supplies `--confirm-key <key>` in
automation). `--dry-run` never needs confirmation and writes nothing.

`apply` reconciles **creates and updates** only, saving state after each action
(crash-safe / resumable). It **never deletes**: a resource dropped from the
Expand Down Expand Up @@ -290,6 +301,9 @@ ct apply --env prod # protected env: type the env name to confirm
- [**CI usage**](docs/ci.md) — the auth model and token-from-secret setup,
`--detailed-exitcode`, Markdown/JSON plan projections, deterministic sidecar
names, drift-vs-config attribution, and copy-pasteable PR artifacts.
- [**External ct-cli resources**](docs/external-resources.md) — the terminology
boundary, read-only `ct use` workflow, identity checks, state operations, and
cross-project ownership analysis.

## Guardrails (by design)

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ A page with no code behaviour to track declares `sources: []` plus a
| Page | About |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| [`adoption-contract.md`](adoption-contract.md) | What else comes along when `ct adopt` adopts one resource — the five categories and their defaults |
| [`external-resources.md`](external-resources.md) | Read-only cross-project bindings, `ct use`, identity validation, state, and ownership checks |
| [`api-coverage.md`](api-coverage.md) | Which ChurchTools endpoints support which CRUD verbs |
| [`group-field-decisions.md`](group-field-decisions.md) | Which group fields are managed vs. left to the CT UI, and why |
| [`runbook-manual-surface.md`](runbook-manual-surface.md) | What `ct` cannot automate today — where the write path is missing, and the manual steps around it |
Expand Down
32 changes: 21 additions & 11 deletions docs/adoption-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ ruleset, a calendar or a bookable room on an event.
the thing that references it, at any depth, under any flag.** Adopting a
resource booking does not adopt the room.

What is emitted instead, in order of preference — all of it already
implemented:
What is emitted instead, in order of preference:

- **already managed** → a portable logical reference. `ReverseResolver.sugarFields`
rewrites `campusId` → `campus: "…"`; `portablizeRuleset` rewrites entity ids
Expand All @@ -138,6 +137,10 @@ implemented:
`unmanaged`. `--strict-rulesets` already turns that from a warning into a
refusal for rulesets; the same escalation applies to any category-4 reference.

The consumer can now replace that interim numeric form with an explicit
external binding (`ct use <type> <id> --key <key>`) and the normal logical
`ref.*` form. See [External ct-cli resources](external-resources.md).

#### Why category 4 has no opt-in

A `--with-referenced` flag would look harmless and would be the single most
Expand All @@ -153,12 +156,19 @@ There are two supported remedies, and both make the ownership claim explicit:

1. `ct adopt <type> <id>` on the referenced object — a deliberate, separate,
visible act of taking ownership;
2. once #143 lands, declaring it as an **external / read-only prerequisite** —
2. declare it as an **external / read-only prerequisite** with `ct use`
resolvable in `ref` positions, never created, updated, deleted, or written.

Remedy 2 is the one this contract expects to become normal. Until it exists,
the numeric-id + `TODO` output is the honest interim: it says "this reference
is not portable yet" rather than pretending it is.
Remedy 2 is the normal consumer workflow. Numeric-id + `TODO` output remains an
honest signal from adoption that the reference is not portable until explicitly
bound; adoption never claims or writes the external object as a side effect.

Lifecycle release is equally explicit and never means deletion. `ct unuse
<type> <key> --env <env>` removes only a consumer binding; `ct unadopt <type>
<key> --env <env>` relinquishes only managed ownership. Both leave the live
ChurchTools object untouched, refuse config references by default, and require
typed environment confirmation. Actual deletion remains exclusively `ct
destroy` and is restricted to managed state.

### 5. Person-related data — permanently excluded

Expand Down Expand Up @@ -390,11 +400,11 @@ This issue decides only. The work the contract implies, roughly in order:
one adopted without its member fields.
5. **The five-verb summary** replacing today's per-command ad-hoc output, and a
test pinning the rendered summary (the same discipline `plan` output has).
6. **External/read-only references** — [#143](https://github.com/eqrm/ct-cli/issues/143).
Until it lands, category 4 falls back to numeric id + `TODO`; after it lands,
the emitted form for an unmanaged reference should become an external
declaration, which is what makes category 4's "no opt-in" rule comfortable
rather than merely correct.
6. **External/read-only references** — implemented by
[#143](https://github.com/eqrm/ct-cli/issues/143). Category 4 still emits a
numeric id + `TODO` until the consumer explicitly runs `ct use`; the persisted
external binding then makes its logical `ref.*` form portable without
transferring lifecycle ownership.

### How #135 applies this contract

Expand Down
192 changes: 192 additions & 0 deletions docs/external-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# External ct-cli resources

Independent ct projects can share one ChurchTools object without sharing its
lifecycle. The owner project declares or adopts it. Each consumer records a
host-specific, read-only binding with `ct use` and keeps using the normal
portable `ref.*` form in config.

```bash
# owner project: lifecycle ownership
ct adopt group 4711 --key ojahr_fuzzies --env prod

# consumer project: read-only consumption
ct use group 4711 --key ojahr_fuzzies --owner shared-masterdata --env prod
```

```ts
ref.group("ojahr_fuzzies");
```

There is deliberately no `ct.external.*` config DSL. The config stays portable;
state binds its logical keys to ids separately on every ChurchTools host.

## Terminology boundary

ChurchTools does not use one official umbrella term for the objects in the
ct-cli registry, and it uses “Resource” for other product concepts. In this
documentation:

- **ct project** means one config plus its environment-specific state files and
therefore one lifecycle boundary.
- **ct-cli resource** means an independently addressable top-level object in
the resource registry. It does not mean a ChurchTools “Resource” feature.
- **managed** means this ct project owns lifecycle responsibility. Its plan,
apply, and an explicit destroy may act on the object.
- **external** means this ct project can resolve the object read-only but does
not own it. This is ct-cli state only; no ChurchTools flag is written.
- **owner project** is the project with the managed state entry; a **consumer
project** has an external binding.
- A **logical key** is the portable, user-controlled name used by `ref.*`. It is
never stored in ChurchTools.
- A **host binding** maps `(ct-cli resource type, logical key)` to a ChurchTools
id for one host.
- An **identity snapshot** is the minimal live identity used to detect a changed
binding meaning. It is validated, never reconciled by the consumer.
- A **coordination scope** is only the explicit directory tree inspected by
`ct ownership check`; it is not a ChurchTools boundary.

## Creating a binding

The deterministic form is suitable for scripts and CI. Both id and key are
mandatory outside a terminal:

```bash
ct use group 4711 --key ojahr_fuzzies --env prod
```

In a terminal, a string selector performs fuzzy discovery within the explicit
type and shows every match with its id, exact name, and disambiguating fields:

```bash
ct use group "OJAHR Fuzzies" --env prod
```

The command never guesses among multiple matches. It proposes an existing
consumer key when the id is already bound; otherwise it proposes a one-time
slug that can be edited. A logical key is never recalculated later.

`ct use` reads ChurchTools to validate the selected object but never writes it.
It is byte-idempotent: the same key, id, owner metadata, and hard identity is a
successful no-op and does not update `boundAt`. A hard identity change or a
replacement id requires interactive confirmation or `--yes`; the command shows
the changed fields or old/new targets first. Display-only changes need no
confirmation. Managed keys and ids cannot be rebound as external, and neither a
key nor `(type, id)` may have a second external alias.

Supported top-level types come directly from the registry: `campus`, `group`,
`group-type`, `age-group`, `target-group`, `relationship-type`,
`person-status`, `department`, `security-level`, `comment-viewer`, and
`group-role`. Permissions, relationship edges, owned child structures, and all
person-related data are not independent external types.

## State and identity

State version 2 keeps lifecycle ownership and consumption separate:

```json
{
"version": 2,
"host": "https://example.church.tools",
"resources": {},
"externals": {
"ojahr_fuzzies": {
"type": "group",
"key": "ojahr_fuzzies",
"id": 4711,
"owner": "shared-masterdata",
"identity": { "name": "OJAHR Fuzzies", "groupTypeId": 17 },
"boundAt": "2026-08-27T12:00:00.000Z"
}
}
}
```

Version-1 files load in memory as version 2 with an empty `externals` map and
are written as version 2 on the next state mutation. Managed entries remain in
`resources`; external entries never carry managed fields or lifecycle flags.

The registry defines hard identity: name for every type, plus group type id for
groups and group roles. Short names, translated names, campus/status, sort
order, relationship labels, member-status meaning, numeric security level, and
leader/participant role type are selection display only. Thus moving a group to
another campus does not block a consumer plan; renaming it or changing its group
type does.

Inspect or maintain either state partition with the shared commands:

```bash
ct state list --env prod # managed and external, with explicit kind
ct state list --managed --env prod
ct state list --external --env prod
ct unuse group ojahr_fuzzies --env prod
ct unadopt group owned_group --env prod
ct state rekey group old_key new_key --env prod
```

`unuse` removes only external bindings; `unadopt` removes only managed ownership.
Neither contacts ChurchTools or deletes the live object. Both first check every
known config declaration and `ref.*` position, show the exact entry and state
file, then require the environment name to be typed. A referenced key blocks by
default; `--force` overrides that check only when the config and state changes
are deliberately made together. `--dry-run` previews without confirmation or a
write. In non-interactive use, confirmation remains explicit:

```bash
ct unuse group ojahr_fuzzies --env prod --confirm-env prod
```

`ct state rm` remains the low-level repair escape hatch and carries the same
typed confirmation plus a best-effort reference check; unlike the public
lifecycle commands it can proceed with a warning when a broken config cannot be
inspected. Prefer `unuse`/`unadopt`. Rekeying requires every config and `ref.*`
use to be changed consistently.

## Planning and safety boundary

Resolution checks managed state first, same-run managed declarations second,
and persisted external bindings third. A bound external is read live by id and
its hard identity is verified before use in supported positions such as
permissions, hierarchy parents, and dynamic rulesets.

An external is never a desired or pending resource. It therefore cannot emit a
create, update, or delete action; consumer `apply` and `destroy` enumerate only
managed state. If an external prerequisite is missing, stale, ambiguous, or has
changed identity, plan fails before writes. Discovery may provide complete
`ct use` commands, but plan never persists or temporarily consumes a candidate.
The consumer never applies or repairs the owner project.

Blocking diagnostics include a stable reason code and structured context,
evidence, consequence, numbered remedies, and an exact verification command.
Typical recovery is one of:

```bash
# create/repair the object from its owner project first
cd ../shared-masterdata && ct plan --env prod

# bind the verified live object in the consumer
ct use group 4711 --key ojahr_fuzzies --env prod
ct plan --env prod
```

Do not bind an id that returns 404. Repair the owner's stale state or restore the
object first.

## Checking ownership across projects

Run the check with an explicit complete directory scope:

```bash
ct ownership check .. --env prod
```

It recursively finds ct projects below that root, ignores `.git`,
`node_modules`, and build output, groups results by ChurchTools host, and makes
no network calls. It reports duplicate managed owners, missing or mismatching
owner hints, different keys for the same `(type, id)`, conflicting bindings,
and incompatible identity snapshots. Conflicts return a non-zero exit code for
CI and include `ct state rekey`, `ct unuse`, `ct unadopt`, or broader-scope
remediation.

The guarantee is intentionally scope-limited. Projects outside the supplied
root remain unknowable; global atomic ownership would require a separate shared
registry.
Loading
Loading