@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,99 @@
1
+ # Declare what a document node reads
2
+
3
+ **When to use:** a contract or offer letter is produced by hand and **uploaded**
4
+ (a school without a contract generator), or a generated contract prints answers it does not bind.
5
+ Such a node binds no form field, so without `reads` a correction on the child's
6
+ name never holds it, and an issued contract does not protect the fields it
7
+ printed. `reads` names those fields, each by its full address
8
+ (`$node.<formNode>.admission.<field>` — the address names WHICH form): the readiness gate holds
9
+ „Freigabe anfordern" / release / issue while one of them has an open correction
10
+ or is empty where the form requires it, and once the document is issued they are
11
+ **consumed** — a correction on them is refused until the document is rescinded.
12
+
13
+ ## Prerequisites
14
+ - An offer bundle on disk: a **fresh export of the target environment**
15
+ (`offer export`) into a private scratch dir. A live tenant's bundle is never kept
16
+ in a shared or versioned place — the server is its source of truth; any older copy can lag behind what is live (fields added
17
+ later are then refused as unknown).
18
+ - Authoring is offline; `offer plan` / `offer apply` need a token for `--env`.
19
+ - A bundle exported before addresses were introduced carries bare keys and is refused (`[address/bareIdentifier]`,
20
+ `[graph/retiredField]` for `wires`) — export it again. Model: [Name what a node reads](./addresses.md).
21
+
22
+ ## Steps
23
+ ```bash
24
+ # 1. Export what is live (read-only).
25
+ plexus-cli --env staging --tenant <slug> offer export --slug <offer-slug> --out ./bundle
26
+
27
+ # 2. Declare the fields the document carries (replaces the list). Each entry is
28
+ # an address of the form node that CREATES the instance (here `form`) —
29
+ # entered or computed fields (a computed key such as derivedConstellation
30
+ # expands to the answers it is computed from). --address is repeatable and
31
+ # comma-separated (single quotes!); --from <formNode> --field <path>,… is the
32
+ # short form that composes the same addresses:
33
+ plexus-cli offer workflow node set-reads --dir ./bundle --node contract \
34
+ --from form --field kind.firstMiddleNames,kind.surname,kind.geburtsdatum,derivedConstellation \
35
+ --address '$node.form.admission.elternteil_1.vorname,$node.form.admission.elternteil_1.nachname'
36
+
37
+ # Later edits:
38
+ plexus-cli offer workflow node add-reads --dir ./bundle --node contract --address '$node.form.admission.kind.homeAddress'
39
+ plexus-cli offer workflow node remove-reads --dir ./bundle --node contract --from form --field kind.homeAddress
40
+ plexus-cli offer workflow node set-reads --dir ./bundle --node contract --clear
41
+
42
+ # A bare key (`--address kind.surname`, or `--field` without `--from`) is
43
+ # refused with the addresses visible at the node.
44
+
45
+ # 3. Validate offline, then see what the apply would change.
46
+ plexus-cli offer validate --dir ./bundle
47
+ plexus-cli --env staging --tenant <slug> offer plan --dir ./bundle # ~ workflow change + ~reads:contract +… (against the live head)
48
+
49
+ # 4. Publish (a new workflow version).
50
+ plexus-cli --env staging --tenant <slug> offer apply --publish --dir ./bundle
51
+ ```
52
+
53
+ ## Verify
54
+ ```bash
55
+ plexus-cli offer workflow node list --dir ./bundle # `reads` column: "30 Felder"
56
+ plexus-cli offer workflow node reads --dir ./bundle --node contract # every address, „geschrieben" in nodes.contract.reads.<i>
57
+ plexus-cli --env staging --tenant <slug> offer plan --dir ./bundle # everything "same", no ~reads rows
58
+ plexus-cli --env staging --tenant <slug> offer export --slug <offer-slug> --out ./check
59
+ jq '.graph.nodes[] | select(.id=="contract") | .reads | length' ./check/workflow.json
60
+ ```
61
+
62
+ ## Idempotency / gotchas
63
+ - **Only new errors block.** The verb compares the bundle's errors before and
64
+ after the edit: an error the edit introduces (`[address/pathNotInTarget]` for a field
65
+ the form lacks, a repeated-block field, a duplicate, `[address/notVisible]` for a form that
66
+ does not run before the node, `[address/mixedForms]` for two forms in one list, `reads` on a
67
+ form/end node → `[readiness/readsNotGated]`) writes
68
+ nothing; an unrelated error the bundle already had (e.g. an undecided
69
+ assignment row) is printed as `⚠ vorbestehend` and does not stop the edit —
70
+ `offer apply --publish` will still refuse it.
71
+ - **`add-reads` is rerunnable** (addresses already read are reported, not an error);
72
+ `remove-reads` fails on an address the node does not read.
73
+ - **One form per list.** A node's `reads` is judged on ONE form instance; a second form's
74
+ fields in the same list are `[address/mixedForms]`. An adopting form node (`adoptForm`)
75
+ produces nothing — read the fields at the creating node's address.
76
+ - **What to read:** exactly what the document prints — names, birth date,
77
+ address of the child and of every party that can sign, the answers that decide
78
+ who signs (the constellation), start date, fee answers. Not uploads, free text
79
+ or health data: every read field is **locked** once the document is issued.
80
+ A read field that the form does not currently require is never awaited.
81
+ - **`[readiness/readsMissesPartyCondition]`** (warning, resolver documents only):
82
+ the contract's parties depend on answers neither read nor bound — add them
83
+ (or the computed constellation key).
84
+ - **Running applications keep their pinned version.** Publishing does not give
85
+ applications already in flight the new `reads`; they stay on the version they
86
+ were pinned to. Either re-pin them, discard a test application
87
+ (`plexus-cli --env <env> applications discard-test <id>`, test applications only), or —
88
+ on a test environment — delete them all:
89
+ `plexus-cli --env staging config wipe --tenant <slug> --target applications`
90
+ (dry-run count first, then `--confirm`). A real application has no
91
+ per-application hard delete; its personal data go only by erasure
92
+ (`retention apply`, [Erase due applications](../retention/erase-due-applications.md)).
93
+ - **Removing a form field that a node reads** fails the form's publish with
94
+ `[address/pathNotInTarget]` at the workflow — `remove-reads` it first.
95
+
96
+ ## Related
97
+ - [Name what a node reads](./addresses.md) · [Edit a workflow node](./edit-workflow-node.md) · [Release an offer bundle](./publish-offer-bundle.md)
98
+ - [Wipe tenant config/data](../config/wipe-tenant-config.md)
99
+ - `plexus-cli offer workflow node set-reads --help` — the verb's flags
@@ -0,0 +1,144 @@
1
+ # Edit a live tenant's offer (prod is the source of truth)
2
+
3
+ **Use it to** change anything about a live tenant's offer — close a Schuljahr, change
4
+ capacities, edit the form, the workflow, the assignment rule, the resolver setup — and
5
+ be sure prod ends up exactly as you meant, with nothing else rolled back.
6
+
7
+ ## The rule
8
+
9
+ A live tenant's offer bundles and config exist **only on the server**. Prod is their
10
+ only source of truth; there is no other copy that a deploy would apply. A copy kept
11
+ somewhere else drifts: editing an old copy to close a Schuljahr changes nothing on prod,
12
+ and applying it later can roll prod back. So every edit starts from a **fresh prod
13
+ export** and ends with a **plan that shows only your change**.
14
+
15
+ Working copies live in a **private scratch directory** (a temp dir, or a dated folder
16
+ you keep for the session) — never in a shared or versioned place: a live tenant's
17
+ bundle carries the school's data.
18
+
19
+ ## Steps
20
+
21
+ ```bash
22
+ WORK=$(mktemp -d) # any private scratch dir
23
+ T=<slug>; S=<offer-slug> # tenant slug, manifest slug
24
+
25
+ # 1. Export what is live. ALWAYS pass --env (the default is prod) and --out
26
+ # (the default is ./offers/<tenant>/<slug> of the current directory).
27
+ plexus-cli --env prod offer export --tenant $T --slug $S --out $WORK/$S
28
+ # The export has NO contract/ (resolver-setup.json + DOCX): to change the
29
+ # resolver setup, copy contract/ from the last rollout's working copy and
30
+ # prove it equals prod — every row `same`:
31
+ # plexus-cli --env prod resolver setup plan --tenant $T --file $WORK/$S/contract/resolver-setup.json
32
+ cp -a $WORK/$S $WORK/$S.orig # keep the untouched export for the diff
33
+
34
+ # 2. Edit bundle.yaml / manifest.json / workflow.json / smartforms/*.json in $WORK/$S
35
+ # (or with the offer verbs: offer form …, offer workflow node …, --dir $WORK/$S).
36
+ diff -ru $WORK/$S.orig $WORK/$S # exactly your change, nothing else
37
+
38
+ # 3. Offline gate, then the remote diff. Read every row.
39
+ # The offline gate cannot see the tenant's catalog: a `pricedItem` task whose
40
+ # article is not assigned to the offer's program passes here and is refused
41
+ # only by the live publish ([taskResult/pricedItemNotInProgram]) —
42
+ # check the assignment under Finanzen → Artikel first.
43
+ # Nor does it run the signature-anchor rules: [contract/missingSignatureAnchors]
44
+ # and [contract/undeclaredPartyRole] come from the sealed revision's code + config against
45
+ # the form: the seal (`resolver setup apply`) checks the resolver names anchors at all,
46
+ # only the live publish checks every signer of the form. A green validate or plan proves
47
+ # nothing there. Changing the resolver: publish on staging first (below).
48
+ plexus-cli offer validate --dir $WORK/$S
49
+ plexus-cli --env prod offer plan --tenant $T --dir $WORK/$S
50
+
51
+ # 4. Apply. --publish when the form or workflow changed (plan's `publish` row says
52
+ # PENDING); capacities, stages and runs need no publish.
53
+ plexus-cli --env prod offer apply --allow-prod --tenant $T --dir $WORK/$S [--publish]
54
+
55
+ # 5. Converged: every row `same`, publish `up to date`.
56
+ plexus-cli --env prod offer plan --tenant $T --dir $WORK/$S
57
+ ```
58
+
59
+ Tenant config (Stammdaten shape, roles, units, …) follows the same shape with the
60
+ `config` verbs: `plexus-cli --env prod config export --tenant $T` into the scratch dir →
61
+ edit → `config apply` (dry-run first) → `--confirm --allow-prod`. Recipe:
62
+ [Update a tenant's config](../config/update-tenant-config.md).
63
+
64
+ ## Staging
65
+
66
+ Staging gets a live tenant's offer **only by an explicit copy from prod** — nothing flows
67
+ there on its own:
68
+
69
+ ```bash
70
+ plexus-cli --env prod offer export --tenant $T --slug $S --out $WORK/$S
71
+ plexus-cli --env staging offer plan --tenant $T --dir $WORK/$S
72
+ plexus-cli --env staging offer apply --tenant $T --dir $WORK/$S --publish
73
+ ```
74
+
75
+ To try a change on staging first, apply the edited copy there, check it, then run the
76
+ prod plan from the same directory — the prod plan still has to show only your change.
77
+
78
+ A prod offer whose contract node uses `eSignature` publishes **externally** on the staging
79
+ mirror only once the mirror tenant has an active signing connection a test reached
80
+ (`[signing/noActiveProvider]`; `--stage internal` needs none — its test applications run on
81
+ the simulator). On staging that connection is always the provider's sandbox with a
82
+ redirect, never the school's production account; setting it up needs a platform operator.
83
+
84
+ ## What the address rows in `offer plan` mean
85
+
86
+ Against the **live head** (not the draft), `offer plan` lists what the bundle changes at the
87
+ addresses — display only, never a finding, the exit code stays 0:
88
+
89
+ ```
90
+ ~ address:$task.rang.rang Knoten pruefung → freigabe # the task moved to another node
91
+ + address:$task.rang.rangSchule neu (freigabe) # a key renamed = one added …
92
+ - address:$task.rang.rang entfernt (pruefung) # … and one removed
93
+ ~ reads:vertrag +$node.form.admission.kind.nachname −$node.form.admission.kind.vorname
94
+ ! pinned 3 Anmeldungen (2 echt, 1 Test) auf Fassung 7 bleiben auf ihrer Version — Umpinnen nur per Migration mit Abbildung
95
+ ```
96
+
97
+ - Renaming a key or moving a task is **allowed** in a new version. Every application keeps
98
+ running on the version it pinned; the new version applies to applications started after the
99
+ publish.
100
+ - The `pinned` row counts the applications on the live head. They are **not** moved: an
101
+ identity re-pin refuses a version whose addresses differ (topology hash). Moving them needs
102
+ a migration with a mapping (old address → new address) — that is platform code, not a CLI
103
+ verb; ask a platform operator before you publish if those applications must follow.
104
+ - No rows at all: the bundle produces and reads the same addresses as the live head.
105
+
106
+ ## Gotchas
107
+
108
+ - **Stop if the plan shows more than your change.** Extra rows mean prod moved since
109
+ your export, or your copy was not a fresh export. Re-export; never apply an older copy
110
+ (an earlier export, a backup): it can be behind prod in one place and ahead in
111
+ another, and apply rolls prod back where it is behind.
112
+ - **A change that needs new platform code ships in two steps.** A resolver implementation,
113
+ a node type or a validator reaches prod with a platform release; only then apply the
114
+ bundle that uses it. Before the release, `offer plan --env prod` refuses it (e.g.
115
+ `extra field taxTreatment`, an unknown implementation key). Keep the edited copy in your
116
+ private scratch dir with a note saying which release it waits for.
117
+ - **An export from before node ids lost their dots is refused** (`[graph/nodeIdFormat]` on
118
+ ids like `n.form`): prod data was already renamed on the server. Export again — never
119
+ rename ids by hand in an old copy.
120
+ - **A config export from before the file-reference rename is refused**
121
+ (`[config/fileRefRenamed]`): its library files still say `fileName` / `size` and its brand
122
+ files `size` (now `filename` / `sizeBytes`). Pull a fresh `config export` — never rename
123
+ the keys by hand in an old copy. A CLI that already uses the new names refuses a server
124
+ that does not yet (`[cli/serverPredatesFileRef]`): use the CLI version that matches the server.
125
+ - **`offer export` leaves out `contract/`.** The resolver setup file and its DOCX are
126
+ bundle data that exists only in the last rollout's working copy, not in the export. An
127
+ edit of the resolver setup starts from that copy, and `resolver setup plan` must show
128
+ every row `same` BEFORE you change anything — else the copy is not what prod runs.
129
+ - **The signature-anchor rules are server-side only.** Every resolver names a line
130
+ for every party the form lets sign; a resolver that does not is refused at publish with
131
+ `[contract/missingSignatureAnchors]`, a form role the resolver does not declare with
132
+ `[contract/undeclaredPartyRole]`. `offer validate` and `offer plan` do not run them; the
133
+ seal checks only that the resolver names anchors at all, the publish checks every signer
134
+ of the form. A resolver change therefore goes through staging first (`offer apply
135
+ --publish` there), then prod.
136
+ - **Close a Schuljahr per cell:** `capacities.<stage>.isOpen: false` on each cell of the
137
+ run. A run-level `isOpen` is only read when apply creates a new run
138
+ ([set-offer-capacities](./set-offer-capacities.md)).
139
+
140
+ ## Related
141
+
142
+ - [Release an offer bundle](./publish-offer-bundle.md) — `offer status`, `--publish`, `--stage internal`
143
+ - [Set an offer's seat capacities](./set-offer-capacities.md)
144
+ - [Manage a contract resolver](../resolver/manage-contract-resolver.md) — `resolver setup plan|apply` before `offer apply --publish`
@@ -0,0 +1,121 @@
1
+ # Edit a SmartForm in depth
2
+
3
+ **When to use:** change an existing form beyond adding things — edit a field's
4
+ validation/visibility/enum/docRef/consent props, edit an effect's wiring, reorder
5
+ fields or sections, reparent a section, or edit/remove a repeated or computed field —
6
+ then publish a new version.
7
+
8
+ ## Prerequisites
9
+ - An offer bundle on disk carrying the form (`offer export …` or `offer init …`).
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 form --help`).
14
+
15
+ ## Steps
16
+ ```bash
17
+ # Form-level: rename / change school.
18
+ plexus-cli offer form set --dir <bundle> --form <slug> --label "Neuer Titel" --label-en "New title"
19
+
20
+ # Field: edit ANY editable prop (use "" to clear an expression / value).
21
+ plexus-cli offer form field set --dir <bundle> --key kind.vorname \
22
+ --bucket kind --placeholder "Max" --placeholder-en "Max" --required-if "kind.nachname != null"
23
+ plexus-cli offer form field set --dir <bundle> --key kind.geschlecht --enum-ref geschlecht # swap inline→catalog
24
+ plexus-cli offer form field set --dir <bundle> --key kind.nachweis --doc-mime application/pdf --doc-max-size 9000000
25
+ plexus-cli offer form field set --dir <bundle> --key kind.attest --required-for sonderfall # outcome-scoped required
26
+ # Boolean as an explicit Ja | Nein answer (nothing preselected; drop any --default first,
27
+ # publish refuses one: config.yesno-default). "" returns to the checkbox.
28
+ plexus-cli offer form field set --dir <bundle> --key kind.allergien --presentation yesNo --default ""
29
+
30
+ # Outcome: edit its output shape (replace; "" clears).
31
+ plexus-cli offer form outcome set --dir <bundle> --id standard \
32
+ --fields kind.vorname,kind.nachname --effect-refs p.kind:applicant
33
+
34
+ # Effect: edit per-kind props.
35
+ plexus-cli offer form effect set --dir <bundle> --id p.kind --identity keyed --identity-key-from kind.vorname,kind.nachname
36
+ plexus-cli offer form effect set --dir <bundle> --id rel.kindmutter --from p.kind --to p.mutter --rel-kind kind
37
+
38
+ # Repeated / computed: set + rm (add via `offer form repeated|computed add`).
39
+ plexus-cli offer form repeated set --dir <bundle> --key geschwister --max 3 --unique-by name
40
+ plexus-cli offer form computed set --dir <bundle> --key kind.voll --expression "kind.vorname != null && kind.nachname != null"
41
+
42
+ # Reorder + reparent (relative anchors; reparent with --parent "" → top-level).
43
+ plexus-cli offer form field move --dir <bundle> --key kind.nachname --before kind.vorname
44
+ plexus-cli offer form field move --dir <bundle> --key kind.ausweis --section section.kind --after kind.foto # into another section
45
+ plexus-cli offer form section move --dir <bundle> --id section.kind --to-top
46
+ plexus-cli offer form section set --dir <bundle> --id section.eltern --parent section.kind
47
+
48
+ # Section visibility: only these teams / permission groups (plus admins
49
+ # and the family) read the section's values; everyone else sees „Nur für …
50
+ # sichtbar". Comma-separated keys; "" clears.
51
+ plexus-cli offer form section set --dir <bundle> --id section.financialDocs --visible-to finanzen
52
+
53
+ # Validate + publish (publishing re-versions the form).
54
+ plexus-cli offer validate --dir <bundle>
55
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
56
+ ```
57
+
58
+ ### English texts
59
+
60
+ A SmartForm text is ONE field holding `{ de, en }` — `label`, `placeholder`,
61
+ `helpText`, `description`; there is no `localized…` twin. Every verb that writes a German
62
+ text takes the English in the same call: `--label-en` (form, section, field, outcome,
63
+ repeated, scenario), `--placeholder-en`, `--help-text-en`, `--declaration-label-en` on
64
+ fields and `--description-en` on scenarios.
65
+
66
+ ```bash
67
+ plexus-cli offer form field add --dir <bundle> --section section.kind --key kind.vorname --type string \
68
+ --label Vorname --label-en "First name" --placeholder "z. B. Anna" --placeholder-en "e.g. Anna"
69
+ plexus-cli offer form section set --dir <bundle> --id section.kind --label-en "Child" # English only
70
+ ```
71
+
72
+ **The rule:** a new text without its English is refused at the command
73
+ (`[i18n/missingTranslation]`) — the reader's language picks the text with no fallback
74
+ to German, so the publish gate refuses it anyway. The CLI never copies the German into
75
+ `en`. Changing the German of a text whose English is a real translation refuses unless
76
+ you pass the new English too; an English that is blank (a migrated text) or a copy of
77
+ the German becomes blank, and `offer validate` names it until it is translated.
78
+ `--label-en ""` is refused. A field, repeated block or outcome added without any label
79
+ is named by its key in both languages (an identifier, not a German text).
80
+
81
+ ## Verify
82
+ ```bash
83
+ plexus-cli offer form show --dir <bundle> --form <slug> # props/order/repeated/computed reflect the edits
84
+ plexus-cli offer validate --dir <bundle> # "local validation clean"
85
+ ```
86
+
87
+ ## Idempotency / gotchas
88
+ - **Validate-before-write.** Every mutator runs the real publish gate first; on any
89
+ error (bad expression, unknown enum-ref, wrong-kind effect flag, reparent cycle,
90
+ move anchor missing) it writes **nothing**.
91
+ - **Clearing.** Expression/string props clear with `""`; `--required-for ""`, `--map`
92
+ (fieldMap) replaces; repeated `--min/--max -1` clears; `--unique-by ""` clears.
93
+ - **Per-kind effect flags fail loud.** Passing `--org-kind` to a `person` effect (etc.)
94
+ is rejected — no silent drop.
95
+ - **Catalog checks stay server-side.** Effect `role`/`relKind`, fieldMap `to`, and a
96
+ custom-field target resolve against the **tenant catalog** at `apply --publish`, not
97
+ offline. `enum-ref` is the one catalog the CLI carries, so it's checked offline.
98
+ - **Reorder is sibling-scoped.** `field move` reorders within the field's section;
99
+ `section move` reorders among siblings. `field move --section <id>` moves a field into
100
+ another section unchanged — same key, so its values and every address that names it stay
101
+ valid; never rm + add, which drops the field's settings and trips the field-key
102
+ gate. The anchor then names a field of the TARGET section; a section left empty stays
103
+ until `section rm`. Into a `--visible-to` section only an unbound field may move
104
+ (`section.visibleTo-party` refuses a `cf_value`). Moving a section under a new parent =
105
+ `section set --parent`.
106
+ - **`--visible-to` is judged at publish.** `offer validate` checks the shape, nesting,
107
+ party-bound fields (`cf_value` → `section.visibleTo-party`) and expressions outside the
108
+ section that read its keys (`section.visibleTo-read-outside`); whether the key names a
109
+ team or permission group of the school (`section.visibleTo-unknown`) is checked only at
110
+ `apply --publish`. It applies to applications on the NEW revision — existing ones keep
111
+ the definition they were pinned to.
112
+ - **Removing a field/section/repeated breaks publish stability.** `publishSmartForm`
113
+ refuses to drop a field key a prior published version had — the `rm` commands warn.
114
+ - **Re-publishing = a new version.** `offer apply --publish` after edits cuts a new
115
+ `smart_form_versions` row (and the full-bundle path re-pins the workflow).
116
+
117
+ ## Related
118
+ - [Author a SmartForm as code](./author-smartform.md) — the add-from-scratch flow.
119
+ - [Author a form's test scenarios and context](./author-form-scenarios.md).
120
+ - [Edit a workflow node](./edit-workflow-node.md) — rebind which form a node uses.
121
+ - Command reference: `plexus-cli offer form --help`.
@@ -0,0 +1,126 @@
1
+ # Edit a workflow node
2
+
3
+ **When to use:** change an existing node in an offer's workflow — relabel it, change
4
+ who decides on a decision node, rebind a form node to a different
5
+ SmartForm, or rewire where an outcome routes — without removing and re-adding it.
6
+
7
+ ## Prerequisites
8
+ - An offer bundle on disk carrying a `workflow.json` (`offer init …` or `offer export …`).
9
+ - Authoring is **fully offline**; only `offer apply --publish` needs an API token of a
10
+ user who may configure the tenant's offers (a tenant admin — see
11
+ [Add a tenant admin](../users/add-tenant-admin.md)).
12
+ - An installed, current `plexus-cli` (`plexus-cli offer workflow node set --help`).
13
+
14
+ ## Steps
15
+ ```bash
16
+ # Relabel a node — its label is ONE { de, en } field. A German-only change
17
+ # refuses while a real English exists (pass --label-en too); --label-en alone
18
+ # re-translates the current German.
19
+ plexus-cli offer workflow node set --dir <bundle> --id form --label "Bewerbungsformular" --label-en "Application form"
20
+
21
+ # Decision node: hand the decision row to another team (permission-group key) and
22
+ # name the outcomes that need a comment (there are no approver slots).
23
+ plexus-cli offer workflow node set --dir <bundle> --id freigabe \
24
+ --assign-decision schulleitung --comment-required rejected
25
+
26
+ # Form node: rebind to a different carried SmartForm.
27
+ plexus-cli offer workflow node set --dir <bundle> --id form --form <other-form-slug>
28
+
29
+ # Rewire routing: send an outcome to a different existing node.
30
+ plexus-cli offer workflow node set --dir <bundle> --id form --outcomes "standard:freigabe"
31
+
32
+ # End node: re-author the terminal outcome (re-syncs the terminal progress column).
33
+ plexus-cli offer workflow node set --dir <bundle> --id endRejected --outcome rejected
34
+
35
+ # End node: keep or discard the application's data at this end. Changing
36
+ # --outcome never changes it; a node without one must be given it here.
37
+ plexus-cli offer workflow node set --dir <bundle> --id endRejected --data-decision discard
38
+
39
+ # roleAssign node: change the granted role / mode; the person is an address.
40
+ plexus-cli offer workflow node set --dir <bundle> --id roleAssignSchueler --role schueler --mode add \
41
+ --person '$node.form.admission.beneficiary'
42
+
43
+ # What a node reads is named by address (single quotes — the shell expands $):
44
+ # setFlag: the instance it raises the flag on; countersign / engagePush: the
45
+ # contract; engagePush: the payment; a resolver contract / engagePush: the form
46
+ # instance (`--form` takes a SmartForm slug only on a FORM node).
47
+ plexus-cli offer workflow node set --dir <bundle> --id setFlagZusage --target '$node.form.admission'
48
+ plexus-cli offer workflow node set --dir <bundle> --id gegenzeichnung --contract '$node.contract.contract'
49
+ plexus-cli offer workflow node set --dir <bundle> --id contract --form '$node.form.admission'
50
+ plexus-cli offer workflow node set --dir <bundle> --id engagePush --payment '$node.zahlung.payment'
51
+ # A bare key is refused with the addresses visible at the node — see
52
+ # [Name what a node reads](./addresses.md). Check the result with
53
+ # plexus-cli offer workflow node reads --dir <bundle> --node <id>
54
+
55
+ # Document node (governedDocument): contract without a Freigabe, asking before it
56
+ # goes out („„Vertrag" jetzt senden?" on „Bereitstellen"). `--approval none` drops
57
+ # assign.releaseApproval + titles.approve; `--approval release` restores a
58
+ # Freigabe (title via --approve-title, default "<Dokument> freigeben") and drops
59
+ # the confirmation. Same edits as the IDE's Knotendetails → Einstellungen.
60
+ plexus-cli offer workflow node set --dir <bundle> --id contract --approval none --confirm-issue true
61
+
62
+ # Document node that the family returns signed (signedUpload / signedUploadChecked):
63
+ # „Bestätigung der Vertragsbedingungen" is ON by default — before the
64
+ # signed copy is uploaded the uploader confirms the terms, the protocol records
65
+ # who and when. Switch it off, or reword the question in BOTH languages
66
+ # ("" "" restores the default wording):
67
+ plexus-cli offer workflow node set --dir <bundle> --id contract --confirm-terms false
68
+ plexus-cli offer workflow node set --dir <bundle> --id contract \
69
+ --terms-question-de "Haben Sie den Schulvertrag gelesen und verstanden?" \
70
+ --terms-question-en "Have you read and understood the school contract?"
71
+
72
+ # Decision node: no outcome until ONE of its optional actions is COMPLETED —
73
+ # e.g. „Interview oder Taster Day vor der Grundsatzentscheidung". The keys are
74
+ # `optionalActions` keys of THIS node (add the actions first); a foreign or
75
+ # unknown key fails the publish gate ([decision/requiresOneOfUnknownAction]).
76
+ # `--requires-one-of ""` drops the rule. Only on `node set`: the actions must exist.
77
+ plexus-cli offer workflow node set --dir <bundle> --id decision --requires-one-of interview,tasterDay
78
+
79
+ # Then validate + publish (publishing re-versions the workflow).
80
+ plexus-cli offer validate --dir <bundle>
81
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
82
+ ```
83
+
84
+ ## Verify
85
+ ```bash
86
+ plexus-cli offer workflow node list --dir <bundle> # label / outcomes / routing reflect the edit
87
+ plexus-cli offer validate --dir <bundle> # "local validation clean"
88
+ ```
89
+
90
+ ## Idempotency / gotchas
91
+ - **Validate-before-write.** Every `node set` runs the real publish gate before
92
+ touching disk — on any error (bad routing target, unknown outcome, …) it
93
+ writes **nothing**.
94
+ - **A bundle exported before addresses were introduced is refused** (`[graph/retiredField]`: `wires`, a binding
95
+ with `path`, `adoptForm: true`) — export it again; never patch it by hand.
96
+ - **`--type` is not editable.** A type change rewires outcomes/specs/progress, so it is
97
+ a remove + add: `offer workflow node rm` then `offer workflow node add`. Passing
98
+ `--type` to `set` fails loud with that guidance.
99
+ - **Routing is validated at the boundary:** the outcome must be one the node declares
100
+ (or the implicit `done` for a no-outcome node), and the target must be an existing
101
+ node id — else it fails loud and writes nothing.
102
+ - **Approver group existence is server-side.** The CLI authors the
103
+ `permissionGroupKey`; whether it resolves to a real tenant permission group is checked
104
+ at `apply --publish` / runtime, not offline.
105
+ - **Relabel never touches progress.** Progress state/column labels come from the node's
106
+ events, not its label, so a `set` leaves the progress layout/wiring untouched.
107
+ - **`--confirm-issue true` needs approval `none`.** With a Freigabe, „Freigabe
108
+ anfordern" sends nothing to the family and the Freigabe is itself the check — the
109
+ flag would be dead wiring, so the CLI refuses it (and publish would too).
110
+ - **`--confirm-terms` / `--terms-question-*` only on a signing node.** A `confirm`
111
+ document (Platzangebot) has no signed copy to upload — the CLI refuses it, publish
112
+ too (`[governedDocument/deadWiring]`); so does a reworded question while the
113
+ confirmation is off.
114
+ - **`--requires-one-of` holds the whole decision group** — Freigeben, Warteliste and
115
+ Ablehnen alike, for everyone incl. the process responsible; only „Bewerbung
116
+ ablehnen" (reject-anytime) stays open. Running applications on the old version are
117
+ not affected until they are re-pinned.
118
+ - **Re-publishing = a new workflow version.** `offer apply --publish` after an edit cuts
119
+ a new `workflow_def_versions` row (and re-pins the smartform versions).
120
+
121
+ ## Related
122
+ - [Add a workflow node](./add-workflow-node.md) — splice a new node (incl. the
123
+ `contractCreate`/`contractSign` mockup pair).
124
+ - [Attach a form to a workflow](./attach-form-to-workflow.md) — bind a form at add time.
125
+ - [Name what a node reads](./addresses.md) — the address flags and `node reads`.
126
+ - Command reference: `plexus-cli offer workflow --help`.
@@ -0,0 +1,147 @@
1
+ # Push accepted applications to Engage („Engage-Übertragung", TEMPORARY)
2
+
3
+ **When to use:** a school that still runs Engage SMIS wants each accepted
4
+ application transferred once, one-way, into Engage as a prospective pupil with its
5
+ contacts. The node `engagePush` sits right before the „angenommen" end, records
6
+ the push and goes on at once — the engine never waits for Engage. A stopped push opens
7
+ the team task „Engage-Übertragung prüfen". Each push is a document of the application,
8
+ „Übertragungsprotokoll" (Artefakte rail): every step, every request with what was
9
+ sent and what came back, and what was not sent — the acts sit in its action bar.
10
+ The node type is temporary and is removed with its slice.
11
+
12
+ ## Prerequisites
13
+ - A bundle on disk, a live tenant's only in a private scratch dir
14
+ ([Edit a live tenant's offer](./edit-live-tenant-offer.md)).
15
+ - The mapping as a JSON file (`node.engage`; `plexus-cli offer workflow node set-engage --help`).
16
+ It carries the school's Engage ids — lookup ids, AIF field ids, the admission stage,
17
+ the address format — so it lives next to the bundle, **never in version control**.
18
+ The `test` tenant carries no Engage push, so there is no example offer to export;
19
+ `offer validate` names every gap in a mapping you write.
20
+ The application's stage, school year, unit, offer or applicant come from an environment
21
+ variable by its address: `{ "from": { "address": { "scope": "env", "key": "stage.key" } }, "lookup": "yearGroup" }`
22
+ (the keys: IDE tab „Umgebungsvariablen"). An older mapping with
23
+ `{ "from": { "context": "stageKey" } }` is refused — export again.
24
+ Two answers that make ONE Engage value are joined: `{ "from": { "join": [".vorname", ".nachname"] } }`
25
+ (space-separated, in this order, empty unless every part is answered — never half a name;
26
+ a lookup then keys on the joined text, e.g. `"monatlich sepa"`). An answer the form leaves
27
+ optional but Engage requires gets an explicit Engage value: `{ "from": { "field": ".geschlecht" },
28
+ "lookup": "gender", "whenEmpty": "U" }` — sent as it stands, only for a form source and only
29
+ when the whole answer is missing (not when a `pick` finds nothing); it fills a record that
30
+ goes out, it never makes an unanswered optional record (address, e-mail, nationality …) go
31
+ out. Never use it to turn an unanswered (possibly hidden) yes/no question into „no".
32
+ The mapping is Engage vocabulary: a body field like `jobTitle` is fine (publish's i18n gate
33
+ skips `node.engage`).
34
+ - A team (permission-group key) that checks a stopped transfer.
35
+ - The deployment's Engage push mode — deployment configuration a platform operator sets,
36
+ not a CLI verb. Local and staging run `dry`; prod runs `live`, but with production not
37
+ allowed, so only the Sandbox school is written. Every push starts as a dry run; live is the cockpit's „Jetzt live übertragen"
38
+ after a clean dry run. The target `production` of the Engage connection is refused
39
+ unless the platform operator explicitly allowed production on that deployment — also
40
+ in a dry run.
41
+
42
+ ## Steps
43
+ ```bash
44
+ # 1. The accepting edge into the end:
45
+ plexus-cli offer workflow node list --dir <dir>
46
+
47
+ # 2. Splice the push onto it. It names what it reads by address (single
48
+ # quotes!): the form instance always; the contract and the payment when the
49
+ # mapping reads `{ contract: … }` / `{ payment: … }` sources. An uploaded
50
+ # contract is `$node.<contract>.document`, a generated one (`production:
51
+ # resolver`) `$node.<contract>.contract`.
52
+ plexus-cli offer workflow node add --dir <dir> --type engagePush \
53
+ --id engagePush --label "Engage-Übertragung" --label-en "Engage transfer" \
54
+ --on-edge contract:signed \
55
+ --assign-team bewerbungsteam \
56
+ --engage-target sandbox \
57
+ --engage-config <scratch>/engage-mapping.json \
58
+ --form '$node.form.admission' --contract '$node.contract.document' \
59
+ --payment '$node.collectPayment.payment'
60
+
61
+ # Change them later: node set --id engagePush --form|--contract|--payment '<address>'.
62
+ # An old bundle that still carries `wires` is refused
63
+ # ([graph/retiredField]) — export it again.
64
+
65
+ # Contract fields F01–F25 (meaning not confirmed by the school): --engage-contract-fields on
66
+
67
+ # 3. Later changes to the mapping:
68
+ plexus-cli offer workflow node set-engage --dir <dir> --node engagePush --config <scratch>/engage-mapping.json
69
+
70
+ # 4. Validate, plan, publish (publish only with the go of the process owner).
71
+ plexus-cli offer validate --dir <dir>
72
+ plexus-cli --env <env> offer plan --dir <dir>
73
+ ```
74
+
75
+ ## Verify
76
+ - `offer validate` shows no `[engagePush/*]` or `[address/*]` error (`[address/missing]`: no
77
+ `form`); `[engagePush/unmappedField]` (warning) lists mapped fields the form its `form`
78
+ address names does not have.
79
+ - `offer workflow node reads --dir <dir> --node engagePush` lists the three addresses.
80
+ - Every path into an `approved` end runs through the node (`[engagePush/bypassed]`).
81
+ - After an application reaches the node: its cockpit lists the document
82
+ „Übertragungsprotokoll · Engage · Sandbox · Probelauf" with the recorded requests; nothing
83
+ was sent. A finished dry run can run again („Probelauf wiederholen", e.g. after a form
84
+ correction) — a finished LIVE push never.
85
+
86
+ ## Go-live (platform operator only — production school, credentials)
87
+ Order matters; each step names why.
88
+ 1. **Put every offer on target `production` BEFORE the first real application starts.**
89
+ The target belongs to the PINNED version: an application started on a `sandbox` version
90
+ transfers to the Sandbox school forever, also after the offer is switched.
91
+ `node set` has no `--engage-target` flag: on an existing node, set its
92
+ `inputs.target` literal to `production` in the exported bundle, then `offer validate`
93
+ and `offer apply --publish`.
94
+ 2. **Allowing production and the Engage account** are deployment configuration: they need a
95
+ platform operator (an agent never sets them).
96
+ 3. **Sweep the dry runs that finished while the deployment was dry** — this needs a platform
97
+ operator too. A run opens its task only when it finishes, so a clean dry run from before
98
+ the switch would stay „Fertig" and never be transferred; the sweep (idempotent) opens
99
+ those tasks and reports `sandboxPinned`: the clean dry runs whose PINNED version still
100
+ targets the Sandbox school. They get no task (a live push would land in the Sandbox and
101
+ read as done) — re-pin each onto a `production` version (identical topology) or transfer
102
+ it by hand. The same rule refuses „Jetzt live übertragen" on such a run and opens no task
103
+ for it as soon as production is allowed (without that, a Sandbox live test stays
104
+ possible). Applications that ended without a place (rejected, withdrawn, waitlisted,
105
+ expired) are left out, live refuses them, and their open Engage task closes with them.
106
+ Every opened task „Engage-Übertragung prüfen" ends with „Jetzt live übertragen" (or with a
107
+ fixed answer + „Probelauf wiederholen" when the dry run lists issues). Test applications
108
+ are left out — they never go live.
109
+
110
+ ## Gotchas
111
+ - **Engage has no idempotency key.** Never re-run a push by hand: „Erneut übertragen"
112
+ resumes at the first open step and first asks Engage whether an unclear create landed.
113
+ An id the team types in is read back by id before it is taken.
114
+ - A create answers with the new id (`{"newPupilID":"4068"}`, `newContactID`, `newAddressID` —
115
+ observed in the sandbox write test); it is read back by id before it is taken. Without one, the id
116
+ comes from the difference of the list, verified the same way — never from a name search.
117
+ - A write without a body (`LinkAddressToPupil`) is sent with an EMPTY body: Engage answers
118
+ `411 Length Required` to a body-less PUT.
119
+ - A sibling already at the school holds the contact steps for the team: link the
120
+ existing Engage contact id or create a new contact.
121
+ - The API cannot enrol: the pupil lands as a prospective pupil at the configured
122
+ admission stage. Whether Engage enrols from its completion stage is open.
123
+ - A fee billed separately (`settlement: separate`) is never sent as „paid".
124
+ - **A dry run that waits for the team opens its task „Engage-Übertragung prüfen"**: when
125
+ it lists an issue (fix the answer or the mapping, then „Probelauf wiederholen"), or on a live
126
+ deployment when nothing stands between it and „Jetzt live übertragen". A clean re-run on a dry
127
+ deployment or „Jetzt live übertragen" closes it; a stop of the live run opens the live row's
128
+ own task. A gap that lies in the PINNED mapping (a value without an Engage id) keeps the task
129
+ open until the application is re-pinned or the answer changes.
130
+ - **The mapping an application uses is the one of its PINNED version.** A mapping fix reaches
131
+ applications started after the publish; older ones keep theirs (re-pin only with an identical
132
+ topology).
133
+ - **Address lines for address format `CL1-117`:** line 1 street + number (`subdwelling`), line 3
134
+ town (`street`), line 6 postcode (`postcode`). Line 5 feeds `town` — a postcode there is lost.
135
+ - **A lookup table is keyed by the exact value its source delivers — check each source's
136
+ vocabulary, `offer validate` does not.** The address field's `land` is its own closed
137
+ vocabulary in upper case (`DE`/`AT`/`CH`/`OTHER`), not the
138
+ lower-case ISO keys of a `countries` list. One `country` table for both made every
139
+ address „unmapped" (`[engagePush/unmappedLookup] … „DE"`), seen first in a production
140
+ dry run. Give such a source its own table (`addressCountry`). Leave a value
141
+ like `OTHER` unmapped: it names no country, and the push reports it instead of guessing.
142
+ Before going live, a dry run of an application with realistic data must list no issue
143
+ („Jetzt live übertragen" is hidden while any issue is listed).
144
+
145
+ ## Related
146
+ - [Name what a node reads](./addresses.md) · [Edit a live tenant's offer](./edit-live-tenant-offer.md)
147
+ - `plexus-cli offer workflow node add --help` (`--engage-*` flags) · `plexus-cli offer workflow node set-engage --help`