xo-umbrella2/docs/superpowers/plans/2026-07-25-xo-ppsink.md
Roland Conybeare d2e5b8d33e docs: xo-ppsink carve-out implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrL8i59N6i3M5A48rJ3u4J
2026-07-25 23:54:58 -04:00

570 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 to `xo::ppsink`.
- Cross-repo includes use angle brackets `<xo/…>`; same-repo includes use quotes `"…"`.
- Test `.cpp` files: **no anonymous namespaces** (unity-build friendly); give test-local types unique suffixed names (e.g. `Point_PpsinkTest`) to avoid ODR clashes.
- `xo-ppsink` sits below `xo-testutil`, so its utest uses **plain Catch2** (`#define CATCH_CONFIG_MAIN`), never `xo/testutil`.
- The carve set is exactly: headers `PpSink.hpp`, `FlatSink.hpp`, `Pretty.hpp`, `FunctionStyle.hpp`; sources `PpSink.cpp`, `FlatSink.cpp`. Everything else in `xo-indentlog2/.../print/` stays.
- After any change to `xo-indentlog2` inter-repo deps, the `find_dependency()` set in `xo-indentlog2/cmake/xo_indentlog2Config.cmake.in` and the nix inputs in `pkgs/xo-indentlog2.nix` must stay consistent with the `xo_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 lib `xo_ppsink`
- `xo-ppsink/utest/{ppsink_utest_main,FlatSink.test,Pretty.test}.cpp` + `utest/CMakeLists.txt` — new arena-free tests
- `pkgs/xo-ppsink.nix` — nix derivation
- `xo.nix`, `shells.nix`, `ci.nix`, umbrella `CMakeLists.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, add `xo_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` — add `find_dependency(xo_ppsink)`
- `pkgs/xo-indentlog2.nix` — add `xo-ppsink` to `propagatedBuildInputs`
**Task 3 finalizes:**
- Regenerate `xo-cmake/etc/xo/subsystem-edges` from 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 under `xo/ppsink/`; namespace `xo::print` with `PpSink`, `PpSinkInserter`, `FlatSink`, `Pretty<T>`, `has_pretty<T>`, `pp_write(PpSink&, const T&)`, `FunctionStyle`. Nix attr `xo.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.
```bash
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
```cpp
#include <string_view>
#include <cstdint>
```
with (add `<iosfwd>` so `std::ostream` is only forward-declared; callers that stream pull in `<ostream>` themselves):
```cpp
#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**
```bash
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:
```cmake
# 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:
```cmake
@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`:
```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`:
```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):
```cpp
/* @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):
```cmake
# 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`:
```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):
```nix
xo-ppsink = callPackage pkgs/xo-ppsink.nix { stdenv = jitStdenv; };
```
In `shells.nix`, add after the `timeutil` line (line ~325):
```nix
ppsink = pkgs.xo-ppsink;
```
In `ci.nix`, add `xo-ppsink` to the `inherit (xoPkgs)` list, immediately after `xo-timeutil`:
```nix
xo-timeutil
xo-ppsink
xo-subsys
```
In the umbrella `CMakeLists.txt`, add after `add_subdirectory(xo-timeutil …)` (so ppsink precedes indentlog2/arena):
```cmake
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**
```bash
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_ppsink` target + `<xo/ppsink/{PpSink,FlatSink,Pretty,FunctionStyle}.hpp>` (namespace `xo::print`, unchanged) from Task 1.
- Produces: `xo-indentlog2` with no duplicate copies of the carve set; depends on `xo_ppsink`.
- [ ] **Step 1: Delete the carve set from xo-indentlog2**
```bash
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
```cpp
#include "Pretty.hpp"
#include "FunctionStyle.hpp"
```
to
```cpp
#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:
```cmake
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:
```cmake
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)`:
```cmake
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
```nix
# xo dependencies
xo-arena,
xo-cmake,
```
to
```nix
# xo dependencies
xo-arena,
xo-ppsink,
xo-cmake,
```
Change
```nix
propagatedBuildInputs = [ xo-arena ];
```
to
```nix
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:
```bash
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**
```bash
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-edges` reflecting the new `xo-ppsink` edges, consistent with `subsystem-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:
```bash
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:
```bash
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**
```bash
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.cpp` implements `PpSinkInserter` (ctor/dtor/`finish`); `FlatSink.cpp` implements the flat `PpSink`. Both are out-of-line, which is why xo-ppsink is a **shared** library rather than header-only.
- If `nix-build` complains it cannot find `-lxo_timeutil` / `xo_timeutilConfig.cmake` while building a consumer, the propagation is wrong — ensure `xo-timeutil` is in `propagatedBuildInputs` of `pkgs/xo-ppsink.nix` (not `nativeBuildInputs`), mirroring the ratio→reflectutil fix.