@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,67 @@
1
+ # Insert a node on one edge and rewire it
2
+
3
+ **When to use:** a new step must sit between two existing ones — e.g. „Unterlagen prüfen"
4
+ between the form and the first decision — and it must land on an existing board column
5
+ instead of adding one. Replaces the hand-written bundle edits of `apply-doccheck.py`
6
+ (insert a `review` node, re-point `form.next.standard`, add a progress mapping).
7
+
8
+ ## Prerequisites
9
+ - An offer bundle on disk (`offer export --slug <slug> --out <dir>`). Fully offline: every
10
+ command runs the publish gate on the result and writes nothing when it fails.
11
+ - The ids of the edge you cut: `plexus-cli offer workflow node list --dir <dir>` shows each
12
+ node's outcomes and targets.
13
+ - The state ids of the process board: `plexus-cli offer workflow show --dir <dir>`.
14
+
15
+ ## Steps
16
+ ```bash
17
+ # A review step on exactly the edge form —standard→ (whatever it pointed at):
18
+ plexus-cli offer workflow node add --dir <dir> --type review --id docCheck \
19
+ --label "Unterlagen prüfen" --label-en "Check documents" \
20
+ --reads '$node.form.admission.kind.masernschutz,$node.form.admission.kind.proofOfIdentity' \
21
+ --on-edge form:standard \
22
+ --progress reviewing=eingereicht \
23
+ --assign-decision bewerbungsteam
24
+
25
+ # Any other type works the same way, e.g. a waiting list on ONE decision outcome:
26
+ plexus-cli offer workflow node add --dir <dir> --type waitlist --id warteliste \
27
+ --label "Warteliste" --label-en "Waiting list" --on-edge decision:waitlisted
28
+
29
+ # Re-route an existing node's outcome afterwards:
30
+ plexus-cli offer workflow node set --dir <dir> --id docCheck --outcomes reviewed:decision
31
+ ```
32
+
33
+ - `--on-edge <from>:<outcome>` re-points exactly that edge to the new node; the new node's
34
+ outcomes continue to the edge's old target (route others with `--outcomes`). A sibling
35
+ outcome of `<from>` that reaches the same target keeps its route — unlike `--after`, which
36
+ rewires every edge of the anchor that points at its first target. The two exclude each other.
37
+ - `--progress <event>=<stateId>` puts the node's progress event on a state the board already
38
+ has (the family keeps reading „Eingereicht"). Without it the node gets its own state and
39
+ column — for a review that is a second „In Prüfung" column, which the board's
40
+ distinct-label rule (`checkProgressBoardDistinctLabels`) refuses.
41
+ - `--reads` names the fields a reviewer of a `--type review` ticks (entered, non-repeated
42
+ fields only), each by its full address `$node.<formNode>.admission.<field>` (single
43
+ quotes; a bare key is refused with the addresses visible at the new node). Without it the
44
+ review ticks nothing and still parks. Other node types declare reads with
45
+ `node set-reads` (which also takes the short form `--from <formNode> --field <path>,…`).
46
+ The form must run on every path before the new node (`[address/notVisible]` otherwise).
47
+
48
+ ## Verify
49
+ ```bash
50
+ plexus-cli offer validate --dir <dir>
51
+ plexus-cli offer workflow node list --dir <dir>
52
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir> # expect: ~ workflow (+ ~ smartform if fields changed)
53
+ ```
54
+
55
+ ## Idempotency / gotchas
56
+ - Not idempotent: a second `node add` with the same `--id` is refused (`already exists`).
57
+ - An event the node type does not emit, or a state the board does not have, is refused by
58
+ name (`emits no progress event …`, `no state … on the process board`).
59
+ - A review holds every non-rejecting outcome until all `reads` fields are ticked — so a
60
+ waiting-list exit belongs on the decision after it, not on the review.
61
+ - Making the read fields required is a form edit: `offer form field set --key <k> --required`.
62
+ - Nothing here touches a live process until `offer apply --publish`; running applications
63
+ stay on their pinned version.
64
+
65
+ ## Related
66
+ - [Add a workflow node](./add-workflow-node.md) · [Edit a workflow node](./edit-workflow-node.md)
67
+ - [Name what a node reads](./addresses.md) · [Declare what a document node reads](./declare-node-reads.md) · [Edit a live tenant's offer](./edit-live-tenant-offer.md)
@@ -0,0 +1,162 @@
1
+ # Author a workflow node's tasks
2
+
3
+ **When to use:** give a workflow node a task a team must do — with a typed result
4
+ („Empfehlung", „Auswahl", Freitext, a priced item or a reduction), a blocking edge onto the same or a later node, a
5
+ spawn condition, evidence fields or its plain-text description — or change / remove one, without
6
+ hand-editing `workflow.json`. Works on any bundle: the `test` template, a fixture, or a
7
+ live tenant's export (then follow [Edit a live tenant's offer](./edit-live-tenant-offer.md)
8
+ around it).
9
+
10
+ ## The model in four lines
11
+ - A task (`node.tasks[]`, key unique per node) spawns when its node is entered — with
12
+ `--when`, only while that **boolean form export** holds. Everything a task reads names
13
+ its producer by full address (single quotes): `--when` / `--reads`
14
+ (`'$node.<formNode>.admission.<field>'`, one form per task — `[address/mixedForms]`),
15
+ `--documents` (`artifact:<own slot>`, `field:<address>`, `document:$node.<node>.document`),
16
+ `--family-request-fields <addresses>` / `--family-request-from '$node.<formNode>.admission'`,
17
+ a record field's `"suggest":{"kind":"form","address":"$node.…"}`. A bare key is refused
18
+ with the addresses visible at the node; the producer must run on every path before it
19
+ (`[address/notVisible]`). A value a task records is `$task.<taskKey>.<storeKey>` for its
20
+ readers — renaming the task key changes that address.
21
+ - `--blocks <templateKey>` withholds a sibling's actions on the same node;
22
+ `--blocks <nodeId>:<templateKey>` withholds a template on a **later** node (a
23
+ cross-node blocker: it survives its node and needs a `--result`). Intrinsic targets:
24
+ `decision`, `prepareDocument`, `releaseApproval`. The target's **whole** action group
25
+ waits — on a decision Freigeben, Warteliste, Ablehnen, and Nachrücken on the waitlist
26
+ it routes into. There is no partial hold; the staff escape is „Bewerbung
27
+ ablehnen".
28
+ - A task with `--result` closes only by recording it; `--chat` posts a result with text
29
+ into `all`, `team:<group>` or an authored channel key — never `external`.
30
+ - A result **with a value** (`choice`, `pricedItem`, `reduction`) writes it under the
31
+ task's own address — `--writes '$task.<thisTaskKey>.<storeKey>'`, stored as the bare
32
+ `result.writes` — and names it with `--result-label` (`--result-label-en`). A
33
+ `pricedItem` names its article with `--item <catalog key>` (not an address: the key of
34
+ the billing item); a `reduction` the derived price it lowers with
35
+ `--prices '$derived.<key>'`.
36
+ - The CLI checks **shape**; the graph rules (`[workItems/*]`, `[taskResult/*]`) are the
37
+ publish validators, run before every write and by `offer validate`.
38
+
39
+ ## Steps
40
+
41
+ ```bash
42
+ B=<bundle dir>
43
+
44
+ # What a node carries today.
45
+ plexus-cli offer workflow node list --dir $B
46
+ jq '.graph.nodes[] | {id, tasks}' $B/workflow.json # the tasks themselves
47
+
48
+ # 1. A cross-node blocker with a recommendation, posted to the whole team chat:
49
+ # the review team's estimate holds the whole Aufnahme decision.
50
+ plexus-cli offer workflow node task add --dir $B --node reviewEingereicht --key einschaetzung \
51
+ --title "Einschätzung abgeben" --title-en "Give an assessment" \
52
+ --team oeffentlichkeitsarbeit --due-in-days 3 \
53
+ --blocks aufnahme:decision \
54
+ --result recommendation --chat all
55
+
56
+ # 2. A conditional choice task that holds the contract (the reduced-fee shape):
57
+ # it exists only while the family asked for a reduction, comes back when the
58
+ # answer returns to „Ja", and is judged again when the income proofs change.
59
+ plexus-cli offer workflow node task add --dir $B --node contract --key ermaessigung \
60
+ --title "Ermäßigung prüfen" --team finanzen --due-in-days 5 \
61
+ --blocks prepareDocument \
62
+ --result choice --option voll="Voller Beitrag|Full fee" --option halb="Halbiert|Halved" \
63
+ --text-required --chat team:finanzen \
64
+ --when '$node.form.admission.ermaessigungBeantragt' --on-condition-return respawn \
65
+ --reads '$node.form.admission.gebuehren.einkommensnachweise' \
66
+ --description "Nachweise prüfen und die Ermäßigung berechnen." \
67
+ --description-en "Check the proofs and calculate the reduction."
68
+
69
+ # 3. A reduction that lowers the derived tuition (the `test` template's shape): it
70
+ # writes its deduction under its own address and names the price it lowers.
71
+ # The derived price must be declared in graph.processStore with
72
+ # derive.reduction = $task.reducedFee.reducedFee ([process/reductionPrices]).
73
+ plexus-cli offer workflow node task add --dir $B --node reviewEingereicht --key reducedFee \
74
+ --title "Ermäßigung berechnen" --team finanzen \
75
+ --result reduction --writes '$task.reducedFee.reducedFee' --prices '$derived.schulgeld' \
76
+ --result-label "Einkommensermäßigung (Abzug)" --result-label-en "Income-based reduction (deduction)" \
77
+ --chat team:finanzen
78
+ # A priced item names its article instead:
79
+ # --result pricedItem --item tuition --writes '$task.<key>.<storeKey>' --result-label "Schulgeld"
80
+
81
+ # 4. Change only what you name (everything else stays).
82
+ plexus-cli offer workflow node task set --dir $B --node reviewEingereicht --key einschaetzung --text-required
83
+ plexus-cli offer workflow node task set --dir $B --node contract --key ermaessigung --clear-description
84
+
85
+ # 5. Remove a task (refused while another task's --blocks still names it — the
86
+ # refusal lists the dependents; retarget or --clear-blocks them first).
87
+ plexus-cli offer workflow node task rm --dir $B --node contract --key ermaessigung
88
+
89
+ # 6. Gate, diff, release.
90
+ plexus-cli offer validate --dir $B
91
+ plexus-cli --env <env> offer plan --tenant <t> --dir $B
92
+ plexus-cli --env <env> offer apply --tenant <t> --dir $B --publish
93
+ ```
94
+
95
+ Every verb prints a field diff (`before` / `after`); `--json` gives
96
+ `{action, node, key, before, after, changes, dropped}`.
97
+
98
+ ## Example: move a task to another node on a live tenant
99
+
100
+ A form task `oeaEinschaetzung` on `formAnmeldung` should become one recommendation task
101
+ on the review node that holds the Aufnahme decision (reject / waitlist stay open).
102
+ Work in a scratch dir and keep an untouched copy to diff against:
103
+
104
+ ```bash
105
+ WORK=<scratch dir>/$(date +%F)-einschaetzung
106
+ T=<slug>; S=<offer-slug>
107
+ plexus-cli --env prod offer export --tenant $T --slug $S --out $WORK/$S
108
+ cp -a $WORK/$S $WORK/$S.orig
109
+
110
+ plexus-cli offer workflow node task rm --dir $WORK/$S --node formAnmeldung --key oeaEinschaetzung
111
+ plexus-cli offer workflow node task add --dir $WORK/$S --node reviewEingereicht --key einschaetzung \
112
+ --title "Einschätzung abgeben" --team oeffentlichkeitsarbeit --due-in-days 3 \
113
+ --blocks aufnahme:decision \
114
+ --result recommendation --chat all
115
+
116
+ diff -ru $WORK/$S.orig $WORK/$S # exactly these two tasks
117
+ plexus-cli offer validate --dir $WORK/$S
118
+ plexus-cli --env prod offer plan --tenant $T --dir $WORK/$S
119
+ plexus-cli --env prod offer apply --allow-prod --tenant $T --dir $WORK/$S --publish
120
+ ```
121
+
122
+ If `offer plan` refuses task fields that `offer validate` accepts, the deployment runs an
123
+ older release than your CLI — that needs a platform operator, not a bundle edit.
124
+
125
+ ## Verify
126
+ - `offer validate --dir $B` → clean.
127
+ - After publish, a new application reaching the node shows the task in the cockpit's
128
+ task rail („Empfehlung erfassen"), and the blocked target's actions stay held until
129
+ the result is recorded.
130
+
131
+ ## Gotchas
132
+ - **Only errors the edit introduces block the write.** Errors the bundle already carried
133
+ print as `⚠ vorbestehend …` and do not stop you — but `offer apply --publish` still
134
+ refuses them.
135
+ - **`--title` without `--title-en` writes the German into both locales**; `--title-en`
136
+ alone relabels English only.
137
+ - **`blocks.except` is retired** — `offer validate` refuses a bundle that still
138
+ carries it (`[workItems/blocksExceptRetired]`); re-setting `--blocks` drops it
139
+ (`⚠ blocks.except entfällt`). A result kind change drops
140
+ options / `textRequired` / `writes` / `label` / `item` / `prices` that no longer fit
141
+ (each listed under `dropped`), and keeps `chat`.
142
+ - **`--writes` takes this task's own address only** — `'$task.<key>.<storeKey>'` without a
143
+ path; a bare key, another task's address or a path is refused. `--prices` takes
144
+ `'$derived.<key>'`. Whether the store key is declared and the reduction's
145
+ `derive.reduction` points back at this task is the publish gate's check
146
+ (`[process/*]`).
147
+ - **`--option` replaces the whole list** — repeat every entry you keep.
148
+ - **`--description` is plain text under the title, never a checklist.** There are no
149
+ checklist steps any more (a bundle still carrying `steps` is refused at publish,
150
+ `[workItems/unknownKey]`, like any key a task does not declare); several work steps are several tasks (a `spawnTasks`
151
+ optional action, chained by `blocks`). Without `--description-en` the German stands in
152
+ for English.
153
+ - **An old bundle** (bare keys in `when` / `reads` / documents /
154
+ `familyRequest` / `suggest`) is refused (`[address/bareIdentifier]`) — export it again.
155
+ - **Offline, team and channel existence are not checked** — the tenant's permission
156
+ groups are known only at `offer apply --publish`.
157
+ - **The positional `node add|set --task`** replaces a node's whole task list and knows
158
+ none of the fields above; use it only to scaffold a new node.
159
+
160
+ ## Related
161
+ - [Name what a node reads](./addresses.md) · [Edit a workflow node](./edit-workflow-node.md) · [Author default chat channels](./workflow-default-channels.md)
162
+ - `plexus-cli offer workflow node task add --help` (every result kind and flag)
@@ -0,0 +1,65 @@
1
+ # Ask for an optional assessment („Einschätzung einholen")
2
+
3
+ **Use it to** model assessments the decision does not have to wait for: the
4
+ Öffentlichkeitsarbeit may say „Keine Einschätzung abgeben", the Schulleitung attaches a
5
+ Beurteilung as a file, and the Sekretariat can ask the Geschäftsführung for one more
6
+ assessment from the „Weiteres" menu — without a letter to the family.
7
+
8
+ ## The model
9
+
10
+ - **Optional task** (`optional: { skipLabel: { de, en } }`): nothing waits for it. The
11
+ task holds no step, so publish refuses it together with `blocks`, `gatesNode`,
12
+ `locksEvidence` and `completableIf` (`[workItems/optionalHolds]`). Its holder closes it
13
+ with its result or with the skip button. The Sekretariat skips it by deciding: an open
14
+ optional task then reads „Aufgabe hinfällig" in the Verlauf, naming the decider.
15
+ The result must be prose: free text, a recommendation, or a choice without `writes`
16
+ (`[workItems/optionalNeedsProseResult]`). A task of an action listed in `requiresOneOf`
17
+ is never optional (`[workItems/optionalInRequiresOneOf]`).
18
+ - **Files** (`result.files: true`): the person attaches up to 20 files next to the text.
19
+ Text and files go into the result's chat as one message. Find them later in the cockpit
20
+ under Verlauf/Aktivitäten (tab of the chat the result names), shown only to people who
21
+ read that chat — never under „Dokumente"
22
+ ([task-result-files.md](./task-result-files.md)).
23
+ - **„Einschätzung einholen"** is a `spawnTasks` optional action that references an optional
24
+ task assigned to a role (`--team geschaeftsfuehrung`). Pressing the action spawns that
25
+ task. A triggered optional task does not hold the decision. An internal task never
26
+ writes to the family.
27
+
28
+ ## CLI
29
+
30
+ Use a scratch dir for a real tenant
31
+ ([edit-live-tenant-offer.md](./edit-live-tenant-offer.md)).
32
+
33
+ ```bash
34
+ # 1. The ÖA assessment: optional, no longer blocking the decision
35
+ plexus-cli offer workflow node task set --dir <dir> --node aufnahme --key oeaEinschaetzung \
36
+ --clear-blocks --optional "Keine Einschätzung abgeben" --optional-en "No assessment"
37
+
38
+ # 2. The Schulleitung's assessment takes files (it may stay owed — keep its blocks)
39
+ plexus-cli offer workflow node task set --dir <dir> --node aufnahme --key schulleitungPruefung --files
40
+
41
+ # 3. The task the action spawns, then the action
42
+ plexus-cli offer workflow node task add --dir <dir> --node aufnahme --key gfEinschaetzung \
43
+ --title "Einschätzung Geschäftsführung" --title-en "Management assessment" \
44
+ --team geschaeftsfuehrung --due-in-days 3 --result freetext --chat all --files \
45
+ --optional "Keine Einschätzung abgeben" --optional-en "No assessment"
46
+ plexus-cli offer workflow node optional-action set --dir <dir> --node aufnahme --key einschaetzung \
47
+ --label "Einschätzung einholen" --label-en "Request an assessment" \
48
+ --tasks gfEinschaetzung --trigger bewerbungsteam --repeatable
49
+
50
+ plexus-cli offer validate --dir <dir>
51
+ ```
52
+
53
+ The group named by `--team` must hold `aufgaben.*` rights, or its members cannot open the
54
+ task (`tenant_admin` usually holds none). A rights failure blocks nothing at publish; it
55
+ only shows up in the cockpit.
56
+
57
+ ## Verify
58
+
59
+ 1. Cockpit, application on `aufnahme`: the decision row shows „Wartet auf: …" for
60
+ every task that still blocks it, but not for optional ones.
61
+ 2. As the ÖA member, open the task. The dialog shows „Keine Einschätzung abgeben" next to
62
+ „Ergebnis erfassen". After a skip, the Verlauf card reads „Keine Einschätzung abgeben".
63
+ 3. As the Sekretariat, use „Weiteres → Einschätzung einholen". The Geschäftsführung's queue
64
+ gets the task, and the family's Verlauf gets nothing.
65
+ 4. Testmodus: „<Aufgabe> (Test): Keine Einschätzung abgeben" skips through the real verb.
@@ -0,0 +1,117 @@
1
+ # Release an offer bundle — and check that it actually went live
2
+
3
+ **Use it to** take an edited bundle live on a tenant, stage it for testing first,
4
+ or find out why a change you applied is not visible to applicants.
5
+
6
+ ## The model in one paragraph
7
+
8
+ An offer family is one ascending version stream. Exactly one generation is
9
+ **editable** (the draft, always the newest revision); at most one is **live**
10
+ (what real applicants run); at most one sits between them as the **internal
11
+ candidate** (testable, not public). Applying a bundle writes into the *draft*.
12
+ Publishing freezes the draft and spawns a fresh one. Applications already running
13
+ keep the generation they started on — forever, unless migrated explicitly.
14
+
15
+ So there are two different questions, and they have different answers:
16
+
17
+ | Question | Answered by |
18
+ |---|---|
19
+ | What would `offer apply` write into the draft? | `offer plan` / `offer diff` per-target rows |
20
+ | Is anything waiting to go live? | the `publish` row of `plan`/`diff`, and `offer status` |
21
+
22
+ ## Check before you publish
23
+
24
+ ```bash
25
+ plexus-cli --env staging offer status --tenant <slug> --slug <offer-key>
26
+ ```
27
+
28
+ ```
29
+ field value
30
+ ───────── ───────────────────────────────
31
+ manifest published
32
+ workflow bound
33
+ pedagogy Grundschule Beispielstadt
34
+ draft v12
35
+ internal -
36
+ live v11
37
+ publish PENDING (draft is ahead of live)
38
+ ```
39
+
40
+ `publish: PENDING` means the draft says something the live generation does not —
41
+ regardless of whether *your* bundle changed anything. That is the state you are
42
+ in after an `offer apply` without `--publish`, or after someone saved in the
43
+ Workflow IDE.
44
+
45
+ ## Publish
46
+
47
+ ```bash
48
+ # Go live (default).
49
+ plexus-cli --env staging offer apply --tenant <slug> --dir ./bundle --publish
50
+
51
+ # Or stage as the testable candidate only — real applicants keep the live
52
+ # generation; a Testmodus application can be started against this one.
53
+ plexus-cli --env staging offer apply --tenant <slug> --dir ./bundle --publish --stage internal
54
+ ```
55
+
56
+ The final step of the report names what happened:
57
+
58
+ ```
59
+ step action detail
60
+ ──────────────────── ────────── ──────
61
+ manifest unchanged
62
+ smartform:anmeldung unchanged
63
+ workflow created
64
+ offer published
65
+ ```
66
+
67
+ - `offer: published` — a new generation went live. With the detail *"promoted the
68
+ tested candidate"* it released the generation you staged with `--stage internal`
69
+ rather than cutting a fresh copy of it, so what goes live is what you tested.
70
+ - `offer: already-live` — the draft already equalled the live generation. Nothing
71
+ was flipped, and that is correct.
72
+ - `offer:internal: …` — the same two outcomes for `--stage internal`.
73
+
74
+ ## Verify
75
+
76
+ ```bash
77
+ plexus-cli --env staging offer status --tenant <slug> --slug <offer-key>
78
+ ```
79
+
80
+ `live` must have moved to the draft's old revision and `publish` must read
81
+ `up to date`. In the app, the Workflow IDE's version picker shows the new
82
+ generation as **Aktuell**, and the one below it as **Vorgänger**.
83
+
84
+ ## Gotchas
85
+
86
+ - **`--publish` publishes on the distance to the LIVE head, not to your file.**
87
+ Applying a bundle that matches the draft byte-for-byte still publishes when the
88
+ draft is ahead of live. This is deliberate: the older behaviour compared the
89
+ bundle against the draft, so a second run with `--publish` reported `unchanged`
90
+ everywhere and silently released nothing.
91
+ - **The form and workflow publish gates run only when something is released**,
92
+ and always before the flip — so a draft edited in the IDE is validated and
93
+ finalized before it is frozen, and an apply that releases nothing validates
94
+ nothing. A validation error aborts before any generation changes.
95
+ - **`--stage internal` discards an existing internal candidate** (it is archived).
96
+ That candidate never went live, so nothing an applicant sees changes.
97
+ - **Stage then release is two commands, not two generations.** `--stage internal`
98
+ followed by a plain `--publish` of the same bundle promotes the staged
99
+ generation in place. It does not archive it and release a copy.
100
+ - **Running applications are not migrated.** After a release, older applications
101
+ keep their generation and appear in the board's amber *Veraltete
102
+ Workflow-Version* section when their board layout differs.
103
+ - **`offer plan` is read-only and now carries a `publish` row too** — use it to
104
+ see a pending release without applying anything.
105
+ - **`offer plan` shows program stages as `~stage` / `+stage` / `-stage`** —
106
+ `apply` reconciles `program.stages` by bundle `key`, not by name (see
107
+ `../config/rename-program-stage.md`). Run `offer plan` before the first apply
108
+ on an env: a `-stage` row archives that level, and apply refuses it with
109
+ `[stage/hasActiveApplications]` while an active application sits on it.
110
+ **Runs** are still matched by display name and only ever added: a renamed run
111
+ becomes a second run.
112
+
113
+ ## Related
114
+
115
+ - [Author a SmartForm as code](./author-smartform.md), [Add a workflow node](./add-workflow-node.md)
116
+ - [Manage a contract resolver](../resolver/manage-contract-resolver.md) — the
117
+ apply → assign → publish order
@@ -0,0 +1,78 @@
1
+ # Set an offer's seat capacities in its bundle
2
+
3
+ **When to use:** an offer's Schuljahr × Klassenstufe grid (seats per cohort, open or
4
+ closed) should travel with the bundle — a fresh environment gets the whole grid from one
5
+ `offer apply` instead of one `offer capacity set` per cell.
6
+
7
+ ## Prerequisites
8
+ - An offer bundle on disk (`offer export …`) whose `bundle.yaml` has a `program` block.
9
+ - A token with the tenant's offer-configuration rights (a tenant admin has them) for `offer plan` / `offer apply` on the target env.
10
+ - The stage and run keys of the offer (`program.stages[].key`, `program.runs[].key`) —
11
+ cells sit under a run (by its **bare** key) and are keyed by the **bare** stage key,
12
+ never by a name (names are `{ de, en }` display text).
13
+
14
+ ## Steps
15
+
16
+ Each run lists its cells under `capacities`, keyed by stage key:
17
+
18
+ ```yaml
19
+ program:
20
+ unitKey: primary
21
+ stages:
22
+ - { key: grade-1, name: { de: Klassenstufe 1, en: Grade 1 }, gradeLevel: 1 }
23
+ - { key: grade-2, name: { de: Klassenstufe 2, en: Grade 2 }, gradeLevel: 2 }
24
+ runs:
25
+ - key: "2026-27"
26
+ name: { de: Schuljahr 2026/27, en: School year 2026/27 }
27
+ startDate: '2026-08-01'
28
+ endDate: '2027-07-31'
29
+ capacities:
30
+ grade-1: { isOpen: true, capacity: 24 } # 24 seats, accepting
31
+ grade-2: { isOpen: true, capacity: unlimited } # no cap
32
+ # grade-3: { isOpen: false, capacity: null } # null = unconfigured (clears a stored one)
33
+ ```
34
+
35
+ ```bash
36
+ plexus-cli offer validate --dir <bundle> # offline: shape + stage keys
37
+ plexus-cli --env <env> offer plan --dir <bundle> # one `~cap run×stage from → to` row per changed cell
38
+ plexus-cli --env <env> offer apply --dir <bundle> # writes exactly those cells
39
+ ```
40
+
41
+ The fastest way to get the block right is to let the live offer write it:
42
+ `plexus-cli --env <env> offer export --slug <offer> --out <bundle>` emits every run with
43
+ all its cells.
44
+
45
+ ## Verify
46
+ ```bash
47
+ plexus-cli --env <env> offer plan --dir <bundle> # no `cap:` rows left = the grid matches
48
+ plexus-cli --env <env> offer show --slug <offer> # the live grid
49
+ ```
50
+
51
+ ## Idempotency / gotchas
52
+ - **A listed cell is authoritative** — both `isOpen` and `capacity`, every apply;
53
+ `capacity: null` clears a stored capacity. A cell the bundle does **not** list is left
54
+ alone. A re-apply of an unchanged bundle writes nothing.
55
+ - `capacity` is a whole number ≥ 0, `unlimited`, or `null`. **Unconfigured is not
56
+ unlimited:** an application to an open cell with `null` fails loud.
57
+ - A run with `capacities` must not also carry a run-level `isOpen` (validation error):
58
+ the open state lives on each cell. A NEW run with `capacities` is created closed and
59
+ then opened cell by cell. A run **without** `capacities` keeps the old behaviour — its
60
+ `isOpen` (default open) seeds every cell of a newly created run, and existing cells
61
+ are untouched.
62
+ - **Closing an existing Schuljahr = `isOpen: false` on each of its cells.** A run-level
63
+ `isOpen: false` on an existing run changes nothing and plans no row — a Schuljahr
64
+ "closed" that way stays open; the real close shows one `~cap … open → closed` row per
65
+ cell in the plan.
66
+ - For a real tenant the bundle on disk is a fresh `--env prod` export in a scratch dir,
67
+ never a stale or version-controlled copy — see
68
+ [edit-live-tenant-offer](./edit-live-tenant-offer.md).
69
+ - A cell naming a stage key that neither `program.stages` nor the live program has is
70
+ refused before anything is written (`[capacity/unknownStage]`). A stage the same bundle
71
+ adds plans as `(new)` and is written after the stage exists.
72
+ - `offer run add --capacity` / `offer capacity set` still exist for one-off edits; the
73
+ next `offer apply` of a bundle that lists the cell sets it back. `offer capacity set
74
+ --capacity unlimited` is accepted.
75
+
76
+ ## Related
77
+ - [Rename / remove a program stage](../config/rename-program-stage.md) — the stage keys cells use.
78
+ - [Release an offer bundle](./publish-offer-bundle.md).
@@ -0,0 +1,112 @@
1
+ # Branch on a staff input („Eingabe" + `if` node)
2
+
3
+ **When to use:** the process should take a different path depending on something only
4
+ staff know when they start the application — e.g. a staff member's own child
5
+ („Mitarbeiterbewerbung") skips the review and goes straight to the fee. Two
6
+ pieces: a **staff input** declared on the workflow (`graph.staffInputs`, a checkbox
7
+ staff tick in „+ Bewerbung"), and an **`if` node** whose condition reads it as
8
+ `$env.staffInput.<key>` and routes `true` / `false`. A family never sets a staff input:
9
+ a self-service start records every declared input as `false`.
10
+
11
+ In the workflow IDE the same declaration is the header button „Eingaben" (add an
12
+ input with its key, relabel, remove; the key is fixed once set), and the `if` node is a
13
+ diamond reading „if <pill>": the flow enters on the left, `true` leaves at the top,
14
+ `false` at the bottom.
15
+
16
+ ## Prerequisites
17
+ - A bundle on disk (`offer export`, a live tenant's only into a private scratch dir —
18
+ [Edit a live tenant's offer](./edit-live-tenant-offer.md)). Authoring is offline.
19
+ - The node ids around the branch point: `plexus-cli offer workflow node list --dir <dir>`
20
+ (here: `form` routes `standard` → `review`, and the fee node is `fee`).
21
+ - A key: one identifier segment, lower-case first letter, letters and digits only
22
+ (`staffApplication`, not `staff-application`) — it is read as `$env.staffInput.<key>`.
23
+
24
+ ## Rollout order and checks (a live tenant)
25
+ - **Deploy first, publish second.** `if` is a node type of the release: a
26
+ bundle using it publishes only on a deployment that registers it
27
+ (`[graph/unknownNodeType]` otherwise; the deploy needs a platform operator).
28
+ - **Running applications stay on their version** — the if node changes the
29
+ topology, so nobody is re-pinned; an application already past the form keeps
30
+ its fee step (the waiver „Zahlung überspringen" is the way out for a staff
31
+ child among them).
32
+ - **The Engage push may read the fee step** (`payment: $node.<fee>.payment`): it
33
+ passes the gate (`readSkipsAbsentProducer`) and skips the paid fields for a
34
+ staff child („Zahlungsschritt nicht durchlaufen" in the Übertragungsprotokoll).
35
+ - **Check the assignment rule**: a „+ Bewerbung" start is channel `staff`; a rule
36
+ row on `$env.applicant.channel == 'staff'` (creator responsible) now applies
37
+ to every staff child — decide whether that is wanted before publishing.
38
+ - Edit a live tenant's bundle only through [Edit a live tenant's offer](./edit-live-tenant-offer.md)
39
+ (export into a dated scratch dir, plan, `apply --publish --allow-prod`).
40
+
41
+ ## Steps
42
+ ```bash
43
+ # 1. Declare the staff input (type boolean = a checkbox; both label languages required).
44
+ plexus-cli offer workflow staff-input set staffApplication --dir <dir> \
45
+ --label "Mitarbeiterbewerbung" --label-en "Staff application"
46
+ plexus-cli offer workflow staff-input list --dir <dir>
47
+
48
+ # 2. Put an if node on the form's edge. `true` (a staff child) goes straight to
49
+ # the review, `false` to the fee — name both to be explicit.
50
+ plexus-cli offer workflow node add --dir <dir> --type if \
51
+ --id staffCheck --label "Mitarbeiterbewerbung?" --label-en "Staff application?" \
52
+ --on-edge form:standard \
53
+ --condition '$env.staffInput.staffApplication' \
54
+ --outcomes true:review,false:fee
55
+
56
+ # Later: change the condition or re-route an outcome in place.
57
+ plexus-cli offer workflow node set --dir <dir> --id staffCheck --condition '!$env.staffInput.staffApplication'
58
+ plexus-cli offer workflow node set --dir <dir> --id staffCheck --outcomes true:review,false:fee
59
+
60
+ # 3. Validate, plan, publish.
61
+ plexus-cli offer validate --dir <dir>
62
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir>
63
+ plexus-cli --env staging --tenant <t> offer apply --publish --dir <dir>
64
+ ```
65
+
66
+ ## Verify
67
+ ```bash
68
+ plexus-cli offer workflow node list --dir <dir>
69
+ # form form … standard→staffCheck
70
+ # staffCheck if true, false true→review, false→fee
71
+ jq '.graph.staffInputs, (.graph.nodes[] | select(.type=="if"))' <dir>/workflow.json
72
+
73
+ # After publish: start one test application per answer and see where each stands.
74
+ E="--env staging --tenant <t>"
75
+ plexus-cli $E applications start-test --offer <offer> --run <run> --stage <stage> \
76
+ --applicant test@example.com --staff-input staffApplication=true
77
+ plexus-cli $E applications show <id> # after the form: review (true) / fee (false)
78
+ plexus-cli $E applications store <id> # $env.staffInput.staffApplication = true
79
+ ```
80
+ In the cockpit, „+ Bewerbung" on this offer now shows the checkbox „Mitarbeiterbewerbung".
81
+
82
+ ## Idempotency / gotchas
83
+ - **Every staff start sets every declared input.** The cockpit („+ Bewerbung", with or
84
+ without Stellvertretung), a lead conversion (Interessenten → „In Bewerbung umwandeln")
85
+ and `applications start-test --staff-input key=true|false` (once per input) are refused
86
+ with `[staffInput/missingValue]` when one is left out, `[staffInput/unknownKey]` for a key
87
+ the offer does not declare. A self-service start records each input `false` — there is
88
+ no applicant-callable way to set one.
89
+ - **Read only by a condition (v1).** A SmartForm, a document binding, a task condition or
90
+ an assignment rule cannot read `$env.staffInput.*` — `[address/staffInputConditionOnly]`
91
+ (a form/rule refuses it as an unknown key). Readers: the `if` node's condition, the
92
+ process-data view, the IDE's „Umgebungsvariablen".
93
+ - **`staff-input set` is idempotent by key:** a second call with the same key relabels in
94
+ place (`--label-en` alone re-translates; `--label` alone is refused while it would leave a
95
+ stale English). `staff-input remove <key>` fails loud on an unknown key, and is refused by
96
+ the gate (`[if/condition]`) while a condition still reads the input.
97
+ - **Publish rules:** `[staffInput/keyFormat]`, `[staffInput/duplicateKey]`,
98
+ `[staffInput/unknownType]` (only `boolean`), `[i18n/missingTranslation]` (label without a
99
+ language), `[if/condition]` (no / unparsable condition, or one naming an undeclared input
100
+ or an unknown `$env` leaf), `[if/unroutedOutcome]` (`true` or `false` goes nowhere),
101
+ `[address/envCohortBound]` (a condition on stage/run is refused).
102
+ - **The `if` node never parks** and brings no board column; it decides once, on entry.
103
+ `--condition` is required and never defaulted; it is refused on any other node type.
104
+ - **Running applications keep their version:** the inputs and the branch apply to
105
+ applications started on the newly published version; already-started ones have the
106
+ inputs of the version they pinned.
107
+
108
+ ## Related
109
+ - [Add a workflow node](./add-workflow-node.md) · [Insert a node on one edge and rewire it](./insert-and-rewire-node.md)
110
+ - [Edit a workflow node](./edit-workflow-node.md) (`node set --outcomes`)
111
+ - [Start a test application](../applications/start-a-test-application.md) (`--staff-input`)
112
+ - [Name what a node reads](./addresses.md) (`$env` vocabulary)