# Changelog ## v1.0.13 — 2026-08-25 **Ten test files and both tools carried no SPDX tag in a published MIT tree, and the baseline said every source file did.** The library proper was clean, 9 of 9. `tests/` was 3 of 13 — the three v1.0.11 wrote — and `tools/`, shipped deliberately into the public tree at v1.0.10, was 0 of 2. This is not a licensing hole: `LICENSE` covers the repository and the copyright line is intact. It is per-file provenance in a tree whose whole posture is that a third party may take ONE FILE out of it. An SPDX line is what makes that file's terms legible without reading the repo root, and the tree was offering it inconsistently — which is worse than not offering it, because the files that carry one imply the files that do not are different. All twelve are tagged, each in the shape its neighbours already used: for the C++ suites the last line of the leading header block after a copyright line, and for the shell scripts line 2 directly under the shebang — the form `base/antpeer/scripts/shm-residue-check.sh` was already using. **The guard is the part that lasts.** `tools/check-spdx-headers.sh` runs in `make test` beside `spec-check`, and it DERIVES its file set by scanning the shipped directories rather than holding a list. That is deliberate: the defect was ten files nobody had enumerated, and a recorded count would reproduce it the first time a suite was added — the new file would be missing from the tree and from the list, and the list would still agree with itself. Anything matching the scanned extensions owes a tag by existing. `vendor/` is excluded, because a vendored file carries its upstream's terms and must not be stamped with ours. The guard carries a self-test and was mutation-verified: stripping the tag from one suite fails the build naming that file, restoring it returns green. 25 shipped source files, all tagged; `make test` 18/18. ## v1.0.12 — 2026-08-18 **The spec defines twelve word types and this library knew eleven.** DURATION (`+`, 0x2B) was in §6.1's both-forbidden row, in the §6.2 type table, in §6.2.3 with a canonical form, in §6.5 with its exact wire bytes, and in the shared glyph manifest. It was not in `WordType`. So `word_encode` refused it, and the specification's own §6.5 example drove the reference parser into Error with zero words and zero messages — while `antheos::iso::parse_duration` round-tripped the identical body perfectly. A third party could find this by reading §6.5 and running the reference implementation. It fell through two complete-looking tickets. The DURATION body parser scoped itself to the body and delivered exactly what it promised; the word-type registration was covered by no ticket at all. TIMESTAMP, locked in the same release, got both halves. The fix is two lines — the enum member and its case in `is_valid_word_type`. `flags_conform` needed no change, because all three flag predicates end in `default: return false` and DURATION's answer is both-forbidden. The comment above that predicate, written when it was extracted in v1.0.11, said "a future word type changes its answer once"; this is that word type, and it changed none. **The coverage question was the real finding.** DURATION was found by walking the §6.2 table against the enum, not by a test failing, so the whole of §6.5 was swept the same way. Four of its twelve word examples had no byte-for-byte check at all — DURATION, TIMESTAMP, BLOB and the hex INTEGER — and four more appeared only inside frame tests, where the word is a component of a larger assertion rather than the claim being made. The uncovered set included the BLOB example, in the exact word whose radix handling was a shipped defect in v1.0.11. All twelve §6.5 word examples now have a byte-for-byte test of their own, each also asserting its §6.1 flag row: forbidden flags are refused, required flags are required. The suite is 361 tests across 12 suites; `test_conformance` is 43. **The README stops counting.** It was wrong about the suite in nine places and had been since v1.0.0: `296` was that release's figure, carried through eleven more; "10 test suites" missed the two v1.0.11 added; six of the ten per-suite rows understated. This is the front page of an MIT repository and the one document a reader can disprove in a single command. Correcting the numbers was the option not taken. A hand-maintained table drifts again on the next release, and this one had already been carried as a known issue for ten weeks without being fixed — during which the note itself went stale, quoting a third figure. So the README no longer states a test count anywhere. The per-suite table survives as a map of which suite covers what, with the counts removed and the two missing suites added. What `make test` prints is the count. The word-type line moves 11 to 12 in the same pass, for the reason above. ## v1.0.11 — 2026-08-07 Six defects closed and the LOGICAL grammar implemented. The defects were found by loading facts for all 144 functions into a DKE store and reasoning over them with the `code` module, then reading each site in source. **The suite was not testing this tree.** Each test rule depended on the static archive but linked `-L build -lantheos`, which finds the `.so` at link time and pins nothing at run time — so with no rpath the loader took `/usr/local/lib/libantheos.so`, installed 2026-06-05, for every test binary. `make test` could not see a change to `src/` at all, which is how six defects accumulated under a green suite. The tests now link the archive each rule already named. - **§6.1 flag rules** — enforced when encoding and nowhere else, and only their forbidden half: every check sat inside `if (flag present)`, so a type REQUIRING radix and unit encoded happily with neither, and `needs_radix` had no caller. Both readers checked that a flag byte was valid and never that the type could carry it. Now one predicate, `wire::flags_conform`, consulted by `word_encode`, `word_decode` and the parser alike. - **BLOB radix** — `allows_radix(Blob)` permits Base32 and the encoder emits it, but `decode_blob_size` fell through to 0, so the tail was never consumed and its payload was read as protocol until the parser resynchronised on a byte that looked like SOM. §6.3's `U` is duotrigesimal — base-32 numeric — so it is one more radix, not an encoding. A size that cannot be read is now reported rather than answered as zero. - **`Context::feed`** discarded the parser's `ParseState` and returned `len` unconditionally, with none of the parser's error surface forwarded. `parse_state()`, `parse_errors()`, `blob_size_errors()` and `total_messages()` are now reachable; `feed`'s return is unchanged, because bytes-taken is a different question from whether they parsed. - **Empty-argument guards** — `id_plain` did not guard where its sibling `id_base32` did, so `verify_response("","","")` built a frame of three empty ID words; `bus::relay` guarded nothing where both `relay_auth_*` builders did, emitting a relay frame with no route. - **Surplus IDs** past the scratch capacity were discarded with no error, no counter and no state change — a truncated message that read as a complete one. `Context::dropped_ids()` reports them. - **Untested paths** — the BLOB send half and the Context auth delegators are now exercised. **LOGICAL body grammar (§6.2.1)** — `antheos::logic` implements the V1 grammar that had been locked since v1.0.8 with no runtime code, leaving Scaleback handling to per-consumer ad-hoc parsing. - `parse_logical` — recursive descent shaped like the EBNF, with an optional teaching message on rejection. - `evaluate_logical` — evaluates against a consumer-supplied atom evaluator. §6.2.1 makes atom SEMANTICS consumer-defined, so the library parses syntax and the consumer says what `H` or `T` mean. - `emit_logical` — the canonical body, carrying parens exactly where dropping them would change what the string parses back to. **TIMESTAMP (§6.2.2) and DURATION (§6.2.3)** — `antheos::iso` implements both canonical forms, which had been locked since v1.0.6 (drift-corrected at v1.0.9) with no runtime code. - `parse_timestamp` / `emit_timestamp` — parse loose, emit the 27-byte canonical UTC form. Offsets are converted; named zones, extended years, leap seconds and empty bodies are rejected with a teaching message. - `parse_duration` / `emit_duration` — parse loose, emit the minimal component form. Any zero duration is `PT0S`; the weeks form is preserved rather than converted to days, because the component shape is part of what was said. - Sub-microsecond digits are TRUNCATED in both, never rounded. §6.2.2 gives the reason: rounding admits multiple valid implementations and truncation has one. That is the whole discipline — two conformant implementations must produce byte-identical words. 353 tests across 12 suites. ## v1.0.10 — 2026-06-20 Spec layer-drift guard added — build-time grep over the Antheos spec for reasoning-vocabulary tokens that signal wire-protocol-vs-reasoner after the v1.0.6 → v1.0.9 §6.2.3 drift cycle confirmed the value of enforcing the wire-protocol-vs-reasoner boundary structurally. - **`tools/check-spec-layer.sh`** scans `docs/ANTHEOS_PROTOCOL_SPEC.md` for 7 reasoning-vocabulary tokens: `arithmet(ic|ically)`, `calendar-relative`, `calendar-anchored`, `fixed-second`, `semantic intent`, `consumer semantic`, `evaluat(e|es|ed|ing|ion)`. Exits non-zero with one ERROR line per finding. Token list is intentionally conservative — additions follow evidence of drift; removals require maintainer confirmation. - **Two exemption mechanisms:** - Section-heading: any heading containing "Out of Scope" (case-insensitive) exempts content until the next H2+ heading. Use for sub-sections that deliberately discuss reasoning vocabulary in a layer-aware framing (the v1.0.9 "Semantics — Out of Scope" sub-section is the reference case). - Paragraph-marker: `` on a line before a paragraph exempts the next paragraph (until next blank line). Use for one-off legitimate uses outside an Out-of-Scope sub-section. - **`tools/test-spec-layer.sh`** fixture-based test runner with 6 fixtures: 3 good (plain wire-format, paragraph-marker exemption, Out-of-Scope section exemption) + 3 bad (arithmetic semantics, calendar-relative phrase, OoS-exemption-ends-at-H2 boundary). 6/6 fixtures green. - **Makefile integration**: new `spec-check` + `test-spec-layer` targets. `make test` now runs `spec-check + test-spec-layer + all C++ test suites` (309 C++ + 6 fixture + 1 live-spec check). The guard runs on every test invocation. - The current v1.0.9-corrected spec passes the guard — validates the v1.0.9 §6.2.3 correction was complete. Tooling-only release — libantheos source + spec unchanged. The guard is shipped in the public source tree (no posture change). ## v1.0.9 — 2026-06-20 §6.2.3 DURATION layer-drift correction — Antheos is a wire-protocol; reasoning belongs in the consumer. v1.0.6 §6.2.3 had a "Calendar-Relative Semantics" sub-section asserting arithmetic semantics for DURATION (e.g. `2026-01-31 + P1M = 2026-02-28` clamped) + a V2 extension hook "Fixed-second duration variant" that framed an arithmetic-semantic choice as a future Antheos spec concern. Both leak REASONING into the wire-protocol spec — the Antheos spec layer only defines the type + body form, and the arithmetic belongs to a consuming reasoner's own specification; the spec text shipped in v1.0.6 drifted past that layer boundary. Corrected to keep reasoning semantics out of the wire-protocol spec: a reasoner is a consumer, Antheos is a wire protocol. - **§6.2.3 Calendar-Relative Semantics → Semantics — Out of Scope.** Replaced the arithmetic-semantics sub-section with a one-paragraph forward reference: arithmetic and comparison semantics over DURATION are out of Antheos's scope; the consuming reasoner defines them in its own specification. - **V2 extension hook "Fixed-second duration variant" dropped.** That hook framed a consumer-arithmetic choice (calendar-relative vs fixed-second `P1M` interpretation) as a future wire-format extension; it's actually a consumer-layer choice over the SAME wire format. - **Tightened §6.2.3 weeks-form note**: replaced "the weeks form preserves consumer semantic intent" with "the canonical form preserves the input's component shape" — pure wire-format commitment, no semantic-motivation language. Spec-only correction — libantheos source unchanged. The CHANGELOG + baseline history entries from v1.0.6 stay as-is (journals — historical truth preserved); this v1.0.9 entry is the canonical correction record. Filed alongside: `ANTHEOS-SPEC-LAYER-DRIFT-GUARD` on the base/antheos backlog — a build-time grep over the Antheos spec for reasoning- vocabulary tokens would have caught this at draft time. Will structurally enforce the wire-protocol-vs-reasoner boundary going forward. ## v1.0.8 — 2026-06-20 LOGICAL body grammar formalized at §6.2.1 — completes the Antheos V1 §6.2 spec-lock trio (TIMESTAMP + DURATION shipped in v1.0.6; LOGICAL fills the reserved §6.2.1 gap). Spec-only release. - **§6.2.1 — LOGICAL body grammar** added. EBNF defines a recursive-descent parseable grammar: disjunction (`|`) of conjunctions (`&`) of negations (`!`) of atoms or paren-grouped sub-expressions. Operator precedence encoded by nesting depth: `!` > `&` > `|`. Both binary operators are left-associative. - **V1 atom set locked** at 11 distinct CP437 chars: `I`, `O`, `D`, `H`, `U` (radix-flag chars per §6.3) + `B`, `W`, `Q`, `M`, `G`, `T` (unit-flag chars per §6.4; `D` is shared between the two sets). Atom semantics are consumer-defined — Scaleback uses the flag-char semantic; other consumers may overlay different atom semantics over the same grammar. - **D-picks per ticket recommendation** (D1 INCLUDE OR, D2 INCLUDE parens, D3 SINGLE-CHAR atoms only); **D4 RESERVE T/F as literals was DROPPED** — collided with the existing Scaleback example `!Q&!T` (T = Terabyte flag, not literal-true). Literal-vs-flag distinction deferred to V2 + consumer-side interpretation. - §6.2 word types table: LOGICAL description updated from "Logical/boolean expressions" to a precise grammar reference ("Boolean expression per §6.2.1 — flag-char atoms + `!`/`&`/`|`/parens"). - §6.5 LOGICAL example updated with cross-reference to §6.2.1. - §9.2.2 Scaleback Message updated to cross-reference §6.2.1 for the LOGICAL body grammar it uses + names "flag-char atom semantic" as the consumer interpretation Scaleback applies. - V2 extension hooks documented in §6.2.1: multi-character atoms, additional operators (XOR/IMPLIES/IFF), whitespace tolerance, reserved literal atoms, quoted-string atoms. Spec-only release — no source changes to libantheos in v1.0.8. A follow-up parser implementation lands once the V1 grammar is approved. ## v1.0.7 — 2026-06-20 SidPool API tightened — structurally enforces spec §11.2 uniqueness at the call-site naming layer, on the requirement that the Antheos protocol must not allow SID collisions. Closes the gap v1.0.5 partially addressed probabilistically: `acquire()` was the default-named foot-gun that admitted collisions; v1.0.5 raised the SID_MIN_LEN floor to mitigate the probability; v1.0.7 renames the foot-gun so calling the non-conformant path requires typing `_unchecked` at every call site. - **`SidPool::acquire()` renamed to `acquire_unchecked()`** (ABI-breaking). Header comment relabels it as the non-conformant escape hatch and documents the constrained-MCU caveat. `acquire_unique(CollisionCheck)` remains the spec §11.2-conformant default-named path. - **Spec §11.2.1 added — "Non-conformant Escape Hatch"** — codifies the rule for implementations exposing an unchecked path: must be explicitly-named (not the default), must document forfeit of the uniqueness guarantee. The §11.2 algorithm remains the conformance bar. - **Test fleet migrated**: 9 call sites in `tests/test_depth.cpp` + `tests/test_identity.cpp` updated. Tests that did not assert uniqueness use `acquire_unchecked()` (semantic-honest); tests that DO assert uniqueness (test_sid_rotation_no_reuse) migrated to `acquire_unique()` with active-set callback. - **New test**: `test_sidpool_acquire_unique_grows_on_collision` (test_depth.cpp) — exercises the spec-§11.2 collision-check-and-grow algorithm by poisoning the active set at the starting length; asserts the returned SID has grown past SID_MIN_LEN. Structural proof the algorithm actually grows on collision rather than just returning whatever the hash produced. - 309 tests across 10 suites (+1 contention test). The v1.0.5 hardening framing is corrected by retrospective: the probabilistic numbers (birthday-50% at ~38k concurrent sessions) describe the `acquire_unchecked()` path only — `acquire_unique()` is collision-free by construction. ## v1.0.6 — 2026-06-20 TIMESTAMP canonical form locked + DURATION word type added — sibling V1 spec work over §6.2, paired pass (ANTHEOS-TIMESTAMP-V1-CANONICALIZE + ANTHEOS-DURATION-V1-CLASS). - **§6.2.2 — TIMESTAMP canonical form.** Parse loose, store canonical, emit canonical. Canonical emit `YYYY-MM-DDTHH:MM:SS.uuuuuuZ` (27 bytes, microsecond UTC). Accepts second through nanosecond input precision; pads shorter; truncates sub-μs digits deterministically (truncate, not round). Accepts `±HH:MM` offsets and converts to UTC at parse-time. Rejects named zones, extended year range, leap seconds at V1 with teaching errors; each has a V2 extension hook documented. - **§6.2.3 — DURATION word type added** at CP437 `+` (0x2B), complementary to TIMESTAMP's `&` (0x26). ISO 8601 duration form `[-]P[nY][nM][nD][T[nH][nM][nS]]` plus weeks form `[-]PnW` (mutually exclusive with calendar components per ISO 8601). Microsecond sub-second precision matching TIMESTAMP. Accepts negative direction (`-P3D`); canonical zero is `PT0S`. Calendar components are calendar-relative (matches java.time / Temporal / RFC 5545 RRULE), not fixed-second magnitudes. - §6.1 word-type categories table: DURATION added to the "Both forbidden" flag row. - §6.2 word types table: TIMESTAMP description updated to cross-reference §6.2.2; new DURATION row at `+` (0x2B) cross-references §6.2.3. - §6.5 examples: TIMESTAMP example updated to canonical microsecond form (`2026-02-08T12:00:00.000000Z`); new DURATION example added (`P3Y6M4DT12H30M5S`). - §6.2.1 reserved for LOGICAL body grammar formalization (ANTHEOS-LOGICAL-V1-GRAMMAR-FORMALIZE pending). Spec-only release — no source changes to libantheos in v1.0.6. Parser implementations for both word types follow as separate work once the spec is approved. ## v1.0.5 — 2026-05-23 SID collision hardening — raise the length floor; lock in the byte-order choice. - `SID_MIN_LEN` raised 4 → 6. The SID hash's base-32 truncation is collision-prone only at very short lengths (32^4 ≈ 1M); a len-6 floor (32^6 ≈ 1.07e9) moves generation clear of that regime — birthday-50% goes from ~1,200 to ~38,000 concurrent sessions. - The little-endian byte order is documented as load-bearing, not incidental: measured at len=4 over 200k counters, LE collides 9.9% (uniform ideal 8.9%) while big-endian collides 82.7%. The counter varies the trailing input bytes, which drive FNV-1a's low bits hardest, so LE truncation is near-uniform. Expanded the comment in identity.cpp and spec §11.2 step 1 — do not "tidy" it to big-endian. - `SidPool` contract clarified: `acquire()` is best-effort (no uniqueness check); `acquire_unique()` is the spec-§11.2 guaranteed-unique path (checks active sessions, grows length until unique). - 308 tests across 10 suites (+2 identity: SID_MIN_LEN floor lock + distribution headroom at the floor) ## v1.0.4 — 2026-05-23 Parser SOM-recovery — resync at an in-head SOM instead of discarding the frame. - `Parser`: a SOM (0x02) seen mid-head now resynchronizes at that byte (the start of a new message) rather than entering ERROR, consuming the SOM, and scanning for the *next* SOM. Previously a complete frame that began at an unexpected in-head SOM was lost (e.g. `[SOM][SOM]…` discarded the second frame). This restores spec §7.4.1 framing ("all bytes *before* SOM are discarded" — the SOM is retained as the frame start). The resync also clears any pending tail-length set by a BLOB word in the abandoned frame. - The Tail state is excluded: tail bytes are length-delimited binary (§7.3), so a 0x02 byte there is data, not a delimiter. - Protocol spec §7.4: resync-at-SOM behavior made explicit. - 306 tests across 10 suites (+2 parser: SOM-resync frame recovery, tail-SOM is data; `test_edge_parse_double_som` updated — the second SOM now recovers the following frame) ## v1.0.3 — 2026-05-23 Q-verb capability validation — empty capability rejected. - `service::query()` (and `Context::query()`, which delegates to it) returns `nullopt` for an empty capability string. Previously an empty capability was accepted, emitting an empty `[""]` TEXT word. - Protocol spec §10.1: the capability TEXT MUST be non-empty — an empty query describes no service, and a match-all reading would be the capability enumeration the demand-driven model prohibits (§10.2, §15). - 304 tests across 10 suites (`test_service_query_empty` flipped: empty → nullopt) ## v1.0.2 — 2026-04-11 BLOB tail support — Context receives BLOB tails of arbitrary size. - Parser: dynamic tail buffer (vector replaces fixed 4KB array, 64KB initial reserve) - Context: handles BLOB words (type `*`), calls `set_tail_length()` during parse, registers `on_tail()` callback, exposes `last_tail()` accessor - `decode_blob_size()` supports all radix formats (binary/octal/decimal/hex) - Tail cleared on next message start (Symbol word with scratch_verb==0) - 304 tests across 10 suites (+4 BLOB tests: parser 100KB, context 500B/80KB, clearing) ## v1.0.1 — 2026-04-03 SID entropy — unpredictable session IDs. - `sid_generate()` accepts optional entropy bytes, mixed into FNV-1a hash - `SidPool` reads 8 bytes from `/dev/urandom` on each `acquire()` (POSIX dependency) - `bid_generate(len)` overload with internal `/dev/urandom` entropy - `O:\n` body-header convention for session ownership declaration - Protocol spec §16 updated: SIDs now entropy-mixed, `O:` header documented - 300 tests across 10 suites (+4 entropy tests) ## v1.0.0 — 2026-04-03 Open-source release under MIT license. - Added `antheos::exc` namespace with 15 named exception codes (spec §13 + Appendix A) - MIT license (was proprietary) - SPDX-License-Identifier headers in all source files - README rewrite for external audience - CONTRIBUTING.md - 296 tests across 10 suites ## Pre-release History | Date | Change | |------|--------| | 2026-03-30 | AP-12: Multi-bus-hop Z-verb relay. MESSAGE (~) word, relay_auth builders, Context relay dispatch. 287 tests. | | 2026-03-21 | AC-10: SID rotation — removed recycling, fresh SID per session, little-endian byte order fix. 268 tests. | | 2026-03-16 | AC-09: Purity audit + depth test suite. 268 tests. | | 2026-03-16 | AC-08: Standalone purity — caller-provided entropy, removed POSIX deps. | | 2026-03-16 | AC-06: Context — stateful codec with session management. | | 2026-03-16 | AC-05: Conformance + edge test suites (71 tests). | | 2026-03-16 | AC-04: Verb builders — bus/service/session (39 tests). | | 2026-03-16 | AC-03: Identity — base-32, BID/SID, SidPool (12 tests). | | 2026-03-16 | AC-02: Stream parser (12 tests). | | 2026-03-16 | AC-01: Wire encoding + Makefile skeleton (24 tests). |