@smartkomm/plexus-cli 0.0.0-stage → 0.1.1

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 (82) hide show
  1. package/README.md +863 -2
  2. package/dist/cli.mjs +5432 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +224 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/agents/use-shared-tools.md +52 -0
  7. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  8. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  9. package/dist/skill/references/audit/answer-access-request.md +115 -0
  10. package/dist/skill/references/config/attach-a-template.md +117 -0
  11. package/dist/skill/references/config/init-tenant.md +121 -0
  12. package/dist/skill/references/config/onboard-tenant.md +71 -0
  13. package/dist/skill/references/config/rename-program-stage.md +99 -0
  14. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  15. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  16. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  17. package/dist/skill/references/config/update-tenant-config.md +174 -0
  18. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  19. package/dist/skill/references/export/create-named-export.md +47 -0
  20. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  21. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  22. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  23. package/dist/skill/references/finance/record-submission-date.md +33 -0
  24. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  25. package/dist/skill/references/marketing/demo-video-links.md +101 -0
  26. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  27. package/dist/skill/references/notifications/mail-level.md +90 -0
  28. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  29. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  30. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  31. package/dist/skill/references/offer/addresses.md +129 -0
  32. package/dist/skill/references/offer/appointment-task.md +79 -0
  33. package/dist/skill/references/offer/assignment-rule.md +195 -0
  34. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  35. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  36. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  37. package/dist/skill/references/offer/author-smartform.md +102 -0
  38. package/dist/skill/references/offer/bind-node-input.md +76 -0
  39. package/dist/skill/references/offer/completeness-check.md +137 -0
  40. package/dist/skill/references/offer/countersign-node.md +91 -0
  41. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  42. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  43. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  44. package/dist/skill/references/offer/edit-smartform.md +121 -0
  45. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  46. package/dist/skill/references/offer/engage-push-node.md +147 -0
  47. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  48. package/dist/skill/references/offer/node-tasks.md +162 -0
  49. package/dist/skill/references/offer/optional-assessment.md +65 -0
  50. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  51. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  52. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  53. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  54. package/dist/skill/references/offer/task-family-request.md +69 -0
  55. package/dist/skill/references/offer/task-result-files.md +52 -0
  56. package/dist/skill/references/offer/template-document-node.md +113 -0
  57. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  58. package/dist/skill/references/resolver/manage-contract-resolver.md +161 -0
  59. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  60. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  61. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  62. package/dist/skill/references/signing/README.md +113 -0
  63. package/dist/skill/references/signing/connect-a-school.md +90 -0
  64. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  65. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  66. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  67. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  68. package/dist/skill/references/test-actions/drive-a-test-application.md +69 -0
  69. package/dist/skill/references/users/add-platform-admin.md +59 -0
  70. package/dist/skill/references/users/add-platform-user.md +59 -0
  71. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  72. package/dist/skill/references/users/change-login-email.md +55 -0
  73. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  74. package/dist/skill/references/users/delete-permission-group.md +76 -0
  75. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  76. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  77. package/dist/skill/references/users/set-password.md +57 -0
  78. package/dist/skill/references/users/use-test-account.md +79 -0
  79. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  80. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  81. package/dist/spec.snapshot.json +156821 -0
  82. package/package.json +53 -4
@@ -0,0 +1,195 @@
1
+ # Change an offer's assignment rule (Zuständigkeitsregel)
2
+
3
+ **When to use:** decide who becomes responsible for a new application and who owns
4
+ each team's tasks — for a new offer, after staff changes, or when a workflow starts
5
+ giving work to a team the rule does not cover yet. Also: a staff member a rule names
6
+ is leaving.
7
+
8
+ ## What it is (one paragraph)
9
+
10
+ The assignment rule is a **section of the workflow graph**: `graph.assignmentRules`
11
+ in the offer bundle's `workflow.json`. It is **one ordered rule list per slot**: per
12
+ slot the first row whose condition holds wins, the last row has no condition
13
+ (`"when": null`, shown as „sonst"). It is versioned with the offer version — saved
14
+ with the draft, checked at publish, pinned with the graph. Each new application
15
+ evaluates its pinned graph's rule **once** at its start (only the slots its workflow
16
+ needs) and records the matched row per slot; a published change reaches only new
17
+ applications. Slots: `responsible` („Prozesszuständig" — always a person, always
18
+ **first**) and `team:<teamKey>` for every team the workflow gives work to. Targets:
19
+ `{kind:"person", configKey}` (the staff member's `user_tenant_access.configKey`),
20
+ `{kind:"creator"}` („wer die Bewerbung anlegt" — only in a row whose own `when` is,
21
+ or has as an `&&` conjunct, exactly `$env.applicant.channel == 'staff'`),
22
+ `{kind:"slot", slot:"responsible"}` („die zuständige Person") and `{kind:"queue"}`
23
+ („Team-Queue") — both team slots only —, `{kind:"rotation", configKeys:[…], mode}`
24
+ („Reihum" — any slot: ≥ 2 distinct people, `"strict"` takes turns after the
25
+ rotation's last pick, `"leastLoaded"` takes whoever has the fewest open tasks, ties
26
+ in turn; on a team slot every pool member must be in the team) and
27
+ `{kind:"unassigned"}` („Person wählen"),
28
+ which a draft may hold but publish refuses. The process slot's „sonst" row is always
29
+ a fixed person.
30
+
31
+ ## Prerequisites
32
+
33
+ - `plexus-cli` installed; a token that may apply offers on the tenant (for the
34
+ export/apply steps). `offer validate` and `assignment simulate` run offline.
35
+ - The people are active staff of the tenant and members of any team they are named
36
+ for directly. A person is named by config key: choosing a staff member in the
37
+ editor gives them one if they have none (opaque `staff_<random>`, never derived
38
+ from the name).
39
+
40
+ ## Steps — in the workflow editor
41
+
42
+ 1. Open the offer's workflow in the editor → toolbar „Team-Verantwortliche". One
43
+ table per slot. Rows can be added (they land before „sonst"), removed and moved
44
+ up/down; the condition is built from field / operator / value („Erweitert" for
45
+ the raw DSL); the target picker offers a person, „wer die Bewerbung anlegt",
46
+ „die zuständige Person", „Team-Queue" and „Reihum …" (tick ≥ 2 people — team
47
+ members only on a team slot — and „Streng reihum" or „Wer am wenigsten offen
48
+ hat"). Errors show inline, in German, from the
49
+ same checks publish runs.
50
+ 2. Save and publish the workflow like any other change — the rule goes with it.
51
+ 3. Nothing to keep elsewhere: a live tenant's bundle lives on the server only.
52
+ Anyone who changes the offer later starts from a fresh
53
+ `offer export`, so the rule you published is what they edit.
54
+
55
+ ## Steps — in the bundle
56
+
57
+ 1. Export the live offer into a private scratch dir (a live tenant's bundle is
58
+ never kept in a shared or versioned place — see
59
+ [edit-live-tenant-offer](./edit-live-tenant-offer.md)) and edit
60
+ `workflow.json` → `graph.assignmentRules`:
61
+
62
+ ```bash
63
+ WORK=$(mktemp -d) # any private scratch dir
64
+ plexus-cli --env prod offer export --tenant <slug> --slug <offer-slug> --out $WORK/bundle
65
+ ```
66
+
67
+ ```jsonc
68
+ "assignmentRules": [
69
+ { "slot": "responsible", "rules": [
70
+ { "when": "$env.applicant.channel == 'staff'", "assign": { "kind": "creator" } },
71
+ { "when": "$env.unit.key == 'secondary'",
72
+ "assign": { "kind": "person", "configKey": "admissions_secondary" } },
73
+ { "when": null, "assign": { "kind": "person", "configKey": "aufnahme" } }
74
+ ] },
75
+ { "slot": "team:bewerbungsteam", "rules": [
76
+ { "when": null, "assign": { "kind": "slot", "slot": "responsible" } }
77
+ ] },
78
+ { "slot": "team:principals", "rules": [
79
+ { "when": null, "assign": { "kind": "queue" } }
80
+ ] }
81
+ ]
82
+ ```
83
+
84
+ Every list ends with exactly one `"when": null` row; a row below a constant or a
85
+ repeated condition is refused (`[assignment/shadowedRule]`). `$env.unit.key` is
86
+ the unit's **configKey**, never its name or URL slug. A condition reads the
87
+ application's environment variables (`$env.*`, the same vocabulary as the graph
88
+ and the forms — IDE tab „Umgebungsvariablen"). A variable that can be
89
+ EMPTY — `$env.applicant.language` (no applicant account: started by staff),
90
+ `$env.stage.gradeLevel`, `$env.run.startDate` — needs a `… != null &&` guard
91
+ before `!=` / `!` / an ordering (`[condition/unguardedNullable]`); `==` needs
92
+ none. A bundle still carrying `$ctx.*` (exported before `$env` replaced it) is refused —
93
+ export again.
94
+
95
+ 2. Validate offline — the same checks the editor shows and publish runs
96
+ (`[assignment/slotMissing]`, `…/queueOnProcessSlot`, `…/unguardedTarget`,
97
+ `…/targetMissing`, `[condition/*]`, …):
98
+
99
+ ```bash
100
+ plexus-cli offer validate --dir $WORK/bundle
101
+ ```
102
+
103
+ 3. Test bench — evaluate the bundle's rule over an application's environment
104
+ (`$env`), offline:
105
+
106
+ ```bash
107
+ plexus-cli assignment simulate --dir $WORK/bundle \
108
+ --env-json '{"applicant":{"language":"de","channel":"self-service"},"unit":{"key":"secondary","name":"Secondary"}}'
109
+ # A staff start: no applicant account, so no language (null).
110
+ plexus-cli assignment simulate --dir $WORK/bundle --creator "Sekretariat" \
111
+ --env-json '{"applicant":{"language":null,"channel":"staff"},"unit":{"key":"school_primary","name":"Primary"}}'
112
+ ```
113
+
114
+ Every slot is evaluated; the output names the matched row (`#1`, `#2`, …) per slot.
115
+ A „Reihum" row shows mode, pool, last pick and pick. Offline there is no rotation
116
+ position: pass the last pick with `--rotation-previous team:finanzen=finanzen`
117
+ (none = the rotation's first start), open-task counts for `leastLoaded` with
118
+ `--open finanzen=3 --open finanzen_2=1` (absent = 0), and someone who left with
119
+ `--left <key>`. What a real start picked is in its evaluation record
120
+ („Regel anzeigen" in the cockpit).
121
+ Only the keys the conditions read are needed; a key a condition reads but the
122
+ environment lacks fails loud, like a start.
123
+
124
+ 4. Publish — the DB half of the check runs here (people active staff, a team slot's
125
+ fixed person a member, live unit/offer keys):
126
+
127
+ ```bash
128
+ plexus-cli --env prod offer plan --tenant <slug> --dir $WORK/bundle # only your rule change
129
+ plexus-cli --env prod offer apply --allow-prod --tenant <slug> --dir $WORK/bundle --publish
130
+ ```
131
+
132
+ ## A staff member leaves
133
+
134
+ A person a rule names — as a row's person or in a „Reihum" pool — cannot leave the staff (`users remove`, `users unset-group`,
135
+ `users delete`), leave a team whose slot names them, and a team a rule has a slot
136
+ for cannot be deleted — refused (`[CONFLICT] [assignment/ruleReferences] …`), naming offer
137
+ (label + status), slot and row. The check reads every `wip` and published offer
138
+ version (archived ones only run pinned test starts, which fail loud instead). Order:
139
+
140
+ 1. Workflow editor → „Team-Verantwortliche" → „Person ersetzen": pick the leaving
141
+ person and the successor; the preview shows exactly the rows that change (in a
142
+ pool the successor takes their place; one already in the pool is not doubled).
143
+ 2. Publish the workflow — for every offer the refusal named (the draft counts too).
144
+ 3. Only then remove the group membership:
145
+
146
+ ```bash
147
+ plexus-cli --env staging --tenant <slug> users unset-group ada@example.com --group sekretariat \
148
+ --replace-with <successorConfigKey>
149
+ ```
150
+
151
+ `--replace-with` only hands the person's **active applications** over (required
152
+ while they are responsible for any); it never rewrites a rule.
153
+ 4. Nothing to export or keep elsewhere — the server is the source of truth.
154
+
155
+ ## Verify
156
+
157
+ - `plexus-cli offer validate --dir <bundle>` is clean; `offer status` shows the
158
+ change live (see [Release an offer bundle](./publish-offer-bundle.md)).
159
+ - `plexus-cli offer export` of the live offer → `graph.assignmentRules` equals what
160
+ you meant.
161
+ - In the cockpit, a new application's „Zuständig: …" → „Regel anzeigen" shows one
162
+ table per slot („Prozesszuständig", „Team …"): rows above the deciding one read
163
+ „nicht zutreffend", rows below it „übersprungen"; the deciding row itself carries
164
+ no text label (only its accessible name says it applied).
165
+
166
+ ## Idempotency / gotchas
167
+
168
+ - **Edit a fresh export, never an older copy.** An older copy (an earlier export,
169
+ a backup) can be behind the live offer in some places and ahead in
170
+ others; applying it rolls prod back where it is behind.
171
+ - **A config key like `aufnahme` is a role, not a name.** A school can name its
172
+ admissions person by such a key; which user carries it may differ per deployment
173
+ (prod vs. staging). Handing the role over is `Person ersetzen` or a config key
174
+ move — never a bundle edit.
175
+ - **A person the editor picks is not a config user.** Picking staff created in the
176
+ admin UI draws them a key (`staff_…`) marked `configKeyOrigin: "rule"`;
177
+ `config export` leaves such a person out of `foundation.users`. Declaring the key
178
+ in tenant config later makes them an ordinary config user.
179
+ - **Edits in the editor live only in the DB** until exported — an `offer apply` from
180
+ an older bundle reverts them.
181
+ - **A new workflow** starts with the process slot „Eingangsweg intern → wer die
182
+ Bewerbung anlegt; sonst → Person wählen"; a team slot the rule lacks shows
183
+ „sonst → Team-Queue". Publish refuses until „Person wählen" is replaced.
184
+ - Applications already started keep the rule of the version they are pinned to —
185
+ a published change reaches only new applications.
186
+ - **The bundle is canonical JSON (keys sorted).** When you edit `workflow.json` by
187
+ hand or by script, keep that shape — a re-serialised file differs from a fresh
188
+ export far from your edit — and check the result with `offer validate` and
189
+ `offer plan` (only your rule change should show).
190
+
191
+ ## Related
192
+
193
+ - [Release an offer bundle](./publish-offer-bundle.md) — publishing and checking it went live.
194
+ - [Edit a live tenant's offer](./edit-live-tenant-offer.md) — export, plan, apply.
195
+ - `plexus-cli assignment simulate --help` — the test bench's flags.
@@ -0,0 +1,79 @@
1
+ # Attach a SmartForm to a workflow form node
2
+
3
+ **When to use:** an offer's workflow needs a `form` step — bind an existing
4
+ SmartForm to a new form node, or scaffold a brand-new form and bind it in one
5
+ step. Replaces hand-editing `workflow.json` + `bundle.yaml` + `smartforms/<slug>.json`
6
+ in lockstep.
7
+
8
+ ## Prerequisites
9
+ - A bundle on disk (`offer init` / `offer export`). Authoring is **offline**;
10
+ only `offer apply --publish` is online (needs `--env` + an API token).
11
+ - An installed, current `plexus-cli` (`plexus-cli --help`).
12
+
13
+ ## Steps
14
+ ```bash
15
+ # A. Bind an EXISTING form (created earlier with `offer form add`).
16
+ plexus-cli offer workflow node add --dir <bundle> \
17
+ --type form --form <form-slug> --label "<Step label>" --label-en "<English step label>" --after <nodeId>
18
+
19
+ # B. Scaffold a NEW form AND bind it, in one command.
20
+ # --form is the new form's slug; --label is the node + form label.
21
+ plexus-cli offer workflow node add --dir <bundle> \
22
+ --type form --new-form --form zusatzformular --label "Zusatzangaben" --label-en "Additional details" --after approval
23
+
24
+ # C. CONTINUE the instance an earlier form node created (a second step of the same
25
+ # form, e.g. the Grundschule's `formZusage` after the Zusage): no --form; the node
26
+ # names the instance it continues (single quotes — the shell expands $).
27
+ plexus-cli offer workflow node add --dir <bundle> \
28
+ --type form --id formZusage --label "Angaben nach der Zusage" --label-en "Details after the offer" \
29
+ --after setFlagZusage --adopt-form '$node.form.admission'
30
+
31
+ # Then fill the (new) form out — see Author a SmartForm as code.
32
+ plexus-cli offer form field add --dir <bundle> --form zusatzformular \
33
+ --section section.allgemein --key zusatz.notiz --type string --label "Notiz"
34
+
35
+ # Validate offline against the real publish gate, then publish.
36
+ plexus-cli offer validate --dir <bundle>
37
+ plexus-cli --env <env> --tenant test offer apply --publish --dir <bundle>
38
+ ```
39
+
40
+ The form node is written with `inputs.smartForm = { kind: "literal", value: <slug> }`,
41
+ a default `standard` outcome, and the SmartForm lifecycle progress states
42
+ (`started` / `submitted`) materialised into the process board + wiring automatically.
43
+ The outcome only routes: later nodes read the answers at the node's address,
44
+ `$node.<formNode>.admission.<field>` (see [Name what a node reads](./addresses.md)).
45
+ An adopting node (C) produces nothing: the instance stays its creator's, so every later
46
+ node still reads `$node.<creator>.admission.…`.
47
+
48
+ ## Verify
49
+ ```bash
50
+ plexus-cli offer workflow show --dir <bundle> # the form node + routing appear
51
+ plexus-cli offer form list --dir <bundle> # the bound form shows bound=yes
52
+ plexus-cli offer validate --dir <bundle> # "local validation clean"
53
+ ```
54
+
55
+ ## Idempotency / gotchas
56
+ - **`--type form` requires `--form <slug>`** — bind target (existing) or new slug
57
+ (with `--new-form`) — or `--adopt-form '<instance address>'` (never both). Without
58
+ either, the command fails loud. A bare `--adopt-form form` is refused with the
59
+ addresses visible at the node; an adopting node pointing at a non-creating form is
60
+ `[address/notFormInstance]`.
61
+ - **`--new-form` fails if the slug already exists** (no silent overwrite) and is
62
+ only valid with `--type form`.
63
+ - **Binding a non-existent form fails loud** (`no smartform "<slug>"`) — create it
64
+ with `offer form add` first, or use `--new-form`.
65
+ - **A scaffolded form starts empty** (one section, default outcome). Add fields
66
+ with `offer form field add` before publishing anything user-facing.
67
+ - **Publish pins the form version.** `offer apply --publish` publishes the
68
+ smartform(s) *then* the workflow, so the workflow pins the just-published version.
69
+ Re-publishing the whole bundle after a form edit re-pins it; a standalone form
70
+ publish would leave the workflow on the old version.
71
+ - **`offer workflow node rm`** of the form node leaves the form carried-but-unbound
72
+ (a warning); remove the form too with `offer form rm` if it's no longer needed. It is
73
+ refused while any address still names the node (`$node.<formNode>.admission…` in a
74
+ binding, `reads`, `requires`, a task, `target`, `adoptForm`) — the refusal lists them.
75
+
76
+ ## Related
77
+ - [Add a workflow node](./add-workflow-node.md) — other node types + splice rules.
78
+ - [Author a SmartForm as code](./author-smartform.md) — build the form's contents.
79
+ - `plexus-cli offer --help` — the full `offer …` command reference.
@@ -0,0 +1,97 @@
1
+ # Author a form's test scenarios and environment variables
2
+
3
+ **When to use:** give a SmartForm one-click test-mode prefills (`testScenarios`) or let
4
+ its expressions read where the form is filled in (`env`: `$env.stage.key`, …) —
5
+ without hand-editing the form JSON. `$env` is the ONE root for a fact about the
6
+ application; the graph and the assignment rules read the same variables.
7
+
8
+ ## Prerequisites
9
+ - An offer bundle on disk carrying the form (`offer export …` / `offer init …`).
10
+ - Fully offline; every command runs the publish gate on the result and writes nothing
11
+ when it fails.
12
+
13
+ ## Steps
14
+ ```bash
15
+ # A scenario: id (kebab-case), label, prefill. Values are typed by the FIELD:
16
+ # boolean true|false, a multi-select enum a,b, everything else a string.
17
+ plexus-cli offer form scenario add --dir <bundle> --id standard --label "Standardfall" \
18
+ --label-en "Standard case" \
19
+ --prefill kind.vorname=Anna --prefill kind.nachname=Muster --prefill agb=true
20
+
21
+ # Addresses and repeated rows do not fit key=value — give the whole prefill as JSON:
22
+ plexus-cli offer form scenario add --dir <bundle> --id geschwister --label "Mit Geschwistern" --label-en "With siblings" \
23
+ --prefill-json '{"kind.vorname":"Anna","geschwister":[{"name":"Ben"}]}'
24
+
25
+ # A variant of an existing scenario: --from copies its prefill and environment sample,
26
+ # --prefill overrides single keys (e.g. „Gemeinsames Sorgerecht“ + reduced-fee request).
27
+ plexus-cli offer form scenario add --dir <bundle> --id joint-reduced-fee --from joint \
28
+ --label "Gemeinsames Sorgerecht mit Gebührenreduzierung" --label-en "Joint custody with reduced fee" \
29
+ --prefill elternteil_1.financialAid=true
30
+
31
+ # Edit: --prefill MERGES, --prefill-json REPLACES, --unset removes keys.
32
+ plexus-cli offer form scenario set --dir <bundle> --id standard --prefill kind.vorname=Lea --unset agb
33
+ plexus-cli offer form scenario rm --dir <bundle> --id geschwister
34
+
35
+ # Environment: declare the keys the form reads AND sample them for every scenario in ONE
36
+ # step (the gate refuses a declared key a scenario does not sample, and a sample of an
37
+ # undeclared key — so the two cannot be done one after the other).
38
+ plexus-cli offer form env set --dir <bundle> --keys stage \
39
+ --sample 'standard={"stage":{"key":"klasse-1","name":"Klasse 1","gradeLevel":1}}'
40
+
41
+ # The applicant's language and the intake channel are ONE key, `applicant`:
42
+ plexus-cli offer form env set --dir <bundle> --keys stage,applicant \
43
+ --sample 'standard={"stage":{"key":"klasse-1","name":"Klasse 1","gradeLevel":1},"applicant":{"language":"de","channel":"self-service"}}'
44
+
45
+ # Remove the declaration (the scenarios' samples of dropped keys go with it):
46
+ plexus-cli offer form env set --dir <bundle> --keys ""
47
+ ```
48
+
49
+ Environment keys: `offer`, `unit`, `stage`, `run`, `applicant` — the one vocabulary,
50
+ listed with labels in the IDE tab „Umgebungsvariablen".
51
+ A sample carries every field of its key (`stage` = `key`, `name`, `gradeLevel`;
52
+ `applicant` = `language`, `channel`), an empty one as `null`. `stage.key` is the
53
+ BUNDLE key (`program.stages[].key`).
54
+
55
+ **No fallback.** `$env.applicant.language` is EMPTY (`null`) for an application
56
+ without an applicant account (started by staff); `$env.stage.gradeLevel` and
57
+ `$env.run.startDate` can be empty too. `==` is false over null and needs nothing; `!=`
58
+ and `!` are TRUE over null, so the gate refuses them without a guard
59
+ (`[env/unguardedNullable]`) — write what an empty value means:
60
+ `$env.applicant.language != null && $env.applicant.language != 'de'`. The names
61
+ (`offer.title`, `unit.name`, `stage.name`, `run.name`) are in the applicant's language,
62
+ German without an applicant account (declared at the variable).
63
+
64
+ ## Verify
65
+ ```bash
66
+ plexus-cli offer validate --dir <bundle> # scenarios are submitted through the real submit gate
67
+ plexus-cli offer form show --dir <bundle>
68
+ ```
69
+
70
+ After publishing, run a scenario on a test application from the terminal:
71
+ `plexus-cli --env staging --tenant <t> test-actions run --application <id> --action load-scenario:<scenarioId>`
72
+ — see [Drive a test application through test mode](../test-actions/drive-a-test-application.md).
73
+
74
+ ## Idempotency / gotchas
75
+ - A scenario that leaves a visible required field empty, or reaches no outcome, is
76
+ refused (`scenario.required-field-unfilled` / `scenario.no-outcome`). docRef fields are
77
+ never prefilled — test mode stubs files itself.
78
+ - A bundle exported before `$env` replaced `context` carries `context` (declaration and samples) and
79
+ `$ctx.*` expressions: refused (`[env/retiredField]`, unknown identifier). Export
80
+ again — the stored definitions on the server already use `$env`.
81
+ - `--from` copies prefill and environment sample, not the base's description (it describes the base);
82
+ `--from` with `--prefill-json` is refused — that would discard the copy. The copy is a
83
+ snapshot: a later change to the base does not reach the variant.
84
+ - File fields need no prefill: with „Automatisch absenden“ (default on) loading a scenario
85
+ fills every docRef field of the form's sections and groups with an openable stub file
86
+ (named after the field, of a type the field accepts) and then submits — also the fields
87
+ the scenario only makes visible (income proofs behind `financialAid=true`). The fill runs
88
+ as a scheduled action, so the submit lands a moment later. Without auto-submit, run the
89
+ test action „File-Felder mit Dummy füllen“ (`auto-fill-files`) yourself. A docRef inside
90
+ a repeated block is not filled.
91
+ - `--prefill` names an unknown key → refused; so does a docRef or an address key (use
92
+ `--prefill-json` for the address).
93
+ - Label and description need their English in the same call (`--label-en`,
94
+ `--description-en`); see [Edit a SmartForm in depth](./edit-smartform.md) → English texts.
95
+
96
+ ## Related
97
+ - [Author a SmartForm as code](./author-smartform.md) · [Edit a SmartForm in depth](./edit-smartform.md)
@@ -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)