diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..724bc93 --- /dev/null +++ b/.github/workflows/docs.yml @@ -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 `\0\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 diff --git a/.gitignore b/.gitignore index 8098f6f..515e26f 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 01d902f..675f0f4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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 @@ -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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f0b72a3 --- /dev/null +++ b/docs/README.md @@ -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 +`\0\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 ` 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 | diff --git a/docs/api-coverage.md b/docs/api-coverage.md index b5e4b49..66169db 100644 --- a/docs/api-coverage.md +++ b/docs/api-coverage.md @@ -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. diff --git a/docs/group-field-decisions.md b/docs/group-field-decisions.md index 90ff357..9a1ffa8 100644 --- a/docs/group-field-decisions.md +++ b/docs/group-field-decisions.md @@ -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. | diff --git a/docs/blueprints.md b/docs/handbuch/blueprints.md similarity index 87% rename from docs/blueprints.md rename to docs/handbuch/blueprints.md index e4b687f..4f459be 100644 --- a/docs/blueprints.md +++ b/docs/handbuch/blueprints.md @@ -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. @@ -58,7 +68,7 @@ The **numeric escape hatch** stays available: pass `campusId: ` (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. @@ -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 @@ -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 @@ -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: @@ -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: @@ -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. @@ -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 @@ -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 diff --git a/docs/dynamic-groups.md b/docs/handbuch/dynamic-groups.md similarity index 97% rename from docs/dynamic-groups.md rename to docs/handbuch/dynamic-groups.md index cd50e1c..84a28f0 100644 --- a/docs/dynamic-groups.md +++ b/docs/handbuch/dynamic-groups.md @@ -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 @@ -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. diff --git a/docs/field-definitions.md b/docs/handbuch/field-definitions.md similarity index 95% rename from docs/field-definitions.md rename to docs/handbuch/field-definitions.md index f84d682..83afb31 100644 --- a/docs/field-definitions.md +++ b/docs/handbuch/field-definitions.md @@ -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 @@ -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. diff --git a/docs/handbuch/index.md b/docs/handbuch/index.md new file mode 100644 index 0000000..1347b3e --- /dev/null +++ b/docs/handbuch/index.md @@ -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). diff --git a/docs/handbuch/mkdocs.yml b/docs/handbuch/mkdocs.yml new file mode 100644 index 0000000..3e7ab0f --- /dev/null +++ b/docs/handbuch/mkdocs.yml @@ -0,0 +1,83 @@ +# Mini config for ct-cli's section of the Handbuch (#89, publishing contract in +# eqrm/churchtools-connector#1114). +# +# A page publishes only if it lives under docs/handbuch/ and is reachable from this +# nav — everything else in docs/ (api-coverage.md, group-field-decisions.md, +# runbook-manual-surface.md, superpowers/) stays invisible on purpose: it is +# developer- and operator-facing material, not reader-facing reference. +# +# These pages are the GENERIC ChurchTools reference — how ChurchTools' own group, +# permission and dynamic-group model actually works — as opposed to eqrm/ct-structure's +# docs/, which describe one specific instance. That is why they belong in the Handbuch's +# "ChurchTools-Grundlagen" section rather than in this repo's README. +# +# LANGUAGE MISMATCH (deliberate, see #89): the Handbuch is German, these pages are +# English. They are placed here now and translated (or moved to Betrieb) by the +# follow-on content epic — hence `language: en` below, so search and UI chrome at +# least match the page text while the mismatch stands. +# +# Built standalone via `mkdocs build -f docs/handbuch/mkdocs.yml --strict`; +# eqrm/churchtools-connector#1115 replaces that with an `!include` from the root config +# once mkdocs-monorepo-plugin exists. +site_name: ct-cli — ChurchTools-Grundlagen +site_description: Generic ChurchTools reference — permissions, dynamic groups, field definitions, blueprints +docs_dir: . +site_dir: ../../site/handbuch +strict: true + +# docs_dir is the section directory itself, so this config would otherwise be copied +# into the built site as a static file. +exclude_docs: | + mkdocs.yml + +theme: + name: material + language: en + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/weather-night + name: Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/weather-sunny + name: Light mode + features: + - navigation.instant + - navigation.tracking + - navigation.sections + - navigation.top + - content.code.copy + - search.suggest + - search.highlight + +plugins: + - search: + lang: en + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.details + - pymdownx.highlight + - pymdownx.inlinehilite + +nav: + - Overview: index.md + - Permissions: permissions.md + - Dynamic groups: dynamic-groups.md + - Field definitions: field-definitions.md + - Blueprints: blueprints.md diff --git a/docs/permissions.md b/docs/handbuch/permissions.md similarity index 98% rename from docs/permissions.md rename to docs/handbuch/permissions.md index e808a7c..9e4e854 100644 --- a/docs/permissions.md +++ b/docs/handbuch/permissions.md @@ -1,3 +1,14 @@ +--- +title: Permissions +sources: + - src/permissions/** + - src/resolve/resolver.ts + - src/resolve/refs.ts + - src/config/context.ts +sources_hash: 49ef267c4843eeb4 +reviewed: 2026-08-10 +--- + # Permissions (`ct.groupRole` / `ct.groupTypeRole` / `ct.status`) Declare ChurchTools permission grants — group-role, group-type-role and @@ -387,6 +398,6 @@ is idempotent. ## Example -See [`examples/permissions.config.ts`](../examples/permissions.config.ts) for +See [`examples/permissions.config.ts`](https://github.com/eqrm/ct-cli/blob/main/examples/permissions.config.ts) for a runnable `ct.groupTypeRole` declaration with one global grant and one scoped grant. diff --git a/docs/handbuch/requirements.txt b/docs/handbuch/requirements.txt new file mode 100644 index 0000000..be750bc --- /dev/null +++ b/docs/handbuch/requirements.txt @@ -0,0 +1,7 @@ +# Pinned toolchain for this section's standalone MkDocs build (`.github/workflows/docs.yml`). +# Kept in lockstep with eqrm/churchtools-connector's `requirements-docs.txt` so the section +# renders identically here and in the site that consumes it — bump both together, and +# deliberately (`pip index versions `), never via `pip install -U`. +mkdocs==1.6.1 +mkdocs-material==9.7.7 +pymdown-extensions==11.0.1 diff --git a/docs/runbook-manual-surface.md b/docs/runbook-manual-surface.md index 95f21ad..0901903 100644 --- a/docs/runbook-manual-surface.md +++ b/docs/runbook-manual-surface.md @@ -36,10 +36,10 @@ this doc's structure. | Group/group-type field decision table | Fields deliberately left unmanaged (decided out of scope): visibility, note, `autoAccept`/open-for-members, chat status, sort key. The triage **shipped** as a committed decision table ([`docs/group-field-decisions.md`](group-field-decisions.md)) | [#21](https://github.com/eqrm/ct-cli/issues/21) (decided) | Set by hand; these fields are intentionally not diffed — `ct` will neither preserve nor revert them. Promote one later only with its own registry entry + tests | | Portable/logical references | **Shipped (#20, #25).** Configs reference master data by name/key — `campus`/`groupType` on a group, `ref.campus(...)` in ruleset `var` values, `groupType: ""` for a `group_type_role` domain, and now `group: "", role: ""` for a `group_role` domain (#25) — and the per-host resolver maps each to that instance's id at plan time (managed resources ∪ live catalogs). A same-run campus resolves at apply time. Numeric ids still work as an escape hatch. **`status` (group status) is NOT part of this** (#67) — group statuses have no REST catalog, so `status:` fails fast at eval time; declare the numeric `groupStatusId` directly | [#20](https://github.com/eqrm/ct-cli/issues/20) (done), [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | None needed for the shipped surface. Write logical names; run `ct plan`. See the `group_role` resolution assumption below (one-time live check) | | Environments (dev → prod promotion) | Named `(host, token, state file)` profiles and a `--env` flag; today one config + one state file = one host | [#22](https://github.com/eqrm/ct-cli/issues/22) | Point `CT_HOST`/state file manually at each target and re-run; keep dev and prod state files apart yourself, and be careful — nothing stops you from applying a dev-shaped config against prod today | -| Permission `group_role` domain by reference **(shipped, assumption unverified)** | `ct.groupRole({ group, role })` now resolves the (group, role) pair to its pairing domainId at plan time (#25). **ASSUMPTION not yet confirmed live:** it reads the group's role list (`GET /groups/{groupId}/roles`) and takes the matched role row's `id` as the pairing domainId — endpoint/field unverified (pinned in a unit test + a comment in `src/resolve/resolver.ts`) | [#25](https://github.com/eqrm/ct-cli/issues/25) (done, verify live) | Works today by reference for managed, already-created groups. If a live check shows the pairing id is elsewhere, flip the two constants at the top of `resolver.ts`. Numeric `id:` remains the guaranteed escape hatch ([`docs/permissions.md`](permissions.md) "domainId semantics") | -| ~~Grant adoption~~ **(shipped)** | ~~existing rights structures must be hand-transcribed~~ — **`ct adopt grants ` ships this** (#25): it reads the live rows, applies the planner's normalization, and prints a paste-ready `ct.groupRole` / `ct.groupTypeRole` block (baseline/inherited excluded, denies noted-and-preserved, scope dataIds mapped back to managed-group keys). See [`docs/permissions.md`](permissions.md) "Adopting existing grants" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run `ct adopt grants group_role ` (or `group_type_role`), review the `WARNING`/`NOTE` comments, paste into config | -| ~~Permission catalog lifecycle~~ **(shipped)** | ~~`catalog.json` is a one-off HAR-trace snapshot with no staleness detection~~ — **shipped (#25):** `npm run regenerate:permission-catalog` rewrites it from a live instance (records the CT version in `$meta`), and `ct plan` now warns on a version mismatch or an unknown-authId live grant (which it leaves untouched, never revoking a right it cannot name). See [`docs/permissions.md`](permissions.md) "Catalog lifecycle & staleness" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run the command; heed the `ct plan` warnings | -| Field definitions & security levels (person + group custom fields) **(read-only, shipped #47/#48)** | The person master-data model, the security-level enumeration, and the data-field DEFINITIONS ("Datenfelder") for persons and groups — structural schema, not per-record values | [#47](https://github.com/eqrm/ct-cli/issues/47), [#48](https://github.com/eqrm/ct-cli/issues/48) (read shipped; write is an API gap — see note) | Read with `ct get person-masterdata` (model + security levels) and `ct get data-fields` (all field definitions, person + group, discriminated by `fieldCategory`). **Mutation stays manual:** field definitions have no REST write endpoint — only the legacy churchdb admin AJAX (`db_insertfields`/`db_updatefields`/`db_deletefields`) — so create/edit/delete them by hand in the master-data admin UI. Decision + evidence: [`docs/field-definitions.md`](field-definitions.md) | +| Permission `group_role` domain by reference **(shipped, assumption unverified)** | `ct.groupRole({ group, role })` now resolves the (group, role) pair to its pairing domainId at plan time (#25). **ASSUMPTION not yet confirmed live:** it reads the group's role list (`GET /groups/{groupId}/roles`) and takes the matched role row's `id` as the pairing domainId — endpoint/field unverified (pinned in a unit test + a comment in `src/resolve/resolver.ts`) | [#25](https://github.com/eqrm/ct-cli/issues/25) (done, verify live) | Works today by reference for managed, already-created groups. If a live check shows the pairing id is elsewhere, flip the two constants at the top of `resolver.ts`. Numeric `id:` remains the guaranteed escape hatch ([`docs/handbuch/permissions.md`](handbuch/permissions.md) "domainId semantics") | +| ~~Grant adoption~~ **(shipped)** | ~~existing rights structures must be hand-transcribed~~ — **`ct adopt grants ` ships this** (#25): it reads the live rows, applies the planner's normalization, and prints a paste-ready `ct.groupRole` / `ct.groupTypeRole` block (baseline/inherited excluded, denies noted-and-preserved, scope dataIds mapped back to managed-group keys). See [`docs/handbuch/permissions.md`](handbuch/permissions.md) "Adopting existing grants" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run `ct adopt grants group_role ` (or `group_type_role`), review the `WARNING`/`NOTE` comments, paste into config | +| ~~Permission catalog lifecycle~~ **(shipped)** | ~~`catalog.json` is a one-off HAR-trace snapshot with no staleness detection~~ — **shipped (#25):** `npm run regenerate:permission-catalog` rewrites it from a live instance (records the CT version in `$meta`), and `ct plan` now warns on a version mismatch or an unknown-authId live grant (which it leaves untouched, never revoking a right it cannot name). See [`docs/handbuch/permissions.md`](handbuch/permissions.md) "Catalog lifecycle & staleness" | [#25](https://github.com/eqrm/ct-cli/issues/25) (done) | No workaround needed — run the command; heed the `ct plan` warnings | +| Field definitions & security levels (person + group custom fields) **(read-only, shipped #47/#48)** | The person master-data model, the security-level enumeration, and the data-field DEFINITIONS ("Datenfelder") for persons and groups — structural schema, not per-record values | [#47](https://github.com/eqrm/ct-cli/issues/47), [#48](https://github.com/eqrm/ct-cli/issues/48) (read shipped; write is an API gap — see note) | Read with `ct get person-masterdata` (model + security levels) and `ct get data-fields` (all field definitions, person + group, discriminated by `fieldCategory`). **Mutation stays manual:** field definitions have no REST write endpoint — only the legacy churchdb admin AJAX (`db_insertfields`/`db_updatefields`/`db_deletefields`) — so create/edit/delete them by hand in the master-data admin UI. Decision + evidence: [`docs/handbuch/field-definitions.md`](handbuch/field-definitions.md) | | API re-audit for new CT releases | CT's OpenAPI spec is self-trimming (only shows endpoints your version has), so a new write endpoint (e.g. a member-status write, or — separately — a first-ever group-status list/write endpoint, #67) appears silently between CT upgrades | tracked by this issue ([#26](https://github.com/eqrm/ct-cli/issues/26)) | Procedure below (**Re-audit procedure for new CT releases**) | ## Out of tool scope — deliberate, not a gap @@ -49,7 +49,7 @@ this doc's structure. | People, memberships, group member lists | Hard boundary enforced in code (`assertNotPeople`, `src/engine/guard.ts`) — the tool manages rights-bearing _structure_ only, never who's in it. This is permanent by design, not a roadmap item; see README's "People are never managed" | | Other CT modules — calendars, services (`churchservice`), resource booking (`churchresource`), forms, check-in, wiki, finance, sync | Never in the tool's stated mandate ("campuses, structural groups, hierarchies, group types/roles, permission & auto-groups" — README). Phase 0's coverage matrix (`docs/api-coverage.md`) only analyzed the 12 resource types relevant to that structural mandate; nothing else was assessed for CRUD support and nothing else is planned | | Module-level settings, i18n | Out of tool scope by design — global instance configuration, not per-resource declarative structure | -| Custom field / master-data field **VALUES on individual person/group records** | People/record data is the permanent people boundary (`assertNotPeople`). Note: the field **DEFINITIONS** (schema) are **no longer out of scope** — they moved to read-only supported above (#47/#48, [`docs/field-definitions.md`](field-definitions.md)); only the per-record *values* stay out of scope. | +| Custom field / master-data field **VALUES on individual person/group records** | People/record data is the permanent people boundary (`assertNotPeople`). Note: the field **DEFINITIONS** (schema) are **no longer out of scope** — they moved to read-only supported above (#47/#48, [`docs/handbuch/field-definitions.md`](handbuch/field-definitions.md)); only the per-record *values* stay out of scope. | ## Permission catalog lifecycle (regeneration procedure) @@ -140,7 +140,7 @@ in where they'd otherwise be silently skipped: `ct get person-masterdata` and `ct get data-fields` to confirm the expected fields, field groups, and security levels exist; create/edit any missing *definitions* by hand in the master-data admin UI (no REST write endpoint — - see the field-definitions row above and [`docs/field-definitions.md`](field-definitions.md)). + see the field-definitions row above and [`docs/handbuch/field-definitions.md`](handbuch/field-definitions.md)). 8. **Anything from the "out of tool scope" table** — persons, memberships, per-record field values, calendars, services, resource booking, forms, check-in, wiki, finance, sync, module-level settings, i18n — configure per diff --git a/examples/dynamic-group.config.ts b/examples/dynamic-group.config.ts index f0e1b3a..20c732b 100644 --- a/examples/dynamic-group.config.ts +++ b/examples/dynamic-group.config.ts @@ -1,6 +1,6 @@ /** * Runnable example: a campus + a dynamic ("auto") group whose ruleset is - * built with the typed query DSL. See docs/dynamic-groups.md for the full + * built with the typed query DSL. See docs/handbuch/dynamic-groups.md for the full * feature guide. * * Portable references (#20): the ruleset's `campusId` filter is written as a @@ -19,7 +19,7 @@ export default (ct: ConfigContext): void => { // this ordering automatically. `dynamic` is a synthetic sub-resource field // (like `parents`), not a separate resource type: its ruleset/status write is // folded into the group's OWN tier-1 apply and routed to the dynamic-group - // endpoints inline (see src/engine/synthetic.ts and docs/dynamic-groups.md). + // endpoints inline (see src/engine/synthetic.ts and docs/handbuch/dynamic-groups.md). ct.group({ key: "all_mainz", name: "Alle Mainz", diff --git a/examples/permissions.config.ts b/examples/permissions.config.ts index 466b7ed..d4f1a56 100644 --- a/examples/permissions.config.ts +++ b/examples/permissions.config.ts @@ -1,14 +1,14 @@ /** * Runnable example: a group-type-role declaration with one unscoped (global) * grant and one scoped grant, plus a group-role declared by (group, role) - * reference (#25). See docs/permissions.md for the full feature guide, and + * reference (#25). See docs/handbuch/permissions.md for the full feature guide, and * `ct get permissions-catalog` to discover right names. * * Portable references (#20): the permission domain is declared by name * (`groupType: "kids"`) instead of a hardcoded numeric domainId — the per-host * resolver maps it to that instance's group-type id at plan time. The numeric * escape hatch still works: pass `id: ` to target one directly (see - * docs/permissions.md "domainId semantics"). + * docs/handbuch/permissions.md "domainId semantics"). */ import type { ConfigContext } from "../src/config/context.js"; @@ -29,7 +29,7 @@ export default (ct: ConfigContext): void => { // (`scopeField: "cdb_gruppe"` in the catalog — a scoped right). // `scope` entries are logical group keys here; a right whose scopeField // is NOT a group (e.g. "cc_securitylevel") instead takes a raw numeric - // dataId, e.g. `scope: [1, 2]` — see docs/permissions.md "Numeric scope + // dataId, e.g. `scope: [1, 2]` — see docs/handbuch/permissions.md "Numeric scope // escape hatch (#49)". { right: "churchgroup:view group", scope: ["kids_area"] }, ], @@ -39,7 +39,7 @@ export default (ct: ConfigContext): void => { // pair instead of a numeric domainId. The group must be managed (declared // above / adopted) and already created; the resolver maps the pair to the // pairing domainId per host. Numeric escape hatch: `id: ` instead - // of `group`/`role`. (See docs/permissions.md "domainId semantics" for the + // of `group`/`role`. (See docs/handbuch/permissions.md "domainId semantics" for the // resolution assumption still to be confirmed live.) ct.groupRole({ key: "kids_leiter_grant", diff --git a/src/commands/get.ts b/src/commands/get.ts index 25e768b..2c7519f 100644 --- a/src/commands/get.ts +++ b/src/commands/get.ts @@ -35,13 +35,13 @@ const RESOURCE_PATHS: Record = { "target-groups": { path: "/group/targetgroups" }, "dynamic-groups": { path: "/dynamicgroups" }, "relationship-types": { path: "/person/relationshiptypes" }, - // Schema/DEFINITIONS only — never person records or field VALUES (#47/#48; see docs/field-definitions.md). + // Schema/DEFINITIONS only — never person records or field VALUES (#47/#48; see docs/handbuch/field-definitions.md). // The person master-data MODEL: sexes/titles/statuses/campuses plus the security-level enumeration // that churchdb permission scopes (cc_securitylevel) reference. Single object → unpaginated. "person-masterdata": { path: "/person/masterdata", paginated: false }, // Unified data-field DEFINITION catalog (Datenfelder): person master-data fields AND group custom // fields in one list, discriminated per-row by `fieldCategory` (e.g. table `cdb_gruppe` = group). - // Read-only: mutation is only via the legacy churchdb admin AJAX, not REST — see docs/field-definitions.md. + // Read-only: mutation is only via the legacy churchdb admin AJAX, not REST — see docs/handbuch/field-definitions.md. "data-fields": { path: "/dbfields" }, permissions: { path: "/permissions/global", paginated: false }, }; diff --git a/src/permissions/README.md b/src/permissions/README.md index f9d7c56..ae4df85 100644 --- a/src/permissions/README.md +++ b/src/permissions/README.md @@ -31,7 +31,7 @@ changes materially. `capturedFrom`, `ctVersion`, `capturedAt`, `rightCount`. `catalog.ts` splits it off at load so no consumer (`resolveAuthId`, `ct get permissions-catalog`, grant adoption) ever sees it as a right. `ctVersion` drives `ct plan`'s - staleness warning — see `docs/permissions.md` "Catalog lifecycle & staleness". + staleness warning — see `docs/handbuch/permissions.md` "Catalog lifecycle & staleness". - **key** — `":"`, the DSL vocabulary (e.g. `churchgroup:view group`). - **authId** — numeric id sent in the `PermissionRequest` write body. @@ -41,7 +41,7 @@ changes materially. declarable via a **logical group key** in the DSL's `scope: [...]`. Any other non-null `scopeField` (`cc_securitylevel`, `cdb_comment_viewer`, `cdb_station`, …) names a dimension this tool has no managed representation for, so it can only be declared via the **numeric scope - escape hatch** (`scope: [1, 2, 3]`, #49) — see `docs/permissions.md`. + escape hatch** (`scope: [1, 2, 3]`, #49) — see `docs/handbuch/permissions.md`. - **revocable** — whether the right supports `type: "revoke"` (`isRevocable` in the source). - **desc** — human description (`bezeichnung`). diff --git a/src/permissions/plan.ts b/src/permissions/plan.ts index d9edbd9..e9b5e58 100644 --- a/src/permissions/plan.ts +++ b/src/permissions/plan.ts @@ -143,7 +143,7 @@ export async function buildPermissionPlan( warnings.push( `Permission catalog was captured from ChurchTools ${CATALOG_META.ctVersion} but this instance ` + `runs ${instanceVersion}. Right names/authIds may be stale — regenerate it with ` + - `\`npm run regenerate:permission-catalog\` (see docs/permissions.md).`, + `\`npm run regenerate:permission-catalog\` (see docs/handbuch/permissions.md).`, ); } // Resolve logical domainIds (#20) up front. Shares the command layer's resolver so master-data diff --git a/src/resolve/resolver.ts b/src/resolve/resolver.ts index 90857a4..522a0c8 100644 --- a/src/resolve/resolver.ts +++ b/src/resolve/resolver.ts @@ -98,7 +98,7 @@ export interface ResolverDeps { * * 1. A `group_role` domain is keyed by CT's internal (group, role) PAIRING id — one id per * (this specific group, this specific role). It is NEITHER the group's id NOR the shared - * role-definition id (this matches the long-standing code/docs comment; docs/permissions.md + * role-definition id (this matches the long-standing code/docs comment; docs/handbuch/permissions.md * "domainId semantics"). * 2. That pairing id is exposed on the group's OWN role list, `GET /groups/{groupId}/roles`, as * each row's {@link GROUP_ROLE_PAIRING_FIELD} (`id`), and rows carry a `name` we match the diff --git a/tests/dynamic.integration.test.ts b/tests/dynamic.integration.test.ts index 3b556e4..3b44993 100644 --- a/tests/dynamic.integration.test.ts +++ b/tests/dynamic.integration.test.ts @@ -38,7 +38,7 @@ describe.runIf(live)("dynamic round-trip (live)", () => { * * The two tests above write back CT's OWN GET output, so they are drift-free by construction: if * CT silently rewrote/normalized a RuleSet-level field (`description`, `shorty`, `importance`, - * `personIdFieldName`, `process` — see docs/dynamic-groups.md "Drift, normalization, and no-op + * `personIdFieldName`, `process` — see docs/handbuch/dynamic-groups.md "Drift, normalization, and no-op * re-applies") on PUT, those tests could never observe it, because the value they wrote back * already matched. This test authors fresh field values instead, so it CAN observe such rewrites. * If it fails, the failure message names exactly which RuleSet-level field(s) changed — add those @@ -53,7 +53,7 @@ describe.runIf(live)("dynamic round-trip (live)", () => { * instance — one whose ruleset this test may freely overwrite. The test captures the group's * current ruleset before writing and restores it in a `finally`, but a process kill / crash mid-run * would leave the group holding the test-authored ruleset — never point this at a group anyone - * depends on. See docs/dynamic-groups.md and tests/fixtures/dynamic/README.md for how to run this. + * depends on. See docs/handbuch/dynamic-groups.md and tests/fixtures/dynamic/README.md for how to run this. */ describe.runIf(live && liveWrite)("dynamic ruleset round-trip pin (#36, live write)", () => { it("a user-authored PUT round-trips through GET, and a plan built from it is a no-op", async () => { diff --git a/tests/synthetic-dynamic.test.ts b/tests/synthetic-dynamic.test.ts index d0d908b..4507e4b 100644 --- a/tests/synthetic-dynamic.test.ts +++ b/tests/synthetic-dynamic.test.ts @@ -73,7 +73,7 @@ describe("dynamic synthetic field — fold", () => { }); it("demote-to-none converges: a kept authored ruleset folds to the same sentinel as the actual side (no-op)", async () => { - // docs/dynamic-groups.md tells users to KEEP the dynamic block when demoting, so the authored + // docs/handbuch/dynamic-groups.md tells users to KEEP the dynamic block when demoting, so the authored // ruleset is still present with status "none". The actual side is the { status:"none", ruleset:{} } // sentinel — folding the full ruleset would diff forever. Both must collapse to the same sentinel. const state: State = { version: 1, host: "h",