@smartkomm/plexus-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +863 -2
  2. package/dist/cli.mjs +5432 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +224 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/agents/use-shared-tools.md +52 -0
  7. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  8. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  9. package/dist/skill/references/audit/answer-access-request.md +115 -0
  10. package/dist/skill/references/config/attach-a-template.md +117 -0
  11. package/dist/skill/references/config/init-tenant.md +121 -0
  12. package/dist/skill/references/config/onboard-tenant.md +71 -0
  13. package/dist/skill/references/config/rename-program-stage.md +99 -0
  14. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  15. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  16. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  17. package/dist/skill/references/config/update-tenant-config.md +174 -0
  18. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  19. package/dist/skill/references/export/create-named-export.md +47 -0
  20. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  21. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  22. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  23. package/dist/skill/references/finance/record-submission-date.md +33 -0
  24. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  25. package/dist/skill/references/marketing/demo-video-links.md +101 -0
  26. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  27. package/dist/skill/references/notifications/mail-level.md +90 -0
  28. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  29. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  30. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  31. package/dist/skill/references/offer/addresses.md +129 -0
  32. package/dist/skill/references/offer/appointment-task.md +79 -0
  33. package/dist/skill/references/offer/assignment-rule.md +195 -0
  34. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  35. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  36. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  37. package/dist/skill/references/offer/author-smartform.md +102 -0
  38. package/dist/skill/references/offer/bind-node-input.md +76 -0
  39. package/dist/skill/references/offer/completeness-check.md +137 -0
  40. package/dist/skill/references/offer/countersign-node.md +91 -0
  41. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  42. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  43. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  44. package/dist/skill/references/offer/edit-smartform.md +121 -0
  45. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  46. package/dist/skill/references/offer/engage-push-node.md +147 -0
  47. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  48. package/dist/skill/references/offer/node-tasks.md +162 -0
  49. package/dist/skill/references/offer/optional-assessment.md +65 -0
  50. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  51. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  52. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  53. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  54. package/dist/skill/references/offer/task-family-request.md +69 -0
  55. package/dist/skill/references/offer/task-result-files.md +52 -0
  56. package/dist/skill/references/offer/template-document-node.md +113 -0
  57. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  58. package/dist/skill/references/resolver/manage-contract-resolver.md +161 -0
  59. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  60. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  61. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  62. package/dist/skill/references/signing/README.md +113 -0
  63. package/dist/skill/references/signing/connect-a-school.md +90 -0
  64. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  65. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  66. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  67. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  68. package/dist/skill/references/test-actions/drive-a-test-application.md +69 -0
  69. package/dist/skill/references/users/add-platform-admin.md +59 -0
  70. package/dist/skill/references/users/add-platform-user.md +59 -0
  71. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  72. package/dist/skill/references/users/change-login-email.md +55 -0
  73. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  74. package/dist/skill/references/users/delete-permission-group.md +76 -0
  75. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  76. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  77. package/dist/skill/references/users/set-password.md +57 -0
  78. package/dist/skill/references/users/use-test-account.md +79 -0
  79. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  80. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  81. package/dist/spec.snapshot.json +156821 -0
  82. package/package.json +53 -4
@@ -0,0 +1,137 @@
1
+ # Wait for outstanding documents (completeness check)
2
+
3
+ **When to use:** a school lets the process run on while documents are still
4
+ missing — the Masernschutz-Nachweis, the birth certificate — and wants it to end
5
+ only once they are there, with a deadline („erst wenn wirklich alle Unterlagen da
6
+ sind, ist es vorbei"). The `completenessCheck` node names SmartForm fields
7
+ (`requires`) and a deadline. Entered with nothing missing it completes at once;
8
+ otherwise the family gets its fields in **one** task „Offene Felder beantworten" due by the
9
+ deadline, the optional guest role is granted, and the node parks until the last
10
+ field arrives (`complete`) or a person decides: „Ohne Nachreichung abschließen"
11
+ (`acceptedIncomplete`) or „Bewerbung ablehnen" (`rejected`). The family's
12
+ withdrawal arrives as `absage`.
13
+
14
+ ## Prerequisites
15
+ - An offer bundle on disk: `offer init` for a new one, or a **fresh export** of
16
+ the target environment into a private scratch dir (a live tenant's bundle is
17
+ never kept in a shared or versioned place — the server is its source of truth).
18
+ - The fields exist in the form the bundle's form node pins, as **entered,
19
+ non-repeated** fields (a computed key or a field inside a repeated group
20
+ cannot be handed in). Each is named by its full address,
21
+ `$node.<formNode>.admission.<field>` — the form node that CREATES the
22
+ instance, in single quotes. A bundle exported before addresses were introduced is
23
+ refused — export again.
24
+ - For a guest role: a **leaf** role in the tenant's role catalog (e.g.
25
+ `gasthoerer`) — checked at `offer apply --publish`, not offline.
26
+ - The graph has somewhere to send `rejected` and `absage` (the paths to the
27
+ end of a rejection and of a withdrawal — an end with `dataDecision: discard`
28
+ takes the application's records back itself, no discard node needed).
29
+
30
+ ## Steps
31
+ ```bash
32
+ # 1. Export what is live (read-only).
33
+ plexus-cli --env staging --tenant <tenant> offer export --slug <offer> --out ./bundle
34
+
35
+ # 2. Add the check. Every flag below except --guest-role/--person is required:
36
+ # - at least one --require address (repeatable, comma-separated),
37
+ # - exactly one deadline: --deadline-days <n> (calendar days after entering)
38
+ # OR --deadline-until <YYYY-MM-DD> (a fixed due day, tenant zone),
39
+ # - the `requested` letter (the node type declares it REQUIRED),
40
+ # - rejected/absage routed to the rejection/withdrawal paths.
41
+ # `requires` and the guest role's person name the form they read; nothing is
42
+ # bound implicitly (a bare key is refused with the addresses the node can read).
43
+ plexus-cli offer workflow node add --dir ./bundle --type completenessCheck \
44
+ --id nachreichen --label "Vollständigkeitsprüfung" --label-en "Completeness check" \
45
+ --require '$node.form.admission.kind.masernschutz,$node.form.admission.kind.geburtsurkunde' \
46
+ --deadline-days 14 \
47
+ --guest-role gasthoerer --person '$node.form.admission.beneficiary' \
48
+ --notification requested=admission.documentsRequested \
49
+ --outcomes complete:end,acceptedIncomplete:end,rejected:joinAbsage,absage:endAbsage
50
+
51
+ # Later edits:
52
+ plexus-cli offer workflow node add-requires --dir ./bundle --node nachreichen --address '$node.form.admission.kind.impfpass'
53
+ plexus-cli offer workflow node remove-requires --dir ./bundle --node nachreichen --from form --field kind.impfpass
54
+ plexus-cli offer workflow node set-requires --dir ./bundle --node nachreichen --from form --field kind.masernschutz
55
+ plexus-cli offer workflow node set --dir ./bundle --id nachreichen --deadline-until 2026-10-15
56
+ plexus-cli offer workflow node set --dir ./bundle --id nachreichen --guest-role "" # drops role AND person
57
+ plexus-cli offer workflow node set-notification --dir ./bundle \
58
+ --node nachreichen --event requested --template admission.documentsRequested
59
+
60
+ # 3. Validate offline, see the difference, publish.
61
+ plexus-cli offer validate --dir ./bundle
62
+ plexus-cli --env staging offer plan --dir ./bundle
63
+ plexus-cli --env staging offer apply --publish --dir ./bundle
64
+ ```
65
+
66
+ ## Verify
67
+ ```bash
68
+ plexus-cli offer workflow node list --dir ./bundle
69
+ # nachreichen completenessCheck … complete→…, acceptedIncomplete→…, rejected→…, absage→…
70
+ # letters: requested→admission.documentsRequested
71
+ jq '.graph.nodes[] | select(.id=="nachreichen") | {requires, deadline, guestRole, inputs}' ./bundle/workflow.json
72
+ plexus-cli offer workflow node reads --dir ./bundle --node nachreichen
73
+ ```
74
+ After publish, a fresh application that reaches the node with a field empty
75
+ shows the family task „Offene Felder beantworten" and the board column of the same
76
+ name; a deployment that does not send mail records the letter in Dev Rail
77
+ → „Mail" instead.
78
+
79
+ ## Idempotency / gotchas
80
+ - **The `requested` letter is required.** Without a binding the family would get
81
+ no mail about the request, so every write refuses it
82
+ (`[notification/missingBinding]`) — bind it in the `add` call with
83
+ `--notification requested=<templateId>`. `--disabled` counts as missing.
84
+ Offline validation cannot see the letter catalog: a misspelled template id is
85
+ caught only at `offer apply --publish`.
86
+ - **Deadline rules:** exactly one form. `--deadline-days` is a whole number 0–366
87
+ (`[completenessCheck/deadlineDays]`); `--deadline-until` must be a real
88
+ calendar day (`2026-02-30` is refused, `[completenessCheck/deadlineDate]`).
89
+ Passing both, or neither on `add`, fails before anything is written. Past the
90
+ deadline nothing happens by itself: the family and the responsible person are
91
+ told, and a person decides.
92
+ - **Guest role:** `--guest-role` needs `--person`, and `--person` needs
93
+ `--guest-role` — nothing is defaulted. The person is a party of a form,
94
+ `$node.form.admission.beneficiary` (the beneficiary the form's contract parties
95
+ name), read live at that address whenever the role is granted or ended; a form
96
+ that does not run before the check is `[address/notVisible]`. The role must be a **leaf** tenant
97
+ role (`[completenessCheck/unknownRole]`, `[completenessCheck/nonLeafRole]`,
98
+ both at publish). It is granted only when something is missing, ended on
99
+ `complete` / `rejected` / `absage`, and **kept on `acceptedIncomplete`** — the
100
+ school still has to know whose documents are outstanding. A role the person
101
+ already held is never ended.
102
+ - **Entering with nothing missing completes automatically** — no request, no
103
+ letter, no role; the node passes straight through on `complete`.
104
+ - **Where it lands:** without `--after` the check is spliced in front of the
105
+ FIRST `end` node in `workflow.json` (every edge into that end now enters the
106
+ check) — make sure that is the positive end. With `--after <node>` the
107
+ anchor's FIRST onward edge is the one rerouted; on an anchor with several
108
+ outcomes (a contract: `signed` + `absage`) that may be the wrong one. Check
109
+ with `node list` and fix with `node set --outcomes`.
110
+ - **Routing:** `complete`, `rejected` and `absage` must be routed
111
+ (`[completenessCheck/unroutedOutcome]`); the staff buttons appear only for the
112
+ outcomes that are wired. Left to the splice default, `rejected`/`absage` run
113
+ into whatever follows (usually the positive end) — `add` warns about that; route
114
+ them with `--outcomes`.
115
+ - **`requires` edits:** only a completenessCheck carries `requires`
116
+ (`*-requires` on another node fails; for fields a decider reads use
117
+ `*-reads`). Removing the last address is refused (`[completenessCheck/missingRequires]`);
118
+ two forms in one list are `[address/mixedForms]`, a field the form lacks is
119
+ `[address/pathNotInTarget]`.
120
+ Only errors an edit introduces block it.
121
+ - **Hand-authored boards (known CLI gap):** `node add` derives the progress
122
+ states and appends the check's column „Unterlagen nachreichen"; its
123
+ `rejected`/`withdrawn` states go into the global `col.abgelehnt` /
124
+ `col.abgesagt` columns. A bundle whose board already names its own exit
125
+ columns (e.g. a `c.absage` column labelled „Abgelehnt")
126
+ then carries two columns with one label, and `node add` refuses the write —
127
+ for ANY node type with such an event (`waitlist`, `decision`, …), not only this
128
+ one. Until `node add` can map events onto existing states, author the node in
129
+ `workflow.json` + the states/columns/wiring in `manifest.json`/`workflow.json`
130
+ by hand (shape: the Verify `jq` output above) and gate it with `offer validate`
131
+ / `offer apply --publish`. Scaffolded bundles (`offer init`) are not affected.
132
+ - **Running applications keep their pinned version** — see
133
+ [Declare what a document node reads](./declare-node-reads.md) for re-pin / wipe.
134
+
135
+ ## Related
136
+ - [Add a workflow node](./add-workflow-node.md) · [Edit a workflow node](./edit-workflow-node.md) · [Release an offer bundle](./publish-offer-bundle.md)
137
+ - [Reword a letter](../notifications/reword-a-letter.md)
@@ -0,0 +1,91 @@
1
+ # Countersign the contract („Unterschrift (intern)")
2
+
3
+ **When to use:** the school countersigns the admission contract the family signed. A `countersign` node after the contract opens one row „Vertrag gegenzeichnen" for a
4
+ team: the clerk downloads the family's signed copy, signs it for the Träger, uploads the
5
+ scan with the date. That opens the Träger's own Signaturvorgang on the revision the family
6
+ signed and books the Träger („vertreten durch <Mitarbeiter/in>") on it, so the contract is
7
+ signed and the family's portal shows the countersigned paper; it sends the node's
8
+ `countersigned` letter, and the contract's event moves the node along `countersigned`. It replaces
9
+ the retired task result `countersignature` (`--result countersignature` is now an unknown
10
+ kind) and its review host node.
11
+
12
+ ## Prerequisites
13
+ - A bundle on disk (`offer export`, a live tenant's only into a private scratch dir —
14
+ [Edit a live tenant's offer](./edit-live-tenant-offer.md)). Authoring is offline.
15
+ - The contract node is a `governedDocument` with `production: "resolver"` and
16
+ `responseStrategy: "signedUploadChecked"` (the school checks the signed return; only that
17
+ freezes a revision with declared signers) — `[countersign/sourceNotCheckedContract]`
18
+ otherwise.
19
+ - A team (permission-group key) that countersigns, e.g. `schulverwaltung`.
20
+
21
+ ## Steps
22
+ ```bash
23
+ # 1. The contract's accepting edge (`signed`) and what follows it:
24
+ plexus-cli offer workflow node list --dir <dir>
25
+
26
+ # 2. Splice the countersignature onto that edge. Required: the team, the contract's
27
+ # address (a resolver contract is `$node.<contractNode>.contract`) and the
28
+ # letter (the node type declares it REQUIRED).
29
+ plexus-cli offer workflow node add --dir <dir> --type countersign \
30
+ --id gegenzeichnung --label "Gegenzeichnung" --label-en "Countersignature" \
31
+ --on-edge contract:signed \
32
+ --assign-team schulverwaltung \
33
+ --title "Vertrag gegenzeichnen" --title-en "Countersign the contract" \
34
+ --due-in-days 5 \
35
+ --contract '$node.contract.contract' \
36
+ --notification countersigned=admission.contractCountersigned
37
+
38
+ # Optional: put its board event on an existing state instead of a new
39
+ # „Gegenzeichnung" column: --progress countersigning=<stateId>
40
+
41
+ # 3. Validate, plan, publish.
42
+ plexus-cli offer validate --dir <dir>
43
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir>
44
+ plexus-cli --env staging --tenant <t> offer apply --publish --dir <dir>
45
+ ```
46
+
47
+ ## Verify
48
+ ```bash
49
+ plexus-cli offer workflow node list --dir <dir>
50
+ # contract governedDocument … signed→gegenzeichnung
51
+ # gegenzeichnung countersign … countersigned→<old target>
52
+ # letters: countersigned→admission.contractCountersigned
53
+ plexus-cli offer workflow node reads --dir <dir> --node gegenzeichnung # $node.contract.contract, written in nodes.gegenzeichnung.contract
54
+ jq '.graph.nodes[] | select(.type=="countersign")' <dir>/workflow.json
55
+ ```
56
+ After publish, an application whose signed return the school accepted parks on the node;
57
+ the team's queue shows „Vertrag gegenzeichnen", the family's portal „Die Schule zeichnet
58
+ Ihren Vertrag gegen". After the upload the revision reads „3 von 3" and the family gets the
59
+ letter (on a deployment that does not send mail: Dev Rail → „Mail").
60
+
61
+ ## Idempotency / gotchas
62
+ - **The publish rules:** `[countersign/assign]` (no team), `[countersign/dueInDays]` (a whole
63
+ number ≥ 1), `[countersign/sourceNotCheckedContract]`, `[countersign/twice]` (one
64
+ countersignature per contract), `[address/missing]` (no `contract`),
65
+ `[address/unknownKey]` (a resolver contract produces `contract`, not `document`),
66
+ `[address/notVisible]` (the contract must lie on every path to the node),
67
+ `[notification/missingBinding]` (no `countersigned` letter; `--disabled` counts as missing).
68
+ Offline the letter catalog is not loaded: a misspelled template id fails only at
69
+ `offer apply --publish`.
70
+ - **The letter moved:** `countersigned` is bound on the countersign node, not on the contract.
71
+ A bundle that still binds it on the contract is refused there (`[notification/unknownEvent]`)
72
+ — clear it with `node clear-notification --node contract --event countersigned`.
73
+ - **`--title` needs `--title-en`** (every config text is `{ de, en }`); without both the row
74
+ is titled „Vertrag gegenzeichnen" / „Countersign the contract". `--assign-team`, `--title`,
75
+ `--title-en` and `--due-in-days` are refused on any other node type.
76
+ - **The row closes only through the countersignature dialog**, never by a bare dispatch: the
77
+ engine moves the node only on the contract's `signed` event and refuses it while the contract
78
+ is not signed by every side („Der Vertrag ist noch nicht von allen Seiten unterschrieben."). Withdrawal and „Bewerbung ablehnen" sweep
79
+ the open row; nothing of the school is booked.
80
+ - **The representative is whoever completes the row** — active staff of the tenant with a
81
+ name; nobody can book a colleague's signature.
82
+ - **Migrating a bundle with the old shape** (a review „Gegenzeichnung" + task
83
+ `result: { kind: 'countersignature' }`): remove the task and the review
84
+ (`node task rm`, `node rm`), add the countersign node as above (team from the old
85
+ `assign.decision`, title / SLA from the task), move the letter, and re-key the progress
86
+ wiring from `reviewing` to `countersigning`.
87
+
88
+ ## Related
89
+ - [Name what a node reads](./addresses.md) · [Add a workflow node](./add-workflow-node.md)
90
+ - [Insert a node on one edge and rewire it](./insert-and-rewire-node.md) (`--on-edge`, `--progress`)
91
+ - [Edit a live tenant's offer](./edit-live-tenant-offer.md)
@@ -0,0 +1,55 @@
1
+ # Make a new school applyable (create a pedagogy unit)
2
+
3
+ **When to use:** a brand-new school (or an existing plain unit) needs to host an
4
+ admission offer, but `offer create --unit <unit>` fails because the unit isn't a
5
+ *pedagogy unit* yet (no `pedagogy_units` bridge row). This creates the unit + bridge
6
+ so the external "bewerben" portal can resolve it.
7
+
8
+ ## Prerequisites
9
+ - A configured `plexus-cli` targeting the env (`--env local|staging|prod`).
10
+ - A token with tenant-wide master-data access (creating units) +
11
+ tenant access (the bridge writer is tenant-gated).
12
+ - An installed, current `plexus-cli` (`plexus-cli offer unit create --help`).
13
+
14
+ ## Steps
15
+ ```bash
16
+ # 1a. Brand-new school: create the unit AND its pedagogy bridge in one step.
17
+ # The name is German AND English.
18
+ plexus-cli --env <env> --tenant <slug> offer unit create --name "Grundschule Neustadt" --name-en "Neustadt Primary School"
19
+ # (--kind school by default; --parent <unit key> to nest; output prints the unitId)
20
+
21
+ # --unit takes the unit's config key (`plexus-cli units` lists it) — never its
22
+ # name. A unit made by `offer unit create` has no config key until config-as-code
23
+ # adopts it: address it by its URL slug meanwhile (here: grundschule-neustadt).
24
+
25
+ # 1b. OR upgrade an EXISTING plain unit to a pedagogy unit (idempotent).
26
+ plexus-cli --env <env> --tenant <slug> offer unit ensure-pedagogy --unit grundschule-neustadt
27
+
28
+ # 2. Now the offer can link it. (offer create/set --unit also ensures the bridge,
29
+ # so an existing non-pedagogy unit is upgraded automatically on link.)
30
+ plexus-cli --env <env> --tenant <slug> offer create --slug gs-neustadt --title "Grundschule Neustadt" --title-en "Neustadt Primary School" --unit grundschule-neustadt
31
+ ```
32
+
33
+ ## Verify
34
+ ```bash
35
+ plexus-cli --env <env> --tenant <slug> units # the unit shows pedagogic ✓
36
+ plexus-cli --env <env> --tenant <slug> offer status --slug gs-neustadt # program/pedagogy links resolve
37
+ ```
38
+
39
+ ## Idempotency / gotchas
40
+ - **`ensure-pedagogy` is idempotent** — re-running returns the existing bridge
41
+ (`created: false`); it never inserts a duplicate.
42
+ - **`offer create/set --unit` ensures-then-links** — you don't strictly need a
43
+ separate `ensure-pedagogy` step for an existing unit; the link path upgrades it.
44
+ - **A unit needs a tenant legal entity.** `offer unit create` auto-resolves the
45
+ tenant's first legal entity; a tenant with none will fail loud (set one up first).
46
+ - **Tenant-scoped, fail-loud.** The bridge writer (`programAdmin.ensurePedagogyUnit`)
47
+ verifies the unit belongs to the tenant; it never bridges a foreign unit.
48
+ - **Config is authoritative for the flag.** Also set it in the tenant config
49
+ (`plexus-cli config set --tenant <slug> --target units --key <unitKey> --field pedagogyUnit --value true`),
50
+ or the next `config apply` of a config without it removes the bridge again — or,
51
+ once an offer is linked, fails with `[unit/pedagogyUnitInUse]`.
52
+
53
+ ## Related
54
+ - [Author the manifest meta layer](./author-manifest-meta.md) · [Author a SmartForm as code](./author-smartform.md).
55
+ - Command reference: `plexus-cli offer unit --help`.
@@ -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`.