xff v0.8.0

Agent rules - xff

Conventions for AI agents and contributors working on this repository.
Companion to docs/design.md (decisions) and
docs/implementation-plan.md (build & sequencing).
The full C++ coding style is STYLE_CPP.md; project-level rules are
RULES.md; the contribution flow is CONTRIBUTING.md. The
GoogleTest section below is the quick reference; STYLE_CPP.md is canonical.

Build & test: bazel test //... / sanitizers: bazel test //... --config=clang --config=asan
(also --config=tsan, and --config=msan on Linux).
Toolchain: clang-22 minimum (hermetic LLVM under --config=clang).

C++ API design

Xff-owned C++ interfaces use no raw object pointers. Pass required observers by reference,
ownership by value or smart pointer, and optional observers as mbo::types::OptionalRef; lookups
return an optional reference rather than a nullable pointer. Prefer pure, value-returning helpers:
return a value, absl::StatusOr, or a named result record instead of mutating caller-owned output
parameters. Mutation remains valid when it is the explicit purpose of a stateful object or action.
Exact raw-pointer spellings required by C, POSIX, or third-party ABIs stay inside narrow adapters
and must carry XFF_ABI_POINTER: <reason> on that declaration or the immediately preceding line;
do not broaden that exception to xff-owned interfaces or internal convenience pointers.
STYLE_CPP.md is canonical.

Outside required OS or third-party interfaces and explicitly designated adapters, file I/O APIs are
forbidden: this includes STL streams, std::filesystem file operations, C/POSIX stdio and file
descriptor calls, and path-based mbo::file/Abseil helpers. Route reads and writes through the xff
VFS or a value-returning absl::Status/absl::StatusOr<T> adapter. Prefer pure functions over
side-effecting output parameters. Narrow host-I/O adapters must carry XFF_HOST_IO: <reason> on
the declaration or immediately preceding line; the pre-commit policy enforces this boundary.
Host mutations must use xff/vfs/mutations.h; only xff_extras_api/mutations.cc may open writable
host handles or mutate host paths. An XFF_HOST_IO annotation cannot exempt a new mutation path.
Archive libraries receive authorized handles, never output filenames. The pager pipe is the
explicit non-file write exception.

Bazel package policy

Every BUILD / BUILD.bazel file declares
package(default_visibility = ["//visibility:private"]). Cross-package APIs opt in with a narrow
target-level visibility; never make a whole package public for convenience. Target and exec C++
configurations both keep Bazel's layering_check and parse_headers features enabled. The
check-bazel-policy pre-commit hook enforces all three invariants, including for new packages.

Each C++ test source has its own cc_test target. Do not combine independent test sources
under one rule; put reusable test support in a library.

Pull request descriptions

Every PR description has two layers in this order:

  1. a short, human-readable explanation of the outcome and why it matters;
  2. an ## AG;DR heading, followed by the full implementation details, reasoning, validation,
    portability notes, and known limitations needed by reviewers or a future agent.

Update the PR description whenever a commit is added to the PR, so the detailed section describes
the complete current change rather than the state at PR creation. The short human-readable section
should remain stable and should rarely need updating: most additional commits refine implementation
or validation and therefore belong only below ## AG;DR. Change the human section only when the
PR's user-visible outcome or motivation actually changes.

Writing tests (GoogleTest)

  1. Always TEST_F with a fixture; never bare TEST(...) - even when the
    harness is a one-line struct FooTest : ::testing::Test {};.

  2. The fixture is a struct (not a class) inheriting from
    ::testing::Test (or a friend such as ::testing::TestWithParam<T>).

  3. Use EXPECT_THAT / ASSERT_THAT; convenience assertion macros are forbidden. Never use either
    EXPECT_ or ASSERT_ with EQ, NE, LT, LE, GT, GE, STREQ, STRNE, STRCASEEQ,
    STRCASENE, FLOAT_EQ, DOUBLE_EQ, or NEAR, and do not use EXPECT_TRUE, EXPECT_FALSE,
    ASSERT_TRUE, or ASSERT_FALSE. Use EXPECT_THAT / ASSERT_THAT with IsTrue / IsFalse
    instead. This is enforced by pre-commit with no exceptions.

  4. Multi-line text: use mbo::testing::EqualsText. For a
    multi-line string use EXPECT_THAT(actual, EqualsText(golden)) (unified diff,
    line by line). Write the golden as a DropIndent-filtered indented raw string,
    not concatenated "...\n" literals (which clang-format shoves against the
    EqualsText( paren): WithDropIndent(EqualsText(R"out( ... )out")).
    WithDropIndent de-indents the expected text only; DropIndentAndSplit yields
    the lines as a vector. Caveat: a raw-string golden cannot carry significant
    trailing whitespace (the trim-trailing-whitespace hook strips it) - use the
    literal form there. STYLE_CPP.md is canonical; use EqualsText for text comparisons.

  5. Typed and parameterized tests supply names from the types/values (name
    generators for TYPED_TEST_SUITE / INSTANTIATE_TEST_SUITE_P), so the
    output never shows numbered tests (Suite/0, Suite/1).

  6. Test absl::Status / absl::StatusOr<T> with status matchers - never raw
    .ok().
    Raw EXPECT_TRUE(s.ok()) / EXPECT_FALSE(s.ok()) throws away the
    code and message on failure. Use mbo::testing
    (@mboworks_mbo//mbo/testing:status_cc):

    • EXPECT_THAT(s, IsOk())
    • EXPECT_THAT(s, StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("...")))
    • EXPECT_THAT(so, IsOkAndHolds(Eq(42)))
    • ASSERT_OK_AND_ASSIGN(const auto value, MakeThing()); to unwrap a StatusOr.

    mbo's set is the MBO Works canonical superset: it works on both Status and
    StatusOr, and adds payload matchers (StatusHasPayload) plus the
    EXPECT_OK / ASSERT_OK / ASSERT_OK_AND_ASSIGN macros over abseil's
    absl_testing.

  7. Match optional-like values directly; never assert .has_value(). Use
    Optional(matcher) for a present std::optional or mbo::types::OptionalRef, and
    Eq(std::nullopt) for absence. StatusOr uses IsOkAndHolds(matcher) as described above.

Unicode is not inherently unwanted; unnecessary mixing of test purposes is. Tests specifically
about Unicode handling belong in separate Unicode-specific files explicitly excluded from the
ASCII-source check. Ordinary functional fixtures stay ASCII unless an approved glyph is the
actual output under test: histogram blocks and tree connectors belong in their functional tests.
Visible Unicode samples may be literal in dedicated test files; retain escapes for invisible
combining marks, joiners, or exact byte sequences, and explain the property each sample exercises.

Markdown

Keep GitHub-flavored Markdown tables vertically aligned (the | pipes line
up, every column padded to its widest cell, honoring the :-- / --: / :-:
alignment markers). Do not hand-align them: run the formatter
tools/align_markdown_tables.py (FILE..., or
--check to only report). It is enforced by the align-markdown-tables
pre-commit hook, so a misaligned table fails CI; let the tool do the spacing.

Self-documenting features (the registry is the single source of truth)

The expression vocabulary (xff/registry) and the global options
(xff/cli/globals.cc) are the single sources of truth from which --help,
--help=TOPIC, the --help=list index, the --man roff page, and the
--help=full --help-format=markdown reference are all generated. So every feature add or change carries
its self-documentation in the same change
- this is part of "done", never a
follow-up:

The generated --help / --man / --help=full --help-format=markdown then stay complete by construction.

When implementing a new feature, consider whether it belongs in the README's
Core Highlights & Architectural Advantages section and Tool Feature Comparison Matrix.
Those sections are curated rather than exhaustive: add only major or genuinely differentiating
features.

Markup in help prose

Help text is authored in a small inline markup (ParseInline), and the backends render it per
target: the plain backend keeps the backticks, roff bolds the span, Markdown makes it a code span.
So write concrete literals as `code` in details, in ValueDoc.meaning, and in topic
bodies: option values (`auto`, `all`), flag names (`--no-pager`), primaries
(`-exec`), and literal tokens (`*.{cc,h}`). A reader should be able to tell a value from
a word without parsing the sentence.

Two limits, both about noise:

Fuzz-test isolation

CLI conventions