AGENTS.md
June 29, 2026 ยท View on GitHub
Working notes for agents modifying this repository. For the design read
ARCHITECTURE.md; for usage read README.md. This file covers the repo layout,
how to build and test, the invariants you must not break, and the traps that are
easy to fall into.
Repo map
strings.hh Inline/template utilities: format, join, split, indent, left/center/right, startswith/endswith/hasupper, upper/lower, replace, ltrim/rtrim/trim, and the from_* declarations. Plus a std::to_string extension. From Xapiand, with the log decoupling below.
strings.cc The compiled humanizers: from_bytes / from_time / from_small_time / from_delta, on a Humanize helper. Includes "colors.h". From Xapiand, verbatim.
test/test.cc Runnable smoke test: format (incl. silent fallback), join, split, indent, padding, predicates, case, replace, trim, humanizers.
CMakeLists.txt STATIC library `strings` (+ alias strings::strings); FetchContent of five sibling deps; CTest test `strings`.
LICENSE MIT, Copyright (c) 2015-2019 Dubalu LLC.
README.md What it is, install, usage, provenance.
ARCHITECTURE.md Internal design: header/.cc split, format fallback, join, the humanizers.
This is not header-only. strings.cc must be compiled and linked; the header
holds the inline/template utilities. The CMake target is a STATIC library that
links the five sibling deps so their headers resolve.
Build and run the test
cmake -B build -DCMAKE_CXX_COMPILER=$(brew --prefix llvm)/bin/clang++
cmake --build build
ctest --test-dir build
The first configure fetches the five sibling libraries over the network
(FetchContent, GIT_TAG main). Expected output ends with
all strings tests passed, exit 0. The test target is strings_test; the
registered CTest name is strings. The test is only added when this repo is the
top-level project (CMakeLists.txt), so consumers vendoring it via FetchContent
won't build it.
Building with the Homebrew LLVM clang++ is the family convention (Apple clang is
unreliable for some of the C++20 / sanitizer paths on recent macOS). A plain
cmake -B build works too if the default compiler is new enough for
std::format.
Dependencies
Five sibling libraries, each resolving one local include, all linked PUBLIC:
| Include | Library | CMake target to link | What for |
|---|---|---|---|
"chars.hh" | char-classify | char_classify::char_classify | chars::toupper / chars::tolower |
"split.h" | split | split (no namespaced alias) | Split<> for split() and indent() |
"static_string.hh" | static-string | static_string::static_string | to_string overload; color literal type |
"repr.hh" | repr | repr::repr | included in the original header |
"colors.h" | term-color | term_color::term_color | the humanizer color palette |
Note the repo dir names use hyphens (char-classify, static-string,
term-color) while the CMake target/alias names use underscores. split is the
odd one out: it exposes only the bare split target, with no split::split
alias, so link the bare name. We track every dep's tip with GIT_TAG main, like
the rest of the family.
The one decoupling delta
Unlike most of the family, this extraction is not verbatim. The original Xapiand
strings.hh was coupled to Xapiand's logging:
- It did
#include "log.h"(comment:for L_DEBUG_TRY). - The
format()error path loggedL_EXC("Cannot format {}", repr(format))before falling back to the raw string.
A standalone string library must not log. On extraction both were removed: the
#include "log.h" is gone, and the format() catch now keeps only the silent
fallback (str = format;) with a comment explaining why. The observable behavior
on a bad format string is unchanged. There are zero remaining L_* /
log.h / opts / config references; keep it that way.
strings.cc was copied verbatim.
Conventions
- C++20. The target requests
cxx_std_20PUBLIC(the code usesstd::format). Don't drop the target below it. - Filenames are stable. The header and source keep their original Xapiand
names (
strings.hh,strings.cc) so a consumer that already#includes the header just needs this repo on the include path. Don't rename them. - Tabs for indentation, double quotes in code, no em dashes in prose.
- MIT-licensed; keep the copyright header (Copyright (c) 2015-2019 Dubalu LLC) on source files.
Load-bearing invariants
- No logging, ever. The whole point of the extraction was to cut the logging
dependency. Don't reintroduce
log.h,L_*macros, or any other "report an error somewhere" behavior. Theformat()error path stays a silent fallback to the raw format string. format()swallows format errors. A format string that doesn't match its arguments must not throw out offormat(); it returns the raw, unformatted string. The smoke test pins this. Don't "fix" it into a throw.- The
std::to_stringextension is what makesjoin()generic. The overloads added tonamespace std(forstd::string,string_view, literal arrays,static_string, and any.to_string()-having type) are whyjoin()works over a vector of arbitrary elements. Removing one narrowsjoin. - Humanizer color literals rely on the implicit
string_viewconversion. Instrings.ccthergb(...)/CLEAR_COLORvalues from term-color are stored in astd::vector<std::string_view>; they work becauseansi_colorconverts implicitly tostd::string_view. Don't change those vectors to hold a concrete color type.
How to extend
- Add a string helper. Most utilities are inline/template in
strings.hh; add yours there and give it a case intest/test.cc. Keep it free of any I/O or logging. - Add a humanizer unit/tier. Edit the
Humanizetables instrings.cc(scaling exponents, units, and the colors vector, which must stay exactly one longer than units). The constructor asserts these sizes line up. - Always extend the smoke test.
test/test.ccis the only executable check. Add a case for any new helper or tier and assert the exact result.
Traps
splithas no namespaced alias. Link the baresplittarget, notsplit::split(which doesn't exist). The other four use thename::nameform.- Trim helpers return a
std::string_viewinto the input.ltrim/rtrim/trimdon't allocate; they return a view over the caller's storage. Don't let that view outlive the string it points into. - The humanizer color vector is one longer than the units. The extra slot is
the reset color appended at the end. The constructor asserts
colors.size() == units.size() + 1; off-by-one here trips the assert. - Upstream
split.hwarns under recent libc++. It derives an iterator from the deprecatedstd::iterator. That warning comes from the dependency, not this code; don't try to silence it here.
Standalone vs. Xapiand
This is a standalone extraction from
Xapiand. strings.cc was copied verbatim;
strings.hh was copied with the single logging decoupling described above (the
log.h include and the L_EXC call in format() removed). The five local
includes are resolved against the standalone sibling libraries through CMake
instead of Xapiand's source tree. Any change here should be reconcilable with
upstream as a plain edit, modulo that one decoupling.