@klok-integration/cli 0.7.248 → 0.7.269

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.
package/README.md CHANGED
@@ -25,8 +25,8 @@ variable: `KLOK_AGENT_URL`, `KLOK_TENANT`, `KLOK_DOMAIN`, `KLOK_DEV_URL`, `KLOK_
25
25
  `KLOK_KEYCLOAK_CLIENT_ID`, `KLOK_GIT_URL`, `KLOK_GIT_LOCAL_WORKSPACE`, `KLOK_GIT_APIKEY`. Run
26
26
  `klok config list` to see what is set and where it comes from.
27
27
 
28
- The GitLab token can be stored with `klok config set klokApiKey` (a hidden prompt, so it stays out of the shell
29
- history). It is shown as `****` everywhere unless you run `klok config get klokApiKey --reveal`, and the file
28
+ The GitLab token can be stored with `klok config set klokApiKey`: it asks `Enter API key:` and shows a `*` for
29
+ each character, so the token stays off the screen and out of the shell history. It is shown as `****` everywhere unless you run `klok config get klokApiKey --reveal`, and the file
30
30
  is kept at mode 0600.
31
31
 
32
32
  Credentials are written to `~/.config/klok/credentials.json` with mode 0600.
@@ -53,7 +53,9 @@ Credentials are written to `~/.config/klok/credentials.json` with mode 0600.
53
53
  | `klok docs id [path]` | The doc id of a repository path. |
54
54
  | `klok docs publish` | Build this repository's znai documentation and upload it to the docs portal. |
55
55
  | `klok docs build` | Build this repository's `klok-docs/` with znai the way its documentation project will, uploading nothing. |
56
- | `klok solutions validate` | Check the descriptors of an integration solution registry against the descriptor contract. |
56
+ | `klok solutions next` | The next free solution id of the tenant, from the registry, its open merge requests and the route classes on every branch of the workspace. |
57
+ | `klok solutions new "<name>"` | Reserve the next solution id by a merge request in the registry, from a task worktree. |
58
+ | `klok solutions validate` | Check the descriptors of an integration solution registry against the descriptor contract and, in a merge request, that no solution is removed and no solution id reused or taken twice. |
57
59
  | `klok ci env` | Show what klok-cli detected about the CI job it runs in. |
58
60
  | `klok fleet status\|rebuild` | Every deployed service: what it was built from, whether that is the latest commit and the catalog's broker client, and which clients the broker sees; rebuild the stale ones one pipeline at a time. |
59
61
  | `klok manifest update` | Set a container's image in a deploy manifest of your GitOps repository, then commit and push. |
@@ -8,19 +8,23 @@ dependency catalog. Read `klok docs read klok-framework-klok-common` for naming
8
8
  ### Rules
9
9
 
10
10
  * One root element `<Name>_vN` that references every complex type; a single `xs:` prefix; `targetNamespace` and
11
- `xmlns:tns` set; `elementFormDefault="qualified"`. Validate with the XSD validation of the CI template before
12
- pushing (`python3 scripts/validate-xsd.py` from build-ci-templates, or the pipeline's `generate_avro_from_xsd`).
11
+ `xmlns:tns` set; `elementFormDefault="qualified"`. Validate with `klok datamodel validate` before pushing; it
12
+ applies the rules of the pipeline's `generate_avro_from_xsd`.
13
13
  * One name, in kebab case, from the repository down: `customer-v1` is the repository, the XSD file
14
14
  (`customer-v1.xsd`), the Maven artifact and the npm and Python packages, and `customer_v1` ends the Java package.
15
15
  Inside the XSD the model keeps its PascalCase form: root element `Customer_v1`, root type and class `CustomerV1`.
16
16
  * The major version is part of the model name; a breaking change is a new major version and a new topic, never a
17
17
  silent change of an existing one.
18
- * `meta.json` uses the flat form with `targetNamespace`; it drives the generated project's group, name and
19
- documentation.
20
- * Consumers reference the artifact only through the catalog alias (`klokLibs.<key>`), never by literal version.
18
+ * `klok-datamodel-project.json` at the root is the model's metadata (name, description, class, model type,
19
+ `targetNamespace`, template version), rendered from `klok/environment/project-template-datamodel`; klok.dev and
20
+ the documentation payload read it. The pipeline takes the artifact and the Java package from the repository path
21
+ and the XSD file name, never from metadata.
22
+ * Consumers reference the artifact only through the catalog alias (`klokLibs.<key>` for tenant klok's models,
23
+ `tenantLibs.<key>` for another tenant's), never by literal version.
21
24
  * Datamodel documentation lives in `klok-docs/` (purpose, fields that need explanation, producers and consumers,
22
- design decisions). The datamodel pipeline's `klok_docs_publish` job sends it with the XSD to docs.klok.dev, where
23
- the model is documented from the XSD (a structure page, then AI sections from the complex types up) under
25
+ design decisions); `klok-docs/README.md` says which page holds which, and is never published. The datamodel
26
+ pipeline's `klok_docs_publish` job sends it with the XSD to docs.klok.dev, where the model is documented from the
27
+ XSD (a structure page, then AI sections from the complex types up) under
24
28
  `<tenant>/documentation/datamodel/<same path>`. Put `xs:documentation` on types and fields; that is what the
25
29
  generated text is written from.
26
30
  * The repository's path is its class: `public` (A), `common` (B), `domain/<d>` (C) or `system/<s>` (X). The wider
@@ -24,6 +24,10 @@ content arrives here. There are two kinds, and they are edited differently:
24
24
  * The doc id must start with the tenant prefix (`<tenant>-...`); the pipeline refuses others. Do not set the
25
25
  tenant, the Vault path or upload tokens in the project CI. This project's `.gitlab-ci.yml` is the include of
26
26
  `klok/environment/build-ci-templates:/klok-docs-publish.yml` and nothing else; the job is `klok docs publish`.
27
+ * `klok-documented-project.json` at the root names the source project and the doc ids, and KlokDocumentationEgress
28
+ writes it. `"state": "retired"` there makes `klok docs publish` take every doc id it names (`docId`,
29
+ `previousDocIds`) off the portal instead of uploading; `--dry-run` lists them. Do not edit it to keep a project
30
+ published: its state follows the source project.
27
31
  * Visibility follows `meta.json` `scope` (`global`, `tenant`, `domain`, `system`); a page that must not be seen by
28
32
  another domain belongs in a project with that scope, not behind a note.
29
33
  * Preview locally with `znai preview --source=docs`, and check what a publish would do with
@@ -18,7 +18,8 @@ needs. Read `klok docs read klok-framework-klok-quarkus-extension` before writin
18
18
 
19
19
  ### Rules
20
20
 
21
- * Route class name = route id `<TENANTSHORT><NNN><LEG><seq>_<Description>`; one solution id per business flow.
21
+ * Route class name = route id `<TENANTSHORT><NNN><LEG><seq>_<Description>`; one solution id per business flow, and
22
+ its prefix is a solution registered in `<tenant>/integration-solutions` (a new one: `klok solutions new`).
22
23
  Configuration keys are `<RouteId>.<key>` with fallback to `<SolutionId>.<key>` and `klok.route-defaults.<key>`.
23
24
  * Datamodels are the type parameters of the pattern class and come from the catalog bundles; never hand write DTOs
24
25
  for something a datamodel project should own.
@@ -50,7 +51,9 @@ needs. Read `klok docs read klok-framework-klok-quarkus-extension` before writin
50
51
  reached with `<RouteId>.topic.tenant-override`. Never reuse another service's credentials.
51
52
  * `application.properties` keeps every `klok.*` key explicit; secrets come from environment variables that the
52
53
  Vault env secret provides in Kubernetes (`KLOK_BROKER_AUTH_CLIENT_ID`, ...).
53
- * Update `klok-docs/` (overview, design decisions, routes, operations) in the same merge request as the change.
54
+ * Update `klok-docs/` (overview, design decisions, routes, usage, operations) in the same merge request as the
55
+ change. `klok-docs/README.md` says which page answers which question and how the folder reaches docs.klok.dev;
56
+ it is the folder's guide and is never published.
54
57
  * On `main` the pipeline builds the image, updates the deployment manifest and publishes the documentation payload
55
58
  that KlokDocumentationEgress turns into the documentation project; check it.
56
59
  * The `Dockerfile` copies `build/quarkus-app/lib/` in a `COPY` of its own before `quarkus-run.jar`, `quarkus/`
@@ -14,7 +14,8 @@ what went wrong (no token, no network); say so instead of guessing.
14
14
 
15
15
  On a workstation, `klok sync pull` then brings the workspace current before you read it: it fast-forwards every
16
16
  canonical checkout under the workspace root, never merges or resets one, and skips `<root>/worktrees`. It is the one
17
- sanctioned update of a canonical checkout. A service never clones a repository just to read it: klok.dev and the agent
17
+ sanctioned update of a canonical checkout. A checkout whose GitLab project moved, was archived or is gone is reported
18
+ with the commands that fix it, and those are the person's to run. A service never clones a repository just to read it: klok.dev and the agent
18
19
  read through the GitLab API, docs.klok.dev and the integration context. The `klok-cli` skill says which klok command
19
20
  does what.
20
21
 
@@ -300,6 +301,11 @@ solution it belongs to, and new development and maintenance follow the same proc
300
301
  * Route ids: `<TENANTSHORT><NNN><LEG><seq>_<Description>` (`KLK002I1_OdlScheduledQuery`); legs `I` ingress,
301
302
  `P` processing, `E` egress, `A` API, `S` sub route, `T` timer. The class name is the route id, the config prefix
302
303
  and the solution key.
304
+ * `<TENANTSHORT>` is the tenant's `shortname` in tenants.yml of `klok/environment/klok-environment-setup`: the first
305
+ three consonants of the tenant slug in upper case (`klok` KLK, `degerfors` DGR, `konvoj` KNV), decided by the
306
+ platform. A solution id `<TENANTSHORT><NNN>` is allocated by the merge request that adds its folder to
307
+ `<tenant>/integration-solutions` (`klok solutions new`) and never reused; a solution that stops running is retired
308
+ with `"status": "retired"`.
303
309
  * Topics: `persistent://<tenant>[_<env>]/<domain>/<Model>[_<purpose>]`, request/reply
304
310
  `ReqRes_<Req>_<Res>_Request|_Response`, dead letters `<topic>_DLQ`. The datamodel major version is in the class
305
311
  name (`CustomerV1`). The purpose is optional and the publisher's: the solution requires it so that one datamodel
@@ -327,7 +333,7 @@ access, Keycloak broker clients or a service that serves several tenants.
327
333
  * **Write narrow, consume wide.** A tenant publishes only in its own namespace, `<tenant>[_<env>]/<domain>`; nobody
328
334
  holds `publish` in another tenant's namespace, not even on one topic. Reading elsewhere is a `consume` grant on a
329
335
  named topic, approved by the owner and recorded in Git in the owning tenant. The topic keeps its own name; the
330
- reader is known by its subscription (its route id, `DEG012E1_...`). When the content belongs to one reader, the
336
+ reader is known by its subscription (its route id, `DGR012E1_...`). When the content belongs to one reader, the
331
337
  publisher may choose that reader's tenant slug as the purpose
332
338
  (`persistent://konvoj/hr/PersonnelScheduleExportV1_degerfors`); that is the publisher's choice, not a condition
333
339
  for reading across tenants.
@@ -392,6 +398,9 @@ Klok documentation has three levels, and a change is finished only when all thre
392
398
  Write both **as the work happens**, not at the end of the branch. By the end nobody remembers why a choice was
393
399
  made, and a guide written from memory describes the product somebody meant to build.
394
400
 
401
+ `klok-docs/README.md` is the folder's own guide for the people and agents who write it: a project made from a
402
+ Klok template has one that says which page answers which question. It is neither published nor read by Kloker.
403
+
395
404
  Keep `klok-docs/` as the single copy. Do not mirror a page into a second `docs/` folder: two copies of one page
396
405
  are two pages that quietly stop agreeing. `docs/` may hold what is genuinely not product documentation, such as
397
406
  a strategy document that predates the code.
@@ -16,7 +16,8 @@ it as the source does.
16
16
  | Domain | A business area inside a tenant. `common` is shared by the tenant's domains, `public` is open to anyone |
17
17
  | Route, route id | One Camel route per Java class. Its id is `<TENANTSHORT><NNN><LEG><seq>_<Description>`, for example `KLK007T1_PublishKlokStandard`; the class name is the id |
18
18
  | Leg | The letter in a route id: I ingress (onramp, brings data in), P processing, E egress (offramp, delivers data out), A API, S sub route, T timer |
19
- | Solution | The business flow a set of routes implements; its id is the route id's prefix, for example `KLK007` |
19
+ | Tenant short name | `TENANTSHORT`, three upper case letters that start every solution and route id of a tenant: the first three consonants of the tenant's name (`KLK` for klok, `DGR` for degerfors), decided by the platform |
20
+ | Solution | The business flow a set of routes implements; its id is the route id's prefix, for example `KLK007`. The id is allocated once, by a merge request in the tenant's `integration-solutions` registry, and never reused; a solution is reserved, active or retired |
20
21
  | Solution document | The `README.md` of a solution in the tenant's `integration-solutions` registry: its services, legs and flows, kept true to what is built |
21
22
  | Correlation id | `klok-correlation-id`: follows one business flow across routes and messages |
22
23
  | Task | Long-running work tracked by the platform (`KlokTaskV1` events, visible in klok.dev and the task API), for example an `ai.chat` task that asks Kloker for text |
@@ -29,9 +29,11 @@ documentation or the dependency catalog can give you.
29
29
  ## Routes and naming
30
30
 
31
31
  * One Camel route per Java class. The route id is the class name, the configuration prefix and the solution key:
32
- `<TENANTSHORT><NNN><LEG><seq>_<Description>` (`KLK002I1_OdlScheduledQuery`, `ACME001E1_FortnoxInvoiceOfframp`).
33
- * `TENANTSHORT`: the tenant short name, upper case. `NNN`: the solution number from the klok.dev registry, at
34
- least three digits. The solution id is `<TENANTSHORT><NNN>` (`KLK002`).
32
+ `<TENANTSHORT><NNN><LEG><seq>_<Description>` (`KLK002I1_OdlScheduledQuery`, `ACM001E1_FortnoxInvoiceOfframp`).
33
+ * `TENANTSHORT`: the tenant's short name, three upper case letters: the first three consonants of the tenant
34
+ slug (`klok` KLK, `degerfors` DGR, `konvoj` KNV), decided by the platform and recorded as `shortname` in
35
+ tenants.yml of `klok/environment/klok-environment-setup`. `NNN`: the solution number, three digits; 900–999
36
+ are the framework's. The solution id is `<TENANTSHORT><NNN>` (`KLK002`).
35
37
  * Legs: `I` ingress, `P` processing, `E` egress, `A` API (REST, SOAP, MCP), `S` sub route or custom route,
36
38
  `T` timer. `seq` numbers the routes of one leg in a solution.
37
39
  * Every route extends exactly one pattern base class; never write a bare Camel `RouteBuilder`:
@@ -65,7 +67,10 @@ documentation or the dependency catalog can give you.
65
67
 
66
68
  * Every route belongs to a solution, and every solution has a document: `README.md` and `solution.meta.json` in
67
69
  `<tenant>/integration-solutions/solutions/<SOLUTIONID>_<slug>/`. A new solution gets its id and its document before
68
- its first route; the id is taken from the code, never reused, and a leg and sequence (`KLK002A11`) names one route.
70
+ its first route. The id is allocated by the merge request that adds the folder (`klok solutions new`; `klok
71
+ solutions next` shows the next free number and why), and a number is never reused, not even a removed
72
+ solution's. A solution is `reserved`, `active` or `retired` (`status` in `solution.meta.json`); one that stops
73
+ running is retired, not deleted. A leg and sequence (`KLK002A11`) names one route.
69
74
  * The document classifies every service the solution needs: new project, new routes in an existing project, reused
70
75
  with a change, reused as is, client change. It maps the legs (route id, project, pattern, topics with purpose,
71
76
  datamodels) and links each project's `klok-docs/` for the details.
@@ -87,7 +92,7 @@ documentation or the dependency catalog can give you.
87
92
  several flows, the publisher sets it and the reader consumes the purpose the publisher chose. A topic name has at
88
93
  most one purpose, never a second suffix.
89
94
  * A reader is identified by its subscription, whose name is its route id (`<RouteId>.subscription.name` overrides
90
- it), so the reading tenant's short name is in it (`DEG012E1_...`). A reader never appears in the topic name.
95
+ it), so the reading tenant's short name is in it (`DGR012E1_...`). A reader never appears in the topic name.
91
96
  When the content belongs to one reader, the publisher may choose that reader's tenant slug as the purpose
92
97
  (`persistent://konvoj/hr/PersonnelScheduleExportV1_degerfors`); that is the publisher's choice, not a condition
93
98
  for reading across tenants.
@@ -17,6 +17,12 @@
17
17
  # Route ids are <TENANTSHORT><NNN><LEG><seq>_<Description>, legs I P E A S T.
18
18
  \bS<(NNNN|ID|SolutionID|Solution|NNN)> # old S-prefixed integration ids; use <TENANTSHORT><NNN><LEG><seq>_<Description>
19
19
  \bS[0-9]{4}[IEP][0-9] # old S-prefixed route id such as S0001I1; use KLK002I1_... style ids
20
+ # Solution ids are allocated by a merge request in <tenant>/integration-solutions and never reused (2026-10-08); the
21
+ # tenant short name is the first three consonants of the tenant slug, shortname in tenants.yml.
22
+ (?i)\bsolution (number|id)s?\s+(is\s+|are\s+)?(taken\s+)?from\s+the\s+(klok\.dev|portal|klok\.dev\s+solution|portal\s+solution)\s+registry # a solution id is allocated by a merge request in <tenant>/integration-solutions (klok solutions new), never reused
23
+ (?i)\bid is taken from the code # a solution id is allocated by a merge request in <tenant>/integration-solutions (klok solutions new); the code on every branch only counts as used
24
+ (?i)\bfirst\s+(three|3)\s+letters\s+of\s+the\s+tenant # the tenant short name is the first three consonants of the tenant slug, its shortname in tenants.yml
25
+ (?i)\bshort\s+(name|code)s?\s+(is|are)\s+the\s+(tenant'?s?\s+)?(name'?s?\s+|slug'?s?\s+)?first\s+(three|3)\s+letters # the tenant short name is the first three consonants of the tenant slug, its shortname in tenants.yml
20
26
  #
21
27
  # Topics are persistent://<tenant>[_<env>]/<domain>/<Model>[_<purpose>].
22
28
  pulsar:// # topics are persistent://<tenant>[_<env>]/<domain>/<Model>[_<purpose>]
@@ -56,3 +62,8 @@ klok-(ingress|egress)-< # integration repositories are named <tenant>-<system>-<
56
62
  (^|[^<`\\])/think\b # a prompt token of an old Qwen model; the model name is the only switch (a quoted mention in a rule is fine)
57
63
  (^|[^`])/no_think\b # a prompt token of an old Qwen model; the model name is the only switch
58
64
  Kloker-Long # not a gateway model name; use Kloker-nothink or Kloker
65
+ #
66
+ # A datamodel's metadata is klok-datamodel-project.json, rendered from project-template-datamodel; the pipeline reads
67
+ # no metadata, and klok.dev does not create datamodel repositories yet (frontend-klok-dev#33, 2026-10-10).
68
+ (?i)meta\.json`? (uses|in) the flat form # a datamodel's metadata is klok-datamodel-project.json; *.meta.json is only klok.dev's fallback for an older repository
69
+ (?i)create it with klok\.dev's datamodel wizard # klok.dev's wizard edits existing models; it does not create datamodel repositories yet (frontend-klok-dev#33), they are made by hand from project-template-datamodel
@@ -13,7 +13,8 @@ name the parts, and say what is wrong when it is wrong.
13
13
  1. Name the datamodel class of every payload you mention (A, B, C or X) and who reads it.
14
14
  2. Name the legs: ingress, processing, egress, API, or a solution of several legs.
15
15
  3. Choose one pattern base class per leg, as a design decision.
16
- 4. Propose the solution id, the route ids and the topic names, following the platform rules.
16
+ 4. Propose the route ids and the topic names, following the platform rules. A new solution's id is never made up:
17
+ it is the next free number, reserved by a merge request in the tenant's registry (`klok solutions new`).
17
18
  5. Flag every tactical deviation with `[TACTICAL]`, its cost and its unwind condition, or say the design is
18
19
  strategy-pure.
19
20
 
@@ -21,7 +22,7 @@ A correct answer looks like this:
21
22
 
22
23
  "Visma exports invoices in its own format, a class X model. Finance and sales both read invoices, so the ingress
23
24
  maps each invoice to the class B model `InvoiceV1` before it reaches the broker. Use `KlokOnrampRoute` as
24
- `ACME001I1_VismaInvoiceOnramp`, split the nightly file into one fact event per invoice, and publish on
25
+ `ACM001I1_VismaInvoiceOnramp`, split the nightly file into one fact event per invoice, and publish on
25
26
  `persistent://acme/finance/InvoiceV1`. If a consumer later needs Visma's format, that mapping belongs in an egress."
26
27
 
27
28
  A rejection names the problem and gives the broker-shaped replacement:
@@ -164,10 +165,10 @@ direct database or file share only for a system older than the team).
164
165
  Imitate the density of these answers, with the user's names.
165
166
 
166
167
  **Visma nightly invoice file.** The export is class X. Invoices are read by more than one domain, so the target is
167
- the class B model `InvoiceV1`. Ingress: `KlokOnrampRoute` as `ACME001I1_VismaInvoiceOnramp`, splitting the file
168
+ the class B model `InvoiceV1`. Ingress: `KlokOnrampRoute` as `ACM001I1_VismaInvoiceOnramp`, splitting the file
168
169
  into one fact event per invoice (a processing splitter if the mapping is heavy), published on
169
170
  `persistent://acme/finance/InvoiceV1`. Do not publish the file as one large message unless the claim check carries
170
- it and the event holds only the reference. A later Visma-shaped export is an egress, `ACME001E1_...`, mapping B to
171
+ it and the event holds only the reference. A later Visma-shaped export is an egress, `ACM001E1_...`, mapping B to
171
172
  X. Strategy-pure while each message stays small.
172
173
 
173
174
  **A front end that wants a JSON body now.** Ingress that cannot be fire and forget: `KlokOnrampRouteSync` as a
@@ -62,7 +62,7 @@ as the CloudEvent's data.
62
62
 
63
63
  ```java
64
64
  @ApplicationScoped
65
- public class ACME001I1_OrderOnramp extends KlokOnrampRoute<ShopOrder, OrderV1> {
65
+ public class ACM001I1_OrderOnramp extends KlokOnrampRoute<ShopOrder, OrderV1> {
66
66
  @Override
67
67
  protected String fromUri() {
68
68
  return "platform-http:/orders";
@@ -84,7 +84,7 @@ system's reply type; the framework handles the request/reply topics and the time
84
84
 
85
85
  ```java
86
86
  @ApplicationScoped
87
- public class ACME002I1_CustomerLookup
87
+ public class ACM002I1_CustomerLookup
88
88
  extends KlokOnrampRouteSync<CustomerRequest, CustomerResponse, CustomerLookupV1, CustomerV1> {
89
89
  @Override
90
90
  protected String fromUri() {
@@ -118,7 +118,7 @@ dead letters. Override `toUri()` and `transformBody(DOMAIN_MODEL body, Message m
118
118
 
119
119
  ```java
120
120
  @ApplicationScoped
121
- public class ACME001E1_InvoiceOfframp extends KlokOfframpRoute<InvoiceV1, LegacyInvoice> {
121
+ public class ACM001E1_InvoiceOfframp extends KlokOfframpRoute<InvoiceV1, LegacyInvoice> {
122
122
  @Override
123
123
  protected String toUri() {
124
124
  return "https://legacy-invoice-system/invoices";
@@ -198,7 +198,8 @@ blindly. `klok-mock-*` headers are for test and staging and are stripped in prod
198
198
  choose between a splitter and the claim check.
199
199
  2. **Reuse.** Before a new model, search the existing class B and C models, topics and routes, in klok.dev's
200
200
  context and on docs.klok.dev. A new topic only for a new contract.
201
- 3. **Solution design.** Solution id from the klok.dev registry, the legs and their route ids, the models per leg,
201
+ 3. **Solution design.** The solution id, reserved by a merge request in `<tenant>/integration-solutions` (`klok
202
+ solutions new`), the legs and their route ids, the models per leg,
202
203
  `[TACTICAL]` for any deviation, written in the solution document before the code and kept current by every change
203
204
  of a leg (platform rules, *Solutions*).
204
205
  4. **Design per leg.** Base class, exchange pattern, URI, authentication (a Vault path reference, never a secret),
@@ -237,11 +238,11 @@ blindly. `klok-mock-*` headers are for test and staging and are stripped in prod
237
238
 
238
239
  ## Worked answers
239
240
 
240
- **CRM over HTTP to Visma over JMS.** `ACME001I1_CrmCustomerOnramp extends KlokOnrampRoute<CrmCustomer, CustomerV1>`
241
+ **CRM over HTTP to Visma over JMS.** `ACM001I1_CrmCustomerOnramp extends KlokOnrampRoute<CrmCustomer, CustomerV1>`
241
242
  with `fromUri` on the CRM webhook and a `transformBody` that sets `customerId`, `fullName` and `email`; it publishes
242
- `persistent://acme/sales/CustomerV1`. `ACME001E1_VismaCustomerOfframp extends KlokOfframpRoute<CustomerV1,
243
+ `persistent://acme/sales/CustomerV1`. `ACM001E1_VismaCustomerOfframp extends KlokOfframpRoute<CustomerV1,
243
244
  VismaCustomer>` with `toUri` on the JMS queue and a `transformBody` into Visma's model. Validation that is a business
244
- rule is `ACME001P1_CustomerValidation`, a `KlokProcessingRoute` between two topics, not extra code in the onramp.
245
+ rule is `ACM001P1_CustomerValidation`, a `KlokProcessingRoute` between two topics, not extra code in the onramp.
245
246
 
246
247
  **Synchronous lookup.** First choice: `KlokOnrampRoute` answering 202 with a task reference and a status the caller
247
248
  can poll. If the UI must have a synchronous answer: `KlokOnrampRouteSync` as above, a timeout of 10 to 20 seconds, the
@@ -18,11 +18,12 @@ CLI version, generated from the program itself; when it is missing (an installer
18
18
  | to know the agent instructions and skills are current, at the start of a task | `klok agents update --check` (exit 5: run `klok agents update`) |
19
19
  | the local workspace current before you read it | `klok sync pull` |
20
20
  | a repository you can see in GitLab but do not have locally | `klok sync clone --dry-run`, then `klok sync clone` |
21
- | what is dirty, unpushed or behind in the workspace | `klok sync status` (`--fetch` for a current "behind") |
21
+ | what is dirty, unpushed or behind in the workspace, or moved, archived or gone in GitLab | `klok sync status` (`--fetch` for a current "behind") |
22
22
  | the documentation projects you may read | `klok docs list` |
23
23
  | one project's documentation | `klok docs read <doc-id>`; `klok docs id <tenant/group/.../repo>` derives a doc id |
24
24
  | an answer about the integration landscape, with links | `klok agent ask "<question>"`; `--session <first task id>` for a follow-up |
25
25
  | the impact of a datamodel change | `klok agent impact <datamodel> --change minor\|major\|remove` |
26
+ | the next free solution id of the tenant, with why, or a new solution's id reserved by a merge request | `klok solutions next`, `klok solutions new "<name>" --dry-run`, then without `--dry-run` |
26
27
  | the solution registry checked against its contract, or against the code | `klok solutions validate`, `klok solutions check` |
27
28
  | whether the deployed services run what `main` and the catalog say | `klok fleet status` |
28
29
  | the platform catalog's agent assets or its dependents | `klok catalog publish-agents`, `klok catalog release-dependents` (both run in CI) |
@@ -40,17 +41,30 @@ CLI version, generated from the program itself; when it is missing (an installer
40
41
  key `workspace`, `KLOK_GIT_LOCAL_WORKSPACE`, default `~/devklok`) is fetched and fast-forwarded (`git pull
41
42
  --ff-only`). It never merges, rebases or resets, so a checkout with local commits or a diverged branch is reported,
42
43
  not changed, and `<root>/worktrees` (the task worktrees) is skipped. It is the one sanctioned update of a canonical
43
- checkout, made on a workstation. Edits still happen in a task worktree (the `klok-worktree` skill). A service never
44
+ checkout, made on a workstation. `pull` and `status` also ask GitLab about each checkout and report one whose project
45
+ moved, was archived or is gone, with the commands that fix it (`remote set-url`, `mv`, `git worktree repair`); they
46
+ move nothing, and those commands are the person's to run. `klok sync clone` does not clone a project that is already
47
+ checked out under its old path. Edits still happen in a task worktree (the `klok-worktree` skill). A service never
44
48
  clones a repository just to read it: klok.dev and the agent read through the GitLab API, docs.klok.dev and the
45
49
  integration context.
46
50
 
47
51
  ## Reading
48
52
 
49
53
  `klok docs list`, then `klok docs read <doc-id>`, is how to read the platform's documentation; the `klok-docs` skill
50
- has the doc ids that matter most and what a 401, 403 or 404 means. `klok agent ask` asks Kloker, which reads the
54
+ has the doc ids that matter most and what a 401, 403 or 404 means. A doc id that moved with its renamed project is
55
+ followed to the new one, and stderr says `<old-id> moved to <new-id>`: use the new id from then on. `klok agent ask` asks Kloker, which reads the
51
56
  solution registry, the integration context and the documentation you may read, and answers with links; it is the
52
57
  quickest way to find which project, leg or topic handles something.
53
58
 
59
+ ## klok.dev
60
+
61
+ The CLI is meant to do everything klok.dev does, as the person, but only part of the portal has its command yet
62
+ (`klok whoami`, the agent commands, `klok solutions`). When a task needs something klok.dev shows and no klok command
63
+ prints it, such as a broker topic's publishers and subscriptions, an ODL record or a task's events, say so and ask the
64
+ person to look it up in klok.dev or to send it. Do not scrape the portal, and do not reach for credentials the task
65
+ was not given (a service token, a secret read out of Vault) to get around the gap. `klok fleet status --broker` is an
66
+ operator command for the person to run, not a way in.
67
+
54
68
  ## Exit codes
55
69
 
56
70
  `0` ok, `1` error, `2` a task waits for input, `3` denied by policy, `4` not authenticated, `5` outdated (instructions,
@@ -118,7 +118,7 @@ Print one effective configuration value; secrets show as **** unless --reveal
118
118
  klok config set [options] <key> [value]
119
119
  ```
120
120
 
121
- Set a value. Keys: agentUrl, tenant, klokDevUrl, keycloakIssuer, keycloakClientId, protocol, domain, gitUrl, workspace, klokerBasePath, klokerUrl, klokerModel, summaryLanguage, klokApiKey. Leave out the value of a secret (klokApiKey) to type it at a hidden prompt or pipe it in, so it stays out of the shell history
121
+ Set a value. Keys: agentUrl, tenant, klokDevUrl, keycloakIssuer, keycloakClientId, protocol, domain, gitUrl, workspace, klokerBasePath, klokerUrl, klokerModel, summaryLanguage, klokApiKey. Leave out the value of a secret (klokApiKey) to type it at a prompt that shows * for each character, or pipe it in, so it stays off the screen and out of the shell history
122
122
 
123
123
  | Argument | Description |
124
124
  |---|---|
@@ -661,7 +661,7 @@ Read the platform documentation on docs.klok.dev, and publish this repository’
661
661
  klok docs read [options] <doc-id>
662
662
  ```
663
663
 
664
- Print the llm.txt of a documentation project on docs.klok.dev, the one markdown file with its whole content, read with your GitLab token. Cached under ~/.cache/klok/docs with the ETag, so a second read is one conditional request; without network the cache is served with a warning. With --json the document carries the content and where it came from; otherwise the content goes to stdout and one status line to stderr.
664
+ Print the llm.txt of a documentation project on docs.klok.dev, the one markdown file with its whole content, read with your GitLab token. Cached under ~/.cache/klok/docs with the ETag, so a second read is one conditional request; without network the cache is served with a warning. A doc id that moved (its project was renamed) is followed once to the new one, said on stderr and cached under the new id; with --json, docId is the id read, requestedDocId the one asked for and movedTo is set. With --json the document carries the content and where it came from; otherwise the content goes to stdout and one status line to stderr.
665
665
 
666
666
  | Argument | Description |
667
667
  |---|---|
@@ -756,7 +756,7 @@ Publish this repository’s klok-docs/ to the documentation chain (projects not
756
756
  klok docs publish [options]
757
757
  ```
758
758
 
759
- Build the znai project and upload it under this project’s doc id
759
+ Build the znai project and upload it under this project’s doc id. When klok-documented-project.json at the repository root says state "retired", unpublish its docId and every previousDocIds entry instead (DELETE with the upload token; 404 counts as done) and upload nothing.
760
760
 
761
761
  | Option | Description |
762
762
  |---|---|
@@ -764,7 +764,7 @@ Build the znai project and upload it under this project’s doc id
764
764
  | `--doc-id <id>` | Publish under this doc id instead of the derived one |
765
765
  | `--repo-path <path>` | Repository path to derive from (default: CI_PROJECT_PATH or the git remote) |
766
766
  | `--znai <path>` | The znai executable (default: znai on PATH) |
767
- | `-n, --dry-run` | Resolve what would be published and build it with znai, upload nothing |
767
+ | `-n, --dry-run` | Resolve what would be published and build it with znai, upload nothing; for a retired project, say what would be unpublished |
768
768
  | `--no-build` | With --dry-run: only resolve the plan, do not run znai |
769
769
 
770
770
  ## klok fleet
@@ -1017,11 +1017,86 @@ The integration solution registry (<tenant>/integration-solutions)
1017
1017
  klok solutions validate [options]
1018
1018
  ```
1019
1019
 
1020
- Check every solutions/<SOLUTIONID>_<slug>/solution.meta.json against the descriptor contract
1020
+ Check every solutions/<SOLUTIONID>_<slug>/solution.meta.json against the descriptor contract and, against a base, that no solution is removed, no number reused, no retired solution revived and no id taken by an earlier merge request
1021
1021
 
1022
1022
  | Option | Description |
1023
1023
  |---|---|
1024
1024
  | `--root <dir>` | Checkout of the registry (default: the current directory) |
1025
+ | `--base <ref>` | Compare the change with this commit (default: the diff base of a merge request pipeline; none elsewhere) |
1026
+ | `--token <token>` | GitLab token that lists the registry's open merge requests (default: KLOK_GIT_APIKEY, then config klokApiKey, then the CI job token) |
1027
+ | `--offline` | Do not ask GitLab for the open merge requests (skips solution-id-claimed) |
1028
+
1029
+ ```
1030
+ In a merge request pipeline the base is CI_MERGE_REQUEST_DIFF_BASE_SHA and the job needs the whole history
1031
+ (GIT_DEPTH: "0"). On a branch or main pipeline there is no base, so removing a solution is possible there: that is
1032
+ how a platform admin removes one, on main. A number stays spent either way.
1033
+
1034
+ Examples:
1035
+ klok solutions validate # in a checkout of the registry
1036
+ klok solutions validate --base origin/main # a change on a branch, before pushing it
1037
+ ```
1038
+
1039
+ ### klok solutions next
1040
+
1041
+ ```
1042
+ klok solutions next [options]
1043
+ ```
1044
+
1045
+ The next free solution id of the tenant: one more than the highest number in the registry, its history, its branches and open merge requests, and the route classes on every branch of the tenant's repositories
1046
+
1047
+ | Option | Description |
1048
+ |---|---|
1049
+ | `--root <dir>` | Checkout of the registry (default: <workspace>/<tenant>/integration-solutions) |
1050
+ | `--workspace <dir>` | Workspace root that `klok sync` mirrors into (default: the configured workspace) |
1051
+ | `--tenant-short <short>` | The tenant's short name, three upper case letters (default: the one the registry's solution ids share) |
1052
+ | `--token <token>` | GitLab token that reads the registry's open merge requests (default: KLOK_GIT_APIKEY, then config klokApiKey) |
1053
+ | `--offline` | Neither fetch the registry nor ask GitLab for its open merge requests |
1054
+
1055
+ ```
1056
+ Fetches the registry first (git fetch --prune origin). The repositories are read as last fetched; when one was
1057
+ fetched more than a day ago it says so, and `klok sync pull` refreshes them. Numbers 900-999 are the framework's and
1058
+ a gap is never filled. Nothing is written: reserve the id with `klok solutions new`.
1059
+
1060
+ Examples:
1061
+ klok solutions next
1062
+ klok solutions next --root . --json # in a checkout or worktree of the registry
1063
+ ```
1064
+
1065
+ ### klok solutions new
1066
+
1067
+ ```
1068
+ klok solutions new [options] <name>
1069
+ ```
1070
+
1071
+ Reserve the next solution id: a task worktree of the registry, a folder with a reserved descriptor and a stub document, one commit, and a push that opens the merge request, which is the allocation
1072
+
1073
+ | Argument | Description |
1074
+ |---|---|
1075
+ | `<name>` | required |
1076
+
1077
+ | Option | Description |
1078
+ |---|---|
1079
+ | `--root <dir>` | Checkout of the registry (default: <workspace>/<tenant>/integration-solutions) |
1080
+ | `--workspace <dir>` | Workspace root that `klok sync` mirrors into (default: the configured workspace) |
1081
+ | `--tenant-short <short>` | The tenant's short name, three upper case letters (default: the one the registry's solution ids share) |
1082
+ | `--token <token>` | GitLab token that reads the registry's open merge requests (default: KLOK_GIT_APIKEY, then config klokApiKey) |
1083
+ | `--offline` | Neither fetch the registry nor ask GitLab for its open merge requests |
1084
+ | `--slug <slug>` | kebab-case name of the folder and the url (default: from the name) |
1085
+ | `--domain <domain>` | Information domain (Finance, HR, AI, ...) |
1086
+ | `--description <text>` | One or two sentences: what the solution keeps in step, in business terms |
1087
+ | `--dry-run` | Print the id, the worktree and the files, and create nothing |
1088
+
1089
+ ```
1090
+ The id is the one `klok solutions next` gives, after fetching the registry. The worktree is
1091
+ <workspace>/worktrees/<tenant>/integration-solutions/<YYYYMMDD>-solution-<id>-<8 hex> on the branch agent/<task id>,
1092
+ based on the registry's default branch; the canonical checkout is not touched. The push uses your own git setup and
1093
+ asks GitLab, with push options, to open the merge request against the default branch. The commit is
1094
+ docs(<ID>): reserve <ID> for <name>.
1095
+
1096
+ Examples:
1097
+ klok solutions new "Payroll export to Personec" --domain HR --dry-run
1098
+ klok solutions new "Payroll export to Personec" --domain HR
1099
+ ```
1025
1100
 
1026
1101
  ### klok solutions check
1027
1102
 
@@ -1070,10 +1145,11 @@ Discover every repository you can see and clone the missing ones
1070
1145
  klok sync pull [options]
1071
1146
  ```
1072
1147
 
1073
- Fast-forward every repository in the workspace
1148
+ Fast-forward every repository in the workspace, and report checkouts whose GitLab project moved, was archived or is gone
1074
1149
 
1075
1150
  | Option | Description |
1076
1151
  |---|---|
1152
+ | `--no-remote-check` | Do not ask GitLab whether the checkouts’ projects moved, were archived or are gone |
1077
1153
  | `-w, --workspace <path>` | Workspace directory (default: config workspace) |
1078
1154
  | `--token <token>` | GitLab token (default: KLOK_GIT_APIKEY, then config klokApiKey) |
1079
1155
  | `-j, --jobs <n>` | Parallel git operations (default: `6`) |
@@ -1086,7 +1162,7 @@ Fast-forward every repository in the workspace
1086
1162
  klok sync status [options]
1087
1163
  ```
1088
1164
 
1089
- Show dirty trees, unpushed commits and branches behind their remote (as of the last fetch)
1165
+ Show dirty trees, unpushed commits and branches behind their remote (as of the last fetch), and checkouts whose GitLab project moved, was archived or is gone
1090
1166
 
1091
1167
  | Option | Description |
1092
1168
  |---|---|
@@ -1094,3 +1170,5 @@ Show dirty trees, unpushed commits and branches behind their remote (as of the l
1094
1170
  | `-j, --jobs <n>` | Parallel git operations (default: `12`) |
1095
1171
  | `-a, --all` | List every repository, not only the ones needing attention |
1096
1172
  | `--fetch` | Fetch every repository first, so "behind" is current |
1173
+ | `--token <token>` | GitLab token for the GitLab check (default: KLOK_GIT_APIKEY, then config klokApiKey) |
1174
+ | `--no-remote-check` | Do not ask GitLab whether the checkouts’ projects moved, were archived or are gone |
@@ -34,27 +34,34 @@ domains read should still travel as class B or C (transform at the edge).
34
34
  ends in `supplier_invoice_v1`. Inside the XSD the model keeps its PascalCase form: root element
35
35
  `SupplierInvoice_v1`, root type `SupplierInvoiceV1`.
36
36
 
37
- **Create it with klok.dev's datamodel wizard.** It pushes a new repository from `project-template-datamodel`
38
- with the XSD and its `meta.json`, but it does not render the template's `__KLOK_*__` placeholders yet
39
- (klok/application/frontend-klok-dev#33): check the
40
- new repository and render any that are left, as below, before relying on its pipeline. The agent at
41
- https://agent.klok.dev cannot create datamodel projects yet (klok/application/frontend-klok-dev#42).
37
+ **Create it by hand from the template.** klok.dev edits existing models through a merge request but does not
38
+ create datamodel repositories yet, because it cannot render the template's `__KLOK_*__` placeholders for them
39
+ (klok/application/frontend-klok-dev#33); it points to the way by hand instead. The agent at
40
+ https://agent.klok.dev cannot create datamodel projects either (klok/application/frontend-klok-dev#42).
41
+ `klok datamodel names --class <A|B|C|X> --name <PascalCase> --major <n>` (with `--system` for class X and the
42
+ global `--domain` for class C) prints every derived value: repository, XSD file, root element, root type,
43
+ `targetNamespace` and the documentation project.
42
44
 
43
45
  By hand it is: `git clone` the template, `git remote rename origin template`, `git remote add origin <new
44
46
  repo>`, render every `__KLOK_*__` token listed in `klok-template.json` (including the XSD file name), push
45
47
  `main`. Keep the `template` remote so later template improvements can be pulled in. Never assemble the
46
48
  project by copying files out of a sibling datamodel: the copies go stale and whole files get missed
47
- (`klok-datamodel-project.json`, `klok-template.json`, the agent instruction files, the three page
48
- `klok-docs/datamodel/` layout).
49
+ (`klok-datamodel-project.json`, `klok-template.json`, the agent instruction files, `klok-docs/` with its
50
+ guide `README.md` and the `datamodel/` pages).
49
51
 
50
52
  Vault needs nothing per project: the tenant CI role `<tenant>-ci` accepts every project under the tenant's
51
53
  root group. If `publish_java_projects` fails at `auth/jwt/login`, the tenant itself is not set up; see the
52
54
  `klok-release` skill.
53
55
  2. Write the XSD under `schema-definition-xsd/`: single `xs:` prefix, `targetNamespace` with `xmlns:tns`,
54
56
  `elementFormDefault="qualified"`, one root element `<Name>_vN` that references every complex type. Enumerations
55
- as `xs:enumeration`.
56
- 3. `meta.json` in the flat form with `targetNamespace`, and `klok-docs/` with the purpose, the important fields and
57
- the producers and consumers.
57
+ as `xs:enumeration`. `klok datamodel validate` checks it with the pipeline's rules before you push.
58
+ 3. `klok-datamodel-project.json` at the root is the model's metadata (name, description, class, model type,
59
+ `targetNamespace`, template version), rendered from the template; klok.dev and the documentation payload read
60
+ it. An older repository's `schema-definition-xsd/*.meta.json` is only klok.dev's fallback for a repository
61
+ without it, so keep one of the two, not both. The pipeline takes the artifact and the Java package from the
62
+ repository path and the XSD file name, never from metadata. `klok-docs/datamodel/` holds the three pages of the
63
+ template (`index`, `usage`, `changes`): the purpose, the fields that need explaining, the producers and consumers
64
+ and the history of compatible changes.
58
65
  4. A breaking change is a new major version (`Customer_v2`), which is a new class, a new topic and a new catalog
59
66
  key; a compatible change (optional field) stays in the same version. The old and the new version run in
60
67
  parallel while readers move, at least the class's minimum (table above) and until every known reader
@@ -72,7 +79,8 @@ domains read should still travel as class B or C (transform at the edge).
72
79
  ## Using a model in a project
73
80
 
74
81
  * Run `./gradlew updateKlokLibsToml` in the consumer, commit the refreshed `gradle/klok-libs.versions.toml`, and
75
- reference the alias (`klokLibs.klokOdlV1` or the bundle). Never type a version.
82
+ reference the alias, the project name in camel case (`klokLibs.klokOdlV1` or the bundle for tenant klok's
83
+ models, `tenantLibs.<alias>` for another tenant's). Never type a version.
76
84
  * The generated class is `dev.klok.<tenant>.datamodel.<group>.<name_with_underscores>_v<n>.<Name>Vn` (for
77
85
  `klok-task-event-v1`: `dev.klok.klok.datamodel.common.klok_task_event_v1.KlokTaskStatusEventV1`); the ODL type of an object is the
78
86
  fully qualified class name, so moving a class to another package is a data migration of its ODL index.
@@ -12,7 +12,7 @@ serves it to a browser session and to a GitLab personal access token, which is w
12
12
  ```
13
13
  klok docs list # the projects you may read: doc id, title, description
14
14
  klok docs read <doc-id> # the markdown on stdout, one status line on stderr
15
- klok docs read <doc-id> --json # {docId, url, source, status, etag, fetchedAt, bytes, content}
15
+ klok docs read <doc-id> --json # {docId, requestedDocId, movedTo, url, source, status, etag, fetchedAt, bytes, content}
16
16
  klok docs read <doc-id> --out x.md # to a file
17
17
  klok docs id [tenant/group/.../repo] # the doc id of a repository, this one by default
18
18
  ```
@@ -59,6 +59,7 @@ question is about what a project does.
59
59
  | `klok docs read` says | Meaning | What to do |
60
60
  |---|---|---|
61
61
  | the content, `source: server` or `source: cache` | the page as published, or the cached copy | read it |
62
+ | `klok docs read: <old-id> moved to <new-id>` on stderr, then the content | the project was renamed or moved; the content is the new id's | read it, cite the new id (`docId` of `--json`), and fix the link or note the old id came from |
62
63
  | exit 4, "No or invalid GitLab token" | no `KLOK_GIT_APIKEY` (or `--token`), or the token is not valid | say so and ask for a token with `read_api`; do not guess |
63
64
  | "not a member of the tenant or domain that owns …" (403) | the page exists and your GitLab account may not read it | say so and ask for access; do not guess and do not try another way in |
64
65
  | "… is not published; read the repository's klok-docs/ on main" (404) | the project has no published documentation yet | read the source pages: `git -C <repo> fetch origin && git show origin/main:klok-docs/<page>.md`; `klok-docs/toc-manifest.json` lists them. The generated pages (routes, datamodels, configuration) exist only on the portal |
@@ -75,4 +76,5 @@ question is about what a project does.
75
76
  runbook in `klok-framework-development-guide` when it touches onboarding; read the framework documentation
76
77
  before writing or changing a route.
77
78
  5. Cite the doc id and the `fetchedAt` from `klok docs read <doc-id> --json` in your answer or merge request
78
- description, so reviewers can tell which version of the page you relied on.
79
+ description, so reviewers can tell which version of the page you relied on. After a move, `docId` is the new id;
80
+ cite that one.
@@ -17,8 +17,9 @@ motivates.
17
17
  `KlokProcessingRoute<IN, OUT>`; egress to a system: `KlokOfframpRoute<DOM, SYS>`; synchronous request/reply:
18
18
  the `Sync` variants; REST: `KlokRestApi` plus one `KlokRestApiHandlerRoute` per operation; long running work:
19
19
  `KlokAsyncTaskWorkerRoute<REQ, RES>`; anything else: `KlokCustomRoute`.
20
- 2. **Name it.** `<TENANTSHORT><NNN><LEG><seq>_<Description>`, one class per route, the solution number from the
21
- portal registry. Sub routes are `S`, timers `T`.
20
+ 2. **Name it.** `<TENANTSHORT><NNN><LEG><seq>_<Description>`, one class per route. `<TENANTSHORT><NNN>` is the id of
21
+ the solution in `<tenant>/integration-solutions`, allocated there by a merge request (`klok solutions new`).
22
+ Sub routes are `S`, timers `T`.
22
23
  3. **Bind the datamodels** as type parameters; take them from the catalog bundle, never hand written DTOs.
23
24
  4. **Choose the topic purpose** of every topic the route publishes or consumes (*Topic purpose*), and make sure every
24
25
  topic exists (*Topics on the broker*): the broker never creates one.
@@ -74,7 +75,7 @@ the purpose.
74
75
  * **Name** the flow as both sides understand it (`created`, `reminder`, `documentation`): letters, digits, `-` and
75
76
  `.`, never `_`, and no version or environment. Task types are `<domain>.<verb>-<object>`.
76
77
  * **One purpose, no reader.** A topic has at most one purpose. A reader, also one in another tenant, is identified by
77
- its subscription, whose name is its route id (`DEG012E1_...`; `<RouteId>.subscription.name` overrides it), and never
78
+ its subscription, whose name is its route id (`DGR012E1_...`; `<RouteId>.subscription.name` overrides it), and never
78
79
  adds a suffix to the topic. When the content belongs to one reader, the publisher may choose that reader's tenant
79
80
  slug as the purpose (`PersonnelScheduleExportV1_degerfors`); that is the publisher's choice, not something a reader
80
81
  in another tenant needs.