From b2083adf2fe38a14083ba35350f081b14829acb2 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Fri, 21 Aug 2026 21:49:09 +0100 Subject: [PATCH 1/3] docs: point the issue chooser at the platform adapter vote (#781) The four platform-adapter epics - wolfSSL (#691), lwIP Sockets (#692), LittleFS (#693) and Zephyr (#694) - are deliberately left unmilestoned, so that a thumbs-up on one carries real signal about demand rather than being a reaction to something already committed. Nothing told a reader that. A third contact link sends them to those issues, filtered to the platform label and sorted by reactions, so the list arrives already ordered by demand and the most-wanted adapter is the first thing seen. It sits below the security routes and above commercial licensing: the free way to influence what gets built should come before the paid one. Co-authored-by: Claude Opus 5 (1M context) --- .github/ISSUE_TEMPLATE/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ae1db627..9383e773 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,9 @@ contact_links: - name: Security vulnerability (can't use GitHub?) url: https://cososo.co.uk/security/report about: Private web form if you can't use GitHub's "Report a vulnerability" button above. See SECURITY.md. + - name: Vote for a platform adapter + url: https://github.com/cososo-ltd/solid-syslog/issues?q=is%3Aissue+is%3Aopen+label%3Aplatform+sort%3Areactions-%2B1-desc + about: Adapter candidates are open issues, ordered by demand. Add a thumbs-up to the one you need. - name: Commercial licensing & paid support url: https://www.cososo.co.uk/?service=solidsyslog#contact about: Commercial licence, guaranteed response times, or Platform-adapter contributions. From fd4d43a341eaa1dfca1e7677b2c201ac8a540e2d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 31 Aug 2026 20:41:39 +0100 Subject: [PATCH 2/3] ci: bump github/codeql-action to 4.37.9 and group its bumps (#796) Dependabot raised #793 and #794, each bumping one half of the init/analyze pair. init stamps its version into the config file analyze then loads, so either PR alone fails analyze-codeql with "Loaded a configuration file for version '4.37.6', but running version '4.37.9'". Both are required checks, so merging either would have blocked main. Bumps both pins to cdf488f, verified as the commit tag v4.37.9 resolves to, and groups github/codeql-action* so the split cannot recur. Co-authored-by: Claude Opus 5 (1M context) --- .github/dependabot.yml | 11 +++++++++++ .github/workflows/codeql.yml | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 25c57103..ae859506 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,17 @@ updates: open-pull-requests-limit: 5 commit-message: prefix: ci + # An action with several sub-actions is several dependencies to Dependabot + # and one version to the workflow using them. CodeQL enforces that itself: + # init stamps its version into the config file analyze then loads, and + # analyze refuses a config another version wrote, so a PR bumping one of the + # pair fails the required check every time. Grouping makes the bump one PR + # holding both pins, which is also the only way the SHA pinning means what it + # says — two halves of one action cannot drift apart. + groups: + codeql-action: + patterns: + - "github/codeql-action*" # Python packages used by the BDD test harness. - package-ecosystem: pip diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6e7e2b74..9232a5bb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -48,7 +48,7 @@ jobs: sudo apt-get install -y libssl-dev - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: c-cpp build-mode: manual @@ -83,4 +83,4 @@ jobs: done - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 From acaab18a1acf077dc3f90045b2571321069fa97d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 31 Aug 2026 20:50:28 +0100 Subject: [PATCH 3/3] chore: assert that a symbol named in the docs exists (#795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: assert that a symbol named in the docs exists Adds the symbol arm to check_references.py, the seam #773 left for it. A SolidSyslog token in a page under docs/ must resolve to a symbol a public header declares, a file the repository holds, or a target its CMake declares. It found one live defect: the FreeRTOS setup page called SolidSyslogCircularBuffer_Create with a compound literal of a struct SolidSyslogCircularBufferConfig that has never existed. Fixed here, along with the sentence beneath it offering to leave the mutex unfilled, which the positional signature does not allow. Co-Authored-By: Claude Opus 5 (1M context) * chore: check the whole of a namespaced CMake target, not its prefix SYMBOL stopped at the colon, so `SolidSyslog::FreeRtos` extracted as bare `SolidSyslog` — a real target — and resolved with the half that names the pack never looked at. Any `SolidSyslog::Anything` passed. The pattern now takes the `::` segments as part of the token, and the target extractor keeps them too, so the aliases the platform pages tell integrators to link are matched whole against what CMake declares. --------- Co-authored-by: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 9 +- docs/platforms/freertos/setup.md | 12 +- scripts/check_references.py | 214 +++++++++++++++++++++++++++++-- 3 files changed, 212 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55fad9b1..d6079c39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -803,9 +803,12 @@ jobs: # A path written in code font in a sentence, or in a CMake list, a # Compose mount or a path filter, is text nothing reads back out — the - # link check below sees Markdown links only. This asserts every such path - # still exists, so a rename cannot leave a green reference to nothing. - - name: Check the paths documents and build files name exist + # link check below sees Markdown links only. Nor does anything read back + # the library symbols a page names, in prose or in a fenced example. This + # asserts both still exist, so a rename cannot leave a green reference to + # nothing, and a page cannot offer an integrator a symbol that never + # compiled. + - name: Check the paths and symbols documents and build files name exist run: python3 scripts/check_references.py # Guard the build hooks against regression — the source-link rewrite's diff --git a/docs/platforms/freertos/setup.md b/docs/platforms/freertos/setup.md index 0b1c1a38..de5502e6 100644 --- a/docs/platforms/freertos/setup.md +++ b/docs/platforms/freertos/setup.md @@ -33,20 +33,18 @@ The mutex exists to make a buffer safe when the task calling `SolidSyslog_Log` is not the task calling `SolidSyslog_Service`: ```c +static uint8_t ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(8)]; + struct SolidSyslogMutex* mutex = SolidSyslogFreeRtosMutex_Create(); struct SolidSyslogBuffer* buffer = - SolidSyslogCircularBuffer_Create(&(struct SolidSyslogCircularBufferConfig) { - .Sender = sender, - .Mutex = mutex, - /* ring storage sized with SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES */ - }); + SolidSyslogCircularBuffer_Create(mutex, ring, sizeof(ring)); ``` The ring memory and the mutex must both outlive the buffer. -If both calls happen on one task, leave the role unfilled — the Null mutex is -the right answer and costs nothing. +If both calls happen on one task, pass `SolidSyslogNullMutex_Get()` — it is the +right answer and costs nothing. ## Uptime diff --git a/scripts/check_references.py b/scripts/check_references.py index ace1753f..978e8c33 100644 --- a/scripts/check_references.py +++ b/scripts/check_references.py @@ -9,10 +9,26 @@ out. A rename leaves the reference behind, green, and pointing nowhere. This is one pass over those files, pulling out every reference of a known kind -and asserting each resolves. Today there is one kind, the repo-relative path. -The second, deferred to #740, is the SolidSyslog symbol a page names: the same -files, the same extraction pass, and the same exception problem, which is why it -belongs here as another KINDS row rather than in a second script. +and asserting each resolves. There are two kinds. The first is the repo-relative +path. The second is the SolidSyslog symbol a page under `docs/` names, in prose +or in a fenced example: nothing compiles those, so a page can name a symbol that +was renamed, or invent one outright, and still build green under +`mkdocs build --strict` while handing an integrator who copies it an +undefined-symbol error. + +**What a symbol token may resolve to** is three things, because a page has three +legitimate reasons to write one: a symbol a *public* header declares, a file the +repository holds, or a target its CMake declares. Public headers only — an +integrator can call nothing else, so widening the corpus to `Core/Source` would +let a page offer an internal function as though it were API. The few pages that +name one deliberately say so in the exception list. + +Two limits are worth stating, since neither is a bug to be fixed here. A token +naming the *header* where the *function* was meant resolves, because the file +arm accepts it — this catches names that do not exist, not names used wrongly. +And an example whose call is wrong in some way that invents no name — a +transposed argument, a wrong type — passes; compiling the fenced examples is a +larger and different check. **The extraction is deliberately narrow**, because a heuristic loose enough to need a long exception list is one that will be switched off. Three rules do the @@ -31,14 +47,23 @@ or a path filter does not do. **The exception list is the part to watch.** Each entry says why the reference -is meant not to resolve. All of them so far are one thing: a document quoting a -path as some *other* file would write it, to state a rule about how paths are -written. Past a handful of entries the extraction is wrong and should be -tightened rather than the list grown. +is meant not to resolve, and they fall into two groups. A document quotes a path +as some *other* file would write it, to state a rule about how paths are written; +or a page names an internal symbol deliberately, because what it is explaining is +how the library is built rather than how it is called. Past a handful of entries +the extraction is wrong and should be tightened rather than the list grown. + +One file is exempted whole, which is the mechanism to use sparingly and never +reach for to silence a page that is merely noisy. `docs/NAMING.md` is a document +*about* identifiers: it quotes spellings the project rejected, placeholders +standing for any class, and shapes no file has yet. Twenty-six rows each saying +so would be one fact written twenty-six times, and the extraction that produced +them is not wrong. Run: python3 scripts/check_references.py """ +import functools import os import re import subprocess @@ -46,6 +71,9 @@ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Stands where a token would, for a file every reference in which is exempt. +WHOLE_FILE = "*" + # Deliberate exceptions, each with the reason the reference is meant not to # resolve. Matched on (file, token). Printed on every run: an exception nobody # sees is an exception nobody revisits. @@ -77,6 +105,67 @@ "Tests/X/", "the shape the next platform's test directory would take, not one that exists", ), + ( + "docs/NAMING.md", + WHOLE_FILE, + "is about identifiers rather than written with them, so it quotes rejected " + "spellings, placeholders standing for any class, and shapes no file has yet", + ), + ( + "docs/misra-deviations.md", + "SolidSyslogX", + "stands for any class, in a rule about what a name may not collide with", + ), + ( + "docs/misra-deviations.md", + "SolidSyslogClass_Function", + "the public-function shape itself, named where the rule is about the shape", + ), + ( + "docs/misra-deviations.md", + "SolidSyslogStaticAssertViolated", + "the identifier the static-assert trick declares to fail on, which exists only " + "in a build that is already broken", + ), + ( + "docs/misra-deviations.md", + "SolidSyslogFormatterStorage", + "Core-internal: the deviation is about how the Formatter is built, not how it " + "is called, and it is not on the public surface", + ), + ( + "docs/misra-deviations.md", + "SolidSyslogMessageFormatter_Format", + "Core-internal, named in the deviation covering its own body", + ), + ( + "docs/misra-deviations.md", + "SolidSyslog_Install", + "the family of config-installing functions written as a stem, not one symbol", + ), + ( + "docs/porting.md", + "SolidSyslogPoolAllocator_AcquireFirstFree", + "Core-internal by design: an adapter calls it, and the porting guide is where " + "that is explained, but it is not API an integrator may reach for", + ), + ( + "docs/porting.md", + "SolidSyslogPoolAllocator_IndexIsValid", + "its sibling, in the same worked example", + ), + ( + "docs/rfc-compliance.md", + "SolidSyslogFormatter_BoundedString", + "Core-internal: named as the place the UTF-8 rule is enforced, which is a " + "statement about the implementation rather than an entry point", + ), + ( + "docs/containers.md", + "SolidSyslogTests", + "a real target, declared as `${PROJECT_NAME}Tests`, so the name resolves only " + "after CMake expands it and the extraction cannot see it", + ), ] # Where references are looked for. Build files are here because nothing else @@ -110,6 +199,27 @@ # this repository uses. Each names something other than one file here. NOT_A_PATH = re.compile(r"://|[^A-Za-z0-9._/+:-]") +# Every identifier this library owns carries the project prefix, so the prefix is +# what finds them and nothing else has to be recognised. The `::` segments are +# part of the token rather than a boundary, because a namespaced alias must be +# matched whole: stopping at the colon would resolve `SolidSyslog::AnythingAtAll` +# on the strength of the bare prefix and never look at the half that was wrong. +SYMBOL = re.compile(r"\bSolidSyslog(?:[A-Za-z0-9_]*(?:::[A-Za-z0-9_]+)*)") + +# A target CMake declares by a literal name, the `SolidSyslog::Pack` aliases +# included. One composed from a variable is not matched, and is why +# `SolidSyslogTests` is in the exception list. +CMAKE_TARGET = re.compile(r"^\s*add_(?:executable|library|custom_target)\s*\(\s*([A-Za-z0-9_:]+)") + +# A public header: the only declarations an integrator may call. `Interface/` is +# the API boundary in Core and in every platform pack alike. +PUBLIC_HEADER = "/Interface/" + +# Where symbols are asserted. `docs/` is what an integrator reads; the repository's +# own guides and READMEs name internal classes and build targets legitimately, and +# checking them would be checking notes to ourselves. +DOCUMENTED = "docs/" + def read(relative): with open(os.path.join(ROOT, relative), encoding="utf-8") as handle: @@ -238,6 +348,69 @@ def path_artefacts(found): } +@functools.lru_cache(maxsize=None) +def declared_names(): + """Every name a `SolidSyslog…` token in a page may legitimately be. + + Three sources, because a page has three reasons to write one: the symbols a + public header declares, the files the repository holds, and the targets its + CMake declares. Read once per run — the files are the same files `scanned()` + walks, and reading them per line would be reading the tree per line, which + is what the cache is for. + """ + names = set() + for relative in git("ls-files"): + base = os.path.basename(relative) + if relative.endswith(".h") and PUBLIC_HEADER in relative: + names |= set(SYMBOL.findall(read(relative))) + if base in SCANNED_NAMES: + names |= cmake_targets(read(relative)) + if base.startswith("SolidSyslog"): + names.add(base.split(".")[0]) + return names + + +def cmake_targets(text): + """The targets one CMake file declares by a literal name.""" + matched = (CMAKE_TARGET.match(line) for line in text.splitlines()) + return {found.group(1) for found in matched if found} + + +def symbols_in(relative, line, verbatim, roots): + """Every token on this line that claims a symbol of this library exists. + + Mined from code spans and fenced blocks, and deliberately not from link + targets: a link to `../api/SolidSyslogConfig_8h.md` names a generated page, + which is a path and already the other kind's business. A word holding `/` is + dropped for the same reason. + + A token followed by `<` is a placeholder — `SolidSyslogNull_Get`, + `SolidSyslogErrors` — and names the pattern rather than a class. + `docs/NAMING.md` requires that spelling where the statement is about the + pattern, so recognising it is cheaper than exempting every page that obeys. + """ + if not (relative.startswith(DOCUMENTED) and relative.endswith(".md")): + return + texts = [line] if verbatim else [m.group(1) for m in CODE_SPAN.finditer(line)] + for text in texts: + for word in text.split(): + if "/" in word: + continue + for match in SYMBOL.finditer(word): + if word[match.end() : match.end() + 1] != "<": + yield match.group(0) + + +def symbol_resolves(relative, token): + return token in declared_names() + + +def nothing(found): + """No symbol reference is unassertable: unlike a path, none of them can name + something only a build produces.""" + return set() + + class Kind: """One class of reference: how to find it, how to resolve it, what to say. @@ -253,8 +426,7 @@ def __init__(self, extract, resolves, complaint, unassertable): self.unassertable = unassertable -# The kinds of reference asserted. #740 adds the SolidSyslog symbol a page names -# as a second row here, reusing the pass above and the exception list below. +# The kinds of reference asserted, each one pass over the same files. KINDS = ( Kind( extract=paths_in, @@ -262,6 +434,12 @@ def __init__(self, extract, resolves, complaint, unassertable): complaint="names a path that does not exist", unassertable=path_artefacts, ), + Kind( + extract=symbols_in, + resolves=symbol_resolves, + complaint="names a symbol, file or target that does not exist", + unassertable=nothing, + ), ) @@ -304,9 +482,14 @@ def check(): sys.exit(f"no documents or build files found under {ROOT} — is this a git checkout?") exempt = {(path, token) for path, token, _ in ALLOWED} + exempt_files = {path for path, token, _ in ALLOWED if token == WHOLE_FILE} faults = [] for kind in KINDS: - found = {r: n for r, n in references(kind).items() if r not in exempt} + found = { + r: n + for r, n in references(kind).items() + if r not in exempt and r[0] not in exempt_files + } assertable = set(found) - kind.unassertable(set(found)) faults += [ f"{relative}:{found[(relative, token)]} {kind.complaint}: {token}" @@ -328,5 +511,10 @@ def check(): ) sys.exit(1) for path, token, reason in ALLOWED: - print(f"allowed: {path} may name {token} — {reason}") - print(f"every path named by {len(scanned())} documents and build files exists") + named = "anything" if token == WHOLE_FILE else token + print(f"allowed: {path} may name {named} — {reason}") + print( + f"every path named by {len(scanned())} documents and build files exists, " + f"and every symbol named under {DOCUMENTED} resolves to one of " + f"{len(declared_names())} declared names" + )