Skip to content
Merged
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
53 changes: 53 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Docs

# Two gates over docs/handbuch/ — the section published into the Handbuch's
# "ChurchTools-Grundlagen" (#89, publishing contract in eqrm/churchtools-connector#1114):
#
# 1. `build` — mkdocs --strict, so a broken link or a page missing from the nav fails
# the PR here rather than silently in the consuming site build.
# 2. `staleness` — the estate-wide reusable checker (eqrm/churchtools-connector#1120).
# It reads each page's `sources:` globs and fails a PR that changes documented code
# without re-signing the page. The single implementation lives in the connector repo
# on purpose: reimplementing it per language would give us four subtly different gates.
#
# Re-signing a page means recomputing `sources_hash` over its `sources:` — a sha256 over
# each resolved file's `<relative-path>\0<contents>\0`, sorted, truncated to 16 hex chars
# (see the connector's `App\Services\Docs\SourcesHasher`). Bumping `reviewed:` alone does
# NOT satisfy the gate; the point is that someone re-read the page against the new code.

on:
push:
branches: [main]
paths:
- "docs/handbuch/**"
- "src/**"
- ".github/workflows/docs.yml"
pull_request:
paths:
- "docs/handbuch/**"
- "src/**"
- ".github/workflows/docs.yml"

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install MkDocs
run: pip install -r docs/handbuch/requirements.txt
- name: Build the section (strict)
run: mkdocs build -f docs/handbuch/mkdocs.yml --strict

# `src/**` changed without the matching page re-signed → this fails. Signing is the
# acknowledgement, not the file appearing in the diff.
staleness:
uses: eqrm/churchtools-connector/.github/workflows/docs-staleness.yml@main
with:
docs-path: docs/handbuch
secrets: inherit
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ src/api/schema.d.ts
# Test/coverage output
coverage/
.vitest/

# MkDocs output for docs/handbuch/ (built by .github/workflows/docs.yml, never committed)
site/
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Early scaffold. See the [epic (#1)](https://github.com/eqrm/ct-cli/issues/1) and
readable (`ct get person-masterdata`, `ct get data-fields`) — schema in scope,
per-record field **values** never. Definitions are read-only (no REST write
endpoint); the boundary + writability evidence is in
[`docs/field-definitions.md`](docs/field-definitions.md).
[`docs/handbuch/field-definitions.md`](docs/handbuch/field-definitions.md).

## Requirements

Expand Down Expand Up @@ -203,7 +203,7 @@ into a function and call it once per campus, prefixing every key with
hierarchy (`parents`) scoped to that campus. `ct plan`/`ct apply` order
campuses → groups → hierarchy automatically via the dependency graph, and
an undeclared `parents` reference throws at config-load time (typo guard).
See [`docs/blueprints.md`](docs/blueprints.md) for the full guide and
See [`docs/handbuch/blueprints.md`](docs/handbuch/blueprints.md) for the full guide and
[`examples/campus-blueprint.config.ts`](examples/campus-blueprint.config.ts)
for a runnable example.

Expand All @@ -223,7 +223,7 @@ ct.group({

`ct apply --refresh` opts in to a post-apply, per-group membership
recompute for every dynamic group that changed. See
[`docs/dynamic-groups.md`](docs/dynamic-groups.md) for the full guide (the
[`docs/handbuch/dynamic-groups.md`](docs/handbuch/dynamic-groups.md) for the full guide (the
`status` states, the three ways to supply a ruleset, the typed query DSL,
and how drift normalization avoids cosmetic false diffs) and
[`examples/dynamic-group.config.ts`](examples/dynamic-group.config.ts) for a
Expand Down Expand Up @@ -258,7 +258,7 @@ Right names (`"module:right"`) are validated against a static, offline
catalog — discover them with `ct get permissions-catalog`. Grants you remove
from the config are diffed as deletions and reconciled via `ct apply`, same
as any other resource; system-default and inherited grants are never touched
or surfaced. See [`docs/permissions.md`](docs/permissions.md) for the full
or surfaced. See [`docs/handbuch/permissions.md`](docs/handbuch/permissions.md) for the full
guide (domainId semantics, scope resolution, domain rules, the
baseline-tolerance model) and
[`examples/permissions.config.ts`](examples/permissions.config.ts) for a
Expand Down
51 changes: 51 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ct-cli documentation

Two audiences, kept apart on purpose (#89).

## `handbuch/` — published, reader-facing

The **generic ChurchTools reference**: how ChurchTools' own permission model,
dynamic groups and field definitions behave, independent of any one instance.
These pages are published into the Handbuch's "ChurchTools-Grundlagen" section
(eqrm/churchtools-connector#1114), so they carry `sources:` frontmatter and are
gated by the estate-wide staleness checker.

| Page | About |
| --- | --- |
| [`handbuch/index.md`](handbuch/index.md) | Section overview |
| [`handbuch/permissions.md`](handbuch/permissions.md) | Grant domains, `domainId` semantics, scope resolution |
| [`handbuch/dynamic-groups.md`](handbuch/dynamic-groups.md) | Auto-groups, the ChurchQuery DSL, portable rulesets |
| [`handbuch/field-definitions.md`](handbuch/field-definitions.md) | Person master data, security levels, custom-field definitions |
| [`handbuch/blueprints.md`](handbuch/blueprints.md) | Parametrized, reusable structure |

**A page publishes only if it lives under `handbuch/` and is reachable from
`handbuch/mkdocs.yml`'s nav.** Everything else in `docs/` stays invisible.

Build it locally:

```bash
python3 -m venv .venv-docs && .venv-docs/bin/pip install -r docs/handbuch/requirements.txt
.venv-docs/bin/mkdocs build -f docs/handbuch/mkdocs.yml --strict
```

### Re-signing a page

Each page declares the code it documents in `sources:` and signs it with
`sources_hash`. A PR that changes a declared source without re-signing the page
fails the `Docs / staleness` check — the point being that someone re-read the
page against the new code. Bumping `reviewed:` alone does **not** satisfy it.

The signature is a sha256 over each resolved file's
`<repo-relative-path>\0<contents>\0`, files sorted, truncated to 16 hex chars —
the canonical implementation is `App\Services\Docs\SourcesHasher` in
eqrm/churchtools-connector, and `php artisan docs:sign <page>` in a checkout of
that repo writes it.

## Everything else — developer- and operator-facing, unpublished

| Page | About |
| --- | --- |
| [`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 — API gaps and the manual steps around them |
| `superpowers/` | Historical implementation plans; kept as a record, never published |
2 changes: 1 addition & 1 deletion docs/api-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,5 @@ the runbook's re-audit procedure once the schema is regenerated.
| Person master-data model | `/person/masterdata` | ✅ (single object) | ❌ | **Read-only.** Versionable master-data model incl. the `securityLevels` enumeration. No write endpoint (edited in the CT master-data admin UI). |
| Data-field definitions (Datenfelder) | `/dbfields`, `/dbfields/{id}` | list ✅ / by-id ✅ | ❌ | **Read-only.** Unified person + group field definitions, discriminated by `fieldCategory`. Mutation only via legacy churchdb AJAX (`db_insert/update/deletefields`), not REST. |

See [`docs/field-definitions.md`](field-definitions.md) for the full writability
See [`docs/handbuch/field-definitions.md`](handbuch/field-definitions.md) for the full writability
decision, evidence, and the schema/values boundary.
2 changes: 1 addition & 1 deletion docs/group-field-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ field — a plain top-level key. `campusId` is wired the same deliberate way as
| `groupStatusId` | **managed** | Lifecycle status; already managed. |
| **`campusId`** | **managed (new, #21)** | The campus link is the tool's core "instantiate this area per campus" requirement. Numeric escape hatch only — an existing CT campus id (or `null` to clear). A *logical* `campus: "key"` reference (resolving a same-run campus by key) is **deferred to [#20](https://github.com/eqrm/ct-cli/issues/20)**; the DSL rejects a `campus` field with a pointer to #20 so it can't slip through as an un-diffable phantom. |
| `parents` (hierarchy) | **opt-in synthetic** | Group→group hierarchy is reconciled through its own endpoint, not the group body — see `src/engine/synthetic.ts`. Opt-in via `parents: [...]`. |
| `dynamic` (auto-group ruleset) | **opt-in synthetic** | Ruleset + status live behind a dedicated endpoint (#14); opt-in via the `dynamic` block. See `docs/dynamic-groups.md`. |
| `dynamic` (auto-group ruleset) | **opt-in synthetic** | Ruleset + status live behind a dedicated endpoint (#14); opt-in via the `dynamic` block. See `docs/handbuch/dynamic-groups.md`. |
| `allowDuplicateName` (CT's `force` create flag, #75) | **create-only, unmanaged** | Not a field on the live group at all — it's a request-only escape hatch for `POST /groups`' same-name guard (`forbidden.duplicate.group`). Opt-in per declaration; sent as `force: true` on CREATE only. Deliberately NOT a registry `managedFields` entry: it has no live value to diff against, so it is destructured out of the DSL input before the field bag (never triggers the unknown-field warning, never lands in state, never adopted, never touched on update). |
| `visibility` | **out of scope** | Not rights-bearing structure; instance-/policy-specific and easily changed in the UI. No demand in #21 to manage it. Promote later only with its own registry entry + tests. |
| `note` | **out of scope** | Free-text annotation, not structure. Managing it would fight human edits in the UI for no structural benefit. |
Expand Down
36 changes: 23 additions & 13 deletions docs/blueprints.md → docs/handbuch/blueprints.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
---
title: Blueprints
sources:
- src/config/context.ts
- src/engine/graph.ts
- src/engine/hierarchy.ts
sources_hash: d69dbf11ba71e8eb
reviewed: 2026-08-10
---

# Blueprints (parametrized, reusable config)

A "blueprint" is not a feature the tool implements — it's a naming for a
pattern the existing config DSL already supports for free. `ct.config.ts`
default-exports a plain function that receives the injected
[`ConfigContext`](../src/config/context.ts) (`ct.campus`, `ct.group`,
[`ConfigContext`](https://github.com/eqrm/ct-cli/blob/main/src/config/context.ts) (`ct.campus`, `ct.group`,
`ct.groupTypeRole`, ...) and calls it however you like: in a loop, from a
helper function, across multiple files. There is no special "blueprint"
API, decorator, or registration step — just TypeScript functions closing
over `ct` (issue #7).

See [`examples/campus-blueprint.config.ts`](../examples/campus-blueprint.config.ts)
See [`examples/campus-blueprint.config.ts`](https://github.com/eqrm/ct-cli/blob/main/examples/campus-blueprint.config.ts)
for a complete, runnable example; this doc walks through what it does and
why.

Expand Down Expand Up @@ -58,7 +68,7 @@ The **numeric escape hatch** stays available: pass `campusId: <existing id>`
(CT stores it at `information.campusId`; `campusId: null` clears it) or
`groupTypeId: 2` to target one instance's id directly. `ct plan` diffs a campus
assign/move/clear as a normal field update — see
[`docs/group-field-decisions.md`](group-field-decisions.md). Declaring both the
[`docs/group-field-decisions.md`](https://github.com/eqrm/ct-cli/blob/main/docs/group-field-decisions.md). Declaring both the
logical and the numeric form for one field (`campus` + `campusId`) is a conflict
and throws at eval time.

Expand Down Expand Up @@ -90,8 +100,8 @@ itself.
## `parents` scopes hierarchy per campus

Group hierarchy (`parents`) is opt-in and references other groups **by
key** (see [`docs/dynamic-groups.md`](dynamic-groups.md) and the DSL doc
comment in [`src/config/context.ts`](../src/config/context.ts)). Because
key** (see [`dynamic-groups.md`](dynamic-groups.md) and the DSL doc
comment in [`src/config/context.ts`](https://github.com/eqrm/ct-cli/blob/main/src/config/context.ts)). Because
each campus's group keys are prefixed, a blueprint's `parents: [lead]`
inside `kidsArea` naturally scopes hierarchy to that one campus's own
`lead` variable — `berlin_kids_0_3`'s `parents` can only ever point at
Expand Down Expand Up @@ -120,7 +130,7 @@ convention and lexical scoping do that for you.

Because a blueprint is just code calling `ct.*`, it can freely mix resource
types. `kidsArea` declares a `dynamic` block (an auto-group, #14 — see
[`docs/dynamic-groups.md`](dynamic-groups.md)) on the "all members" group
[`dynamic-groups.md`](dynamic-groups.md)) on the "all members" group
right alongside the plain groups:

```ts
Expand All @@ -143,7 +153,7 @@ ct.group({
```

And the outer default export layers a permission grant (#13 — see
[`docs/permissions.md`](permissions.md)) on a shared `groupTypeRole`
[`permissions.md`](permissions.md)) on a shared `groupTypeRole`
template, declared once and applying across every campus's groups of that
type:

Expand All @@ -167,14 +177,14 @@ export default (ct: ConfigContext): void => {

There's nothing blueprint-specific about `dynamic` or `groupTypeRole` here
— they're the same DSL calls documented in
[`docs/dynamic-groups.md`](dynamic-groups.md) and
[`docs/permissions.md`](permissions.md), just invoked from inside a
[`dynamic-groups.md`](dynamic-groups.md) and
[`permissions.md`](permissions.md), just invoked from inside a
reusable function instead of inline at the top level.

## Ordering is automatic

`ct plan` / `ct apply` order every declared resource with
[`orderKeys`](../src/engine/graph.ts), which sorts by a fixed type tier
[`orderKeys`](https://github.com/eqrm/ct-cli/blob/main/src/engine/graph.ts), which sorts by a fixed type tier
(`campus` / `group-type` / ... at tier 0, `group` at tier 1) and then, within
a tier, by dependency edges — honouring ties by original declaration order.
A blueprint doesn't need to worry about sequencing:
Expand All @@ -189,7 +199,7 @@ A blueprint doesn't need to worry about sequencing:
guarantees it structurally, not by declaration order.
- **Hierarchy and auto-group state ride along with their group.** `parents`
and `dynamic` are *synthetic fields* on the `group` resource itself (see
`SYNTHETIC_FIELDS` in [`src/engine/synthetic.ts`](../src/engine/synthetic.ts)),
`SYNTHETIC_FIELDS` in [`src/engine/synthetic.ts`](https://github.com/eqrm/ct-cli/blob/main/src/engine/synthetic.ts)),
not separate resources with their own tier — they're diffed and applied as
part of that same group's create/update, once the group (and, for
`parents`, its referenced parent groups) already exist.
Expand All @@ -212,7 +222,7 @@ beyond the `parents: [lead]` you'd write anyway.

`parents` references are validated **at config-evaluation time**, before
any plan or diff is computed (`validateReferences` in
[`src/config/context.ts`](../src/config/context.ts), run by
[`src/config/context.ts`](https://github.com/eqrm/ct-cli/blob/main/src/config/context.ts), run by
`evaluateConfig`). Every key listed in a `parents` array must resolve to a
`group` declared *somewhere in the same config* — including inside a
blueprint function called from the top-level export. A typo, a forgotten
Expand All @@ -233,7 +243,7 @@ catch before it ever reaches `ct plan` against a live instance.

## Full example

See [`examples/campus-blueprint.config.ts`](../examples/campus-blueprint.config.ts)
See [`examples/campus-blueprint.config.ts`](https://github.com/eqrm/ct-cli/blob/main/examples/campus-blueprint.config.ts)
for the complete, runnable config this doc describes: a `kidsArea(ct,
campus)` blueprint instantiated over two campuses (`mainz`, `berlin`),
each producing a lead group, three ministry-team groups (managed hierarchy
Expand Down
14 changes: 13 additions & 1 deletion docs/dynamic-groups.md → docs/handbuch/dynamic-groups.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
---
title: Dynamic groups (auto-groups)
sources:
- src/config/query.ts
- src/config/query-refs.ts
- src/engine/dynamic.ts
- src/engine/synthetic.ts
- src/commands/adopt-group.ts
sources_hash: 785c613f106cbd8a
reviewed: 2026-08-10
---

# Auto-groups (dynamic groups)

ChurchTools "dynamic groups" (aka auto-groups) compute their membership from a
Expand Down Expand Up @@ -320,6 +332,6 @@ instance.

## Full example

See [`examples/dynamic-group.config.ts`](../examples/dynamic-group.config.ts)
See [`examples/dynamic-group.config.ts`](https://github.com/eqrm/ct-cli/blob/main/examples/dynamic-group.config.ts)
for a runnable config declaring a campus and a dynamic group built with the
typed query DSL.
11 changes: 10 additions & 1 deletion docs/field-definitions.md → docs/handbuch/field-definitions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
---
title: Field definitions & security levels
sources:
- src/commands/get.ts
- src/api/ctClient.ts
sources_hash: 580645532edbdf9a
reviewed: 2026-08-10
---

# Field definitions & security levels (#47, #48)

`ct` can **read** the structural *schema* that defines what data a person or a
Expand Down Expand Up @@ -26,7 +35,7 @@ ct get data-fields # all data-field definitions (person + group), one ro
- `ct get person-masterdata` → `GET /person/masterdata`. A single object (not a
paged list) — the versionable person master-data model. It carries the
security-level enumeration (`securityLevels`), which is the model the churchdb
permission scopes reference (`cc_securitylevel`; see `docs/permissions.md`).
permission scopes reference (`cc_securitylevel`; see [`permissions.md`](permissions.md)).
This is where **security levels** are surfaced — ChurchTools has no dedicated
standalone security-levels REST resource; the Persons/Groups master-data module
is where they are defined, and `/person/masterdata` is its download.
Expand Down
48 changes: 48 additions & 0 deletions docs/handbuch/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: ChurchTools-Grundlagen (ct-cli)
sources: []
sources_exempt_reason: "Section index — links and framing only, no code behaviour to keep in sync."
reviewed: 2026-08-10
---

# ChurchTools-Grundlagen

These pages are the **generic ChurchTools reference**: how ChurchTools' own
permission model, dynamic groups, field definitions and reusable structure
patterns actually behave — independent of any one instance.

They were written while building [`ct-cli`](https://github.com/eqrm/ct-cli), a
structure-as-code CLI that reconciles a ChurchTools instance's scaffold from
versioned desired-state code. Much of what is written down here is not in the
official API documentation; it was established by probing live instances, and
each page marks explicitly what was **verified** and what is still an
**assumption**.

## Where each kind of documentation lives

| Layer | Repo | Example |
| --- | --- | --- |
| **Generic ChurchTools behaviour** | `eqrm/ct-cli` `docs/handbuch/` (these pages) | "A `group_type_role` grant reaches every group of that type" |
| **This instance's structure** | `eqrm/ct-structure` `docs/` | "Which Bereiche exist at Equippers and why" |
| **Connector-specific how-tos** | `eqrm/churchtools-connector` Handbuch | "How to run check-in" |

## The pages

- **[Permissions](permissions.md)** — the `group_role`, `group_type_role` and
person-`status` grant domains; what a `domainId` actually is per domain; scope
resolution and the `-1` "all values" sentinel; which grants are yours versus
the platform's system baseline.
- **[Dynamic groups](dynamic-groups.md)** — ChurchTools auto-groups: the
ruleset/status model, the typed ChurchQuery DSL, how membership recompute is
triggered, and how a captured ruleset is made portable across instances.
- **[Field definitions](field-definitions.md)** — the person master-data model,
security levels, and person/group custom-field *definitions*: what is readable
over REST, what is writable, and where the boundary to per-record values runs.
- **[Blueprints](blueprints.md)** — describing a repeated structure (e.g. one
campus's area scaffold) once and instantiating it per campus.

!!! note "Language"
These pages are English while the rest of the Handbuch is German. That is a
known, deliberate mismatch — they are placed here first and translated (or
relocated) by the follow-on content work; see
[ct-cli#89](https://github.com/eqrm/ct-cli/issues/89).
Loading
Loading