@smartkomm/plexus-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +826 -2
  2. package/dist/cli.mjs +483 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +185 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  7. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  8. package/dist/skill/references/audit/answer-access-request.md +115 -0
  9. package/dist/skill/references/config/attach-a-template.md +117 -0
  10. package/dist/skill/references/config/init-tenant.md +121 -0
  11. package/dist/skill/references/config/onboard-tenant.md +71 -0
  12. package/dist/skill/references/config/rename-program-stage.md +99 -0
  13. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  14. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  15. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  16. package/dist/skill/references/config/update-tenant-config.md +170 -0
  17. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  18. package/dist/skill/references/export/create-named-export.md +47 -0
  19. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  20. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  21. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  22. package/dist/skill/references/finance/record-submission-date.md +33 -0
  23. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  24. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  25. package/dist/skill/references/notifications/mail-level.md +90 -0
  26. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  27. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  28. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  29. package/dist/skill/references/offer/addresses.md +129 -0
  30. package/dist/skill/references/offer/appointment-task.md +79 -0
  31. package/dist/skill/references/offer/assignment-rule.md +195 -0
  32. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  33. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  34. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  35. package/dist/skill/references/offer/author-smartform.md +102 -0
  36. package/dist/skill/references/offer/bind-node-input.md +76 -0
  37. package/dist/skill/references/offer/completeness-check.md +137 -0
  38. package/dist/skill/references/offer/countersign-node.md +91 -0
  39. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  40. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  41. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  42. package/dist/skill/references/offer/edit-smartform.md +121 -0
  43. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  44. package/dist/skill/references/offer/engage-push-node.md +147 -0
  45. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  46. package/dist/skill/references/offer/node-tasks.md +162 -0
  47. package/dist/skill/references/offer/optional-assessment.md +65 -0
  48. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  49. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  50. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  51. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  52. package/dist/skill/references/offer/task-family-request.md +69 -0
  53. package/dist/skill/references/offer/task-result-files.md +52 -0
  54. package/dist/skill/references/offer/template-document-node.md +113 -0
  55. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  56. package/dist/skill/references/resolver/manage-contract-resolver.md +159 -0
  57. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  58. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  59. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  60. package/dist/skill/references/signing/README.md +113 -0
  61. package/dist/skill/references/signing/connect-a-school.md +90 -0
  62. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  63. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  64. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  65. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  66. package/dist/skill/references/test-actions/drive-a-test-application.md +59 -0
  67. package/dist/skill/references/users/add-platform-admin.md +59 -0
  68. package/dist/skill/references/users/add-platform-user.md +59 -0
  69. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  70. package/dist/skill/references/users/change-login-email.md +55 -0
  71. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  72. package/dist/skill/references/users/delete-permission-group.md +76 -0
  73. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  74. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  75. package/dist/skill/references/users/set-password.md +57 -0
  76. package/dist/skill/references/users/use-test-account.md +79 -0
  77. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  78. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  79. package/dist/spec.snapshot.json +145815 -0
  80. package/package.json +53 -4
@@ -0,0 +1,94 @@
1
+ # Author the manifest "meta" layer
2
+
3
+ **When to use:** configure an offer's manifest settings that aren't the form or the
4
+ workflow — the applicant mode, signature level, application window, total capacity,
5
+ cohort-selection strategy, the `roleInstances` (legal roles like Erziehungsberechtigte),
6
+ and the `leistungen` — entirely from the CLI instead of hand-editing `manifest.json`.
7
+
8
+ ## Prerequisites
9
+ - An offer bundle on disk (`offer init …` or `offer export …`).
10
+ - Authoring is **fully offline**; only `offer apply --publish` needs an API token of a
11
+ user who may configure the tenant's offers (a tenant admin — see
12
+ [Add a tenant admin](../users/add-tenant-admin.md)).
13
+ - An installed, current `plexus-cli` (`plexus-cli offer manifest set --help`).
14
+
15
+ ## Steps
16
+ ```bash
17
+ # Top-level settings. Enum values are validated fail-loud (self|proxy|both,
18
+ # wet_ink|FES|QES, immediate|batch, arrival|manual).
19
+ plexus-cli offer manifest set --dir <bundle> \
20
+ --applicant-mode both --signature-level FES \
21
+ --window-start 2026-02-01 --window-end 2026-05-31 --capacity 30
22
+
23
+ # Selection config (FCFS/committee/Stichtag are emergent from these knobs).
24
+ # Set both required knobs the first time.
25
+ plexus-cli offer manifest set --dir <bundle> \
26
+ --selection-release batch --selection-waitlist-order manual \
27
+ --selection-auto-promote --selection-deadline 2026-06-01
28
+
29
+ # roleInstances — legal/contract roles. --custom-role-id binds the tenant custom-role.
30
+ # A role's (and a service's) label is { de, en }: --label AND --label-en on add.
31
+ plexus-cli offer role add --dir <bundle> --key erzb1 --label "Erziehungsberechtigte:r 1" --label-en "Guardian 1" \
32
+ --custom-role-id role_eb --min 1 --max 2 --legally-relevant --can-sign
33
+ plexus-cli offer role set --dir <bundle> --key erzb1 --max 3 --requires-evidence
34
+ plexus-cli offer role list --dir <bundle>
35
+
36
+ # leistungen — selectable/assigned services.
37
+ plexus-cli offer leistung add --dir <bundle> --key betreuung --label "Nachmittagsbetreuung" --label-en "Afternoon care" --selectable-by-applicant
38
+
39
+ # Display order among the unit's offers (lower = first; "" clears). Handle-level
40
+ # property like the title — lives on bundle.yaml, applied via createManifest.
41
+ plexus-cli offer manifest set --dir <bundle> --sort-order 20
42
+
43
+ # Validate + publish.
44
+ plexus-cli offer validate --dir <bundle>
45
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
46
+ ```
47
+
48
+ ## Verify
49
+ ```bash
50
+ plexus-cli offer role list --dir <bundle> # roleInstances reflect the edits
51
+ plexus-cli offer validate --dir <bundle> # "local validation clean"
52
+ ```
53
+
54
+ ## Idempotency / gotchas
55
+ - **Offer title is a BARE name — never embed the run/year or levels.** The offer
56
+ `label` (set via `offer init/set --title`) is just the program name, e.g.
57
+ "Grundschule" / "Primary School". The school-year cycle is a separate entity
58
+ (`program_runs.name`, e.g. "2026/27") and grade levels are `program_stages` — the UI
59
+ shows runs nested under the offer (internal sidebar) and as a separate dropdown
60
+ (external portal), so it **never composes** them into the title. Putting
61
+ "· Aufnahme 2026/27" in the title makes the cycle show twice. Levels/runs are managed
62
+ with `offer level …` / `offer run …`, not the title.
63
+ - **`--title` (offer label) is the offer-title SoT — `--program-name` is separate.**
64
+ `offer set --title` (+ `--title-en`) writes the offer title (`offer_versions.label`), the single source the staff Angebote
65
+ tree, external portal, and applicant "Meine Bewerbungen" all read. The underlying
66
+ master-data `programs.name` is a *copy* made at authoring time that only shows in the
67
+ staff master-data admin; it can drift from the label. Use `offer set --program-name
68
+ "<bare name>" --program-name-en "<English>"` (→ `programAdmin.updateProgram`) to bring a drifted program name back in
69
+ line — e.g. after fixing a label that had been authored with a run/year baked in. You
70
+ almost never need `--program-name`; reach for it only to clean up master-data drift.
71
+ - **`--sort-order` orders offers within a unit** (lower = first); the sibling unit
72
+ order is set as code via tenant config (`TenantConfigUnitDefinition.sortOrder`). Both
73
+ apply to the internal Angebote sidebar AND the external `/e` portal; offers/units
74
+ without a sortOrder fall back to creation-time / alphabetical respectively.
75
+ - **Validate-before-write.** Every mutator runs the real publish gate first; on any
76
+ error it writes **nothing**.
77
+ - **Enum values are checked client-side, fail-loud.** The manifest publish gate
78
+ (`validateManifestBlob`) enforces required fields + roleInstances/leistungen *structure*
79
+ (unique keys, customRoleId, cardinality) but NOT the enum *values* of
80
+ applicantMode/signatureLevel/selection — so the CLI validates those at the boundary.
81
+ - **`customRoleId` resolution is server-side.** The CLI authors the binding; whether
82
+ `--custom-role-id` resolves to a real tenant custom-role is checked at runtime, not
83
+ offline (the bundle has no tenant role catalog).
84
+ - **No graph↔role cross-check.** The static graph carries no reference to a
85
+ `roleInstance.key` (those are referenced only by runtime persons; approval approvers
86
+ reference tenant permission *groups*, validated server-side) — so there is nothing
87
+ offline to cross-validate beyond the structure the manifest gate already enforces.
88
+ - **`manifest set` is additive per-flag.** Pass only the flags you want to change;
89
+ selection needs both `--selection-release` and `--selection-waitlist-order` present
90
+ the first time so a half-config is never written.
91
+
92
+ ## Related
93
+ - [Author a SmartForm as code](./author-smartform.md) · [Edit a workflow node](./edit-workflow-node.md).
94
+ - Command reference: `plexus-cli offer manifest --help`, `plexus-cli offer role --help`, `plexus-cli offer leistung --help`.
@@ -0,0 +1,102 @@
1
+ # Author a SmartForm as code
2
+
3
+ **When to use:** build or change a customer's application form (sections, fields,
4
+ validation/visibility rules, outcomes) entirely from the CLI — then publish a new
5
+ version — without hand-editing `smartforms/<slug>.json`.
6
+
7
+ ## Prerequisites
8
+ - An offer bundle on disk (`offer init …` or `offer export …`).
9
+ - **Authoring is fully offline** — no deployment, no auth. `offer apply --publish`
10
+ (the publish step) needs an API token of a user who may configure the tenant's offers
11
+ (a tenant admin — see [Add a tenant admin](../users/add-tenant-admin.md)).
12
+ - An installed, current `plexus-cli` (`plexus-cli offer form --help`).
13
+
14
+ ## Steps
15
+ ```bash
16
+ # 0. (If the bundle has no form yet, or you want a second form) scaffold one.
17
+ # A new form starts with one empty section + a default `standard` outcome.
18
+ plexus-cli offer form add --dir <bundle> --key <form-slug> --label "<Label>" --label-en "<English>"
19
+
20
+ # 1. Add sections (nest with --parent; gate with --visible-if).
21
+ plexus-cli offer form section add --dir <bundle> [--form <slug>] \
22
+ --id section.kind --label "Kind" --label-en "Child" [--visible-if "<expr>"]
23
+
24
+ # 2. Add fields. Types: string|boolean|date|enum|email|tel|address|docRef.
25
+ # enum options come from exactly ONE source (mutually exclusive):
26
+ # --enum a,b,c inline options
27
+ # --enum-ref <catalog> static central catalog (konfession, geschlecht, …)
28
+ # --enum-list-key <list> tenant custom_field_lists key (runtime-resolved;
29
+ # central management — nationalities, languages,
30
+ # countries, …). Add --multiple for multi-select.
31
+ plexus-cli offer form field add --dir <bundle> [--form <slug>] \
32
+ --section section.kind --key kind.vorname --type string --required --bucket kind --label "Vorname" --label-en "First name"
33
+ plexus-cli offer form field add --dir <bundle> --section section.kind \
34
+ --key kind.geschlecht --type enum --enum-list-key gender --label "Geschlecht" --label-en "Gender"
35
+ plexus-cli offer form field add --dir <bundle> --section section.kind \
36
+ --key kind.nationalitaeten --type enum --enum-list-key nationalities --multiple --label "Staatsangehörigkeit(en)" --label-en "Nationality(ies)"
37
+ # NB: --enum-list-key requires the list to exist on the tenant BEFORE publish
38
+ # (config apply the list first) — else the publish gate throws form.unknown-enum-list.
39
+
40
+ # 3. Attach validation / visibility rules to a field.
41
+ # "Dieses Feld soll Pflicht sein, wenn …" → --required-if.
42
+ # "Dieses Feld soll sich ändern" → --visible-if / --disabled-if.
43
+ plexus-cli offer form field set --dir <bundle> --key kind.nachweis \
44
+ --required-if "kind.vorname != null"
45
+
46
+ # 4. Add outcomes (the branch the form emits to the workflow).
47
+ plexus-cli offer form outcome add --dir <bundle> --id sonderfall \
48
+ --when "kind.konfession == 'sonstige'" --fields kind.vorname
49
+
50
+ # 4b. (Full depth) effects, repeated (arrays-of-records), and computed fields.
51
+ # person/organization/relationship/fieldMap materialize parties + links.
52
+ plexus-cli offer form effect add --dir <bundle> --id p.kind --kind person \
53
+ --bucket kind --policy auto-on-any-write --name-from "kind.vorname,kind.nachname" --role applicant
54
+ plexus-cli offer form repeated add --dir <bundle> --section section.kind \
55
+ --key geschwister --item-fields "name:string,alter:string" --min 0 --max 5
56
+ plexus-cli offer form computed add --dir <bundle> --section section.kind \
57
+ --key kind.hasName --expression "kind.vorname != null"
58
+
59
+ # 5. Validate offline against the REAL publish gate, then publish a new version.
60
+ plexus-cli offer validate --dir <bundle>
61
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
62
+ ```
63
+
64
+ `--form` is optional when the bundle carries exactly one form; with several forms
65
+ it is required (the CLI fails loud listing the choices).
66
+
67
+ ## Verify
68
+ ```bash
69
+ plexus-cli offer form list --dir <bundle> # forms + field/outcome counts + bound?
70
+ plexus-cli offer form show --dir <bundle> --form <s> # per-field type/required/flags
71
+ plexus-cli offer validate --dir <bundle> # "local validation clean"
72
+ ```
73
+
74
+ ## Idempotency / gotchas
75
+ - **Every mutator validates before writing.** On any publish-gate error (or a
76
+ malformed `--visible-if`/`--when` expression) the command writes **nothing** and
77
+ prints the issues — the bundle is never left half-edited.
78
+ - **Field-key stability (publish invariant).** `publishSmartForm` lets you **add**
79
+ fields across versions but **refuses to remove** a field key that a prior published
80
+ version had. `offer form field rm` / `section rm` warn about this — a later
81
+ `apply --publish` over an already-published form will be rejected by the server.
82
+ - **Re-publishing = new version.** Each `offer apply --publish` with a changed form
83
+ definition creates a new `smart_form_versions` row (never edits in place).
84
+ - **Workflows pin the form version.** A published workflow is pinned to the form
85
+ version that was current at *workflow*-publish time (`smartFormPins`). A new form
86
+ version does **not** propagate automatically — the workflow must be re-published.
87
+ `offer apply --publish` over the **whole bundle** re-publishes the workflow too, so
88
+ the full-bundle path picks up the new form; a standalone form publish would leave
89
+ existing workflows on the old version.
90
+ - **Effects / repeated / computed fields** are CLI-authorable (`offer form effect|repeated|computed`).
91
+ The offline gate checks structure + refs + effect-graph + `nameFrom`/`fieldMap.from`
92
+ resolution; the **catalog-dependent** checks (effect `role` → tenant role, `relKind` →
93
+ relationship field, fieldMap `to` → custom field) run **server-side** at `apply --publish`
94
+ and fail loud there if the tenant catalog doesn't have them.
95
+ - An unbound form (no workflow form node binds it yet) is a **warning**, not an error
96
+ — bind it with `offer workflow node add --type form --form <slug>`.
97
+
98
+ ## Related
99
+ - [Attach a form to a workflow](./attach-form-to-workflow.md) — bind the form to a workflow
100
+ form node (`offer workflow node add --type form`).
101
+ - [Add a tenant admin](../users/add-tenant-admin.md) — the token `offer apply --publish` needs.
102
+ - Command reference: `plexus-cli offer form --help`.
@@ -0,0 +1,76 @@
1
+ # Bind a node input to an address (form field, task value, derived price, applicant language)
2
+
3
+ **When to use:** a node's input slot must read a value produced elsewhere — the applicant's
4
+ language for a template letter (`$env.applicant.language`), the tuition a rule set derives
5
+ (`$derived.schulgeld`), a value a task records (`$task.reducedFee.reducedFee`), a form field
6
+ (`$node.form.admission.kind.vorname`) — or carry a fixed value, or go back to its default.
7
+ Replaces hand edits of `inputs` in the bundle's `workflow.json`.
8
+
9
+ ## Prerequisites
10
+ - An offer bundle on disk (`offer export --slug <slug> --out <dir>`). Fully offline: the command
11
+ runs the publish gate on the result and writes nothing when the edit introduces an error. A
12
+ bundle exported before addresses were introduced is refused (`[graph/retiredField]`: a binding with `path`, `wires`)
13
+ — export it again.
14
+ - The input is a **slot** the node declares. For a template document (`production: 'template'`)
15
+ the slots are its `template.fields` — add the field first (bundle or workflow IDE), then bind.
16
+ `offer workflow node list --dir <dir>` shows the nodes; an unknown slot is refused with the
17
+ node's slots.
18
+ - The **address** names its producer, which must run on every path before the node — the model
19
+ and the spelling: [Name what a node reads](./addresses.md). Quote it with single quotes.
20
+
21
+ ## Steps
22
+ ```bash
23
+ # The offer letter in the applicant's language (demo tenant `test`: platzangebot, field `sprache`):
24
+ plexus-cli offer workflow node set-input --dir <dir> --node platzangebot --input sprache \
25
+ --address '$env.applicant.language'
26
+
27
+ # Tuition: the derived price (sibling discount, then the reduction `reducedFee` records):
28
+ plexus-cli offer workflow node set-input --dir <dir> --node contract --input schulgeld \
29
+ --address '$derived.schulgeld'
30
+
31
+ # A form field — full address, or the short form that composes the same address:
32
+ plexus-cli offer workflow node set-input --dir <dir> --node contract --input startDate \
33
+ --address '$node.form.admission.kind.einschulung'
34
+ plexus-cli offer workflow node set-input --dir <dir> --node contract --input startDate \
35
+ --from form --field kind.einschulung
36
+
37
+ # A fixed value (JSON: a text needs its quotes):
38
+ plexus-cli offer workflow node set-input --dir <dir> --node platzangebot --input sprache --literal '"de"'
39
+
40
+ # Back to the slot's default:
41
+ plexus-cli offer workflow node set-input --dir <dir> --node platzangebot --input sprache --clear
42
+ ```
43
+
44
+ The command prints the cell before and after (`binding $env.applicant.language`, `literal "de"`,
45
+ `— (default)`).
46
+
47
+ ## Verify
48
+ ```bash
49
+ plexus-cli offer validate --dir <dir>
50
+ plexus-cli offer workflow node reads --dir <dir> --node contract # the binding is listed, origin „geschrieben"
51
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir> # expect: ~ workflow
52
+ ```
53
+
54
+ ## Idempotency / gotchas
55
+ - Exactly one of `--address`, `--from`+`--field`, `--literal`, `--clear`. A bare key
56
+ (`--address applicant.language`, `--address schulgeld`) is refused with the addresses visible at
57
+ the node — there is no default source and no guessing.
58
+ - A literal is JSON: `"de"`, `3`, `true`, `null`, or a priced item `{"itemKey":…,"grantKeys":[…]}`
59
+ — and must fit the slot's value type: an option field takes one
60
+ of its options, a party slot (a roleAssign's `person`) only a binding. This check runs in the
61
+ verb; the publish gate does not (yet) check literals against their slot type.
62
+ - An end node's outcome cells are set with `offer workflow node set --outcome|--terminal-status|--termination-kind`.
63
+ - `--clear` also removes a cell no slot declares (a dead cell from an old edit).
64
+ - The gate decides whether the address can be read here, before writing: `[address/unknownOwner]`,
65
+ `[address/unknownKey]`, `[address/pathNotInTarget]` (the form has no such field / the stored value no
66
+ such leaf), `[address/notVisible]` (the producer does not run on every path before the node),
67
+ `[address/typeMismatch]` (the value does not fit the slot), `[governedDocument/envNotCovered]`
68
+ (a language field needs an option per language `$env.applicant.language` can deliver), the
69
+ uncovered-input contract. An error the bundle already had is named as „vorbestehend" and does
70
+ not block this edit.
71
+ - Re-running with the same value rewrites the same cell (no change).
72
+ - Nothing reaches a live process until `offer apply --publish`.
73
+
74
+ ## Related
75
+ - [Name what a node reads](./addresses.md) · [Generate a letter from a Word template](./template-document-node.md)
76
+ - [Declare what a document node reads](./declare-node-reads.md) · [Edit a live tenant's offer](./edit-live-tenant-offer.md)
@@ -0,0 +1,137 @@
1
+ # Wait for outstanding documents (completeness check)
2
+
3
+ **When to use:** a school lets the process run on while documents are still
4
+ missing — the Masernschutz-Nachweis, the birth certificate — and wants it to end
5
+ only once they are there, with a deadline („erst wenn wirklich alle Unterlagen da
6
+ sind, ist es vorbei"). The `completenessCheck` node names SmartForm fields
7
+ (`requires`) and a deadline. Entered with nothing missing it completes at once;
8
+ otherwise the family gets its fields in **one** task „Offene Felder beantworten" due by the
9
+ deadline, the optional guest role is granted, and the node parks until the last
10
+ field arrives (`complete`) or a person decides: „Ohne Nachreichung abschließen"
11
+ (`acceptedIncomplete`) or „Bewerbung ablehnen" (`rejected`). The family's
12
+ withdrawal arrives as `absage`.
13
+
14
+ ## Prerequisites
15
+ - An offer bundle on disk: `offer init` for a new one, or a **fresh export** of
16
+ the target environment into a private scratch dir (a live tenant's bundle is
17
+ never kept in a shared or versioned place — the server is its source of truth).
18
+ - The fields exist in the form the bundle's form node pins, as **entered,
19
+ non-repeated** fields (a computed key or a field inside a repeated group
20
+ cannot be handed in). Each is named by its full address,
21
+ `$node.<formNode>.admission.<field>` — the form node that CREATES the
22
+ instance, in single quotes. A bundle exported before addresses were introduced is
23
+ refused — export again.
24
+ - For a guest role: a **leaf** role in the tenant's role catalog (e.g.
25
+ `gasthoerer`) — checked at `offer apply --publish`, not offline.
26
+ - The graph has somewhere to send `rejected` and `absage` (the paths to the
27
+ end of a rejection and of a withdrawal — an end with `dataDecision: discard`
28
+ takes the application's records back itself, no discard node needed).
29
+
30
+ ## Steps
31
+ ```bash
32
+ # 1. Export what is live (read-only).
33
+ plexus-cli --env staging --tenant <tenant> offer export --slug <offer> --out ./bundle
34
+
35
+ # 2. Add the check. Every flag below except --guest-role/--person is required:
36
+ # - at least one --require address (repeatable, comma-separated),
37
+ # - exactly one deadline: --deadline-days <n> (calendar days after entering)
38
+ # OR --deadline-until <YYYY-MM-DD> (a fixed due day, tenant zone),
39
+ # - the `requested` letter (the node type declares it REQUIRED),
40
+ # - rejected/absage routed to the rejection/withdrawal paths.
41
+ # `requires` and the guest role's person name the form they read; nothing is
42
+ # bound implicitly (a bare key is refused with the addresses the node can read).
43
+ plexus-cli offer workflow node add --dir ./bundle --type completenessCheck \
44
+ --id nachreichen --label "Vollständigkeitsprüfung" --label-en "Completeness check" \
45
+ --require '$node.form.admission.kind.masernschutz,$node.form.admission.kind.geburtsurkunde' \
46
+ --deadline-days 14 \
47
+ --guest-role gasthoerer --person '$node.form.admission.beneficiary' \
48
+ --notification requested=admission.documentsRequested \
49
+ --outcomes complete:end,acceptedIncomplete:end,rejected:joinAbsage,absage:endAbsage
50
+
51
+ # Later edits:
52
+ plexus-cli offer workflow node add-requires --dir ./bundle --node nachreichen --address '$node.form.admission.kind.impfpass'
53
+ plexus-cli offer workflow node remove-requires --dir ./bundle --node nachreichen --from form --field kind.impfpass
54
+ plexus-cli offer workflow node set-requires --dir ./bundle --node nachreichen --from form --field kind.masernschutz
55
+ plexus-cli offer workflow node set --dir ./bundle --id nachreichen --deadline-until 2026-10-15
56
+ plexus-cli offer workflow node set --dir ./bundle --id nachreichen --guest-role "" # drops role AND person
57
+ plexus-cli offer workflow node set-notification --dir ./bundle \
58
+ --node nachreichen --event requested --template admission.documentsRequested
59
+
60
+ # 3. Validate offline, see the difference, publish.
61
+ plexus-cli offer validate --dir ./bundle
62
+ plexus-cli --env staging offer plan --dir ./bundle
63
+ plexus-cli --env staging offer apply --publish --dir ./bundle
64
+ ```
65
+
66
+ ## Verify
67
+ ```bash
68
+ plexus-cli offer workflow node list --dir ./bundle
69
+ # nachreichen completenessCheck … complete→…, acceptedIncomplete→…, rejected→…, absage→…
70
+ # letters: requested→admission.documentsRequested
71
+ jq '.graph.nodes[] | select(.id=="nachreichen") | {requires, deadline, guestRole, inputs}' ./bundle/workflow.json
72
+ plexus-cli offer workflow node reads --dir ./bundle --node nachreichen
73
+ ```
74
+ After publish, a fresh application that reaches the node with a field empty
75
+ shows the family task „Offene Felder beantworten" and the board column of the same
76
+ name; a deployment that does not send mail records the letter in Dev Rail
77
+ → „Mail" instead.
78
+
79
+ ## Idempotency / gotchas
80
+ - **The `requested` letter is required.** Without a binding the family would get
81
+ no mail about the request, so every write refuses it
82
+ (`[notification/missingBinding]`) — bind it in the `add` call with
83
+ `--notification requested=<templateId>`. `--disabled` counts as missing.
84
+ Offline validation cannot see the letter catalog: a misspelled template id is
85
+ caught only at `offer apply --publish`.
86
+ - **Deadline rules:** exactly one form. `--deadline-days` is a whole number 0–366
87
+ (`[completenessCheck/deadlineDays]`); `--deadline-until` must be a real
88
+ calendar day (`2026-02-30` is refused, `[completenessCheck/deadlineDate]`).
89
+ Passing both, or neither on `add`, fails before anything is written. Past the
90
+ deadline nothing happens by itself: the family and the responsible person are
91
+ told, and a person decides.
92
+ - **Guest role:** `--guest-role` needs `--person`, and `--person` needs
93
+ `--guest-role` — nothing is defaulted. The person is a party of a form,
94
+ `$node.form.admission.beneficiary` (the beneficiary the form's contract parties
95
+ name), read live at that address whenever the role is granted or ended; a form
96
+ that does not run before the check is `[address/notVisible]`. The role must be a **leaf** tenant
97
+ role (`[completenessCheck/unknownRole]`, `[completenessCheck/nonLeafRole]`,
98
+ both at publish). It is granted only when something is missing, ended on
99
+ `complete` / `rejected` / `absage`, and **kept on `acceptedIncomplete`** — the
100
+ school still has to know whose documents are outstanding. A role the person
101
+ already held is never ended.
102
+ - **Entering with nothing missing completes automatically** — no request, no
103
+ letter, no role; the node passes straight through on `complete`.
104
+ - **Where it lands:** without `--after` the check is spliced in front of the
105
+ FIRST `end` node in `workflow.json` (every edge into that end now enters the
106
+ check) — make sure that is the positive end. With `--after <node>` the
107
+ anchor's FIRST onward edge is the one rerouted; on an anchor with several
108
+ outcomes (a contract: `signed` + `absage`) that may be the wrong one. Check
109
+ with `node list` and fix with `node set --outcomes`.
110
+ - **Routing:** `complete`, `rejected` and `absage` must be routed
111
+ (`[completenessCheck/unroutedOutcome]`); the staff buttons appear only for the
112
+ outcomes that are wired. Left to the splice default, `rejected`/`absage` run
113
+ into whatever follows (usually the positive end) — `add` warns about that; route
114
+ them with `--outcomes`.
115
+ - **`requires` edits:** only a completenessCheck carries `requires`
116
+ (`*-requires` on another node fails; for fields a decider reads use
117
+ `*-reads`). Removing the last address is refused (`[completenessCheck/missingRequires]`);
118
+ two forms in one list are `[address/mixedForms]`, a field the form lacks is
119
+ `[address/pathNotInTarget]`.
120
+ Only errors an edit introduces block it.
121
+ - **Hand-authored boards (known CLI gap):** `node add` derives the progress
122
+ states and appends the check's column „Unterlagen nachreichen"; its
123
+ `rejected`/`withdrawn` states go into the global `col.abgelehnt` /
124
+ `col.abgesagt` columns. A bundle whose board already names its own exit
125
+ columns (e.g. a `c.absage` column labelled „Abgelehnt")
126
+ then carries two columns with one label, and `node add` refuses the write —
127
+ for ANY node type with such an event (`waitlist`, `decision`, …), not only this
128
+ one. Until `node add` can map events onto existing states, author the node in
129
+ `workflow.json` + the states/columns/wiring in `manifest.json`/`workflow.json`
130
+ by hand (shape: the Verify `jq` output above) and gate it with `offer validate`
131
+ / `offer apply --publish`. Scaffolded bundles (`offer init`) are not affected.
132
+ - **Running applications keep their pinned version** — see
133
+ [Declare what a document node reads](./declare-node-reads.md) for re-pin / wipe.
134
+
135
+ ## Related
136
+ - [Add a workflow node](./add-workflow-node.md) · [Edit a workflow node](./edit-workflow-node.md) · [Release an offer bundle](./publish-offer-bundle.md)
137
+ - [Reword a letter](../notifications/reword-a-letter.md)
@@ -0,0 +1,91 @@
1
+ # Countersign the contract („Unterschrift (intern)")
2
+
3
+ **When to use:** the school countersigns the admission contract the family signed. A `countersign` node after the contract opens one row „Vertrag gegenzeichnen" for a
4
+ team: the clerk downloads the family's signed copy, signs it for the Träger, uploads the
5
+ scan with the date. That opens the Träger's own Signaturvorgang on the revision the family
6
+ signed and books the Träger („vertreten durch <Mitarbeiter/in>") on it, so the contract is
7
+ signed and the family's portal shows the countersigned paper; it sends the node's
8
+ `countersigned` letter, and the contract's event moves the node along `countersigned`. It replaces
9
+ the retired task result `countersignature` (`--result countersignature` is now an unknown
10
+ kind) and its review host node.
11
+
12
+ ## Prerequisites
13
+ - A bundle on disk (`offer export`, a live tenant's only into a private scratch dir —
14
+ [Edit a live tenant's offer](./edit-live-tenant-offer.md)). Authoring is offline.
15
+ - The contract node is a `governedDocument` with `production: "resolver"` and
16
+ `responseStrategy: "signedUploadChecked"` (the school checks the signed return; only that
17
+ freezes a revision with declared signers) — `[countersign/sourceNotCheckedContract]`
18
+ otherwise.
19
+ - A team (permission-group key) that countersigns, e.g. `schulverwaltung`.
20
+
21
+ ## Steps
22
+ ```bash
23
+ # 1. The contract's accepting edge (`signed`) and what follows it:
24
+ plexus-cli offer workflow node list --dir <dir>
25
+
26
+ # 2. Splice the countersignature onto that edge. Required: the team, the contract's
27
+ # address (a resolver contract is `$node.<contractNode>.contract`) and the
28
+ # letter (the node type declares it REQUIRED).
29
+ plexus-cli offer workflow node add --dir <dir> --type countersign \
30
+ --id gegenzeichnung --label "Gegenzeichnung" --label-en "Countersignature" \
31
+ --on-edge contract:signed \
32
+ --assign-team schulverwaltung \
33
+ --title "Vertrag gegenzeichnen" --title-en "Countersign the contract" \
34
+ --due-in-days 5 \
35
+ --contract '$node.contract.contract' \
36
+ --notification countersigned=admission.contractCountersigned
37
+
38
+ # Optional: put its board event on an existing state instead of a new
39
+ # „Gegenzeichnung" column: --progress countersigning=<stateId>
40
+
41
+ # 3. Validate, plan, publish.
42
+ plexus-cli offer validate --dir <dir>
43
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir>
44
+ plexus-cli --env staging --tenant <t> offer apply --publish --dir <dir>
45
+ ```
46
+
47
+ ## Verify
48
+ ```bash
49
+ plexus-cli offer workflow node list --dir <dir>
50
+ # contract governedDocument … signed→gegenzeichnung
51
+ # gegenzeichnung countersign … countersigned→<old target>
52
+ # letters: countersigned→admission.contractCountersigned
53
+ plexus-cli offer workflow node reads --dir <dir> --node gegenzeichnung # $node.contract.contract, written in nodes.gegenzeichnung.contract
54
+ jq '.graph.nodes[] | select(.type=="countersign")' <dir>/workflow.json
55
+ ```
56
+ After publish, an application whose signed return the school accepted parks on the node;
57
+ the team's queue shows „Vertrag gegenzeichnen", the family's portal „Die Schule zeichnet
58
+ Ihren Vertrag gegen". After the upload the revision reads „3 von 3" and the family gets the
59
+ letter (on a deployment that does not send mail: Dev Rail → „Mail").
60
+
61
+ ## Idempotency / gotchas
62
+ - **The publish rules:** `[countersign/assign]` (no team), `[countersign/dueInDays]` (a whole
63
+ number ≥ 1), `[countersign/sourceNotCheckedContract]`, `[countersign/twice]` (one
64
+ countersignature per contract), `[address/missing]` (no `contract`),
65
+ `[address/unknownKey]` (a resolver contract produces `contract`, not `document`),
66
+ `[address/notVisible]` (the contract must lie on every path to the node),
67
+ `[notification/missingBinding]` (no `countersigned` letter; `--disabled` counts as missing).
68
+ Offline the letter catalog is not loaded: a misspelled template id fails only at
69
+ `offer apply --publish`.
70
+ - **The letter moved:** `countersigned` is bound on the countersign node, not on the contract.
71
+ A bundle that still binds it on the contract is refused there (`[notification/unknownEvent]`)
72
+ — clear it with `node clear-notification --node contract --event countersigned`.
73
+ - **`--title` needs `--title-en`** (every config text is `{ de, en }`); without both the row
74
+ is titled „Vertrag gegenzeichnen" / „Countersign the contract". `--assign-team`, `--title`,
75
+ `--title-en` and `--due-in-days` are refused on any other node type.
76
+ - **The row closes only through the countersignature dialog**, never by a bare dispatch: the
77
+ engine moves the node only on the contract's `signed` event and refuses it while the contract
78
+ is not signed by every side („Der Vertrag ist noch nicht von allen Seiten unterschrieben."). Withdrawal and „Bewerbung ablehnen" sweep
79
+ the open row; nothing of the school is booked.
80
+ - **The representative is whoever completes the row** — active staff of the tenant with a
81
+ name; nobody can book a colleague's signature.
82
+ - **Migrating a bundle with the old shape** (a review „Gegenzeichnung" + task
83
+ `result: { kind: 'countersignature' }`): remove the task and the review
84
+ (`node task rm`, `node rm`), add the countersign node as above (team from the old
85
+ `assign.decision`, title / SLA from the task), move the letter, and re-key the progress
86
+ wiring from `reviewing` to `countersigning`.
87
+
88
+ ## Related
89
+ - [Name what a node reads](./addresses.md) · [Add a workflow node](./add-workflow-node.md)
90
+ - [Insert a node on one edge and rewire it](./insert-and-rewire-node.md) (`--on-edge`, `--progress`)
91
+ - [Edit a live tenant's offer](./edit-live-tenant-offer.md)
@@ -0,0 +1,55 @@
1
+ # Make a new school applyable (create a pedagogy unit)
2
+
3
+ **When to use:** a brand-new school (or an existing plain unit) needs to host an
4
+ admission offer, but `offer create --unit <unit>` fails because the unit isn't a
5
+ *pedagogy unit* yet (no `pedagogy_units` bridge row). This creates the unit + bridge
6
+ so the external "bewerben" portal can resolve it.
7
+
8
+ ## Prerequisites
9
+ - A configured `plexus-cli` targeting the env (`--env local|staging|prod`).
10
+ - A token with tenant-wide master-data access (creating units) +
11
+ tenant access (the bridge writer is tenant-gated).
12
+ - An installed, current `plexus-cli` (`plexus-cli offer unit create --help`).
13
+
14
+ ## Steps
15
+ ```bash
16
+ # 1a. Brand-new school: create the unit AND its pedagogy bridge in one step.
17
+ # The name is German AND English.
18
+ plexus-cli --env <env> --tenant <slug> offer unit create --name "Grundschule Neustadt" --name-en "Neustadt Primary School"
19
+ # (--kind school by default; --parent <unit key> to nest; output prints the unitId)
20
+
21
+ # --unit takes the unit's config key (`plexus-cli units` lists it) — never its
22
+ # name. A unit made by `offer unit create` has no config key until config-as-code
23
+ # adopts it: address it by its URL slug meanwhile (here: grundschule-neustadt).
24
+
25
+ # 1b. OR upgrade an EXISTING plain unit to a pedagogy unit (idempotent).
26
+ plexus-cli --env <env> --tenant <slug> offer unit ensure-pedagogy --unit grundschule-neustadt
27
+
28
+ # 2. Now the offer can link it. (offer create/set --unit also ensures the bridge,
29
+ # so an existing non-pedagogy unit is upgraded automatically on link.)
30
+ plexus-cli --env <env> --tenant <slug> offer create --slug gs-neustadt --title "Grundschule Neustadt" --title-en "Neustadt Primary School" --unit grundschule-neustadt
31
+ ```
32
+
33
+ ## Verify
34
+ ```bash
35
+ plexus-cli --env <env> --tenant <slug> units # the unit shows pedagogic ✓
36
+ plexus-cli --env <env> --tenant <slug> offer status --slug gs-neustadt # program/pedagogy links resolve
37
+ ```
38
+
39
+ ## Idempotency / gotchas
40
+ - **`ensure-pedagogy` is idempotent** — re-running returns the existing bridge
41
+ (`created: false`); it never inserts a duplicate.
42
+ - **`offer create/set --unit` ensures-then-links** — you don't strictly need a
43
+ separate `ensure-pedagogy` step for an existing unit; the link path upgrades it.
44
+ - **A unit needs a tenant legal entity.** `offer unit create` auto-resolves the
45
+ tenant's first legal entity; a tenant with none will fail loud (set one up first).
46
+ - **Tenant-scoped, fail-loud.** The bridge writer (`programAdmin.ensurePedagogyUnit`)
47
+ verifies the unit belongs to the tenant; it never bridges a foreign unit.
48
+ - **Config is authoritative for the flag.** Also set it in the tenant config
49
+ (`plexus-cli config set --tenant <slug> --target units --key <unitKey> --field pedagogyUnit --value true`),
50
+ or the next `config apply` of a config without it removes the bridge again — or,
51
+ once an offer is linked, fails with `[unit/pedagogyUnitInUse]`.
52
+
53
+ ## Related
54
+ - [Author the manifest meta layer](./author-manifest-meta.md) · [Author a SmartForm as code](./author-smartform.md).
55
+ - Command reference: `plexus-cli offer unit --help`.