From c6ec902f7c5329aeaaf9b16a750680eec6d893ec Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 11 Nov 2026 22:44:58 -0500 Subject: [PATCH] docs(trogonstack-nats): derive topology from channel contracts Signed-off-by: Yordis Prieto --- .../skills/nats-design-subject/SKILL.md | 59 +++++++------------ .../references/channel-contract.md | 36 +++++++---- .../gateway-asyncapi-fragments.yaml | 2 +- .../references/patterns.md | 17 +++--- .../references/subject-fixtures.yaml | 2 +- 5 files changed, 55 insertions(+), 61 deletions(-) diff --git a/plugins/trogonstack-nats/skills/nats-design-subject/SKILL.md b/plugins/trogonstack-nats/skills/nats-design-subject/SKILL.md index 603ed9e..5098e05 100644 --- a/plugins/trogonstack-nats/skills/nats-design-subject/SKILL.md +++ b/plugins/trogonstack-nats/skills/nats-design-subject/SKILL.md @@ -10,17 +10,17 @@ allowed-tools: # Design NATS Channels -Design a NATS channel contract that stays understandable as the system grows. A subject is the routing key. Core NATS and JetStream are delivery and storage choices that must remain explicit in the contract. +Design a NATS channel contract that stays understandable as the system grows. A subject is the routing key, not a declaration of transport or storage. Record delivery, acknowledgement, and ownership separately. JetStream is not a second application subject tree. Streams bind to ordinary application subjects. The `$JS` subjects are the control plane and need separate permissions and management ownership. -## Start with the channel kind +## Start with the message behavior -Choose the semantic kind before choosing the subject shape: +Choose what the publisher and reader expect before choosing a subject shape. These kinds describe behavior; they are not required subject tokens: -| Kind | Transport | Contract | +| Kind | Typical transport | Contract | | --- | --- | --- | -| RPC | Core NATS request-reply | A caller waits for one response or a timeout. | +| Request/reply | Core NATS | A caller waits for a business response or a timeout. | | Live signal | Core NATS publish-subscribe | Delivery is best effort while subscribers are connected. | | Durable event | JetStream | The event can be replayed and consumers acknowledge progress. | | Durable work | JetStream | A worker pool shares a durable consumer and retries unacknowledged work. | @@ -34,7 +34,7 @@ If a stream captures a subject, a Core publish can be stored without giving the Skip questions that the user already answered. Ask for the missing contract decisions: - Is this a new design or a migration? -- What channel kinds are needed: RPC, live signal, durable event, or durable work? +- What behavior is needed: request/reply, live signal, durable event, or durable work? - Is tenancy an NATS account boundary, a shared-account subject prefix, or both? - Which principals publish, subscribe, request, respond, or manage JetStream resources? - What must survive a disconnect, and what can be lost? @@ -60,19 +60,19 @@ kind: durable-event subjects: - gateway.ingress.raw.github.v1. transport: jetstream -stream: GATEWAY_GITHUB_PRIMARY +stream: GITHUB_PRIMARY owner: gitops retention: 7d ack: explicit ``` -For an RPC channel: +For a request/reply channel: ```yaml -id: gateway.rpc.v1.health -kind: rpc +id: gateway.health.v1.check +kind: request-reply request_subjects: - - gateway.rpc.v1.health + - gateway.health.v1.check reply_subjects: - _INBOX. transport: core @@ -80,15 +80,11 @@ storage: none owner: gateway ``` -The channel record is the source for the subjects, stream filters, consumer filters, permissions, and management status. Do not make an application silently create production streams that are also declared by GitOps or an operator. +The request subject has no `.rpc.` token. Such a token is an optional local naming convention, not a NATS requirement. The channel contract states the behavior. Combine it with provisioned resources, such as NACK manifests, and observed state to show topology; do not infer transport from spelling. An application must not create a stream that GitOps or an operator already owns. ### Choose the subject shape -Use a stable, low-cardinality prefix followed by the fields subscribers need to filter: - -```text -... -``` +Choose tokens that identify the message and support the intended subscriptions and permissions. A stable, low-cardinality prefix helps with bounded filters. Use a kind token such as `rpc` or `event` only when it serves that design; no kind token or universal token order is required. Examples: @@ -126,6 +122,8 @@ For every subject template, document the intended filters: For a request, document the request subject and reply-to behavior. NATS request-reply uses a generated inbox. Responders publish to the request's reply subject; they do not invent a shared reply subject for each correlation ID. +Compare request subjects with stream filters. A request that expects a business response should not be captured by an unrelated JetStream stream. The [NACK example](references/nack-jetstream.yaml) captures ingress events and work, not `gateway.health.v1.check`. + For each principal, list publish and subscribe permissions separately. A broad `>` permission also reaches the JetStream control plane, so scope permissions to the channel and the required `$JS` or `_INBOX` subjects. ### Design JetStream storage and consumers @@ -158,7 +156,7 @@ logical channel -> retention, backlog, and last error ``` -For the Trogon gateway, GitOps applies the NACK resources while the gateway publishes and validates the ingress subjects. The application must not create or update competing stream definitions. The ownership must be visible in the channel contract. +In the gateway example, GitOps applies the NACK resources while the gateway publishes ingress events. The request/reply channel has no NACK resource. Show that separation in the channel contract and management view. ### Plan migrations @@ -175,27 +173,7 @@ Make consumers idempotent with an application key when a migration can deliver a ## Output -Write a design that includes: - -```markdown -# NATS Channel Architecture: [System] - -## Account and tenancy boundary - -## Channel contracts - -## Subject templates and valid wildcard paths - -## Core NATS request-reply and live signals - -## JetStream streams and consumers - -## Permissions and reserved subjects - -## Provisioning owner and management readback - -## Migration and validation plan -``` +Present a topology a reader can trace from application behavior to the deployed resources. Use the sections that fit the system. Include the relevant account boundary, channel contracts, subject filters, permissions, provisioning owner, and desired versus observed state. Include a migration plan when changing an existing hierarchy. ## Validation @@ -205,6 +183,7 @@ Before presenting the design: - Validate every wildcard expression. - Verify that `>` appears only as the final token. - Verify that request replies use a generated inbox or an explicitly documented alternative. +- Compare request subjects with stream filters. Document any intentional capture and its response behavior. - Check stream and consumer filter overlap. - Check permissions against the actual subject and JetStream control-plane requirements. - Mark machine-readable Markdown fixtures with the `nats-subjects` info tag. For example, use ` ```yaml nats-subjects` and put a `kind` and `subjects` list in each YAML item. Keep the list shape even when it has one value. Ordinary examples stay untagged. @@ -214,6 +193,8 @@ Before presenting the design: node plugins/trogonstack-nats/skills/nats-design-subject/scripts/validate-subjects.mjs --root plugins/trogonstack-nats/skills/nats-design-subject ``` +The bundled validator checks subject syntax and tagged examples. It does not verify stream overlap, permissions, responder availability, or deployed topology. + ## References - [Channel contracts and Core versus JetStream](references/channel-contract.md) diff --git a/plugins/trogonstack-nats/skills/nats-design-subject/references/channel-contract.md b/plugins/trogonstack-nats/skills/nats-design-subject/references/channel-contract.md index 605b0b7..3436870 100644 --- a/plugins/trogonstack-nats/skills/nats-design-subject/references/channel-contract.md +++ b/plugins/trogonstack-nats/skills/nats-design-subject/references/channel-contract.md @@ -1,6 +1,6 @@ # NATS Channel Contracts -A NATS subject is a routing key. Core NATS and JetStream determine how the message is delivered and whether it is stored. Keep those decisions in one channel contract. +A NATS subject is a routing key. Its tokens need not encode transport, storage, or whether a response is expected. Describe those behaviors in the channel contract, then show how the deployed resources implement them. JetStream does not require a separate application subject namespace. A stream captures ordinary subjects. `$JS` subjects are the JetStream control plane and must remain separate from application channels. @@ -18,22 +18,22 @@ The NATS-specific examples below use list-valued `subjects`, `request_subjects`, ## Channel kinds -### RPC +### Request/reply Use Core NATS request-reply when the caller needs one response now: ```yaml -id: gateway.rpc.v1.health -kind: rpc +id: gateway.health.v1.check +kind: request-reply request_subjects: - - gateway.rpc.v1.health + - gateway.health.v1.check reply_subjects: - _INBOX. transport: core storage: none ``` -The client creates a unique inbox and sends it as the reply subject. The responder uses that reply subject. A timeout and retry policy are part of the contract. +The client creates a unique inbox and sends it as the reply subject. The responder uses that reply subject. A timeout and retry policy are part of the contract. The `kind` field expresses request/reply behavior; `.rpc.` is not required in the subject. Add a kind token only when it helps a chosen subscription or permission boundary. ### Live signal @@ -108,11 +108,11 @@ The `consumer` field belongs to the worker's receive-side contract. Omit it from gateway.ingress.raw.github.v1.pull_request gateway.ingress.raw.slack.v1.event_callback gateway.events.v1.conversation.message -gateway.rpc.v1.health +gateway.health.v1.check gateway.work.v1.installation_sync ``` -The GitHub and Slack ingress streams can remain separate when their retention, permissions, or consumers differ. A stream may exist before a reader is deployed. The important rule is that request subjects do not sit under the ingress event wildcard. +The GitHub and Slack ingress streams can remain separate when their retention, permissions, or consumers differ. A stream may exist before a reader is deployed. Their [NACK filters](nack-jetstream.yaml) capture ingress events and work, not the health request. That boundary comes from the declared filters and contract, not from a required `.rpc.` prefix. Compare request subjects with stream filters before treating them as separate paths. The `nats-subjects` tag marks the examples that the bundled validator reads. The `yaml` language tag keeps the fixture readable as a small data document. Narrative examples use ordinary fences and are not treated as machine-readable contracts. @@ -138,7 +138,7 @@ The `nats-subjects` tag marks the examples that the bundled validator reads. The - gateway.ingress.raw.slack.v1.> - kind: publish subjects: - - gateway.rpc.v1.health + - gateway.health.v1.check - kind: deliver subjects: - _INBOX.generated @@ -158,7 +158,7 @@ The application can validate that a required stream exists. It should not silent ## Management readback -Management should expose the same descriptor with desired and observed state. Consumer fields are optional and appear only when a reader owns one: +Build management readback from the declared channel behavior, NACK resources, and observed state. Consumer fields appear only when a reader owns one: ```text channel: gateway.ingress.raw.github.v1 @@ -172,4 +172,18 @@ backlog: 0 owner: gitops ``` -Core channels omit stream fields and instead expose responder, queue group, timeout, and authorization status. +The request/reply channel has no stream or consumer entry. Its readback can show the responder, timeout, and permission path instead: + +```yaml +channel: gateway.health.v1.check +kind: request-reply +transport: core +request_subjects: + - gateway.health.v1.check +reply_subjects: + - _INBOX. +responder: gateway +storage: none +``` + +Observed responder readiness and authorization status belong beside this declared shape. Neither is proved by the subject name. diff --git a/plugins/trogonstack-nats/skills/nats-design-subject/references/gateway-asyncapi-fragments.yaml b/plugins/trogonstack-nats/skills/nats-design-subject/references/gateway-asyncapi-fragments.yaml index 4c8892a..9d8537c 100644 --- a/plugins/trogonstack-nats/skills/nats-design-subject/references/gateway-asyncapi-fragments.yaml +++ b/plugins/trogonstack-nats/skills/nats-design-subject/references/gateway-asyncapi-fragments.yaml @@ -26,7 +26,7 @@ channels: slackWebhook: $ref: '#/components/messages/slackWebhook' healthRequest: - address: gateway.rpc.v1.health + address: gateway.health.v1.check messages: healthRequest: $ref: '#/components/messages/healthRequest' diff --git a/plugins/trogonstack-nats/skills/nats-design-subject/references/patterns.md b/plugins/trogonstack-nats/skills/nats-design-subject/references/patterns.md index d7e2a00..f53a5d8 100644 --- a/plugins/trogonstack-nats/skills/nats-design-subject/references/patterns.md +++ b/plugins/trogonstack-nats/skills/nats-design-subject/references/patterns.md @@ -126,24 +126,23 @@ acme-corp.orders.created.order-123 **Use when**: Synchronous microservices, command-response, RPC-like communication. +Choose a concrete subject that identifies the requested operation. The caller supplies the reply-to address: + ``` -Request Subject: {service}.request.{request-type} +Request Subject: orders.get-order Reply-To: _INBOX. - -Examples: -orders.request.get-order -orders.request.calculate-total -payments.request.authorize +``` The client creates a unique `_INBOX.` reply subject and sends it as the request reply-to. -``` + +`orders.calculate-total` and `payments.authorize` could use the same pattern. The request/reply behavior comes from the contract and reply-to address, not a required `request` or `rpc` token. Use a kind token when it helps the intended filters or permissions. **Implementation**: ``` -Requester publishes to: orders.request.get-order +Requester publishes to: orders.get-order With reply-to: _INBOX.abc123 -Responder subscribes to: orders.request.> +Responder subscribes to: orders.get-order Reads reply-to header Publishes response to: _INBOX.abc123 ``` diff --git a/plugins/trogonstack-nats/skills/nats-design-subject/references/subject-fixtures.yaml b/plugins/trogonstack-nats/skills/nats-design-subject/references/subject-fixtures.yaml index 155cf15..87c578e 100644 --- a/plugins/trogonstack-nats/skills/nats-design-subject/references/subject-fixtures.yaml +++ b/plugins/trogonstack-nats/skills/nats-design-subject/references/subject-fixtures.yaml @@ -18,7 +18,7 @@ - gateway.ingress.raw.slack.v1.> - kind: publish subjects: - - gateway.rpc.v1.health + - gateway.health.v1.check - kind: deliver subjects: - _INBOX.generated