@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,69 @@
1
+ # Make a task close with an appointment („Termin vereinbaren")
2
+
3
+ **Use it to** turn a workflow-step task such as „Termin für das Kennenlerngespräch
4
+ vereinbaren" into one that is completed by entering an appointment: date,
5
+ time, duration, place and host. Completing it books the appointment at the
6
+ application, mails the family (portal owner) date/time/place with a `termin.ics`
7
+ calendar file, frees the task it
8
+ `blocks`, and lists the appointment under „Anstehende Termine" on the staff dashboard.
9
+
10
+ ## The declaration
11
+
12
+ On the task (a composed `kind: "task"` in the node's `tasks`):
13
+
14
+ ```json
15
+ {
16
+ "key": "termin",
17
+ "kind": "task",
18
+ "title": { "de": "Termin für das Kennenlerngespräch vereinbaren", "en": "…" },
19
+ "assign": { "group": "schulverwaltung" },
20
+ "blocks": { "templateKey": "gespraech" },
21
+ "result": {
22
+ "kind": "appointment",
23
+ "title": { "de": "Kennenlerngespräch", "en": "Introductory talk" },
24
+ "durationMinutes": 45
25
+ }
26
+ }
27
+ ```
28
+
29
+ - `result.title` is the **neutral** title the family's letter and every calendar file
30
+ carry — never a family name.
31
+ - `durationMinutes` (5–480) prefills the dialog; staff may change it per appointment.
32
+ - Publish refuses: an unknown `kind` (`[taskResult/unknownKind]`), a result on an
33
+ approval (`[taskResult/onApproval]`), a title missing `de` or `en`
34
+ (`[taskResult/appointmentMissingTitle]`), a bad duration
35
+ (`[taskResult/appointmentInvalidDuration]`), a stray field
36
+ (`[taskResult/unknownField]`). `offer validate` catches all of them offline.
37
+
38
+ ## With the CLI
39
+
40
+ One spelling, the same verb as every other task result — no `:`-separated
41
+ free text, so a title may contain a colon:
42
+
43
+ ```bash
44
+ plexus-cli offer workflow node task add --dir $WORK/$S --node kennenlernen --key termin \
45
+ --title 'Termin für das Kennenlerngespräch vereinbaren' --team schulverwaltung --due-in-days 3 \
46
+ --blocks gespraech \
47
+ --result appointment --duration 45 \
48
+ --result-title 'Kennenlerngespräch' --result-title-en 'Introductory talk'
49
+ # change later: node task set … --duration 60 / --result-title '…'
50
+ plexus-cli offer workflow node list --dir $WORK/$S
51
+ ```
52
+
53
+ Then the usual live-tenant path ([Edit a live tenant's offer](./edit-live-tenant-offer.md)):
54
+ `offer validate` → `--env prod offer plan` → `--env prod offer apply --allow-prod --publish` → plan
55
+ again until every row is `same`.
56
+
57
+ ## Gotchas
58
+
59
+ - **Running applications keep their pinned graph.** An application already sitting on
60
+ the node with an open `termin` row keeps its old declaration until it is re-pinned;
61
+ new entries into the node get „Termin vereinbaren" in the action bar.
62
+ - **Staff books it from the action bar** („Termin vereinbaren"), like every task; the
63
+ dialog names every missing field, refuses a slot more than two years ahead, and
64
+ closes with a toast. In the Testmodus the shortcut „<Aufgabe> (Test): Termin morgen
65
+ 10:00" books through the same verb.
66
+ - **No portal owner, no mail.** An application staff created has no portal account; the
67
+ appointment is saved and the toast warns „Keine Mail an die Familie …". The same
68
+ happens when the school's outgoing mail is off.
69
+ - The zone is the tenant's (`Europe/Berlin` for every DACH tenant today).
@@ -0,0 +1,69 @@
1
+ # Let a task ask the family and wait for its answer („Nachforderung")
2
+
3
+ **Use it to** turn „Nachforderung: auf Unterlagen der Familie warten" from a task the
4
+ team closes by hand into one that asks the family itself and completes itself the
5
+ moment the family sends. The family gets the field in its task „Offene Felder beantworten" in the portal
6
+ (the task letter `applicant.tasksOpen`, the task's deadline, a reminder before it); the
7
+ team's task waits and holds the decision until then. Nobody ticks it off.
8
+
9
+ ## The declaration
10
+
11
+ On a task that a `spawnTasks` optional action references (and only there; at most
12
+ one such task per action):
13
+
14
+ ```json
15
+ {
16
+ "key": "unterlagenAnfordern",
17
+ "kind": "task",
18
+ "title": { "de": "Nachforderung: auf Unterlagen der Familie warten", "en": "Follow-up: waiting for the family's documents" },
19
+ "assign": { "group": "bewerbungsteam" },
20
+ "params": { "dueInDays": 7 },
21
+ "familyRequest": {
22
+ "fields": [{ "scope": "node", "owner": "form", "key": "admission", "path": "kind.weitereUnterlagen" }]
23
+ }
24
+ }
25
+ ```
26
+
27
+ - **What is asked is chosen at the click.** Pressing the action opens a dialog: the
28
+ presser ticks the fields and writes the text the family reads. The text becomes the
29
+ note at each asked field — in the portal and in the task letter.
30
+ - Exactly one of `fields` (the fields offered, each a full address
31
+ `$node.<formNode>.admission.<field>` — one form) or `from` (the form instance
32
+ `$node.<formNode>.admission`: every file field (`docRef`) of that form is offered). There
33
+ is no default form: an empty `familyRequest` is refused. A **free upload** is a `docRef` field
34
+ with `docRefConstraints: { multiple: true }` („Weitere Unterlagen") — the field must
35
+ exist in the application's form version.
36
+ - `params.dueInDays` is the family's deadline and the task's.
37
+ - Put the action into the decision's `requiresOneOf` if the decision should wait for it.
38
+
39
+ ## CLI
40
+
41
+ ```bash
42
+ plexus-cli offer workflow node task set --dir <dir> --node decision --key unterlagenAnfordern \
43
+ --due-in-days 7 \
44
+ --family-request-fields '$node.form.admission.kind.weitereUnterlagen'
45
+ # or --family-request-from '$node.form.admission': every file field of that form
46
+ plexus-cli offer validate --dir <dir>
47
+ ```
48
+
49
+ Remove with `--clear-family-request`. The task must not carry `result`, `when`,
50
+ `locksEvidence` or `blocks` (and nothing may block it) — `offer validate` names each.
51
+
52
+ ## Verify
53
+
54
+ 1. Cockpit: press the action → the dialog offers the fields; tick one, write the text,
55
+ „Anfordern" → the task shows the clock (system-closed), the decision buttons read
56
+ „Wartet auf: …".
57
+ 2. Portal `/e`: „Offene Felder beantworten" (field under „Nachzureichen"), the text at the field, „bis <Datum>".
58
+ 3. Upload and send as the family → the task is `done`, the decision buttons are free.
59
+
60
+ ## Gotchas
61
+
62
+ - A field already under an open request, hidden, or printed into a held document is not
63
+ offered; the trigger refuses it with the reason, and nothing is spawned.
64
+ - An empty or unchanged answer answers nothing (the same file again is no new document):
65
+ the field stays asked.
66
+ - Withdrawing the request („Zurückziehen") cancels the task; the decision stays held by
67
+ `requiresOneOf` until another action completes.
68
+ - Rows of applications on an OLDER offer version keep the old task; only applications
69
+ pinned to the published version get the family request.
@@ -0,0 +1,52 @@
1
+ # Let a task's result carry files
2
+
3
+ **Use it to** let the person who completes a task attach files to their answer. Example: the
4
+ teacher's trial-day feedback with an English and a maths placement sheet, as PDF or photo.
5
+
6
+ ## The model
7
+
8
+ - **One switch on the task:** `result.files: true`. It is allowed only on prose results:
9
+ `freetext`, `recommendation`, or a `choice` without `writes`. Publish refuses it anywhere
10
+ else (`[taskResult/filesOnKind]`).
11
+ - **Text and files are ONE message** in the chat the result names (`result.chat`). The row
12
+ keeps no text, and the chat keeps, shows and erases the files (GDPR erasure included).
13
+ - **Up to 20 files per result**.
14
+ A normal chat message takes 5. Each file may be PDF, JPEG, PNG or WebP of up to 25 MB;
15
+ an iPhone HEIC photo is converted to JPEG in the browser.
16
+ - **Where the files show:** in the result's message — cockpit → Verlauf/Aktivitäten (the
17
+ chat's tab); a click opens the file in the viewer. The task itself shows at most how
18
+ many files its result carries. Never in the „Dokumente" rail:
19
+ that lists the application's documents, not task attachments. Nothing is
20
+ copied, so deleting the message removes its files.
21
+ - **Who sees them:** whoever reads the feedback text — the SAME chat rule. In a
22
+ `team:<group>` chat that means the members of that group plus platform admins. A
23
+ `tenant_admin` who is in no such group sees neither the text nor the files. Before
24
+ choosing `team:principals`, check that the readers really hold that group (`plexus-cli
25
+ users show <email>`).
26
+
27
+ ## CLI
28
+
29
+ Work in a scratch dir for a real tenant
30
+ ([edit-live-tenant-offer.md](./edit-live-tenant-offer.md)).
31
+
32
+ ```bash
33
+ plexus-cli --env prod --tenant <slug> offer export --slug <offer> --out <dir>
34
+ plexus-cli offer workflow node task set --dir <dir> --node decision --key tasterFeedback --files
35
+ plexus-cli offer validate --dir <dir>
36
+ plexus-cli --env prod --tenant <slug> offer plan --dir <dir> # workflow: change
37
+ # apply only with the school's go (the process owner):
38
+ plexus-cli --env prod --tenant <slug> offer apply --dir <dir> --publish --allow-prod
39
+ ```
40
+
41
+ A new task: `node task add … --result freetext --chat <chat> --files`.
42
+
43
+ ## Verify
44
+
45
+ 1. Open the task in the cockpit. The dialog shows „Dateien (optional)" with the paperclip,
46
+ which accepts several files at once.
47
+ 2. Complete it with two files. Verlauf, tab of the result's chat: one message with the
48
+ text and both files.
49
+ 3. „Dokumente": neither file is listed there.
50
+ 4. As staff outside the chat's team: neither the text nor the files show in Verlauf.
51
+
52
+ Test tenant: Grundschule → „Probetag auswerten" (Schulleitung) takes files.
@@ -0,0 +1,113 @@
1
+ # Generate a letter from a Word template (governedDocument `production: 'template'`)
2
+
3
+ **Use it to** let a workflow document node (Platzangebot, Bestätigung, Bescheinigung) fill a tenant Word
4
+ template and render the PDF automatically — no resolver, no deploy.
5
+
6
+ ## Prerequisites
7
+
8
+ - The Word template is uploaded in the tenant's template module (Vorlagen). Its placeholders use
9
+ `<< input.<feld> >>` (sections `<<#input.<feld>>> … <</input.<feld>>>`).
10
+ - **Exactly one language per letter (required):** every template node declares a
11
+ required enum field (options `de`, `en`), names it in `template.localeField`, binds it to
12
+ `$env.applicant.language` (the applicant's language — the default) and wraps each language's
13
+ text in a section `<<#input.<feld>.de>> … <</input.<feld>.de>>` /
14
+ `<<#input.<feld>.en>> … <</input.<feld>.en>>`. Exactly one section renders, and `input.*` prints
15
+ the way that language writes it (numbers, dates, Ja/Yes, prices, names). A node without
16
+ `localeField` is refused at publish (`[governedDocument/localeFieldMissing]`); a missing section
17
+ blocks publish and file replacement. The English twin `en.input.*` of the old fixed bilingual
18
+ letters is gone — a file naming it is `[governedDocument/unknownPlaceholder]`. A German-only
19
+ school declares one option `de` and binds the field to the fixed value `de`.
20
+ - **Switching a LIVE template to language sections — upload it under a NEW key.** A file
21
+ replacement is checked against EVERY published node naming the key, archived versions included
22
+ (`publishedNodesFillingTemplate` — they still carry running applications), and a node published
23
+ without `localeField` accepts no file at all. So the old key stays with the old versions for
24
+ good; the sectioned file goes in as a new template:
25
+ 1. upload the sectioned Word file as a new template and give it a new key (§ 1, e.g.
26
+ `platzangebot-v2`);
27
+ 2. in the exported bundle point `template.key` at it, add the field `sprache` to
28
+ `template.fields`, set `template.localeField: 'sprache'` and bind it:
29
+ `plexus-cli offer workflow node set-input --dir <bundle> --node <id> --input sprache --address '$env.applicant.language'`
30
+ ([Bind a node input](./bind-node-input.md));
31
+ 3. `offer validate` → `plan` → `apply --publish`.
32
+ Applications pinned to the old version produce no letter any more (the mask names
33
+ `localeFieldMissing`); a re-pin onto the new version is refused, because the new binding changes
34
+ the process topology — restart such a (test) application on the new version.
35
+ - The deployment already runs a release that knows template nodes (validators first, then
36
+ publish); if publish refuses the node type, that needs a platform operator.
37
+
38
+ ## 1. Give the template its stable key
39
+
40
+ There is no UI field yet — set it through the public mutation (the same `updateMetadata` the UI calls,
41
+ so name / role / file-name pattern must be passed as they are):
42
+
43
+ ```bash
44
+ plexus-cli call templates:list --env staging client=<slug> --json # id, name, roleId
45
+ plexus-cli call templates:updateMetadata --kind mutation --env staging client=<slug> \
46
+ templateId=<id> name='Platzangebot' roleId=<roleId> configKey=platzangebot --json
47
+ ```
48
+
49
+ The key is unique per tenant and cannot move while a published process names it.
50
+
51
+ ## 2. Author the node in the bundle (JSON)
52
+
53
+ `plexus-cli offer workflow node add` cannot create a governedDocument node; edit `workflow.json`
54
+ (export → edit → plan → apply, see [Edit a live tenant's offer](./edit-live-tenant-offer.md)):
55
+
56
+ ```jsonc
57
+ {
58
+ "id": "platzangebot", "type": "governedDocument", "label": "Platzangebot",
59
+ "production": "template", "approval": "release", "responseStrategy": "confirm",
60
+ "responseDueInDays": 14,
61
+ "template": {
62
+ "key": "platzangebot",
63
+ "fields": [
64
+ { "key": "kind_vorname", "label": "Vorname des Kindes", "type": "string", "required": true },
65
+ { "key": "datum", "label": "Datum", "type": "date", "processSource": "generatedAt" },
66
+ { "key": "antwortfrist", "label": "Antwortfrist", "type": "number", "processSource": "responseDueInDays" },
67
+ { "key": "sprache", "label": { "de": "Sprache des Schreibens", "en": "Language of the letter" },
68
+ "type": "enum", "required": true,
69
+ "options": [
70
+ { "value": "de", "label": { "de": "Deutsch", "en": "German" } },
71
+ { "value": "en", "label": { "de": "Englisch", "en": "English" } }
72
+ ] }
73
+ ],
74
+ "cockpitFields": [],
75
+ "localeField": "sprache"
76
+ },
77
+ "inputs": {
78
+ "kind_vorname": {
79
+ "kind": "binding",
80
+ "address": { "scope": "node", "owner": "form", "key": "admission", "path": "kind.vorname" }
81
+ },
82
+ "sprache": { "kind": "binding", "address": { "scope": "env", "key": "applicant.language" } }
83
+ }
84
+ }
85
+ ```
86
+
87
+ A bound form field must be exported by the form (`export: true`) and is named by its full address
88
+ (`$node.form.admission.kind.vorname` — bind it with
89
+ `plexus-cli offer workflow node set-input --node platzangebot --input kind_vorname --from form --field kind.vorname`);
90
+ a form that does not run before the node is `[address/notVisible]`, a field it does not export
91
+ `[address/pathNotInTarget]`. An old bundle (`wires`, a binding with `path`) is refused
92
+ — export it again. The deadline alone can be changed
93
+ with `plexus-cli offer workflow node set --id platzangebot --response-due-days 14` (`""` removes it).
94
+
95
+ ## 3. Validate, plan, publish
96
+
97
+ `plexus-cli offer validate <dir>` checks the node shape offline (including the language field,
98
+ `[governedDocument/localeFieldMissing]`). **Publish** additionally refuses a
99
+ key no template carries (`[governedDocument/unknownTemplate]`) and any placeholder the fields cannot
100
+ fill (`[governedDocument/unknownPlaceholder]`).
101
+
102
+ ## Verify
103
+
104
+ An application entering the node gets a run in `contract_generation_runs` (producer: the template)
105
+ and a PDF version on the node's slot within seconds; the cockpit's „Überarbeiten" opens the mask.
106
+
107
+ ## Gotchas
108
+
109
+ - Generation is automatic only while the producer row is open and unblocked; after „Freigabe
110
+ anfordern" nothing regenerates. A changed template file is caught at submit („nicht mehr aktuell").
111
+ - Replacing the template file is refused while a published node could not fill its placeholders.
112
+ - **CLI gap:** the `template` pin (key, fields, cockpitFields) has no `offer workflow node` verb yet —
113
+ JSON only.
@@ -0,0 +1,69 @@
1
+ # Author a workflow's default chat channels
2
+
3
+ **When to use:** every application of an offer should start with a chat channel
4
+ for a team combination (e.g. „Bewerbungsteam + Schulleitung"), in addition to the
5
+ automatic channel each participating team already gets.
6
+
7
+ Two kinds of channel exist in every application of a published version:
8
+
9
+ | kind | where it comes from | editable? |
10
+ |---|---|---|
11
+ | automatic | one per team the process gives work to (`assign`, composed tasks, …) | no — follows the graph |
12
+ | default | `graph.channels` in `workflow.json` | yes — this playbook |
13
+
14
+ Both are read from the application's **pinned** workflow version at chat time. No
15
+ row is written per application, so a new default channel reaches applications
16
+ started on the next published version; running applications keep the channels
17
+ of their version.
18
+
19
+ ## Prerequisites
20
+ - An offer bundle on disk with a `workflow.json` (`offer export …` or `offer init …`).
21
+ - Authoring is offline; `offer apply --publish` needs a token with the tenant's offer-configuration rights (a tenant admin has them).
22
+ - Members are **permission-group keys** of the tenant (`plexus-cli --env <env> --tenant <t> users groups`
23
+ → `key`); the applicant group is not a team.
24
+
25
+ ## Steps
26
+ ```bash
27
+ # What exists: automatic (read-only) + authored default channels.
28
+ plexus-cli offer workflow channels list --dir <bundle>
29
+
30
+ # Add one — the key is minted from the German label ONCE (here:
31
+ # bewerbungsteam-schulleitung). The label is German AND English.
32
+ plexus-cli offer workflow channels add --dir <bundle> \
33
+ --label "Bewerbungsteam + Schulleitung" --label-en "Admissions team + school leadership" \
34
+ --team bewerbungsteam --team schulleitung
35
+
36
+ # Rename / re-staff in place — pass the key; a rename never duplicates. A German
37
+ # rename always needs --label-en (the English is never copied from the German,
38
+ # and a channel name without English is refused).
39
+ plexus-cli offer workflow channels add --dir <bundle> --key bewerbungsteam-schulleitung \
40
+ --label "Aufnahme" --label-en "Admissions" --team bewerbungsteam --team schulleitung
41
+
42
+ # Remove an authored one by key (verb: `channels rm`).
43
+
44
+ # Release it.
45
+ plexus-cli offer validate --dir <bundle>
46
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
47
+ ```
48
+
49
+ The same section is editable in the workflow IDE: „Team-Verantwortliche" → tab
50
+ „Kanäle" → „Kanal hinzufügen" (the same team picker as the cockpit chat's „+").
51
+
52
+ ## Verify
53
+ - `offer workflow channels list` shows the entry with source `Standardkanal`.
54
+ - After publish, start an application (e.g. convert an Interessent) and open its
55
+ cockpit: the chat rail carries a tab with the authored label.
56
+
57
+ ## Gotchas
58
+ - **A single team the process already assigns is refused** (`[channels/duplicatesDerived]`)
59
+ — it has its automatic channel.
60
+ - **Unknown team** (`[channels/unknownTeam]`) is only caught at `offer apply --publish`
61
+ (the offline validator has no tenant); the applicant group counts as unknown.
62
+ - A channel's chat identity is its **team set**: a manual „+" channel on the same
63
+ teams is the same thread, and the authored label wins.
64
+ - A permission group named by a default channel cannot be deleted while any
65
+ stored workflow version names it (`referenced-by-workflow`).
66
+
67
+ ## Related
68
+ - [Edit a workflow node](./edit-workflow-node.md)
69
+ - `plexus-cli offer workflow channels add --help` · `channels list` · `channels rm --key <key>`
@@ -0,0 +1,159 @@
1
+ # Manage a contract generator
2
+
3
+ **When to use:** Create or revise a tenant's TypeScript contract generator — or a combined generator over several of them — review scenarios, publish it, and pin an offer to that revision.
4
+
5
+ ## Prerequisites
6
+
7
+ - A configured environment and tenant, with `finanzen_vertraege.contracts.manage`.
8
+ - A contract template with an attached DOCX. Use `generator template upload` if needed.
9
+ - A real tenant's source and configuration live in that tenant's data; work on them in a private scratch directory.
10
+
11
+ ## Steps
12
+
13
+ Create a blank draft, or copy a concrete published revision:
14
+
15
+ ```bash
16
+ plexus-cli --env local --tenant test generator template list
17
+ plexus-cli --env local --tenant test generator create --key school-contract --name "Schulvertrag" --name-en "School contract" --contract-template-id <id>
18
+ plexus-cli --env local --tenant test generator clone school-contract@2
19
+ plexus-cli --env local --tenant test generator scaffold school-contract --out /tmp/generator.ts
20
+ ```
21
+
22
+ For an existing source, export both the TypeScript and its manifest. Edit the source and the explicit article/grant ID lists in the manifest. Saving checks the exported revision number and refuses concurrent changes.
23
+
24
+ ```bash
25
+ plexus-cli --env local --tenant test generator source export school-contract@3 --out /tmp/generator.ts
26
+ plexus-cli --env local --tenant test generator source save school-contract@3 --file /tmp/generator.ts --manifest /tmp/generator.ts.json
27
+ plexus-cli --env local --tenant test generator scenarios test school-contract@3 --input /tmp/input.json --json
28
+ ```
29
+
30
+ A blank draft can use a manifest containing `id`, `expectedSourceRevision: 0`, `billingItemDefinitionIds` and `grantTemplateIds`. Read its ID with `generator show`. Source must be self-contained and export one default object with `inputSchema`, `parties`, `resolve(config, input)`, `previewDocument(config, input)` and `signatureSpots(config, input)`. Imports and host capabilities are unavailable. The metadata describes conditional fields; the functions may use arbitrary synchronous TypeScript logic.
31
+
32
+ Review the result, including its signature declarations. Write `/tmp/reviewed.json` with the returned `sourceRevision` as `expectedSourceRevision`, `artifactHash`, and `scenarios: [{ key, input, expected }]`. `expected` is the entire returned `output`, including `contract` and `signatureSpots`. Keep explicit expected results: a test command never approves its observations automatically.
33
+
34
+ ```bash
35
+ plexus-cli --env local --tenant test generator scenarios approve school-contract@3 --file /tmp/reviewed.json
36
+ plexus-cli --env local --tenant test generator seal school-contract@3
37
+ plexus-cli --env local --tenant test generator assign school-contract@3 --offer-version-id <wip-id> --node contract
38
+ ```
39
+
40
+ Publish the WIP offer through the normal offer workflow. Existing applications retain their pinned generator revision. Archiving a revision hides it from new selections while existing pins remain executable.
41
+
42
+ For one-file export/import on the same environment:
43
+
44
+ ```bash
45
+ plexus-cli --env local --tenant test generator export school-contract@3 --out /tmp/generator.json
46
+ plexus-cli --env local --tenant test generator apply /tmp/generator.json
47
+ ```
48
+
49
+ The export includes source, manifests and reviewed scenarios. Apply saves a draft and checks imported expected results; it does not publish. IDs in this export belong to its environment.
50
+
51
+ ### Offer setup by stable catalog keys
52
+
53
+ The existing `contract/resolver-setup.json` filename and `resolver` property remain the bundle format. A source-backed setup adds these fields under `resolver`:
54
+
55
+ - `source`: a TypeScript file relative to the setup file.
56
+ - `billingItemKeys`: every article the generator may emit, including conditional branches.
57
+ - `grantTemplateKeys`: every grant template it may use. These must already exist in the catalog.
58
+ - `scenarios`: reviewed `{ key, input, expected }` cases. A new revision cannot publish without them.
59
+
60
+ Config and scenario values can use `{ "$billingItem": "article-key" }` and `{ "$grantTemplate": "grant-key" }` to reference declared entries. Prices retain the existing `{ "$config": "preise.key" }` reference. Template names and generator names are `{ de, en }`; `documentPlaceholderKeys` must match the DOCX.
61
+
62
+ ```bash
63
+ plexus-cli --env local --tenant test generator setup plan --file /tmp/offer/contract/resolver-setup.json
64
+ plexus-cli --env local --tenant test generator setup apply --file /tmp/offer/contract/resolver-setup.json
65
+ plexus-cli --env local --tenant test offer plan --dir /tmp/offer
66
+ ```
67
+
68
+ A setup that still names `implementation` can only copy a source already stored in that tenant's database. Ambiguous keys require cloning a concrete revision. New generators need no platform release.
69
+
70
+ ## Combined generator
71
+
72
+ A combined generator is a declaration, not code: one input mask and ordered parts, each pinning a **sealed** document revision by `key@revision`, mapping every part field to a mask field, an expression or a fixed value, optionally running only `when` an expression holds. It has no source, template, manifest or scenarios.
73
+
74
+ Seal the part revisions first (the steps above). Then write the declaration file — easiest by exporting an existing combined revision (the test tenant has `school-with-care`) and editing it. The shape (abridged: a real part maps every field its generator requires — the server names each one missing):
75
+
76
+ ```bash
77
+ plexus-cli --env local --tenant test generator combined export school-with-care@1 --out /tmp/combined.json
78
+ ```
79
+
80
+ ```json
81
+ {
82
+ "formatVersion": 1,
83
+ "kind": "combined",
84
+ "key": "school-with-lunch",
85
+ "name": { "de": "Schulvertrag mit Mittagessen", "en": "School contract with lunch" },
86
+ "description": { "de": "", "en": "" },
87
+ "inputSchema": { "fields": [
88
+ { "key": "startDate", "label": { "de": "Beginn", "en": "Start" }, "type": "date", "required": true },
89
+ { "key": "ganztag", "label": { "de": "Mit Hort", "en": "With after-school care" }, "type": "boolean", "defaultValue": false },
90
+ { "key": "lateCare", "label": { "de": "Spätbetreuung", "en": "Late care" }, "type": "boolean", "defaultValue": false, "visibleIf": "ganztag == true" }
91
+ ] },
92
+ "parts": [
93
+ { "partKey": "school", "generator": "school-contract@1",
94
+ "inputs": [ { "field": "startDate", "source": { "from": "field", "field": "startDate" } },
95
+ { "field": "admissionFeeMode", "source": { "from": "fixed", "value": "standard" } } ] },
96
+ { "partKey": "care", "generator": "after-school-care@1", "when": "ganztag == true",
97
+ "inputs": [ { "field": "pickupTime", "source": { "from": "expression", "expression": "if (lateCare == true) '18:00' else '16:00'" } } ] }
98
+ ]
99
+ }
100
+ ```
101
+
102
+ A file **without `id`** creates the draft of a new key and saves the declaration in one step; a refused declaration removes that empty draft again, so fix the file and save once more. A file **with `id`** (every export) targets exactly that revision, guarded by its `expectedSourceRevision`:
103
+
104
+ ```bash
105
+ plexus-cli --env local --tenant test generator combined save /tmp/combined.json
106
+ plexus-cli --env local --tenant test generator combined plan school-with-lunch@1 --input /tmp/mask-input.json
107
+ plexus-cli --env local --tenant test generator seal school-with-lunch@1
108
+ ```
109
+
110
+ To revise a sealed combined revision, clone it, export the draft, edit, save, seal:
111
+
112
+ ```bash
113
+ plexus-cli --env local --tenant test generator clone school-with-care@1
114
+ plexus-cli --env local --tenant test generator combined export school-with-care@2 --out /tmp/combined.json
115
+ plexus-cli --env local --tenant test generator combined save /tmp/combined.json
116
+ plexus-cli --env local --tenant test generator seal school-with-care@2
117
+ ```
118
+
119
+ `generator combined create --key … --name … --name-en …` makes an empty draft (no mask, no parts) when you prefer to start from the workspace's editor. `plan` is the sandbox's „Positionen“ tab: which parts run for a mask input, the fields each still misses, and its roster issues; a refused input (a `when` that is no boolean) exits with the refusal. The PDFs per part come from the workspace.
120
+
121
+ Pin it on a contract node like a document generator: the node's `resolver: { key, revision }` in the bundle (or `generator assign`), the reserved Träger as the node literal `inputs.traeger`, then `offer apply --publish`. One „Erzeugen“ makes one contract per running part (a generation group), issued and signed together in one Vorgang.
122
+
123
+ - **No change, no write.** `save` of an unedited export prints `outcome: unchanged` and writes nothing; only the names changed → `renamed` (names are renamable at any status); otherwise `saved` (a new `sourceRevision`).
124
+ - **The server judges the declaration.** `save` and `seal` refuse with every gap named — `[generator/combinedDeclaration] …` (a required part field without a source, a type or option that does not fit, a `requiredIf` the mask cannot settle, an impossible roster, a priced mask field no part reads), `[generator/kindChange]` (a key that is already a document generator), the concurrent-save conflict. The CLI prints them verbatim; it does not pre-check.
125
+ - **Parts are pinned, never "newest".** `"generator": "school-contract"` without `@revision` is refused by the file reader. A part must be a sealed document revision when the combined one seals; an archived part blocks the seal, and a part used by a combined revision cannot be archived (`[generator/partInUse]`).
126
+ - **IDs belong to their environment.** An exported `id` only saves on the environment it came from; to bring a declaration to another environment, drop `id`/`expectedSourceRevision`/`revision`/`status` and save it there as a new key (or clone + export there and copy the `inputSchema` and `parts`).
127
+ - `generator export` / `generator source export` on a combined revision refuse and point to `generator combined export`; `show`, `list`, `usages`, `runs`, `clone`, `seal`, `archive`, `restore` and `delete-draft` take either kind.
128
+
129
+ ## Verify
130
+
131
+ ```bash
132
+ plexus-cli --env local --tenant test generator list
133
+ plexus-cli --env local --tenant test generator show school-contract@3 --json
134
+ plexus-cli --env local --tenant test generator usages school-contract@3
135
+ plexus-cli --env local --tenant test generator runs school-contract@3
136
+ ```
137
+
138
+ Also inspect the marked PDF and positions in the contract-template workspace. Scenario evaluation does not require the PDF converter; PDF preview does.
139
+
140
+ ## Idempotency / gotchas
141
+
142
+ - Source is compiled in a Node action and executed in QuickJS-WASM inside the Convex transaction. No database, network, filesystem, environment, timers, randomness or ambient clock reaches the guest. Input and output are bounded JSON; CPU, memory and stack are bounded.
143
+ - Published source, compiler artifact, interpreter identity, bootstrap hash and execution limits are frozen. A compiler/runtime change requires a reviewed new revision or an explicit audited migration. Display names remain renamable.
144
+ - Saving source/configuration or removing a scenario invalidates approval. A changed DOCX requires renewed review. Publishing rechecks every expected result against current resolved inputs.
145
+ - Optional `articleDecisions` output entries explain inclusion/omission. When supplied, they must cover the declared article manifest exactly and agree with actual lines. The editor shows when explanations are absent.
146
+ - Article reverse links come from the finite manifest, so conditional possibilities remain visible even when an example omits them.
147
+ - Signature captions must exist in the PDF. Publication checks declarations; rendering verifies their locations.
148
+ - Prices still come from the pricing kernel. Override lines need a price version for currency. Grants, parties and payer allocations pass the normal tenant and commercial validation before materialization.
149
+ - Historical signed snapshots are never rewritten during the table/source migration. An unavailable historical source remains explicitly unavailable; it is never re-pinned to different code.
150
+ - Production writes need an explicit environment and `--allow-prod`; this playbook does not authorize them.
151
+ - To build on an existing generator's code (a second school's variant of a contract), start from `generator clone` or `generator source export`. If you author it as several modules, bundle them into the one self-contained source first (e.g. esbuild with `bundle: true, format: "esm", target: "es2020"`) — `generator source save` refuses imports. The guest has no `Intl` and throws on it: replace every `toLocaleString` with hand-written German formatting. Run `generator scenarios test` on the draft before approving.
152
+ - Fee groups are part of a source manifest (`feeGroups`, with bilingual names and article IDs). In a setup file use `resolver.feeGroups: [{ key, name: { de, en }, billingItemKeys }]`; every member must also be in `resolver.billingItemKeys`. Source export/apply round-trips groups. The catalog has no `plans` section; declare article availability in `billingItems[].programs`. Workflow prices use `pricedGroup` and `derive.group`, resolved through pinned generator revisions.
153
+
154
+ ## Related
155
+
156
+ - `plexus-cli generator <verb> --help` for every verb above
157
+ - [Set up a billing catalog and Word templates](../config/set-up-billing-catalog.md)
158
+ - [Edit a live tenant offer](../offer/edit-live-tenant-offer.md)
159
+ - [Release an offer bundle](../offer/publish-offer-bundle.md)
@@ -0,0 +1,99 @@
1
+ # Erase applications past their retention period
2
+
3
+ **When to use:** on the monthly erasure date (below), or as soon as the weekly
4
+ GlitchTip warning „[retention] applications past their retention period await
5
+ erasure" names a tenant. Plexus erases nothing on its own: an
6
+ application whose retention period has ended stays, with all its personal data,
7
+ until someone runs this recipe.
8
+
9
+ > **The DSGVO duty behind it.** Storage limitation (Art. 5(1)(e) DSGVO) requires
10
+ > deleting personal data once its purpose and retention period have ended. Since
11
+ > erasure is no longer automatic, it happens only if a person does it, deliberately
12
+ > and regularly:
13
+ >
14
+ > - **Who:** the Plexus platform operator (the maintainer, or whoever holds a
15
+ > `_platform.tenants.accessAll` token for prod), on behalf of the schools.
16
+ > - **How often:** **monthly**, on the first working day, for **every** tenant on
17
+ > prod; additionally within a week whenever the weekly warning appears. The
18
+ > shortest retention period is 180 days, so a monthly run keeps any application at
19
+ > most about a month past its date.
20
+ > - **Record:** keep the `--json` output of each `apply` run (date, tenant, ids) in
21
+ > your operations log, one directory per run (e.g. `retention-<date>/`). Plexus
22
+ > stores no "who erased" row yet: the application only carries `redactedAt`.
23
+
24
+ ## What is due
25
+
26
+ The rule lives in one place on the server (`retentionDueAt`):
27
+ an application is due when it has ENDED (`endedAt` set) and `RETENTION_DAYS` of its
28
+ classification have passed since. The classification follows its outcome:
29
+
30
+ | outcome | classification | kept for |
31
+ |---|---|---|
32
+ | approved | admitted | 3650 days (10 years) |
33
+ | rejected | rejected | 180 days |
34
+ | withdrawn | withdrawn | 365 days |
35
+ | expired | expired | 180 days |
36
+ | waitlist | waitlist | 730 days |
37
+ | none | unknown | 365 days |
38
+
39
+ Test applications and soft-deleted ones („gelöscht") are listed too — both still hold
40
+ personal data. A still-running application is never due, even if soft-deleted.
41
+
42
+ ## Prerequisites
43
+
44
+ - `plexus-cli` with a platform-operator token for the target env (check the
45
+ connection with `plexus-cli --env prod config show` and `plexus-cli --env prod whoami`);
46
+ both functions need `_platform.tenants.accessAll`.
47
+ - The tenant slug (`plexus-cli --env prod platform tenants`).
48
+
49
+ ## Steps
50
+
51
+ ```bash
52
+ # 1. Read-only: what is due in this tenant. Note the number of rows.
53
+ plexus-cli --env prod --tenant <slug> retention plan
54
+
55
+ # 2. Dry run of the erasure — prints the same list, erases nothing.
56
+ plexus-cli --env prod --tenant <slug> retention apply
57
+
58
+ # 3. Erase. --expect is the count from step 1: if the server now finds a different
59
+ # number of due applications, nothing is erased and you read the plan again.
60
+ mkdir -p retention-$(date +%F)
61
+ plexus-cli --env prod --allow-prod --tenant <slug> retention apply --confirm --expect <n> --json \
62
+ > retention-$(date +%F)/<slug>.json
63
+ ```
64
+
65
+ Repeat for every tenant `platform tenants` lists.
66
+
67
+ ## Verify
68
+
69
+ ```bash
70
+ plexus-cli --env prod --tenant <slug> retention plan # → no rows
71
+ ```
72
+
73
+ An erased application keeps its row and its decision log (`application_events`);
74
+ its answers, documents, chat texts, access log and stored mail bodies are gone, and
75
+ `redactedAt` is set (server function `_redactApplicationCore` holds the full list).
76
+
77
+ ## Gotchas
78
+
79
+ - **Erasure cannot be undone.** Read the plan first; there is no restore.
80
+ - **The server decides, not the list.** `retention:eraseDue` refuses an application
81
+ that is not due, belongs to another tenant, or is already erased. A refusal stops the
82
+ run; its message (stderr, not the `--json` file) names the ids erased before it —
83
+ copy them into the record, rerun `plan` and continue.
84
+ - **A different count erases nothing.** Someone ended an application between `plan` and
85
+ `apply`, or another operator erased some — read `plan` again and confirm the new count.
86
+ - **Writes need `--env` named; prod needs `--allow-prod`** (`retention apply` is a
87
+ `confirm` command, its dry run counts as a read).
88
+ - **One request for one person** (Art. 17, a family asks to be erased now) is not this
89
+ recipe: that is „Bewerbung löschen" on the single application
90
+ (`admission.redactApplication`), independent of any period.
91
+
92
+ ## Related
93
+
94
+ - Weekly report: `retentionDueReport` (server cron „retention due report",
95
+ Mondays 03:00 UTC) — a GlitchTip `warning`, `area: admission.retention`, tags
96
+ `tenantSlug`, `dueCount`, `testDueCount`; a tenant whose read failed reports as an
97
+ `error` with `errorClass: scanFailed`.
98
+ - What an erasure removes from the access log is defined in the platform's logging
99
+ concept (Protokollierungskonzept, § 6); ask the platform operator for it.