Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MrL8i59N6i3M5A48rJ3u4J
21 KiB
xo-ppsink Carve-Out Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Extract the arena-free pretty-print front-end (PpSink/FlatSink/Pretty/FunctionStyle) from xo-indentlog2 into a new low-level repo xo-ppsink, so subsystems below xo-arena (e.g. xo-arena, xo-facet) can pretty-print without depending on the arena-backed engine.
Architecture: xo-ppsink is a small shared library (namespace xo::print, include stem xo/ppsink/) depending only on xo-cmake (build tooling) + xo-timeutil (propagated, anticipating a later xo::scope migration). xo-indentlog2 keeps the arena-backed engine (PpState, PrettySink, PpToken*, PpConfig, and the Pretty specializations) and gains a dependency on xo-ppsink. End-state level order: xo-timeutil → xo-ppsink → … → xo-arena → xo-indentlog2.
Tech Stack: C++20, CMake + xo-cmake macros, Nix (pkgs/*.nix + xo.nix/ci.nix/shells.nix), Catch2 for tests, git-subrepo for the umbrella.
Global Constraints
- Namespace stays
xo::print— do NOT rename toxo::ppsink. - Cross-repo includes use angle brackets
<xo/…>; same-repo includes use quotes"…". - Test
.cppfiles: no anonymous namespaces (unity-build friendly); give test-local types unique suffixed names (e.g.Point_PpsinkTest) to avoid ODR clashes. xo-ppsinksits belowxo-testutil, so its utest uses plain Catch2 (#define CATCH_CONFIG_MAIN), neverxo/testutil.- The carve set is exactly: headers
PpSink.hpp,FlatSink.hpp,Pretty.hpp,FunctionStyle.hpp; sourcesPpSink.cpp,FlatSink.cpp. Everything else inxo-indentlog2/.../print/stays. - After any change to
xo-indentlog2inter-repo deps, thefind_dependency()set inxo-indentlog2/cmake/xo_indentlog2Config.cmake.inand the nix inputs inpkgs/xo-indentlog2.nixmust stay consistent with thexo_dependency()calls.
File Structure
Task 1 creates (xo-ppsink):
xo-ppsink/CMakeLists.txt— top-level shared-lib project (scaffolded)xo-ppsink/cmake/xo_ppsinkConfig.cmake.in— exported config (find_dependency(xo_timeutil))xo-ppsink/include/xo/ppsink/{PpSink,FlatSink,Pretty,FunctionStyle}.hpp— copies of the carve set (adjusted includes)xo-ppsink/src/ppsink/{PpSink,FlatSink}.cpp+src/ppsink/CMakeLists.txt— the shared libxo_ppsinkxo-ppsink/utest/{ppsink_utest_main,FlatSink.test,Pretty.test}.cpp+utest/CMakeLists.txt— new arena-free testspkgs/xo-ppsink.nix— nix derivationxo.nix,shells.nix,ci.nix, umbrellaCMakeLists.txt— register xo-ppsink
Task 2 modifies (xo-indentlog2 consumes xo-ppsink):
- Delete
xo-indentlog2/include/xo/indentlog2/print/{PpSink,FlatSink,Pretty,FunctionStyle}.hpp - Delete
xo-indentlog2/src/indentlog2/{PpSink,FlatSink}.cpp xo-indentlog2/src/indentlog2/CMakeLists.txt— drop the two sources, addxo_dependency(xo_ppsink)xo-indentlog2/include/xo/indentlog2/print/{PrettySink,PrettyVector,PrettyFunctionStyle}.hpp— rewrite moved-header includes to<xo/ppsink/…>xo-indentlog2/utest/{FlatSink,Pretty,PrettyVector}.test.cpp— rewrite moved-header includes to<xo/ppsink/…>xo-indentlog2/cmake/xo_indentlog2Config.cmake.in— addfind_dependency(xo_ppsink)pkgs/xo-indentlog2.nix— addxo-ppsinktopropagatedBuildInputs
Task 3 finalizes:
- Regenerate
xo-cmake/etc/xo/subsystem-edgesfrom the umbrella; verify consistency and full builds.
Task 1: Create and wire xo-ppsink (standalone, green)
Files: as listed under "Task 1 creates" above.
Interfaces:
-
Produces: shared lib target
xo_ppsink; headers underxo/ppsink/; namespacexo::printwithPpSink,PpSinkInserter,FlatSink,Pretty<T>,has_pretty<T>,pp_write(PpSink&, const T&),FunctionStyle. Nix attrxo.ppsink. -
Consumes: nothing new (xo-timeutil already exists).
-
Step 1: Scaffold the shared-lib skeleton into the existing subrepo dir
xo-ppsink/ already exists (subrepo: .gitrepo, README.md, cmake/xo-bootstrap-macros.cmake). Run the scaffolder to add the shared-lib skeleton (CMakeLists.txt, cmake/xo_ppsinkConfig.cmake.in, include/xo/ppsink/, src/ppsink/, utest/).
Run: xo-cmake/bin/scaffold-subdir --mode=shared ppsink (from the umbrella root; scaffold-subdir is a source-tree tool, not installed to $PREFIX/bin)
Expected: creates xo-ppsink/CMakeLists.txt, xo-ppsink/cmake/xo_ppsinkConfig.cmake.in, xo-ppsink/src/ppsink/CMakeLists.txt, xo-ppsink/utest/CMakeLists.txt, and empty include/xo/ppsink/ / src/ppsink/ trees. Target name is xo_ppsink.
Inspect the generated xo-ppsink/CMakeLists.txt and confirm it follows the xo-indentlog2 shape: project(xo_ppsink VERSION 1.0), include(cmake/xo-bootstrap-macros.cmake), xo_cxx_toplevel_options3(), add_subdirectory(src/ppsink), add_subdirectory(utest), and xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets). If the scaffold omits any of these, add them to match xo-indentlog2/CMakeLists.txt.
- Step 2: Copy the four carve headers into
include/xo/ppsink/, adjusting includes
Copy each header from xo-indentlog2/include/xo/indentlog2/print/ to xo-ppsink/include/xo/ppsink/, then apply the include edits below.
cd ~/proj/xo-umbrella2
mkdir -p xo-ppsink/include/xo/ppsink
cp xo-indentlog2/include/xo/indentlog2/print/PpSink.hpp xo-ppsink/include/xo/ppsink/PpSink.hpp
cp xo-indentlog2/include/xo/indentlog2/print/FlatSink.hpp xo-ppsink/include/xo/ppsink/FlatSink.hpp
cp xo-indentlog2/include/xo/indentlog2/print/Pretty.hpp xo-ppsink/include/xo/ppsink/Pretty.hpp
cp xo-indentlog2/include/xo/indentlog2/print/FunctionStyle.hpp xo-ppsink/include/xo/ppsink/FunctionStyle.hpp
Edit xo-ppsink/include/xo/ppsink/PpSink.hpp — replace the include block
#include <string_view>
#include <cstdint>
with (add <iosfwd> so std::ostream is only forward-declared; callers that stream pull in <ostream> themselves):
#include <string_view>
#include <iosfwd>
#include <cstdint>
FlatSink.hpp, Pretty.hpp, FunctionStyle.hpp need no edits: their #include "PpSink.hpp" still resolves (same directory), and FunctionStyle.hpp has no includes.
- Step 3: Copy the two carve sources into
src/ppsink/, fixing the header path
cd ~/proj/xo-umbrella2
mkdir -p xo-ppsink/src/ppsink
cp xo-indentlog2/src/indentlog2/PpSink.cpp xo-ppsink/src/ppsink/PpSink.cpp
cp xo-indentlog2/src/indentlog2/FlatSink.cpp xo-ppsink/src/ppsink/FlatSink.cpp
In xo-ppsink/src/ppsink/PpSink.cpp change #include "print/PpSink.hpp" → #include <xo/ppsink/PpSink.hpp>.
In xo-ppsink/src/ppsink/FlatSink.cpp change #include "print/FlatSink.hpp" → #include <xo/ppsink/FlatSink.hpp>.
- Step 4: Write
src/ppsink/CMakeLists.txt
Replace the scaffolded xo-ppsink/src/ppsink/CMakeLists.txt with:
# xo-ppsink/src/ppsink/CMakeLists.txt
set(SELF_LIB xo_ppsink)
set(SELF_SRCS
PpSink.cpp
FlatSink.cpp
)
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
xo_install_include_tree3(include/xo/ppsink)
# ----------------------------------------------------------------
# dependencies
#
# NOTE: dependency set here must be kept consistent with
# xo-ppsink/cmake/xo_ppsinkConfig.cmake.in
xo_headeronly_dependency(${SELF_LIB} xo_timeutil)
# end CMakeLists.txt
- Step 5: Write
cmake/xo_ppsinkConfig.cmake.in
Replace the scaffolded xo-ppsink/cmake/xo_ppsinkConfig.cmake.in with:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# note: changes to find_dependency() calls here
# must coordinate with xo_(headeronly_)dependency() calls
# in xo-ppsink/src/ppsink/CMakeLists.txt
#
find_dependency(xo_timeutil)
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake")
check_required_components("@PROJECT_NAME@")
- Step 6: Write the plain-catch2 utest main
Create xo-ppsink/utest/ppsink_utest_main.cpp:
/* @file ppsink_utest_main.cpp */
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
/* end ppsink_utest_main.cpp */
- Step 7: Write the FlatSink test (arena-free)
Create xo-ppsink/utest/FlatSink.test.cpp:
/* @file FlatSink.test.cpp */
#include <xo/ppsink/FlatSink.hpp>
#include <catch2/catch.hpp>
#include <sstream>
using xo::print::FlatSink;
using xo::print::PpSink;
TEST_CASE("flatsink-put-and-noop-structure", "[flatsink]") {
/* FlatSink writes put() text straight through; begin/split/end are no-ops */
std::stringstream ss;
FlatSink sink(ss);
sink.put("hello").begin().put("a").split().put("b").end().put("!");
REQUIRE(ss.str() == "helloab!");
}
TEST_CASE("flatsink-stream-open", "[flatsink]") {
/* stream_open returns an inserter that writes straight to the ostream */
std::stringstream ss;
FlatSink sink(ss);
{
auto ins = sink.stream_open(64);
ins << 42 << ' ' << "x";
ins.finish();
}
REQUIRE(ss.str() == "42 x");
}
- Step 8: Write the Pretty/pp_write test (arena-free)
Create xo-ppsink/utest/Pretty.test.cpp. Note the unique _PpsinkTest type names and no anonymous namespace (unity-build friendly):
/* @file Pretty.test.cpp */
#include <xo/ppsink/Pretty.hpp>
#include <xo/ppsink/FlatSink.hpp>
#include <catch2/catch.hpp>
#include <ostream>
#include <sstream>
using xo::print::FlatSink;
using xo::print::PpSink;
using xo::print::Pretty;
using xo::print::pp_write;
using xo::print::has_pretty;
/* a type that opts in to Pretty */
struct Point_PpsinkTest { int x; int y; };
/* a type that does NOT opt in -> pp_write falls back to operator<< */
struct Plain_PpsinkTest { int v; };
inline std::ostream &
operator<<(std::ostream & os, const Plain_PpsinkTest & p) {
return os << "Plain{" << p.v << "}";
}
namespace xo::print {
template <>
struct Pretty<Point_PpsinkTest> {
static void print(PpSink & sink, const Point_PpsinkTest & p) {
sink.begin().put("(");
pp_write(sink, p.x);
sink.put(",");
pp_write(sink, p.y);
sink.put(")").end();
}
};
}
TEST_CASE("pp_write-uses-specialization", "[pretty]") {
static_assert(has_pretty<Point_PpsinkTest>);
std::stringstream ss;
FlatSink sink(ss);
pp_write(sink, Point_PpsinkTest{1, 2}); /* begin/end are no-ops in FlatSink */
REQUIRE(ss.str() == "(1,2)");
}
TEST_CASE("pp_write-falls-back-to-operator<<", "[pretty]") {
static_assert(!has_pretty<Plain_PpsinkTest>);
std::stringstream ss;
FlatSink sink(ss);
pp_write(sink, Plain_PpsinkTest{7}); /* stream_open -> operator<< */
REQUIRE(ss.str() == "Plain{7}");
}
- Step 9: Write
utest/CMakeLists.txt
Replace the scaffolded xo-ppsink/utest/CMakeLists.txt with (mirrors xo-timeutil/utest, plain Catch2, no testutil):
# xo-ppsink/utest/CMakeLists.txt
set(SELF_EXECUTABLE_NAME utest.ppsink)
set(SELF_SOURCE_FILES
ppsink_utest_main.cpp
FlatSink.test.cpp
Pretty.test.cpp)
if (ENABLE_TESTING)
xo_add_utest_executable(${SELF_EXECUTABLE_NAME} ${SELF_SOURCE_FILES})
xo_self_dependency(${SELF_EXECUTABLE_NAME} xo_ppsink)
xo_external_target_dependency(${SELF_EXECUTABLE_NAME} Catch2 Catch2::Catch2)
endif()
# end CMakeLists.txt
- Step 10: Write
pkgs/xo-ppsink.nix
Create pkgs/xo-ppsink.nix:
{
# dependencies
lib, stdenv, cmake, catch2,
xo-cmake, xo-timeutil,
doCheck ? true,
} :
stdenv.mkDerivation (finalattrs:
{
name = "xo-ppsink";
version = "1.0";
src = ../xo-ppsink;
cmakeFlags = ["-DCMAKE_MODULE_PATH=${xo-cmake}/share/cmake"];
inherit doCheck;
# xo-ppsink's exported config + public headers pull in xo-timeutil,
# so consumers must receive it transitively.
propagatedBuildInputs = [ xo-timeutil ];
nativeBuildInputs = [ cmake
catch2
xo-cmake ];
})
- Step 11: Register xo-ppsink in nix + umbrella
In xo.nix, add after the xo-timeutil line (line ~29):
xo-ppsink = callPackage pkgs/xo-ppsink.nix { stdenv = jitStdenv; };
In shells.nix, add after the timeutil line (line ~325):
ppsink = pkgs.xo-ppsink;
In ci.nix, add xo-ppsink to the inherit (xoPkgs) list, immediately after xo-timeutil:
xo-timeutil
xo-ppsink
xo-subsys
In the umbrella CMakeLists.txt, add after add_subdirectory(xo-timeutil …) (so ppsink precedes indentlog2/arena):
add_subdirectory(xo-ppsink) # arena-free pretty-print front-end (PpSink/FlatSink/Pretty)
xo-ppsink is already present in xo-cmake/etc/xo/subsystem-list (line 3) — no edit needed there.
- Step 12: Build + test xo-ppsink via nix
Run: nix-build -A xo.ppsink --no-out-link
Expected: builds and doCheck runs utest.ppsink — All tests passed (2 test files, 4 cases). Final output is a /nix/store/…-xo-ppsink path.
- Step 13: Verify the umbrella still configures (ppsink added, indentlog2 untouched)
Run: cmake -S . -B .build >/tmp/ppsink-reconf.log 2>&1; echo exit=$?
Expected: exit=0. (indentlog2 still has its own copies and does not yet depend on ppsink, so nothing is broken; ppsink is now an added subdir.)
- Step 14: Commit
cd ~/proj/xo-umbrella2
git add xo-ppsink pkgs/xo-ppsink.nix xo.nix shells.nix ci.nix CMakeLists.txt
git commit -m "xo-ppsink: new arena-free pretty-print front-end (PpSink/FlatSink/Pretty) [carve-out]"
Task 2: Migrate xo-indentlog2 onto xo-ppsink
Files: as listed under "Task 2 modifies" above.
Interfaces:
-
Consumes:
xo_ppsinktarget +<xo/ppsink/{PpSink,FlatSink,Pretty,FunctionStyle}.hpp>(namespacexo::print, unchanged) from Task 1. -
Produces:
xo-indentlog2with no duplicate copies of the carve set; depends onxo_ppsink. -
Step 1: Delete the carve set from xo-indentlog2
cd ~/proj/xo-umbrella2
git rm xo-indentlog2/include/xo/indentlog2/print/PpSink.hpp \
xo-indentlog2/include/xo/indentlog2/print/FlatSink.hpp \
xo-indentlog2/include/xo/indentlog2/print/Pretty.hpp \
xo-indentlog2/include/xo/indentlog2/print/FunctionStyle.hpp \
xo-indentlog2/src/indentlog2/PpSink.cpp \
xo-indentlog2/src/indentlog2/FlatSink.cpp
- Step 2: Rewrite includes in the staying headers
In xo-indentlog2/include/xo/indentlog2/print/PrettySink.hpp: change #include "PpSink.hpp" → #include <xo/ppsink/PpSink.hpp>.
In xo-indentlog2/include/xo/indentlog2/print/Pretty — the primary Pretty.hpp was moved; the staying PrettyVector.hpp includes it: change #include "Pretty.hpp" → #include <xo/ppsink/Pretty.hpp> in xo-indentlog2/include/xo/indentlog2/print/PrettyVector.hpp.
In xo-indentlog2/include/xo/indentlog2/print/PrettyFunctionStyle.hpp: change
#include "Pretty.hpp"
#include "FunctionStyle.hpp"
to
#include <xo/ppsink/Pretty.hpp>
#include <xo/ppsink/FunctionStyle.hpp>
- Step 3: Rewrite includes in the staying tests
In xo-indentlog2/utest/FlatSink.test.cpp: change #include "print/FlatSink.hpp" → #include <xo/ppsink/FlatSink.hpp>.
In xo-indentlog2/utest/Pretty.test.cpp: change #include "print/Pretty.hpp" → #include <xo/ppsink/Pretty.hpp>, and #include "print/FlatSink.hpp" → #include <xo/ppsink/FlatSink.hpp>.
In xo-indentlog2/utest/PrettyVector.test.cpp: change #include "print/FlatSink.hpp" → #include <xo/ppsink/FlatSink.hpp>. (Its #include "print/PrettyFunctionStyle.hpp" stays — that header remains in indentlog2.)
- Step 4: Drop the carve sources from the indentlog2 library
In xo-indentlog2/src/indentlog2/CMakeLists.txt, remove PpSink.cpp and FlatSink.cpp from SELF_SRCS, and add the ppsink dependency. The SELF_SRCS block becomes:
set(SELF_SRCS
LogStreambuf.cpp
LogConfig.cpp
LogBuffer.cpp
PrettySink.cpp
PpState.cpp
LineState.cpp
PpTokenStreambuf.cpp
PpToken.cpp
PpConfig.cpp
)
and the dependencies section becomes:
xo_dependency(${SELF_LIB} xo_arena)
xo_dependency(${SELF_LIB} xo_ppsink)
- Step 5: Add the ppsink find_dependency to the exported config
In xo-indentlog2/cmake/xo_indentlog2Config.cmake.in, add below find_dependency(xo_arena):
find_dependency(xo_arena)
find_dependency(xo_ppsink)
- Step 6: Add xo-ppsink to the indentlog2 nix inputs
In pkgs/xo-indentlog2.nix, add xo-ppsink to the function args and to propagatedBuildInputs.
Change the xo-deps arg line
# xo dependencies
xo-arena,
xo-cmake,
to
# xo dependencies
xo-arena,
xo-ppsink,
xo-cmake,
Change
propagatedBuildInputs = [ xo-arena ];
to
propagatedBuildInputs = [ xo-arena xo-ppsink ];
- Step 7: Build + test xo-indentlog2 via nix
Run: nix-build -A xo.indentlog2 --no-out-link
Expected: builds; doCheck runs utest.indentlog2 (LineState, LogBuffer, PpState, PpTokenStreambuf, PrettySink, FlatSink, Pretty, PrettyVector) — all pass. Confirms indentlog2 now resolves the carve set from xo-ppsink with no duplicate definitions.
- Step 8: Verify the umbrella links indentlog2 against the ppsink sibling
(Does not use subsystem-edges — that regen is Task 3. This checks the in-umbrella sibling wiring, since xo-ppsink was add_subdirectory'd in Task 1.)
Run:
cd ~/proj/xo-umbrella2
cmake -S . -B .build >/dev/null 2>&1 && \
cmake --build .build --target utest.indentlog2 -j && \
ctest --test-dir .build -R '^utest.indentlog2$' --output-on-failure
Expected: xo_indentlog2 links xo_ppsink, utest.indentlog2 builds and passes.
- Step 9: Commit
cd ~/proj/xo-umbrella2
git add xo-indentlog2 pkgs/xo-indentlog2.nix
git commit -m "xo-indentlog2: depend on xo-ppsink for PpSink/FlatSink/Pretty [levelize]"
Task 3: Regenerate the dependency graph and final verification
Files: xo-cmake/etc/xo/subsystem-edges (regenerated).
Interfaces:
-
Consumes: the finished xo-ppsink/xo-indentlog2 dependency wiring from Tasks 1–2.
-
Produces:
subsystem-edgesreflecting the newxo-ppsinkedges, consistent withsubsystem-list. -
Step 1: Reconfigure the umbrella to emit fresh edges
Run: cmake -S . -B .build >/tmp/edges.log 2>&1; echo exit=$?; grep xo_emit_dependency_edges /tmp/edges.log
Expected: exit=0; the emit message prints .build/xo-subsystem-edges.txt was written.
- Step 2: Confirm xo-ppsink edges appear
Run: grep -E 'ppsink' .build/xo-subsystem-edges.txt
Expected: at least xo-timeutil xo-ppsink (ppsink depends on timeutil) and xo-ppsink xo-indentlog2 (indentlog2 depends on ppsink).
- Step 3: Update the checked-in edges file
Run:
cd ~/proj/xo-umbrella2
cp .build/xo-subsystem-edges.txt xo-cmake/etc/xo/subsystem-edges
git diff --stat xo-cmake/etc/xo/subsystem-edges
Expected: the file gains the ppsink edges.
- Step 4: Verify subsystem-list is still a valid topological order for the new edges
Run: tsort xo-cmake/etc/xo/subsystem-edges >/dev/null; echo "tsort exit=$?"
Expected: tsort exit=0 (no cycle). xo-ppsink already sits early in subsystem-list (after xo-timeutil, before xo-indentlog2), so no reorder is required. If tsort reports a loop, stop and investigate — a cycle means an accidental back-edge was introduced.
- Step 5: Final whole-graph sanity via nix
Run: nix-build -A xo.ppsink -A xo.indentlog2 --no-out-link
Expected: both build green.
- Step 6: Verify the docker closure now resolves ppsink
(Now that subsystem-edges carries xo-ppsink xo-indentlog2, --with-deps pulls ppsink into indentlog2's closure. Reinstall the tooling first so the container snapshot has the new edges.)
Run:
cd ~/proj/xo-umbrella2
cmake --install .build/xo-cmake 2>/dev/null || cmake -S xo-cmake -B .build/xo-cmake -DCMAKE_INSTALL_PREFIX=$HOME/local >/dev/null && cmake --install .build/xo-cmake >/dev/null
xo-docker-build --xo-root-dir xo-indentlog2 2>&1 | grep -E '::group::xo-ppsink|::group::xo-indentlog2|tests passed|Error' | tail
Expected: closure includes xo-ppsink before xo-indentlog2; both build; utest.indentlog2 passes.
- Step 7: Commit
cd ~/proj/xo-umbrella2
git add xo-cmake/etc/xo/subsystem-edges
git commit -m "xo-cmake: regenerate subsystem-edges after xo-ppsink carve-out [BUILD]"
Notes for the executor
- The carve headers are copied in Task 1 and the originals deleted in Task 2, deliberately: it keeps each task at a green, independently-reviewable state (Task 1 leaves indentlog2 self-contained; no translation unit ever includes both copies, so there is no ODR clash in the intermediate state).
PpSink.cppimplementsPpSinkInserter(ctor/dtor/finish);FlatSink.cppimplements the flatPpSink. Both are out-of-line, which is why xo-ppsink is a shared library rather than header-only.- If
nix-buildcomplains it cannot find-lxo_timeutil/xo_timeutilConfig.cmakewhile building a consumer, the propagation is wrong — ensurexo-timeutilis inpropagatedBuildInputsofpkgs/xo-ppsink.nix(notnativeBuildInputs), mirroring the ratio→reflectutil fix.