From 4ed73f239c8e24ac35739c0d7c5f60bd6c404435 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 06:02:25 +0100 Subject: [PATCH 1/8] docs: S23.22 link the tracking record, and write the link policy down The rule since #711 was no links out to GitHub. Applied absolutely it forbids linking an issue, which a reader following a documented divergence wants, and it was written down nowhere while hooks/source_links.py did the opposite. Split the two cases in CLAUDE.md and say what the hook is now for: no authored page under docs/ links outside docs/ any more, so its remaining job is the repository-root documents root_pages.py publishes. Eleven issue references in code font become links, matching the form the same set already uses elsewhere -- #736 appeared both ways. Part of #708 --- CLAUDE.md | 17 +++++++++++++++++ docs/platforms/mbedtls/index.md | 16 ++++++++++------ docs/platforms/openssl/index.md | 13 ++++++++----- docs/platforms/plustcp/index.md | 5 +++-- docs/rfc-compliance.md | 2 +- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0b531654..9c1e3df8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -563,6 +563,23 @@ comes from, couples the two: the eleventh platform then has to be added to ten pages. State this platform's own behaviour completely, and point at the capability matrix in `docs/platforms/index.md` for who fills what. +### Link the record, not the source + +A documentation page does not send the reader into the source tree. Where a +symbol has a generated API page, link that; otherwise name the file in code font +and leave it there. The page's job is to say what the library does, not to show +where it is implemented. + +Issues and pull requests are the opposite case, and are linked. They are the +tracking record, and a reader who has just been told that a platform diverges +from a contract wants to see whether that is still true. + +The repository-root documents are outside this rule rather than an exception to +it. `README.md`, `SECURITY.md`, `SUPPORT.md` and `LICENSE.md` are read on GitHub +as well as published into the site by `hooks/root_pages.py`, so their links stay +repo-relative; `hooks/source_links.py` rewrites whatever escapes `docs/` to a +canonical URL at build time. + --- ## Design Patterns diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md index bf033880..cc066343 100644 --- a/docs/platforms/mbedtls/index.md +++ b/docs/platforms/mbedtls/index.md @@ -61,29 +61,32 @@ A client certificate is presented only when both `ClientCertChain` and connection proceeds with server-authenticated TLS, and nothing is reported — so a device configured for mutual TLS can run without presenting its certificate, and without anyone on the device knowing. The contract requires this to be reported. -Until it is, check for a half-supplied pair before you open the stream. Tracked -as `#718`. +Until it is, check for a half-supplied pair before you open the stream. Tracked as +[#718](https://github.com/cososo-ltd/solid-syslog/issues/718). ### The key is not checked against its certificate No local check confirms that `ClientKey` matches `ClientCertChain`, and a failure to install the pair is not reported either. A mismatch therefore surfaces as a handshake rejection from the collector rather than as a setup error on the -device, which sends you looking in the wrong place. Tracked as `#719`. +device, which sends you looking in the wrong place. Tracked as +[#719](https://github.com/cososo-ltd/solid-syslog/issues/719). ### An expired certificate stops delivery A peer certificate that is expired or not yet valid fails the handshake, even where it still chains to a trusted anchor. The contract asks for it to be reported with delivery continuing, because clock skew is the dominant cause and a -device with a wrong clock is one whose logs you still want. Tracked as `#731`. +device with a wrong clock is one whose logs you still want. Tracked as +[#731](https://github.com/cososo-ltd/solid-syslog/issues/731). ### The cipher policy cannot be expressed The configuration carries no cipher or ciphersuite field, so the ciphersuites your `mbedtls_config.h` enables, filtered by the preset, are what gets negotiated. The contract asks for an integrator's policy to be passed through -where the library allows one to be selected. Tracked as `#733`. +where the library allows one to be selected. Tracked as +[#733](https://github.com/cososo-ltd/solid-syslog/issues/733). ### The configuration is not checked when the stream is created @@ -91,4 +94,5 @@ A configuration missing something the stream cannot work without is accepted, an the fault appears on the first connection attempt rather than at setup. The random source and the trust chain are installed through calls that return no status, so a missing one becomes a handshake failure rather than the -configuration error it is. Tracked as `#732`. +configuration error it is. Tracked as +[#732](https://github.com/cososo-ltd/solid-syslog/issues/732). diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md index 6bf4f4ce..8d7065ed 100644 --- a/docs/platforms/openssl/index.md +++ b/docs/platforms/openssl/index.md @@ -47,14 +47,16 @@ The contract asks for it to be reported with delivery continuing, on the grounds that the collector is the enforcement point for our own credential. This adapter is stricter than the contract rather than weaker, and the stricter -behaviour is safe. Tracked as `#734`. +behaviour is safe. Tracked as +[#734](https://github.com/cososo-ltd/solid-syslog/issues/734). ### An expired certificate stops delivery A peer certificate that is expired or not yet valid fails the handshake, even where it still chains to a trusted anchor. The contract asks for it to be reported with delivery continuing, because clock skew is the dominant cause and a -device with a wrong clock is one whose logs you still want. Tracked as `#731`. +device with a wrong clock is one whose logs you still want. Tracked as +[#731](https://github.com/cososo-ltd/solid-syslog/issues/731). ### The cipher policy does not bind a TLS 1.3 connection @@ -63,10 +65,11 @@ own, as the contract asks. It governs TLS 1.2 and below only. OpenSSL has kept TLS 1.3 ciphersuites in a separate list since 1.1.1, and this adapter sets a protocol floor without a ceiling, so against a modern peer the negotiated connection uses OpenSSL's own TLS 1.3 defaults and the configured list has no -effect on it. Tracked as `#733`. +effect on it. Tracked as +[#733](https://github.com/cososo-ltd/solid-syslog/issues/733). ### The configuration is not checked when the stream is created A configuration missing something the stream cannot work without is accepted, and -the fault appears on the first connection attempt rather than at setup. Tracked -as `#732`. +the fault appears on the first connection attempt rather than at setup. Tracked as +[#732](https://github.com/cososo-ltd/solid-syslog/issues/732). diff --git a/docs/platforms/plustcp/index.md b/docs/platforms/plustcp/index.md index e53454fc..e05db899 100644 --- a/docs/platforms/plustcp/index.md +++ b/docs/platforms/plustcp/index.md @@ -58,8 +58,9 @@ record at its cursor and offers the same one on every servicing pass. Nothing behind it is delivered. `SOLIDSYSLOG_MAX_MESSAGE_SIZE` defaults to 2048, so this reaches any record over -about 1.2 KB rather than only unusual ones. Until `#736` lands, keep records on -this UDP path inside the payload it carries — but note that limit is library-wide +about 1.2 KB rather than only unusual ones. Until +[#736](https://github.com/cososo-ltd/solid-syslog/issues/736) lands, keep records +on this UDP path inside the payload it carries — but note that limit is library-wide rather than per-transport, so lowering it truncates records on every transport the instance uses, not only this one. diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 733cb3fb..8b24663b 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -105,7 +105,7 @@ requirement in force, rather than tabulating it separately. | [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.1) | Certificate-based authentication — client | Supported | A client certificate and its key are optional configuration on the TLS stream, presented only when both are given, and a partially configured pair is reported rather than silently ignored | | [4.2.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.1) | Means to generate a key pair and self-signed certificate | N/A | Deliberately excluded. The library consumes trust material and does not mint it, so key generation belongs to the deployment's provisioning. Directed at a syslog application rather than at a component one is built from | | [4.2.2](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.2) | Certificate fingerprints published through a management interface | N/A | Directed at a syslog application, not a component one is built from: the library has no management interface, and the certificate is the integrator's to hold and to publish. The fingerprint form §4.2.2 defines matters where a peer is authorised by one, which is §5.1 | -| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.3) | Administrators may select the cryptographic level | Partial | The contract requires an integrator's cipher policy to be passed through where the underlying library allows one to be selected. Neither shipped TLS platform delivers that on the connection actually negotiated — see each platform's page, and `#733` | +| [4.2.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.2.3) | Administrators may select the cryptographic level | Partial | The contract requires an integrator's cipher policy to be passed through where the underlying library allows one to be selected. Neither shipped TLS platform delivers that on the connection actually negotiated — see each platform's page, and [#733](https://github.com/cososo-ltd/solid-syslog/issues/733) | | [4.3](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3) | All syslog messages MUST be sent as TLS application data | Supported | The TLS `Stream` carries the frames the sender writes as ordinary application data; nothing is sent outside the session, and §4.3's `APPLICATION-DATA = 1*SYSLOG-FRAME` is what the octet-counting sender produces | | [4.3.1](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.3.1) | Octet-counting framing, and the message length | Supported | Reuses `SolidSyslogStreamSender`, so the frame is `MSG-LEN SP MSG`. `SOLIDSYSLOG_MAX_MESSAGE_SIZE` defaults to 2048, the length §4.3.1 requires every transport receiver to accept | | [4.4](https://www.rfc-editor.org/rfc/rfc5425.html#section-4.4) | `close_notify` before closing | Supported | Close sends `close_notify` before tearing the connection down | From 46bd17290a25dbd78cd2084edd7a7f24988bc340 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 07:11:11 +0100 Subject: [PATCH 2/8] docs: S23.22 correct the README, and type it as you would The documentation site's section names named three tabs that do not exist and omitted two that do, including Core, which the nav calls the product. wolfSSL was named as an example though it is not a supported target and a name reads as a support claim. A hardware offload engine was listed as a peer of a TLS library, when it would arrive the same way -- wrapped in a platform. The BDD line counted one FreeRTOS binary where there are two, one per network stack. The cost figures gain the baseline they are measured against: without it, +13.5 KB flash including mutual TLS and AES-GCM reads as impossible. Six upstream product names go, replaced by a link to the platform matrix that already holds them and is already out of step with this copy. Reference adapters becomes platforms, because a platform is the unit that ships and an adapter is what one is made of. Part of #708 --- README.md | 77 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 09b0330b..da2b76f4 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,22 @@ A structured syslog client library for embedded and industrial systems, implementing RFC 5424 (structured syslog) with RFC 5426 (UDP) and RFC 6587 (TCP) transports. -TLS per RFC 5425 ships two ways — `SolidSyslogOpenSslStream` over OpenSSL and -`SolidSyslogMbedTlsStream` over Mbed TLS — and any other TLS library (wolfSSL, -hardware offload, …) plugs in behind the same Stream vtable. TLS is not a core -dependency: Core carries no reference to any TLS library. +TLS per RFC 5425 is supplied by a platform, so any TLS library plugs in behind the +same Stream vtable. TLS is not a core dependency: Core carries no reference to any +TLS library. It exists to give shipping embedded products the security audit trail the EU -Cyber Resilience Act and IEC 62443 expect — as a component you add, not a -redesign. +Cyber Resilience Act and IEC 62443 expect: a component you add, not a redesign. ## What it costs **+5 KB flash** for a valid, timestamped RFC 5424 record on the wire, and **0.4 KB -of RAM**. **+13.5 KB flash** for the whole path — store-and-forward, device identity, -mutual TLS and AES-GCM encryption at rest — and **37 KB of RAM**, mainly TLS buffers. -Figures measured on FreeRTOS with lwIP on a Cortex-M3, run under QEMU — a -representative device, not a specification. +of RAM**. **+13.5 KB flash** for the whole path - store-and-forward, device identity, +mutual TLS and AES-GCM encryption at rest - and **37 KB of RAM**, mainly TLS buffers. +Each figure is what SolidSyslog adds to a device already running FreeRTOS, lwIP, +FatFs and Mbed TLS: a baseline built to carry the third-party code a real-world +device would already have. Measured on a Cortex-M3 under QEMU, a representative +device rather than a specification. Both are measured by a worked integration, published in full as [solid-syslog-example](https://github.com/cososo-ltd/solid-syslog-example) (consumed @@ -28,28 +28,28 @@ gives the diff and the measured cost. Designed for resource-constrained environments: -- C99, no dynamic memory allocation — every instance lives in a library-internal static pool, sized at compile time -- Transport-agnostic — UDP, TCP, TLS, or bring your own -- Buffer-agnostic — PassthroughBuffer (direct send), portable CircularBuffer (mutex-injected ring), POSIX message queue, or bring your own +- C99, no dynamic memory allocation - every instance lives in a library-internal static pool, sized at compile time +- Transport-agnostic - UDP, TCP, TLS, or bring your own +- Buffer-agnostic - PassthroughBuffer (direct send), portable CircularBuffer (mutex-injected ring), POSIX message queue, or bring your own - MISRA C:2012 informed ## Capabilities RFC 5424 structured formatting over UDP (RFC 5426), TCP (RFC 6587), and TLS / mutual TLS (RFC 5425). Asynchronous buffering, rotating block store-and-forward, -and at-rest record protection — CRC-16 for accidental corruption, or keyed +and at-rest record protection: CRC-16 for accidental corruption, or keyed HMAC-SHA256 / AES-256-GCM where a local attacker is in scope. The audit-logging capabilities an IEC 62443 deployment draws on are mapped control by control in the [IEC 62443 guide](https://docs.cososo.co.uk/solid-syslog/iec62443/). -SolidSyslog is built for embedded and RTOS targets. Every platform dependency — -TCP/IP stack, TLS library, filesystem, OS primitives, clock — is injected through +SolidSyslog is built for embedded and RTOS targets. Every platform dependency - +TCP/IP stack, TLS library, filesystem, OS primitives, clock - is injected through a vtable, so the library ports to an embedded OS by filling roles rather -than editing Core. Reference adapters ship for FreeRTOS on Cortex-M (networking -via FreeRTOS-Plus-TCP or lwIP, transport security via `SolidSyslogMbedTlsStream` -over Mbed TLS, persistent store-and-forward over ChaN FatFs or FreeRTOS-Plus-FAT) -— and for POSIX and Windows, fully supported as development, test, and edge / -gateway hosts. Bring your own stack and the same Core runs unchanged. +than editing Core. Platforms ship for FreeRTOS on Cortex-M, and for POSIX and +Windows, fully supported as development, test, and edge / gateway hosts. Which +upstream fills which capability is in the +[platform matrix](https://docs.cososo.co.uk/solid-syslog/platforms/). Bring your +own stack and the same Core runs unchanged. TLS revocation (CRL / OCSP) is not performed by the library. Whether it is enforced depends on the TLS backend and platform you configure. @@ -58,7 +58,7 @@ enforced depends on the TLS backend and platform you configure. Full documentation lives at [docs.cososo.co.uk/solid-syslog](https://docs.cososo.co.uk/solid-syslog/), organised -around what you came to do: **Overview**, **Adopt**, **Port a new platform**, +around what you came to do: **Core**, **Integrate**, **Platforms**, **Compliance**, **API reference**, and **Maintaining**. New here? [Compliance in one page](https://docs.cososo.co.uk/solid-syslog/overview/) is the fastest orientation for evaluators. @@ -71,15 +71,15 @@ time, stating what each stage adds, the question that decides whether you need i and an indication of what it costs. [Adding it to your build](https://docs.cososo.co.uk/solid-syslog/build-integration/) -is the build detail behind it: the capability matrix, the three ways to consume the -library — CMake, Make, and a source manifest for an IDE project — and the -compile-time tunables. +is the build detail behind it: how the library composes, how to pick your stack, the +three ways to consume it - CMake, Make, and a source manifest for an IDE project - +and the compile-time tunables. ## Building and testing Developing the library itself? See -[Building and testing](https://docs.cososo.co.uk/solid-syslog/builds/) — the -contributor/maintainer preset catalogue — alongside the pre-PR check budget, the BDD +[Building and testing](https://docs.cososo.co.uk/solid-syslog/builds/) - the +contributor/maintainer preset catalogue - alongside the pre-PR check budget, the BDD infrastructure, the CI pipeline, and the container images, all under **Maintaining** on the documentation site. (Consuming the library in your product is the integration path above.) @@ -88,7 +88,7 @@ path above.) SolidSyslog is OO-in-C. Every platform dependency and every optional feature is a vtable role, injected at setup and composed at link time, so a feature you do not -wire is dropped by the linker rather than excluded by the preprocessor — Core's +wire is dropped by the linker rather than excluded by the preprocessor. Core's implementation contains no conditional compilation at all. Public headers are split by audience: application code that logs events includes `SolidSyslog.h` and nothing else, while the setup that builds a logger includes `SolidSyslogConfig.h` plus one @@ -100,17 +100,18 @@ that split and links the generated reference for every header, type and symbol. the anatomy of an adapter, and the Null object that stands in for any role you leave unfilled. -[`Bdd/Targets/`](Bdd/Targets/) holds one BDD-driven binary per platform — Linux, -Windows, and FreeRTOS on QEMU — each exercising the library end to end against a real -syslog server; see [BDD testing](https://docs.cososo.co.uk/solid-syslog/bdd/). +[`Bdd/Targets/`](Bdd/Targets/) holds one BDD-driven binary per platform - Linux, +Windows, and two FreeRTOS-on-QEMU builds, one per network stack - each exercising +the library end to end against a real syslog server; see +[BDD testing](https://docs.cososo.co.uk/solid-syslog/bdd/). ## Compliance -- [CRA guide](https://docs.cososo.co.uk/solid-syslog/cra/) — the Annex I map: the requirement that names logging, the requirements an audit trail contributes to, what the project publishes for your vulnerability handling, and the dates the Regulation applies from -- [Compliance in one page](https://docs.cososo.co.uk/solid-syslog/overview/) — the evaluator's one-screen orientation on CRA and IEC 62443 -- [IEC 62443 compliance guide](https://docs.cososo.co.uk/solid-syslog/iec62443/) — the audit-logging-relevant controls, and what the library provides against each -- [RFC compliance matrix](https://docs.cososo.co.uk/solid-syslog/rfc-compliance/) — sender-side coverage of RFC 5424, 5426, 6587, and 5425 -- [Threat model](https://docs.cososo.co.uk/solid-syslog/security/threat-model/) — the division of responsibility between the library and your product +- [CRA guide](https://docs.cososo.co.uk/solid-syslog/cra/) - the Annex I map: the requirement that names logging, the requirements an audit trail contributes to, what the project publishes for your vulnerability handling, and the dates the Regulation applies from +- [Compliance in one page](https://docs.cososo.co.uk/solid-syslog/overview/) - the evaluator's one-screen orientation on CRA and IEC 62443 +- [IEC 62443 compliance guide](https://docs.cososo.co.uk/solid-syslog/iec62443/) - the audit-logging-relevant controls, and what the library provides against each +- [RFC compliance matrix](https://docs.cososo.co.uk/solid-syslog/rfc-compliance/) - sender-side coverage of RFC 5424, 5426, 6587, and 5425 +- [Threat model](https://docs.cososo.co.uk/solid-syslog/security/threat-model/) - the division of responsibility between the library and your product Reporting a vulnerability: [`SECURITY.md`](SECURITY.md). @@ -121,7 +122,7 @@ Copyright 2026 Cozens Software Solutions Limited. Licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE.md). Free for noncommercial, personal, educational, and government use. -For commercial licensing — including pricing, the early-adopter programme, and -the platform adapter policy — see the +For commercial licensing - including pricing, the early-adopter programme, and +the platform adapter policy - see the [SolidSyslog product page](https://www.cososo.co.uk/products/solid-syslog/), or use the contact form at [cososo.co.uk](https://www.cososo.co.uk/#contact). From 8d89d5ae2dd71ca08fd3ba3e5c84925e3316cf90 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 07:13:46 +0100 Subject: [PATCH 3/8] docs: S23.22 say the thing rather than call it the point Four sentences in the hardening path used "is the point" where the sentence that follows already carries the argument. Part of #708 --- docs/hardening-path.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hardening-path.md b/docs/hardening-path.md index aa458784..aca86e46 100644 --- a/docs/hardening-path.md +++ b/docs/hardening-path.md @@ -112,7 +112,7 @@ its Null object and reported — so the only evidence is what the handler says: [syslog] CRITICAL SolidSyslog bad-config (detail 2) ``` -Doing it in this order is the point. Wire everything at once and see nothing, and you +The order matters. Wire everything at once and see nothing, and you cannot tell a working logger from a silent one. Seeing the faults first, then watching them go quiet as each collaborator arrives, is the difference between believing it works and knowing. @@ -287,7 +287,7 @@ static uint8_t s_ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(SYSLOG_BUFFER_RECOR .Buffer = SolidSyslogCircularBuffer_Create(SolidSyslogFreeRtosMutex_Create(), s_ring, sizeof(s_ring)), ``` -This separates logging an event from sending it, and that is the point. +This separates logging an event from sending it. `SolidSyslog_Log` becomes safe to call from any number of tasks, and potentially cheap enough to call from the place the event actually happens rather than from somewhere convenient later. Nothing that logs waits on the network. @@ -410,7 +410,7 @@ struct SolidSyslogOriginSdConfig originConfig = { sd[3] = SolidSyslogOriginSd_Create(&originConfig); ``` -This lands after storage, and the reason is the point. While records went straight out, +This lands after storage for a reason. While records went straight out, "who sent this" was implied by the connection they arrived on. Once records can replay hours later that stops being true, and the record has to say so itself. @@ -549,8 +549,8 @@ product knows. A collector can use it to confirm a record really did arrive over really was sealed at rest, and to alert on a device whose pipeline has weakened. Those are the values in force at this stage. They change as the remaining stages land — -`transport="mtls"` at stage 19, `atRest="aes-256-gcm"` at stage 20 — which is the point: -derive both from the handles the device actually holds, not from what you intended to +`transport="mtls"` at stage 19, `atRest="aes-256-gcm"` at stage 20 — so derive both from +the handles the device actually holds, not from what you intended to configure. A credential that failed to load leaves the device less protected than its configuration suggests, and an element claiming protection that is not in force is worse than no element at all, because that claim is exactly what a collector is watching for. From 6d9b23e664c87f2109182892c9eb8cc6cd4edd85 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 07:46:04 +0100 Subject: [PATCH 4/8] docs: S23.22 describe the board as it behaves Two rules were wrong. Adding an issue to the board was documented as a manual step, and setting Done on close as another; both are project workflows now. Read the board rather than the workflow list to confirm it: of 278 items every one has a parent and no parentless issue appears, and all 272 closed items are Done without exception -- a field set by hand would have missed one. The add-to-board recipe goes with them. What replaces it is the query for reading board state, which carries the gotcha that issue.projectItems returns zero here because the project is user-owned and the repository org-owned. The WIP limit is reframed as what it now is: a limit on open pull requests, since a linked PR is what moves a story to In Progress. Also: docs/platforms/*.md matched every platform page before the restructure and now matches only the index. Part of #708 --- CLAUDE.md | 71 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9c1e3df8..c5566b16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,9 +101,18 @@ include them), run `addSubIssue` retroactively; it's idempotent-safe on closed i ## Project Board Membership The `SolidSyslog` project board (`gh project list --owner DavidCozens` → project 1) has a -`Status` single-select field with options **Todo**, **In Progress**, **Done**. Adding an -issue to the repo does **not** add it to the board — that is a separate step and must be -done explicitly. +`Status` single-select field with options **Todo**, **In Progress**, **Done**. + +Project workflows keep membership and status; there is no manual step. Linking a story +under its epic with `addSubIssue` puts it on the board at `Todo`, opening a pull request +that links the issue moves it to `In Progress`, and closing it sets `Done`. An issue with +no parent never reaches the board at all. + +Membership and `Done` were verified on 2026-08-12 by reading the board rather than the +workflow list: of 278 items every one has a parent, no parentless issue appears, and all +272 closed items are `Done` without exception. Re-check the same way after any workflow +change — an automation being enabled says nothing about which field it writes, and the +API exposes each workflow's name and enabled flag but not its action. ### Convention @@ -124,7 +133,10 @@ done explicitly. housekeeping step, not a status transition. Archived items stay on the project and still count in the epic's sub-issue roll-up. -### Add-to-board recipe +### Repairing board state by hand + +Nothing routine needs this — the workflows above place items and set status. It is here +for the case where one has not fired, or a status is wrong and needs correcting. ```bash # Project and Status field IDs (stable for this repo): @@ -132,24 +144,27 @@ done explicitly. # statusField = PVTSSF_lAHOAPhEnM4BTETqzhAat7w # options = Todo:f75ad846 In Progress:47fc9ee4 Done:98236657 -# 1. Get the issue's node ID. -gh api graphql -f query=' -query { - repository(owner: "cososo-ltd", name: "solid-syslog") { - issue(number: ) { id } +# Read the board. `issue.projectItems` returns 0 here -- the project is user-owned and +# the repository org-owned -- so enumerate the project's items and paginate past 100. +gh api graphql --paginate -f query=' +query($endCursor: String) { + user(login: "DavidCozens") { + projectV2(number: 1) { + items(first: 100, after: $endCursor) { + pageInfo { hasNextPage endCursor } + nodes { + id + content { ... on Issue { number state } } + fieldValueByName(name: "Status") { + ... on ProjectV2ItemFieldSingleSelectValue { name } + } + } + } + } } }' -# 2. Add to project (returns the new project item's id). -gh api graphql -f query=' -mutation { - addProjectV2ItemById(input: { - projectId: "PVT_kwHOAPhEnM4BTETq", - contentId: "" - }) { item { id } } -}' - -# 3. Set Status (use the option id matching Todo / In Progress / Done). +# Correct a status, using the item id the query above returns. gh api graphql -f query=' mutation { updateProjectV2ItemFieldValue(input: { @@ -203,21 +218,21 @@ For every new story: Pad both the epic and story numbers to two digits. 2. `addSubIssue` it under the parent epic (see **Issue / Epic Linking** above). The Parent-issue link is what groups the story into the correct swimlane. -3. Add the story to the project board with `Status = Todo` using the recipe above. Do - **not** add the parent epic itself — the swimlane appears automatically once a child - story is on the board. + +There is no third step. Step 2 puts the story on the board at `Todo`, and the swimlane +appears with it. Do **not** add the parent epic — it is not an item. ### Work-in-progress limit `In Progress` holds at most two items — typically one functional story and one BDD -story. **Check the count before moving anything into `In Progress`, and say so if the -move would make it three.** The limit is only worth having if someone is watching it, -and the board does not enforce it. +story. A linked pull request is what moves a story there, so this is in practice a limit +on open PRs: **check the count before opening one that would make it three, and say so.** +The limit is only worth having if someone is watching it, and the board does not enforce +it. ### When closing a story -When a PR merges and closes a story, flip its project Status to `Done`. Closing the -issue does not update the Status field automatically. +A merged PR that closes the story sets `Done` for you. Epics don't have a Status field on the board (they aren't items). When every child story is Done, the swimlane naturally becomes all-Done; the epic issue itself should @@ -429,7 +444,7 @@ why.** Read the header rather than a copy of it. For the wider map: - `docs/roles/index.md` — the roles, each with its vtable contract and the backends that realise it. -- `docs/platforms/*.md` — what each platform pack supplies. +- `docs/platforms//index.md` — what each platform supplies. - `docs/api-reference/` plus the generated Doxygen indexes (`docs/api/files.md`, `annotated.md`, `functions.md`, `macros.md`) — every header and symbol. From 1c60dd54d39743bc90644a824ee728da21c9ccc8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 07:48:40 +0100 Subject: [PATCH 5/8] docs: S23.22 state precedence once Both files carried the precedence rule, in the passage that says neither restates the other, and the copies had drifted three ways: CLAUDE.md ranks itself and SKILL.md between the executable files and a conversational briefing, SKILL.md had lost that rung, and the two worded the briefing sentence differently. CLAUDE.md is the entry point -- it is the file that tells you to read this one -- so the rule stays there and this file points at it. Part of #708 --- SKILL.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/SKILL.md b/SKILL.md index 3ed0c955..2f0b1e66 100644 --- a/SKILL.md +++ b/SKILL.md @@ -10,13 +10,9 @@ series by Cozens Software Solutions Limited. This file holds **how we work together**. [`CLAUDE.md`](CLAUDE.md) holds **how the repository works** — git and issue workflow, project structure, naming, code style, -design patterns, and the board and milestone conventions. Neither restates the other. - -When two sources disagree, the repository wins: `.clang-format`, `.clang-tidy`, -`.markdownlint-cli2.jsonc`, `CMakePresets.json` and `.github/workflows/ci.yml` are -executable and cannot drift from what actually happens. A briefing in conversation -outranks both files — it is the most recent intent — but say so when it contradicts a -file, so the file gets fixed rather than quietly bypassed. +design patterns, and the board and milestone conventions. Neither restates the other, +precedence included: what wins when two sources disagree is stated once, in CLAUDE.md +under **Precedence, when two sources disagree**. ## Collaboration modes From cf6fd317fba5d2e192b6a78a630c8b8b12b06c96 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 08:40:03 +0100 Subject: [PATCH 6/8] docs: S23.22 hold the porting guide and roles index to the code Three findings from reading porting.md against what it describes. Cleanup runs under the config lock -- FreeIfInUse holds it across the adapter's cleanup callback -- and the shipped lwIP TCP stream blocks there, so a FreeRTOS integrator wiring taskENTER_CRITICAL as the header recommends hangs on teardown. Raised as #754; the invariant list now says so. Steady-state Send/Read were documented as non-blocking, which no shipped Datagram adapter is. The invariant now states what it really requires, that neither waits on the peer, and points at the platform pages for each stack's own blocking surface -- the Posix page already describes its UDP socket accurately. The roles index promised the porting guide lists each role's Null fallback and a reference implementation, which it does not. Both role counts in prose are gone. check_platform_docs.py already says the count was removed and its enumeration replaces it; two survived, so the check now asserts what its own docstring claims. Proven by injecting one: exit 1 with the fault, exit 0 without. Part of #708 --- docs/porting.md | 16 ++++++++++++++-- docs/roles/index.md | 10 +++++----- scripts/check_platform_docs.py | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/porting.md b/docs/porting.md index 2725408a..66863391 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -144,6 +144,15 @@ This is the only synchronisation primitive the pools use for their own walks. - Idempotent `Close` / `Destroy`. No leak on a partial `Open` failure, no double-free if `Close` and `Destroy` are both called. Release each resource exactly once and null the handle. +- Cleanup runs under the config lock. `FreeIfInUse` holds + `SolidSyslog_LockConfig()` across your cleanup callback, so whatever `Destroy` + does to release a resource happens inside whichever primitive the integrator + installed — a FreeRTOS critical section, at the recommendation above. Blocking + there is not safe: a mutex may not be taken inside `taskENTER_CRITICAL`, and + work that needs another task to run cannot complete while interrupts are off. + The shipped lwIP TCP stream does block this way, tracked as + [#754](https://github.com/cososo-ltd/solid-syslog/issues/754); until that is + resolved, keep teardown in your own adapter non-blocking. - Never free injected handles. An adapter frees only what it created. Handles the integrator passed in (a certificate, an RNG, a caller's socket) are borrowed; the owner frees them. The same applies to an upstream library's @@ -157,8 +166,11 @@ This is the only synchronisation primitive the pools use for their own walks. bounded by an explicit timeout or deadline: a timeout tunable (e.g. `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS`) or a caller-supplied deadline. A `SolidSyslogSleepFunction`, where one is used, only paces the poll loop between - checks; it does not bound the total wait. Steady-state `Send` / `Read` are - non-blocking. + checks; it does not bound the total wait. Steady-state `Send` / `Read` do not + wait on the peer, so a wedged collector cannot stall the servicing pass. That + bounds peer behaviour, not the local stack: a datagram send may still wait on a + full kernel send buffer, and each platform page states where its own blocking + surface lies. - Production-C discipline. Tier 1/2 code is single-return, fully braced, and MISRA-leaning, see [MISRA deviations](misra-deviations.md) and [Naming conventions](NAMING.md). diff --git a/docs/roles/index.md b/docs/roles/index.md index e52a8ead..9931af27 100644 --- a/docs/roles/index.md +++ b/docs/roles/index.md @@ -5,8 +5,8 @@ A role is one capability the library needs filled, defined as a vtable in your own code fills it. Every role has a Null fallback, so an unfilled slot degrades safely instead of dangling at link time. -There are twelve. Each page below is that role's contract — the vtable itself, -and a generated diagram of the backends that realise it. +Each page below is that role's contract — the vtable itself, and a generated +diagram of the backends that realise it. ## Networking @@ -42,9 +42,9 @@ and a generated diagram of the backends that realise it. ## Bring your own -Filling a role is implementing its vtable. [Porting](../porting.md) puts the -twelve contracts side by side, each with its Null fallback and a reference -implementation. +Filling a role is implementing its vtable. [Porting](../porting.md) covers the +anatomy of an adapter and the invariants every one honours, and links each +contract to the backends that already realise it. Which platform fills a role on your target is the [platform × capability matrix](../platforms/index.md). diff --git a/scripts/check_platform_docs.py b/scripts/check_platform_docs.py index 89db9869..3b899157 100644 --- a/scripts/check_platform_docs.py +++ b/scripts/check_platform_docs.py @@ -100,6 +100,14 @@ SCANNED_SUFFIXES = (".c", ".h", ".md") +# A spelt-out count of the roles, which prose must not carry: the enumeration +# checks below are what a thirteenth role trips, and a number beside them just +# rots. Spelt-out only -- a bare digit would match version numbers and sizes. +ROLE_COUNT_IN_PROSE = re.compile( + r"\b(?:eleven|twelve|thirteen|fourteen)\b(?=[^.]{0,40}?\b(?:roles?|contracts?)\b)", + re.IGNORECASE, +) + # An #include names a header the compiler must find, not a platform the prose # is describing. The boundary is editorial; what a translation unit depends on # is the build's business and is governed there. @@ -257,6 +265,13 @@ def role_faults(): for orphan in re.findall(rf"{re.escape(prefix)}structSolidSyslog(\w+)\.md", text): if orphan not in roles: faults.append(f"{listing} links {orphan} as a role, but no SolidSyslog{orphan}Definition.h declares it") + # A count in prose is the thing this check replaced. It cannot be + # asserted, so a thirteenth role would leave it quietly wrong. + for spelt in re.findall(ROLE_COUNT_IN_PROSE, text): + faults.append( + f"{listing} states the number of roles in prose ('{spelt}') — " + "the listing above is what keeps the set honest, so leave the count out" + ) return faults From ab0bb5424f653bf60c837c3f3a88a36f29a9c0bf Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 11:07:10 +0100 Subject: [PATCH 7/8] docs: S23.22 hold the structured-data guide to the value sink's contract BoundedString's parameter is maxDecodedLength, and the bound counts what a reader's un-escaping decoder extracts rather than on-wire bytes -- an escape pair as one, a substituted U+FFFD as three. Documented as "at most maxLength bytes", it would have someone size a receiver field too small. Ill-formed UTF-8 is substituted with U+FFFD, not validated, and a value that was escaped or substituted does not come back unchanged. Part of #708 --- docs/structured-data.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/structured-data.md b/docs/structured-data.md index a80ac1c6..547f6957 100644 --- a/docs/structured-data.md +++ b/docs/structured-data.md @@ -44,7 +44,7 @@ which the library calls with an `SD-ELEMENT` writer when it builds a message: static void ExampleSd_Format(struct SolidSyslogStructuredData* base, struct SolidSyslogSdElement* element) { - (void) base; /* stateless here — see "Carrying data" below */ + (void) base; /* stateless here — see "Carrying instance or per-call data" below */ SolidSyslogSdElement_Begin(element, "example", 32473U); SolidSyslogSdValue_String(SolidSyslogSdElement_Param(element, "detail"), "Hello World"); @@ -135,12 +135,15 @@ first example) is never affected. Write values with `SolidSyslogSdValue`: - `SolidSyslogSdValue_String(value, source)`: a NUL-terminated string. -- `SolidSyslogSdValue_BoundedString(value, source, maxLength)`: at most `maxLength` bytes. +- `SolidSyslogSdValue_BoundedString(value, source, maxDecodedLength)`: capped for a + receiver that parses into a width-limited field. The bound counts what the reader's + un-escaping decoder extracts, not the on-wire bytes. - `SolidSyslogSdValue_Uint32(value, number)`: decimal digits. The library applies the RFC 5424 §6.3.3 escaping for you (`"`, `\`, and `]` are -backslash-escaped) and validates UTF-8; you pass the raw value and the receiver gets it -back unchanged. Output is bounded by the message buffer, so a value can never overrun it. +backslash-escaped) and substitutes ill-formed UTF-8 with U+FFFD; you pass the raw value +and a receiver that un-escapes gets well-formed text back. Output is bounded by the +message buffer, so a value can never overrun it. ## What the library owns, and what you own From d3d38b85f5e476b6ca3625aa1da4f41c593d1367 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 12 Aug 2026 11:38:44 +0100 Subject: [PATCH 8/8] docs: S23.22 answer the review, and write the character rule down CodeRabbit found a real flaw in how the board claim was verified, not in its wording: projectV2.items omits archived items unless archivedStates is passed, and the two it was hiding are both parentless -- so "an issue with no parent never reaches the board" was falsified by items the query had filtered out. The claim is softened and the documented read gains the argument. The counts and the date come out with it. This file sets policy rather than recording what was true one afternoon, and a number nothing asserts is a number that rots -- the same reason the role count came out of the roles index. Adds the source character rule: UK-keyboard characters in .c, .h and .cpp, comments included, with the three authorised exceptions named and anything else referred rather than decided either way. Part of #708 --- CLAUDE.md | 43 +++++++++++++++++++++++++++------ docs/platforms/plustcp/index.md | 2 +- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c5566b16..dcbeb1af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,14 +105,15 @@ The `SolidSyslog` project board (`gh project list --owner DavidCozens` → proje Project workflows keep membership and status; there is no manual step. Linking a story under its epic with `addSubIssue` puts it on the board at `Todo`, opening a pull request -that links the issue moves it to `In Progress`, and closing it sets `Done`. An issue with -no parent never reaches the board at all. +that links the issue moves it to `In Progress`, and closing it sets `Done`. The workflows +add nothing that has no parent, so a chore or docs issue raised without an epic stays off +the board; a few early items predate them. -Membership and `Done` were verified on 2026-08-12 by reading the board rather than the -workflow list: of 278 items every one has a parent, no parentless issue appears, and all -272 closed items are `Done` without exception. Re-check the same way after any workflow -change — an automation being enabled says nothing about which field it writes, and the -API exposes each workflow's name and enabled flag but not its action. +Confirm any of that by reading the board rather than the workflow list — an automation +being enabled says nothing about which field it writes, and the API exposes each +workflow's name and enabled flag but not its action. Pass +`archivedStates: [ARCHIVED, NOT_ARCHIVED]` when you read: `projectV2.items` omits +archived items by default, so a board read without it is a partial one. ### Convention @@ -146,11 +147,12 @@ for the case where one has not fired, or a status is wrong and needs correcting. # Read the board. `issue.projectItems` returns 0 here -- the project is user-owned and # the repository org-owned -- so enumerate the project's items and paginate past 100. +# archivedStates is required: without it the archived items are silently missing. gh api graphql --paginate -f query=' query($endCursor: String) { user(login: "DavidCozens") { projectV2(number: 1) { - items(first: 100, after: $endCursor) { + items(first: 100, after: $endCursor, archivedStates: [ARCHIVED, NOT_ARCHIVED]) { pageInfo { hasNextPage endCursor } nodes { id @@ -508,6 +510,31 @@ Deliberate deviations from the MISRA rule set are recorded in - cppcheck runs with `--error-exitcode=1`. Inline suppressions (`// cppcheck-suppress`) must include a comment explaining why. +### Characters in source + +Write what a UK keyboard types. In `.c`, `.h` and `.cpp` — comments included — that +means `-` and not an em or en dash, `...` and not an ellipsis, `->` and not an arrow, +and the ASCII spelling of a symbol: `<=`, `+/-`, `x`, `||`, "sum of". A continuation +ellipsis takes no leading comma: `first, second...`, not `first, second, ...`. + +Three exceptions stand, each authorised rather than assumed: + +- `µ` in a unit, and `§` in an RFC citation. Both read better as themselves and + neither is ambiguous. +- A character that *is* the subject of its comment. The UTF-8 tests name U+00E9, the + euro sign and an emoji to explain the byte sequences they encode; replacing them + would delete the point. +- String literals were left alone in the sweep that established this. Seven carry an + em dash and are runtime or test output, where the character is data rather than + typography. + +**Anything else non-ASCII: ask.** Do not quietly pick a typographic character, and do +not quietly rewrite a sentence to avoid one — either way the decision goes unrecorded. + +This is a rule about source. Documentation under `docs/` keeps typographic characters, +on the reasoning that prose of that length is written with an authoring tool; `README.md` +is hyphenated by hand and is the deliberate exception. + ### MISRA-load-bearing `.clang-format` settings Two settings in `.clang-format` are not merely stylistic — they enforce MISRA C:2012 rules at diff --git a/docs/platforms/plustcp/index.md b/docs/platforms/plustcp/index.md index e05db899..b4b50d5a 100644 --- a/docs/platforms/plustcp/index.md +++ b/docs/platforms/plustcp/index.md @@ -60,7 +60,7 @@ behind it is delivered. `SOLIDSYSLOG_MAX_MESSAGE_SIZE` defaults to 2048, so this reaches any record over about 1.2 KB rather than only unusual ones. Until [#736](https://github.com/cososo-ltd/solid-syslog/issues/736) lands, keep records -on this UDP path inside the payload it carries — but note that limit is library-wide +on this UDP path inside the payload it carries — but note that the limit is library-wide rather than per-transport, so lowering it truncates records on every transport the instance uses, not only this one.