diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 0bc14cf2eece..17b9c81e8c9d 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -2857,6 +2857,312 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "MasterCI" --ci --timestamp
+ stateless_tests_amd_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_ASAN_UBSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_tsan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_tsan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_tsan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_TSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, content_addressed s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_msan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_msan, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_msan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, content_addressed s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_arm_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_arm_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_ARM_BIN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, content_addressed s3 storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
+ stateless_tests_amd_binary_content_addressed_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed storage, parallel)' --workflow "MasterCI" --ci --timestamp
+
stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8:
runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
needs: [build_amd_llvm_coverage_per_test, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
@@ -5992,7 +6298,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, altinity-on-demand, altinity-style-checker]
- needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_llvm_coverage_per_test, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, sign_release_amd_release, sign_release_arm_release, source_upload, sqllogic_test, sqlstorm_test, sqltest, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_2_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_3_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_4_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_5_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_6_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_7_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_8_8, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, stress_test_azure_amd_msan, stress_test_azure_amd_tsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
+ needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_llvm_coverage_per_test, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, clickbench_amd_release, clickbench_arm_release, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, sign_release_amd_release, sign_release_arm_release, source_upload, sqllogic_test, sqlstorm_test, sqltest, stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_binary_content_addressed_s3_storage_parallel, stateless_tests_amd_binary_content_addressed_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_2_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_3_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_4_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_5_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_6_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_7_8, stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_8_8, stateless_tests_amd_msan_content_addressed_s3_storage_parallel, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_content_addressed_s3_storage_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_binary_content_addressed_s3_storage_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, stress_test_azure_amd_msan, stress_test_azure_amd_tsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
if: ${{ !cancelled() && needs.config_workflow.outputs.pipeline_status != '' }}
name: "Finish Workflow"
outputs:
@@ -6145,6 +6451,12 @@ jobs:
- stateless_tests_amd_tsan_s3_storage_sequential_2_2
- stateless_tests_arm_binary_parallel
- stateless_tests_arm_binary_sequential
+ - stateless_tests_amd_binary_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_tsan_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_msan_content_addressed_s3_storage_parallel
+ - stateless_tests_arm_binary_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_binary_content_addressed_storage_parallel
- stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_1_8
- stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_2_8
- stateless_tests_amd_llvm_coverage_per_test_per_test_coverage_3_8
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 4adc4b1c1cd5..7c59d17c9ff5 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -2447,6 +2447,300 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "PR" --ci --timestamp
+ stateless_tests_amd_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_ASAN_UBSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_tsan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_tsan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_TSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, content_addressed s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_msan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_msan, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_msan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_MSAN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, content_addressed s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_arm_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_ARM_BIN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, content_addressed s3 storage, parallel)' --workflow "PR" --ci --timestamp
+
+ stateless_tests_amd_binary_content_addressed_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_tsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed storage, parallel)' --workflow "PR" --ci --timestamp
+
stateless_tests_arm_asan_ubsan_azure_parallel:
runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
needs: [build_amd_asan_ubsan, build_amd_debug, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, ci_tests, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_arm_binary_parallel]
@@ -5587,7 +5881,7 @@ jobs:
finish_workflow:
runs-on: [self-hosted, altinity-on-demand, altinity-style-checker]
- needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_debug_targeted, ast_fuzzer_amd_debug_targeted_old_compatibility, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tidy, build_arm_tsan, build_arm_ubsan, build_toolchain_pgo_bolt_aarch64, build_toolchain_pgo_bolt_amd64, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, ci_tests, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_asan_ubsan_targeted, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, keeper_stress_tests_pr, quick_functional_tests, source_upload, sqllogic_test, sqlstorm_test, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_asan_ubsan_targeted, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
+ needs: [ast_fuzzer_amd_debug, ast_fuzzer_amd_debug_targeted, ast_fuzzer_amd_debug_targeted_old_compatibility, ast_fuzzer_amd_msan, ast_fuzzer_amd_tsan, ast_fuzzer_arm_asan_ubsan, build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tidy, build_arm_tsan, build_arm_ubsan, build_toolchain_pgo_bolt_aarch64, build_toolchain_pgo_bolt_amd64, buzzhouse_amd_debug, buzzhouse_amd_msan, buzzhouse_amd_tsan, buzzhouse_arm_asan_ubsan, ci_tests, compatibility_check_amd_release, compatibility_check_arm_release, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, fast_test, install_packages_amd_release, install_packages_arm_release, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_2_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_3_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_4_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_5_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_6_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_7_8, integration_tests_amd_asan_ubsan_db_disk_old_analyzer_8_8, integration_tests_amd_asan_ubsan_targeted, integration_tests_amd_msan_10_10, integration_tests_amd_msan_1_10, integration_tests_amd_msan_2_10, integration_tests_amd_msan_3_10, integration_tests_amd_msan_4_10, integration_tests_amd_msan_5_10, integration_tests_amd_msan_6_10, integration_tests_amd_msan_7_10, integration_tests_amd_msan_8_10, integration_tests_amd_msan_9_10, integration_tests_amd_tsan_1_6, integration_tests_amd_tsan_2_6, integration_tests_amd_tsan_3_6, integration_tests_amd_tsan_4_6, integration_tests_amd_tsan_5_6, integration_tests_amd_tsan_6_6, integration_tests_arm_binary_distributed_plan_1_4, integration_tests_arm_binary_distributed_plan_2_4, integration_tests_arm_binary_distributed_plan_3_4, integration_tests_arm_binary_distributed_plan_4_4, keeper_stress_tests_pr, quick_functional_tests, source_upload, sqllogic_test, sqlstorm_test, stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_1_2, stateless_tests_amd_asan_ubsan_db_disk_distributed_plan_sequential_2_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_1_2, stateless_tests_amd_asan_ubsan_distributed_plan_parallel_2_2, stateless_tests_amd_binary_content_addressed_s3_storage_parallel, stateless_tests_amd_binary_content_addressed_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_parallel, stateless_tests_amd_debug_distributed_plan_s3_storage_sequential, stateless_tests_amd_debug_parallel, stateless_tests_amd_debug_sequential, stateless_tests_amd_msan_content_addressed_s3_storage_parallel, stateless_tests_amd_msan_wasmedge_parallel_1_4, stateless_tests_amd_msan_wasmedge_parallel_2_4, stateless_tests_amd_msan_wasmedge_parallel_3_4, stateless_tests_amd_msan_wasmedge_parallel_4_4, stateless_tests_amd_msan_wasmedge_sequential_1_2, stateless_tests_amd_msan_wasmedge_sequential_2_2, stateless_tests_amd_tsan_content_addressed_s3_storage_parallel, stateless_tests_amd_tsan_parallel_1_2, stateless_tests_amd_tsan_parallel_2_2, stateless_tests_amd_tsan_s3_storage_parallel_1_2, stateless_tests_amd_tsan_s3_storage_parallel_2_2, stateless_tests_amd_tsan_s3_storage_sequential_1_2, stateless_tests_amd_tsan_s3_storage_sequential_2_2, stateless_tests_amd_tsan_sequential_1_2, stateless_tests_amd_tsan_sequential_2_2, stateless_tests_arm_asan_ubsan_azure_parallel, stateless_tests_arm_asan_ubsan_azure_sequential_1_2, stateless_tests_arm_asan_ubsan_azure_sequential_2_2, stateless_tests_arm_asan_ubsan_targeted, stateless_tests_arm_binary_content_addressed_s3_storage_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential, stress_test_amd_asan_ubsan, stress_test_amd_debug, stress_test_amd_msan, stress_test_amd_tsan, stress_test_arm_asan_ubsan, stress_test_arm_asan_ubsan_s3, stress_test_arm_debug, stress_test_arm_msan, stress_test_arm_release, stress_test_arm_tsan, stress_test_arm_ubsan, unit_tests_asan_ubsan, unit_tests_asan_ubsan_function_prop_fuzzer, unit_tests_msan, unit_tests_msan_function_prop_fuzzer, unit_tests_tsan, unit_tests_tsan_function_prop_fuzzer]
if: ${{ !cancelled() && needs.config_workflow.outputs.pipeline_status != '' }}
name: "Finish Workflow"
outputs:
@@ -5730,6 +6024,12 @@ jobs:
- stateless_tests_amd_tsan_s3_storage_sequential_2_2
- stateless_tests_arm_binary_parallel
- stateless_tests_arm_binary_sequential
+ - stateless_tests_amd_binary_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_tsan_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_msan_content_addressed_s3_storage_parallel
+ - stateless_tests_arm_binary_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_binary_content_addressed_storage_parallel
- stateless_tests_arm_asan_ubsan_azure_parallel
- stateless_tests_arm_asan_ubsan_azure_sequential_1_2
- stateless_tests_arm_asan_ubsan_azure_sequential_2_2
diff --git a/.github/workflows/pull_request_community.yml b/.github/workflows/pull_request_community.yml
index c576bd5afaba..45a0af14dd64 100644
--- a/.github/workflows/pull_request_community.yml
+++ b/.github/workflows/pull_request_community.yml
@@ -2073,6 +2073,294 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "Community PR" --ci --timestamp
+ stateless_tests_amd_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_BINARY
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_asan_ubsan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_asan_ubsan, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYXNhbl91YnNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_ASAN_UBSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_ASAN_UBSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_asan_ubsan, content_addressed s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_tsan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
+ needs: [build_amd_debug, build_amd_tsan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfdHNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_tsan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_tsan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_TSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_TSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_tsan, content_addressed s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_msan_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester]
+ needs: [build_amd_debug, build_amd_msan, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfbXNhbiwgY29udGVudF9hZGRyZXNzZWQgczMgc3RvcmFnZSwgcGFyYWxsZWwp') }}
+ name: "Stateless tests (amd_msan, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_msan, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_MSAN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_MSAN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_msan, content_addressed s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_arm_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_ARM_BIN
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, content_addressed s3 storage, parallel)' --workflow "Community PR" --ci --timestamp
+
+ stateless_tests_amd_binary_content_addressed_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY
+ uses: actions/download-artifact@v8
+ with:
+ name: CH_AMD_BINARY
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed storage, parallel)' --workflow "Community PR" --ci --timestamp
+
integration_tests_amd_asan_ubsan_db_disk_old_analyzer_1_8:
runs-on: [self-hosted, altinity-on-demand, altinity-func-tester, 16c]
needs: [build_amd_asan_ubsan, build_amd_debug, build_arm_binary, config_workflow, fast_test, stateless_tests_amd_debug_parallel, stateless_tests_arm_binary_parallel]
diff --git a/.github/workflows/release_builds.yml b/.github/workflows/release_builds.yml
index 8f6f6f22525d..2bd9517266cd 100644
--- a/.github/workflows/release_builds.yml
+++ b/.github/workflows/release_builds.yml
@@ -1334,9 +1334,162 @@ jobs:
. ./ci/tmp/praktika_setup_env.sh
PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, sequential)' --workflow "Release Builds" --ci --timestamp
+ stateless_tests_amd_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed s3 storage, parallel)' --workflow "Release Builds" --ci --timestamp
+
+ stateless_tests_arm_binary_content_addressed_s3_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-func-tester-aarch64, 16c]
+ needs: [build_arm_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhcm1fYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzMyBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (arm_binary, content_addressed s3 storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_ARM_BIN_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_ARM_BIN_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (arm_binary, content_addressed s3 storage, parallel)' --workflow "Release Builds" --ci --timestamp
+
+ stateless_tests_amd_binary_content_addressed_storage_parallel:
+ runs-on: [self-hosted, altinity-on-demand, altinity-builder, 16c]
+ needs: [build_amd_binary, config_workflow, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest]
+ if: ${{ !cancelled() && !contains(needs.*.outputs.pipeline_status, 'failure') && !contains(needs.*.outputs.pipeline_status, 'undefined') && !contains(fromJson(needs.config_workflow.outputs.data).workflow_config.cache_success_base64, 'U3RhdGVsZXNzIHRlc3RzIChhbWRfYmluYXJ5LCBjb250ZW50X2FkZHJlc3NlZCBzdG9yYWdlLCBwYXJhbGxlbCk=') }}
+ name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+ outputs:
+ data: ${{ steps.run.outputs.DATA }}
+ pipeline_status: ${{ steps.run.outputs.pipeline_status || 'undefined' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+ with:
+ ref: ${{ env.CHECKOUT_REF }}
+
+ - name: Setup
+ uses: ./.github/actions/runner_setup
+ - name: Docker setup
+ uses: ./.github/actions/docker_setup
+ with:
+ test_name: "Stateless tests (amd_binary, content_addressed storage, parallel)"
+
+ - name: Prepare env script
+ run: |
+ rm -rf ./ci/tmp
+ mkdir -p ./ci/tmp
+ cat > ./ci/tmp/praktika_setup_env.sh << 'ENV_SETUP_SCRIPT_EOF'
+ export PYTHONPATH=./ci:.:
+ cat > ./ci/tmp/workflow_inputs.json << 'EOF'
+ ${{ toJson(github.event.inputs) }}
+ EOF
+ cat > ./ci/tmp/workflow_job.json << 'EOF'
+ ${{ toJson(job) }}
+ EOF
+ cat > ./ci/tmp/workflow_status.json << 'EOF'
+ ${{ toJson(needs) }}
+ EOF
+ ENV_SETUP_SCRIPT_EOF
+
+ - name: Download artifact CH_AMD_BINARY_GH
+ uses: actions/download-artifact@v8
+ continue-on-error: true
+ with:
+ name: CH_AMD_BINARY_GH
+ path: ./ci/tmp
+
+ - name: Run
+ id: run
+ run: |
+ . ./ci/tmp/praktika_setup_env.sh
+ PYTHONUNBUFFERED=1 python3 -m praktika run 'Stateless tests (amd_binary, content_addressed storage, parallel)' --workflow "Release Builds" --ci --timestamp
+
finish_workflow:
runs-on: [self-hosted, altinity-on-demand, altinity-style-checker]
- needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, sign_release_amd_release, sign_release_arm_release, source_upload, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential]
+ needs: [build_amd_asan_ubsan, build_amd_binary, build_amd_debug, build_amd_msan, build_amd_release, build_amd_tsan, build_arm_asan_ubsan, build_arm_binary, build_arm_debug, build_arm_msan, build_arm_release, build_arm_tsan, build_arm_ubsan, config_workflow, docker_keeper_image, docker_server_image, dockers_build_amd, dockers_build_arm, dockers_build_multiplatform_manifest, install_packages_amd_release, install_packages_arm_release, sign_release_amd_release, sign_release_arm_release, source_upload, stateless_tests_amd_binary_content_addressed_s3_storage_parallel, stateless_tests_amd_binary_content_addressed_storage_parallel, stateless_tests_arm_binary_content_addressed_s3_storage_parallel, stateless_tests_arm_binary_parallel, stateless_tests_arm_binary_sequential]
if: ${{ !cancelled() && needs.config_workflow.outputs.pipeline_status != '' }}
name: "Finish Workflow"
outputs:
@@ -1433,6 +1586,9 @@ jobs:
- source_upload
- stateless_tests_arm_binary_parallel
- stateless_tests_arm_binary_sequential
+ - stateless_tests_amd_binary_content_addressed_s3_storage_parallel
+ - stateless_tests_arm_binary_content_addressed_s3_storage_parallel
+ - stateless_tests_amd_binary_content_addressed_storage_parallel
- finish_workflow
- GrypeScanServer
- GrypeScanKeeper
diff --git a/.gitignore b/.gitignore
index 6f33e7e83db8..969ac5d0c623 100644
--- a/.gitignore
+++ b/.gitignore
@@ -198,3 +198,6 @@ tests/casa_del_dolor/_instances*
ci/praktika/json_test.html
ci/praktika/json_test.html.gz
ci/praktika/json.html.gz
+
+# SDD scratch (briefs, reports, review diffs, progress ledger) — never committed
+.superpowers/
diff --git a/.superpowers/sdd/task-5-report.md b/.superpowers/sdd/task-5-report.md
new file mode 100644
index 000000000000..0e524faae94b
--- /dev/null
+++ b/.superpowers/sdd/task-5-report.md
@@ -0,0 +1,163 @@
+# Task 5 report: E3 — in-place replay apply (shipped instead of undo journal)
+
+## Status: COMPLETE — KEEP recommended
+
+## What shipped
+
+The undo journal was **not** shipped. The brief's endorsed simplification was: `replay`'s state is
+local and discarded on any throw, so a throw-away path needs no rollback. That variant is simpler AND
+strictly faster, so it is what shipped. `applyRefLogTxn` now branches on `TxnValidation`:
+
+- **`Full`** (writer live-state install + every trial/shape-check preview): keeps the existing
+ two-phase scratch copy verbatim — "throw ⇒ state byte-for-byte unchanged". Never the regression
+ source: every `Full` caller applies against a materialized (empty-overlay) or batch-bounded state,
+ so the copy is O(1) shared-base pointer bumps.
+- **`TrustedHistory`** (replay only, the sole caller): applies **in place, no copy**. On a throw the
+ state is poisoned; sound because `replay` discards it on any throw (its result reaches a caller only
+ on full success). This removes the entire per-transaction copy cost class.
+
+Whole change: one `if/else` in `CasRefProtocol.cpp` + doc updates on `TxnValidation` and
+`applyRefLogTxn` in `CasRefProtocol.h` + 5 tests. No new type, no `RefStateUndo`, no rollback, no
+alloc-failure-terminate trade. `admits` left untouched (its scratch is O(batch), not a regression).
+
+**Post-review rename note:** `TxnValidation { Full, TrustedHistory }` was subsequently renamed to
+`ApplyMode { LiveAppend, TrustedReplay }` (see the Fix pass section below) — this section otherwise
+keeps the original names since it describes what shipped under those names.
+
+## Step 0 exclusivity audit — PASSED
+
+Every production call site of `admits`/`applyRefLogTxn`/`replay` runs under the ledger `state_mutex`
+or on a detached/local copy. No concurrent reader observes a state mid-mutation; in-place apply is
+safe. COW writes touch only the per-copy overlay, never the shared base. `TrustedHistory`
+(post-review: `ApplyMode::TrustedReplay`) is passed at exactly one place
+(`CasRefProtocol.cpp:400`, inside `replay`, grep-enforced). Full table in the experiments report §E3.
+
+| Site | Target | Protection | Verdict |
+|---|---|---|---|
+| `CasRefLedger.cpp:1138` `applyRefLogTxn(rt->state, wedged)` | LIVE | `state_mutex` | exclusive |
+| `CasRefLedger.cpp:1243` `applyRefLogTxn(shape_check,…)` | local | stack-local | exclusive |
+| `CasRefLedger.cpp:1255` `admits(item_scratch,…)` | local | stack-local | exclusive |
+| `CasRefLedger.cpp:1265` `applyRefLogTxn(item_scratch,…)` | local | stack-local | exclusive |
+| `CasRefLedger.cpp:1377` `applyRefLogTxn(rt->state, final)` | LIVE | `state_mutex` | exclusive |
+| `CasRefLedger.cpp:426` `replay(…)` (recovery) | fresh local; assigned only on success | detached | exclusive |
+| `CasFsck.cpp:226` `replay(…)` (oracle) | fresh local | detached | exclusive |
+| `CasRefProtocol.cpp:638` `replay(snapshot, tail)` inside `recoverRefTableDetailed` | fresh local; `RecoveredRefTable` value-returned, constructed only on success | detached | exclusive |
+
+## Numbers (baseline t4/E2; `build/bench_t5_e3.log`)
+
+- `BM_ReplayHistory` **recovered and crushed**: 50,082 → **1,725.58 ns/row** (-96.6%); ~29× below the
+ t4 regression, ~21× below t3's 36.7k success bar, ~28× below the pre-E1 48.9k baseline.
+ - N=100: 7.43ms → 0.431ms; N=1k: 48.85ms → 1.776ms; N=10k: 473.76ms → 15.92ms; N=100k: 4.98s → 0.172s.
+- `BM_ApplyRefLogTxn` (Full, unchanged code): +0.6…+3.5% — flat, no regression.
+- `BM_Admits` / `BM_AdmitsAddPrecommit` (admits, unchanged): ±1-3% — E2's O(1) win preserved.
+- `BM_ScratchCopy` (reference): flat.
+- Residual O(N) in the fit is the one-time `stateFromSnapshot` base-load, NOT the per-txn cost E3
+ targeted (that is now O(1)/txn, which is why the constant collapsed 29×).
+
+## Tests: 1,096 pass, 0 failures (`build/test_gate_t5.log`)
+
+1,091 (t4) + 5 new E3 tests in `gtest_cas_ref_statemachine.cpp` (test names shown as of this report;
+post-review rename below changed 4 of the 5 — see Fix pass section):
+- `E3FullLaterOpThrowLeavesPopulatedStateByteIdentical` (Full byte-identical, populated later-op throw)
+- `E3FullFirstOpThrowLeavesPopulatedStateByteIdentical` (Full byte-identical, first-op throw)
+- `E3AdmitsPreviewLeavesStateByteIdentical` (admits accept + reject both byte-identical)
+- `E3TrustedHistoryInPlaceMatchesFullAcrossAllArms` (in-place == Full across every applyOp arm — the
+ test only the new machinery can fail)
+- `E3TrustedHistoryPoisonOnBadTailIsInternal` (replay throws on bad tail; poison never escapes)
+
+The 2 disabled tests are the pre-existing pair E2 noted.
+
+## Concern (called out for the controller)
+
+`TrustedHistory` now carries a second meaning ("in-place, poison-on-throw") coupled onto the
+validation-mode enum. Documented loudly, grep-enforced to `replay`, and a test pins poison-containment
+(for the existing caller; single-caller exclusivity is enforced by grep+comment, not testable). It is a
+footgun only if a future author adds a `TrustedHistory` caller that keeps state after a throw. Weighed
+and judged acceptable (that contract already restricts `TrustedHistory` to already-validated durable
+replay), but flagged deliberately.
+
+## Verdict: KEEP
+
+Regression recovery (the round's decisive requirement), ~29× win, simplest option on the table, no
+regressions elsewhere, no new machinery.
+
+## Fix pass (post-review)
+
+Applied the reviewer's rename recommendation plus 3 Minor findings.
+
+**1. Enum rename.** `enum class TxnValidation { Full, TrustedHistory }` →
+`enum class ApplyMode { LiveAppend, TrustedReplay }` in `CasRefProtocol.h`/`.cpp`, all call sites, and
+`gtest_cas_ref_statemachine.cpp` (`benchmark_cas_ref_protocol.cpp` only ever used the default argument,
+so it needed no change). `LiveAppend` = "this is the first time this transaction is validated, against
+a state that must survive a rejection" (two-phase scratch copy, strong exception guarantee).
+`TrustedReplay` = "I am replaying already-committed, already-validated history into a local state I
+own and discard on any error" (skips the O(N) cross-owner re-scan in release builds; applies in place
+and poisons `state` on throw). The two axes are welded into one enum on purpose: both are derived from
+the same caller intent (`replay`, and only `replay`), and welding them keeps the dangerous fourth
+combination -- trusted validation applied to a state that must survive a throw -- inexpressible. The
+enum's doc comment in `CasRefProtocol.h` was rewritten to state this rationale. The default argument
+(`= ApplyMode::LiveAppend`) on `applyRefLogTxn` was kept.
+
+Four of the five new E3 test names embedded the old enum-value words (`Full`/`TrustedHistory`) and were
+renamed to match: `E3FullLaterOpThrowLeavesPopulatedStateByteIdentical` →
+`E3LiveAppendLaterOpThrowLeavesPopulatedStateByteIdentical`,
+`E3FullFirstOpThrowLeavesPopulatedStateByteIdentical` →
+`E3LiveAppendFirstOpThrowLeavesPopulatedStateByteIdentical`,
+`E3TrustedHistoryInPlaceMatchesFullAcrossAllArms` →
+`E3TrustedReplayInPlaceMatchesLiveAppendAcrossAllArms`, `E3TrustedHistoryPoisonOnBadTailIsInternal` →
+`E3TrustedReplayPoisonOnBadTailIsInternal`. Three E1-era test names (from task 3, not this task) were
+also renamed for the same reason: `TrustedHistoryReplaySkipsCrossOwnerScanInRelease` →
+`TrustedReplaySkipsCrossOwnerScanInRelease`, `TrustedHistoryReplayAbortsOnCrossOwnerCollision` →
+`TrustedReplayAbortsOnCrossOwnerCollision`, `TrustedHistoryReplayEquivalentToFullOnValidTail` →
+`TrustedReplayEquivalentToLiveAppendOnValidTail` (the blanket `TrustedHistory`→`TrustedReplay` token
+rename would otherwise have produced a stuttering `TrustedReplayReplay...` name). The experiments
+report and this report both keep the original names in their historical narrative sections and add
+pointers to the renamed identifiers where they cite exact test names, so nothing here should be read as
+rewriting history -- only as making the current source's names discoverable from the old docs.
+
+**2. Audit-table row.** Both exclusivity-audit tables (this report's Step 0 section and the
+experiments report §E3) were missing the third production `replay` caller:
+`CasRefProtocol.cpp:638`, `return RecoveredRefTable{replay(snapshot, tail), ...}` inside
+`recoverRefTableDetailed`. Added as a row: target is a fresh local state, `RecoveredRefTable` is
+value-returned and constructed only on success (a throw inside `replay` propagates before that `return`
+statement ever executes, so no partially-built `RecoveredRefTable` is ever observable) -- detached,
+exclusive, same verdict as the other two `replay` call sites.
+
+**3. Stale line refs.** Both docs cited the `TrustedReplay` (then `TrustedHistory`) pass site as
+`CasRefProtocol.cpp:369`. After the rename, the actual site is `CasRefProtocol.cpp:400`
+(`applyRefLogTxn(state, txn, ApplyMode::TrustedReplay);` inside `replay`'s tail loop) -- the line
+number had already drifted from the doc-writing pass to now for unrelated reasons; re-checked and
+re-cited in both docs.
+
+**4. Wording.** Changed "pinned by a test" / "pinned by a test that the poisoned internal state never
+escapes" to "a test pins poison-containment (for the existing caller; single-caller exclusivity is
+enforced by grep+comment, not testable)" in both docs -- the original phrasing could be read as
+claiming the single-caller exclusivity itself is test-covered, which it is not (no test can observe "no
+other call site exists"; that's a grep+comment invariant). Only the poison-containment behavior
+(`replay` throws → no caller ever observes the poisoned state) is actually exercised by a test.
+
+**5. Death-test gating vs. build type -- verified, no code change needed.** `build/` is an NDEBUG
+release build (`DEBUG_OR_SANITIZER_BUILD` is not defined, so `chassert` compiles out). None of T5's 5
+new tests use `EXPECT_DEATH`/`ASSERT_DEATH` -- checked the full source range of the 5 new tests
+(`gtest_cas_ref_statemachine.cpp` lines 918-1075 pre-rename numbering); all 5 are plain `TEST(...)`
+cases. The one death test the reviewer likely noticed in the gate log,
+`TrustedReplayAbortsOnCrossOwnerCollision` (renamed from `TrustedHistoryReplayAbortsOnCrossOwnerCollision`),
+predates this task -- it was added in E1/task 3, is wrapped in
+`#if defined(DEBUG_OR_SANITIZER_BUILD)`, and does not compile into this release build at all. Confirmed
+by grep on the gate log: zero `DeathTest` suite names ran under the T5 filter. The arithmetic reconciles
+cleanly: 1,096 = 1,091 (t4 baseline, which already excludes that gated-out E1 death test) + 5 new
+T5/E3 tests, all non-death. The stray "Death tests use fork()" warnings visible in the gate log come
+from unrelated pre-existing death tests elsewhere in the binary that the broad `Cas*`/`CaWiring*`
+filter also matches (e.g. `CasWiringOpsDeathTest.MoveDirectoryMutableCollisionPolicyAborts`), not from
+anything in this task.
+
+**Build:** `flock /tmp/claude-1000/ninja.lock ninja -C build unit_tests_dbms benchmark_cas_ref_protocol
+> build/build_t5fix.log 2>&1` -- clean, 0 errors.
+
+**Gate:** `build/src/unit_tests_dbms
+--gtest_filter='Cas*:CaLifecycle*:CaWiring*:ContentAddressed*:CountingBackendShape*:RefSnapshotCodec*:RefTableCacheEviction*:RefWriter*'
+> build/test_gate_t5fix.log 2>&1` → **1,096 tests passed, 0 failures** (identical count to T5's original
+gate). No bench re-run: the rename is a pure token substitution plus a doc-comment rewrite, codegen-neutral.
+
+**Files touched:** `CasRefProtocol.h`, `CasRefProtocol.cpp`, `gtest_cas_ref_statemachine.cpp`,
+`.superpowers/sdd/task-5-report.md` (this file), `docs/superpowers/reports/2026-07-21-reftablestate-experiments.md`.
diff --git a/.superpowers/sdd/tla-rev7-report.md b/.superpowers/sdd/tla-rev7-report.md
new file mode 100644
index 000000000000..080acd8ff1c8
--- /dev/null
+++ b/.superpowers/sdd/tla-rev7-report.md
@@ -0,0 +1,130 @@
+# TLA+ gate report — rev.8 (FORGET-only v1) disk lifecycle/FORGET protocol
+
+Date: 2026-07-23 (reworked the same day for the v1 scope change: the natural `Vanished(erased)`
+proof stack is excised from the code; the erasure-proof model is retained as HISTORICAL evidence —
+see the last section). Model: `docs/superpowers/models/CaDiskLifecycle.tla` (+ 7 configs, runner
+`run_disklifecycle.sh`). TLC 2.19 (`tmp/tla2tools.jar`), `-workers auto`. Raw logs:
+`tmp/tlc_rev7_lifecycle_*.log` (and `tmp/tlc_rev7_proof_*.log` for the historical model).
+
+Modeled against: spec `docs/superpowers/specs/2026-07-22-cas-disk-lease-loss-throw-and-stop-verbs-design.md`
+§§1/3/5/6 (as amended through commit `6c4c05c9802`) and the as-built sources at HEAD
+(`CasPool.cpp` `forgetDisk`/`tryRemountOnce`, `CasMountRuntime.{h,cpp}`, `CasGcScheduler.cpp`,
+`ContentAddressedMetadataStorage.cpp` forgetDisk/gcStop/gcStart), plus two queued hardenings
+modeled as landed: **[C1]** `loop()`/`heartbeatLoop()` check `isVanished()` at each wake and
+self-exit (no new round once `Vanished`); **[M1]** the remount attempt's step-0 bail checks
+`vanished_intent` as well as `isVanished()`.
+
+## Verdict {#verdict}
+
+**`CaDiskLifecycle`: ALL INVARIANTS + BOTH LIVENESS PROPERTIES HOLD.** All three sabotages RED as
+required (the model has teeth), all three witnesses reachable (the checks are not vacuous).
+
+**Task 15 gate recommendation: OPEN.** The v1 protocol Task 15 locks in — the lifecycle state
+machine (`Live`/`Transient`/`IdentityLost`/`VanishedReplaced`/`VanishedForgotten`), the FORGET
+verb, the GC stop/start serialization — is machine-checked clean under every interleaving with the
+keeper, the self-remount thread, a racing natural `Replaced` promotion, and the GC scheduler,
+**conditional on [C1] and [M1] landing as queued** (both are modeled as present; `_sab_nogcselfexit`
+shows exactly what breaks without [C1]).
+
+## Model and scope {#model}
+
+v1 state machine: the observer never promotes to `VanishedErased` (the state does not exist);
+`IdentityLost` is entered directly on authoritative sentinel absence (no prefix-emptiness leg);
+`Vanished` = `Replaced` (identity gate: `Present` + foreign `pool_id`, reachable from `Transient`
+AND `IdentityLost`) | `Forgotten` (the verb). FORGET is modeled as the exact as-built step order:
+mutex-acquire; idempotent `isVanished` short-circuit (the detached scheduler still `stop()`s via
+its local's destructor); publish intent; trip#1; GC stop (signal + join waiting out an in-flight
+round); remount stop (shutdown latch + join); trip#2; drain (nondeterministic outcome);
+`finishTeardown` (farewell ONLY if drained); `keeperReset`; `enterVanished(Forgotten)`
+(first-terminal-wins latch); mutex-release. The remount attempt is honest about the race: the loop
+condition and the [M1] step-0 bail stop any NEW attempt once the intent is published, but an
+attempt already past its bail does not re-check the intent mid-flight — its reclaim can complete
+inside FORGET's join window (that race is a witness, and trip#2 is its cure).
+
+## Results {#results}
+
+| Config | Checks | Result | States (distinct) / depth |
+|---|---|---|---|
+| `_main` | TypeOK, I1, I1b, I2, I3, I4a, I4b, I6 + `ForgetCompletes` + `GcExitsAfterVanished` (liveness, fairness) | **ALL HOLD** | 268 / 21 |
+| `_sab_notrip2` | trip#2 removed | **I1 VIOLATED** (expected) | 272 / 21 (trace len 18) |
+| `_sab_unearnedfarewell` | drain gate removed | **I2 VIOLATED** (expected) | 260 / 22 |
+| `_sab_nogcselfexit` | pre-[C1] code: no GC self-exit on natural `Vanished` | **`GcExitsAfterVanished` VIOLATED** (expected) | 268 / 21 (lasso) |
+| `_witness_forgetdone` | FORGET completion reachable | reachable (good) | 268 / 21 |
+| `_witness_racedreplaced` | FORGET done with a raced natural `VanishedReplaced` | reachable (good) | 268 / 21 |
+| `_witness_joinwindowreclaim` | reclaim completed inside the join window | reachable (good) | 268 / 22 |
+
+Per-invariant meaning and result (all in `_main` unless noted):
+
+- **I1 `ForgetTerminal`** (`forget done => fence latched AND state in Vanished`): HOLDS under all
+ interleavings including the join-window reclaim. The `_sab_notrip2` RED trace is the exact
+ Task-10 race, machine-checked: attempt begins pre-intent; FORGET publishes intent + trip#1; the
+ in-flight attempt's **reclaim completes** (`lost = FALSE`, pool back to `Live`); joins land;
+ without trip#2 FORGET finishes `VanishedForgotten` with `mayMutate() == TRUE`. Trip#2 is
+ **necessary and sufficient** in the model: after the join, no actor can re-arm — `scheduleRemount`
+ is refused by the intent latch and the shutdown latch, and the keeper never clears `lost`.
+- **I1b `ForgetWinsUnlessNatural`**: HOLDS — the final state is `VanishedForgotten` unless a
+ natural `Replaced` transition won `enterVanished` first; `_witness_racedreplaced` proves the race
+ is real and the first-terminal-wins latch handles it (by design, per the `forgetDisk` comment).
+ With [M1], the racing promotion can only come from an attempt already in flight at
+ `publishVanishedIntent` — no new attempt begins after it.
+- **I2 `EarnedFarewell`** (farewell only after a provable drain): HOLDS; sabotage RED.
+- **I3 `OneWay`** (`IdentityLost`/`Vanished` never followed by `Live`/`Transient`;
+ `IdentityLost -> VanishedReplaced/Forgotten` allowed; `Vanished` absorbing): HOLDS.
+- **I4a/I4b** (`Transient`/`IdentityLost` and every `Vanished` state imply a latched fence — the
+ benign-answer gate never coexists with write authority): HOLD.
+- **I6 `GcStoppedAfterForget`**: HOLDS, including the idempotent short-circuit path (the detached
+ scheduler still `stop()`s via its local's destructor) and `GC START` post-FORGET (the Admin gate
+ refuses on a non-`Live` pool).
+- **`ForgetCompletes`** (liveness under weak fairness on the FORGET steps, thread-exit path,
+ attempt resolution, round completion): HOLDS — no interleaving (in-flight attempt, in-flight
+ round, racing natural promotion = the I5 question) wedges FORGET; both racing `enterVanished`
+ calls resolve via the idempotent `terminal_state_published` latch with no stuck half-terminal
+ state.
+- **`GcExitsAfterVanished`** (the [C1] fix): HOLDS — once the pool is `Vanished`, the scheduler
+ eventually stops ticking (self-exit on the natural path, stop+destroy on the FORGET path).
+ `_sab_nogcselfexit` (the pre-fix code) produces the exact bug lasso: natural `VanishedReplaced`
+ with `gcsched = Running` stuttering forever. This makes [C1] a modeled **precondition** of the
+ green verdict — it must land as queued.
+
+## Model-fidelity caveats {#caveats}
+
+1. **Blocking joins are enabled-when conditions**: `stop()` joining an in-flight round and
+ `stopRemountThread` joining the thread are modeled as actions enabled when the join target is
+ gone, with weak fairness supplying progress. Real-time bounds ("one step + one backend
+ timeout") are outside TLA scope — the model proves the joins cannot deadlock or livelock, not
+ how long they take.
+2. **Reclaim is one atomic action** (arm + `noteRemounted`): the real
+ `armMountFence .. noteRemounted` window (fence re-armed, state still `TransientNotLive`) is
+ collapsed — justified because the op gate refuses durable admission while not `Live` and every
+ older fence generation fails its recheck, making the window unobservable to durable writers. If
+ a durable path ever bypasses the op gate, this collapse hides a hazard.
+3. **The keeper's single-shot terminal op** (`doTerminate` throwing on a second call) is modeled
+ only through `keeperReset` making later teardown find no keeper; `~Pool` itself is out of scope.
+4. **Manual `GC RUN` rounds** are not a separate actor: they are `Live`-gated and blocked during
+ FORGET by `gc_scheduler_mutex` (held by `forgetDisk` from entry), so the scheduler-loop rounds
+ dominate them for every property here. A second concurrent FORGET is serialized by
+ `lifecycle_mutex`; only one instance is modeled (its idempotent short-circuit is).
+5. **The natural `Replaced` promotion is nondeterministic** — the model does not check WHEN the
+ identity gate may conclude `Replaced` (that is the probe/verdict logic of
+ `probePoolLifecycleGate`, covered by `gtest_cas_lifecycle_condition.cpp`), only that a
+ promotion at ANY moment interleaves safely with FORGET.
+6. **[C1]/[M1] are modeled as landed.** If either lands differently (e.g. the scheduler self-exit
+ keyed on something other than `isVanished()` at wake, or the step-0 bail omitted), the model
+ must be re-checked against the actual patch.
+
+## Historical: the excised erasure-proof model (`CaErasureProof.tla`) {#historical-erasure}
+
+Before the v1 scope decision, the natural `Vanished(erased)` promotion (spec §2 [C2][C3][D1]) was
+modeled and TLC-checked with the observer's sample split into its two non-atomic halves (the
+gate's LIST, then the qualification reads) exactly as the code ordered them. Verdict, preserved
+for a possible v2 revival: the writer-side machinery was SOUND — op-gate `Live`-only admission +
+the op-scoped `DurableRequestGuard` counter + the LIST/streak reset discipline held at 56k states,
+and the [D1] grace was proven **load-bearing** (without it, a zombie request — guard released on
+timeout while the PUT is still in flight — lands after the second empty sample and falsifies
+"verified: pool prefix empty"). Two REAL GC-side windows were found: a fresh (never-observed)
+scheduler's round CREATES `gc/state` between the observer's final LIST and its `round_in_flight`
+read and completes before it (the `has_observation` guard protects nothing until latched), and
+out-of-round `heartbeatLoop` pulses land `gc/hb` in the same window; the `Live`-gate fix direction
+held at 476k states. These traces are part of the evidence behind excising the natural-erasure
+stack from v1 (and behind [C1], which the surviving model now requires). The model, its 6 configs,
+runner, and logs remain in the tree as the record; any v2 must re-run and extend them.
diff --git a/ci/defs/job_configs.py b/ci/defs/job_configs.py
index 33db34ed1828..2f50db741bb4 100644
--- a/ci/defs/job_configs.py
+++ b/ci/defs/job_configs.py
@@ -777,6 +777,44 @@ class JobConfigs:
runs_on=RunnerLabels.ARM_SMALL,
requires=[ArtifactNames.CH_ARM_BINARY_GH],
),
+ # Content-addressed disk over S3 (RustFS) as the default MergeTree storage — the real-S3
+ # (north star) MergeTree default. RustFS is started by the stateless job itself because the
+ # incarnation pool needs enforced conditional deletes that MinIO OSS lacks; installs only
+ # content_addressed_s3_storage_policy_for_merge_tree_by_default.xml. Covered on amd across
+ # the plain binary and all three sanitizer builds, plus an arm binary run for cross-arch.
+ Job.ParamSet(
+ parameter="amd_binary, content_addressed s3 storage, parallel",
+ runs_on=RunnerLabels.AMD_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_AMD_BINARY_GH],
+ ),
+ Job.ParamSet(
+ parameter="amd_asan_ubsan, content_addressed s3 storage, parallel",
+ runs_on=RunnerLabels.AMD_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_AMD_ASAN_UBSAN_GH],
+ ),
+ Job.ParamSet(
+ parameter="amd_tsan, content_addressed s3 storage, parallel",
+ runs_on=RunnerLabels.AMD_MEDIUM,
+ requires=[ArtifactNames.CH_AMD_TSAN_GH],
+ ),
+ Job.ParamSet(
+ parameter="amd_msan, content_addressed s3 storage, parallel",
+ runs_on=RunnerLabels.FUNC_TESTER_AMD,
+ requires=[ArtifactNames.CH_AMD_MSAN_GH],
+ ),
+ Job.ParamSet(
+ parameter="arm_binary, content_addressed s3 storage, parallel",
+ runs_on=RunnerLabels.ARM_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_ARM_BINARY_GH],
+ ),
+ # Content-addressed disk as the default MergeTree storage, local object storage (CAS M6
+ # drop-in). Mirrors the "s3 storage" variant but installs only
+ # content_addressed_storage_policy_for_merge_tree_by_default.xml.
+ Job.ParamSet(
+ parameter="amd_binary, content_addressed storage, parallel",
+ runs_on=RunnerLabels.AMD_MEDIUM_CPU,
+ requires=[ArtifactNames.CH_AMD_BINARY_GH],
+ ),
)
functional_tests_jobs_coverage = common_ft_job_config.parametrize(
*[
diff --git a/ci/jobs/functional_tests.py b/ci/jobs/functional_tests.py
index fcde1575154b..a828ad47ecb7 100644
--- a/ci/jobs/functional_tests.py
+++ b/ci/jobs/functional_tests.py
@@ -142,6 +142,8 @@ def run_tests(
"old analyzer": "--analyzer",
"WasmEdge": "--wasm-engine wasmedge",
"s3 storage": "--s3-storage",
+ "content_addressed storage": "--content-addressed-storage",
+ "content_addressed s3 storage": "--content-addressed-s3-storage",
"DatabaseReplicated": "--db-replicated",
"DatabaseOrdinary": "--db-ordinary",
"wide parts enabled": "--wide-parts",
@@ -155,6 +157,8 @@ def run_tests(
OPTIONS_TO_TEST_RUNNER_ARGUMENTS = {
"s3 storage": "--s3-storage --no-stateful",
+ "content_addressed storage": "--content-addressed-storage",
+ "content_addressed s3 storage": "--content-addressed-s3-storage",
"ParallelReplicas": "--no-zookeeper --no-shard --no-parallel-replicas",
"AsyncInsert": " --no-async-insert",
"DatabaseReplicated": " --no-stateful --replicated-database",
@@ -242,6 +246,7 @@ def main():
is_targeted_check = False
is_bugfix_validation = False
is_s3_storage = False
+ is_content_addressed_s3 = False
is_azure_storage = False
is_database_replicated = False
is_shared_catalog = False
@@ -295,8 +300,13 @@ def main():
is_excluded_from_llvm = True
if "per_test_coverage" in to:
is_per_test_coverage = True
- if "s3 storage" in to:
+ if "s3 storage" in to and "content_addressed" not in to:
+ # The content-addressed-over-s3 variant ("content_addressed s3 storage") installs
+ # only its own default policy and must not pull in the s3 stateful-data / encrypted
+ # storage machinery, so it is deliberately excluded from is_s3_storage.
is_s3_storage = True
+ if "content_addressed s3 storage" in to:
+ is_content_addressed_s3 = True
if "azure" in to:
is_azure_storage = True
if "DatabaseReplicated" in to:
@@ -637,6 +647,14 @@ def main():
def start():
res = CH.start_minio(test_type="stateless") and CH.start_azurite()
+ if res and is_content_addressed_s3:
+ # The CA-over-S3 pool lives on RustFS (M-W D-W8): the incarnation pool
+ # needs ENFORCED conditional deletes, which MinIO OSS lacks (the
+ # fail-closed capability probe rejects it). start_rustfs wipes its data
+ # dir per run, so no pool state bleeds between runs (the local-CA
+ # analogue is the per-run server-store wipe). MinIO keeps the non-CA
+ # s3 disks.
+ res = CH.start_rustfs()
res = res and CH.start()
res = res and CH.wait_ready()
if res:
diff --git a/ci/jobs/scripts/check_style/check_cpp.sh b/ci/jobs/scripts/check_style/check_cpp.sh
index 60e2f1e303c8..34b64ba6784c 100755
--- a/ci/jobs/scripts/check_style/check_cpp.sh
+++ b/ci/jobs/scripts/check_style/check_cpp.sh
@@ -462,6 +462,17 @@ xargs < "$STYLE_TMPDIR/all_excluded" rg -n '\bassert[[:space:]]*\(' |
echo "Use chassert instead of assert"
} > "$O.18" 2>&1 &
+# 19: CAS wiring must not mutate committed refs through raw Cas::Store / Cas::Build (spec
+# docs/superpowers/specs/2026-07-08-cas-part-folder-cache-design.md): committed part-ref mutations
+# go through CachedPartFolderAccess (dot-syntax on the partAccess() reference). Best-effort textual
+# guard: Core/ (the protocol implementation) and the facade itself are exempt.
+{
+find $ROOT_PATH/src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed -maxdepth 1 \( -name '*.h' -or -name '*.cpp' \) 2>/dev/null |
+ grep -v 'CachedPartFolderAccess' |
+ xargs -r rg -n -e '->(dropRef|updateRefPayload|dropNamespace|promote)\(' &&
+ echo "Committed part-ref mutations in CAS wiring must go through CachedPartFolderAccess (docs/superpowers/specs/2026-07-08-cas-part-folder-cache-design.md)"
+} > "$O.19" 2>&1 &
+
# Wait for all parallel checks to complete, then output results in order
wait
cat "$O".* 2>/dev/null
diff --git a/ci/jobs/scripts/clickhouse_proc.py b/ci/jobs/scripts/clickhouse_proc.py
index 88a4e56c97ba..f94da8748b99 100644
--- a/ci/jobs/scripts/clickhouse_proc.py
+++ b/ci/jobs/scripts/clickhouse_proc.py
@@ -9,6 +9,7 @@
import threading
import traceback
import uuid
+import zipfile
from collections import defaultdict
from pathlib import Path
from typing import List
@@ -163,6 +164,81 @@ def start_minio(self, test_type):
print("Failed to start minio")
return False
+ RUSTFS_VERSION = "1.0.0-beta.9"
+
+ def download_rustfs(self, rustfs_bin):
+ machine = platform.machine()
+ if machine not in ("x86_64", "aarch64", "arm64"):
+ print(f"unsupported architecture for rustfs [{machine}]")
+ return False
+ arch = "aarch64" if machine in ("aarch64", "arm64") else "x86_64"
+ url = (
+ f"https://github.com/rustfs/rustfs/releases/download/{self.RUSTFS_VERSION}"
+ f"/rustfs-linux-{arch}-musl-v{self.RUSTFS_VERSION}.zip"
+ )
+ zip_path = f"{temp_dir}/rustfs.zip"
+ if not Shell.check(
+ f"curl -sSfL --retry 3 --retry-delay 5 -o {zip_path} {url}", verbose=True
+ ):
+ print(f"failed to download rustfs from {url}")
+ return False
+ # The release zip contains the single `rustfs` binary at its root.
+ with zipfile.ZipFile(zip_path) as archive:
+ archive.extract("rustfs", temp_dir)
+ os.remove(zip_path)
+ os.chmod(rustfs_bin, 0o755)
+ return True
+
+ def start_rustfs(self):
+ # RustFS backs the content-addressed-over-S3 pool (M-W D-W8): the incarnation pool needs
+ # ENFORCED conditional operations (a wrong-token DELETE must fail with 412), which MinIO
+ # OSS lacks - the CA disk's fail-closed capability probe rejects it. The static (musl)
+ # binary is downloaded from the RustFS GitHub release into ci/tmp/rustfs when absent
+ # (CI wipes ci/tmp per run); the data dir is wiped per run so no pool state bleeds
+ # between runs. MinIO keeps serving the non-CA s3 disks on its own port.
+ rustfs_bin = f"{temp_dir}/rustfs"
+ if not Path(rustfs_bin).is_file() and not self.download_rustfs(rustfs_bin):
+ print(f"rustfs binary not found at {rustfs_bin} and download failed")
+ return False
+ data_dir = f"{temp_dir}/rustfs_data"
+ Shell.check(f"rm -rf {data_dir} && mkdir -p {data_dir}", verbose=True)
+ # Disable the background data-scanner and auto-heal manager (B93). On a single-disk
+ # ephemeral test pool they do no useful work (no parity to heal from; data-usage/bitrot
+ # accounting on throwaway data is pointless) but their cost is NOT free: the scanner walks
+ # the whole object tree (CPU grows with object count - observed ~120% sustained after the
+ # pool reached ~177k objects in a full lane) and the heal manager's 10s auto-disk-scan
+ # takes namespace locks, producing periodic multi-minute bursts of 503 ServiceUnavailable
+ # (NamespaceLockQuorumUnavailable) that stalled client I/O and caused ~50 test timeouts.
+ # Client GET/PUT/LIST/DELETE do not depend on either service, so disabling them is safe.
+ # Env names: RUSTFS_SCANNER_ENABLED/RUSTFS_HEAL_ENABLED (the RUSTFS_ENABLE_* forms are
+ # deprecated in 1.0.0-beta.8).
+ command = (
+ f"RUSTFS_SCANNER_ENABLED=false RUSTFS_HEAL_ENABLED=false "
+ f"{rustfs_bin} server --address 0.0.0.0:11121 "
+ f"--access-key clickhouse --secret-key clickhouse {data_dir}"
+ )
+ with open(f"{temp_dir}/rustfs.log", "w") as log_file:
+ self.rustfs_proc = subprocess.Popen(
+ command, stdout=log_file, stderr=subprocess.STDOUT, shell=True
+ )
+ print(f"Started rustfs asynchronously with PID {self.rustfs_proc.pid}")
+
+ if not Shell.check(
+ "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:11121/ | grep -qE '403|200'",
+ verbose=False,
+ retries=6,
+ ):
+ print("Failed to start rustfs")
+ return False
+ # The `test` bucket the storage policy expects.
+ res = Shell.check(
+ "/mc alias set carustfs http://localhost:11121 clickhouse clickhouse && /mc mb --ignore-existing carustfs/test",
+ verbose=True,
+ )
+ if not res:
+ print("Failed to create rustfs test bucket")
+ return res
+
def start_azurite(self):
# Raise the open files limit before launching azurite-rs.
# Each concurrent test query opens a TCP connection plus an in-memory
diff --git a/docs/en/operations/storing-data.md b/docs/en/operations/storing-data.md
index 8a2903cd1ebb..cd31ca2ea296 100644
--- a/docs/en/operations/storing-data.md
+++ b/docs/en/operations/storing-data.md
@@ -46,7 +46,7 @@ It requires specifying:
-Optionally, `metadata_type` can be specified (it is equal to `local` by default), but it can also be set to `plain`, `web` and, starting from `24.4`, `plain_rewritable`.
+Optionally, `metadata_type` can be specified (it is equal to `local` by default), but it can also be set to `plain`, `web`, `plain_rewritable` (starting from `24.4`) and `content_addressed`.
Usage of `plain` metadata type is described in [plain storage section](/operations/storing-data#plain-storage), `web` metadata type can be used only with `web` object storage type, `local` metadata type stores metadata files locally (each metadata files contains mapping to files in object storage and some additional meta information about them).
For example:
@@ -452,6 +452,107 @@ is equal to
Starting from `24.5` it is possible to configure any object storage disk
(`s3`, `azure`, `local`) using the `plain_rewritable` metadata type.
+### Using Content-Addressed Storage {#content-addressed-storage}
+
+Setting `metadata_type` to `content_addressed` turns a disk into a content-addressed (CA) disk: every
+object is addressed by the hash of its content rather than by a randomly generated blob name, so
+identical content written by different parts (or different tables) is stored once and shared. A
+background garbage collector reclaims objects once no part references them anymore; see
+[`SYSTEM CONTENT ADDRESSED GC RUN`](/sql-reference/statements/system#content-addressed-garbage-collection),
+[`SYSTEM CONTENT ADDRESSED GC REBUILD`](/sql-reference/statements/system#system-content-addressed-gc-rebuild),
+[`SYSTEM CONTENT ADDRESSED DROP POOL MEMBER`](/sql-reference/statements/system#system-content-addressed-drop-pool-member),
+and the [`system.content_addressed_garbage_collection_log`](/operations/system-tables/content_addressed_garbage_collection_log),
+[`system.content_addressed_mounts`](/operations/system-tables/content_addressed_mounts), and
+[`system.content_addressed_log`](/operations/system-tables/content_addressed_log) system tables.
+
+Configuration:
+
+```xml
+
+ object_storage
+ s3
+ content_addressed
+ https://s3.eu-west-1.amazonaws.com/clickhouse-eu-west-1.clickhouse.com/data/
+ 1
+
+
+ server-{replica}
+ disks/s3_content_addressed/cas_scratch/
+ local
+ cityhash128
+ true
+ 60
+ 1
+ 67108864
+ 67108864
+ always
+
+
+```
+
+All content-addressed-specific settings live under the `` block; the surrounding
+`object_storage` / `` / connection settings are the same as for any
+other `object_storage` disk. Since the `` block already scopes every key to this
+disk, none of the keys below carry a redundant `cas_`/`ca_` prefix.
+
+#### Required parameters {#required-parameters-content-addressed}
+
+- `server_root_id` — the subtree of the shared pool that this server owns. When several replicas
+ mount the same pool (same `endpoint`), each one must own a distinct subtree, so this is normally
+ written with a macro, e.g. `server-{replica}`. Missing this key is
+ a startup error.
+
+#### Optional parameters {#optional-parameters-content-addressed}
+
+- `scratch_path` — a real, server-local filesystem directory used to spill the write buffer before it
+ is committed to the pool (never the object-storage key prefix). Defaults to
+ `/disks//cas_scratch/`. A relative override is anchored to the server
+ data path, not the process's current working directory.
+- `staging_backend` — `local` (default) or `s3`. Selects where in-flight part data is staged before
+ being committed into the pool; `local` is byte-for-byte the original write path, `s3` enables
+ S3-native staging.
+- `blob_hash` — `cityhash128` (default) or `sha256`. Selects the pool's blob content-hash function.
+ The choice is fixed at pool creation; a reopen whose `blob_hash` disagrees with the pool's recorded
+ algorithm fails closed.
+- `blob_hash_allow_new` — `false` by default. Admits a new hash algorithm into an existing pool's set
+ of recorded algorithms; without it, a `blob_hash` that disagrees with what the pool already recorded
+ fails closed instead of silently turning the pool mixed-algorithm.
+- `gc_enabled` — `true` by default. Enables the background garbage collector for this disk.
+- `gc_interval_sec` — `60` by default; must be `>= 1`. Interval between background GC rounds.
+- `gc_shards` — `1` by default; must be `>= 1`. Number of blob-hash-prefix shards the GC reducer
+ splits work across. This is a creation-time-only setting: on reopen the pool's persisted GC state is
+ authoritative.
+- `dedup_cache_bytes` — `64` MiB by default. Size of the in-memory dedup lookup cache.
+- `dedup_head_first_min_bytes` — `1` MiB by default. Minimum object size below which dedup reads the
+ whole head of the object first.
+- `gc_snap_generations_to_keep` — `3` by default. Number of past GC snapshot generations retained.
+- `manifest_sweep_list_budget_keys` — `1000` by default. Per-round key-listing budget for the manifest
+ sweep.
+- `manifest_sweep_delete_budget_keys` — `100` by default. Per-round delete budget for the manifest
+ sweep.
+- `gcs_max_conditional_put_bytes` — `1` GiB by default. On generation-token backends (Google Cloud
+ Storage), the body of a conditional write is RAM-buffered up to this size; a larger conditional
+ write throws `NOT_IMPLEMENTED`. Irrelevant on `ETag`-based backends such as AWS S3.
+- `part_folder_cache_bytes` — `64` MiB by default. Size of the part-folder view cache. `0` disables
+ retention; this is a supported permanent operational configuration, not only a debug aid.
+- `part_folder_cache_max_entries` — `10000` by default. Maximum number of entries in the part-folder
+ view cache.
+- `part_folder_cache_max_entry_bytes` — `16` MiB by default. Maximum size of a single cached
+ part-folder view entry.
+- `part_folder_validate` — `always` (default), `never`, or `age `. Controls how often a
+ `ForceFresh` read re-proves a cached manifest body via a `HEAD` request: `always` re-proves every
+ time (the original, pre-optimization behavior), `never` trusts the cache without re-proving, and
+ `age ` re-proves only once the cached entry is older than the given number of seconds.
+- `manifest_decode_cache_bytes` — `128` MiB by default. Byte bound for the decoded-manifest cache.
+ `0` disables decode caching entirely (a diagnostic mode).
+- `gc_meta_pool_size` — `16` by default. Bounded thread-pool size for the GC's per-hash freshness-meta
+ writes (condemn/spare/delete), so a mass `DROP` condemning millions of blobs does not run fully
+ sequentially.
+- `materialization_grace_ms` — `30000` by default. Conditional post-reclaim wait that `Pool::open`
+ pays over an unclean predecessor incarnation before materializing.
+- `skip_access_check` — `false` by default. Skips the disk's startup access check ("start now, fix
+ later"), unlike the generic disk-wide startup flag.
+
### Using Azure Blob Storage {#azure-blob-storage}
`MergeTree` family table engines can store data to [Azure Blob Storage](https://azure.microsoft.com/en-us/services/storage/blobs/)
diff --git a/docs/en/operations/system-tables/content_addressed_garbage_collection_log.md b/docs/en/operations/system-tables/content_addressed_garbage_collection_log.md
new file mode 100644
index 000000000000..15b5b008ef72
--- /dev/null
+++ b/docs/en/operations/system-tables/content_addressed_garbage_collection_log.md
@@ -0,0 +1,75 @@
+---
+description: 'System table containing per-round records of the content-addressed (CA) MergeTree garbage collector.'
+sidebar_label: 'content_addressed_garbage_collection_log'
+sidebar_position: 30
+slug: /operations/system-tables/content_addressed_garbage_collection_log
+title: 'system.content_addressed_garbage_collection_log'
+doc_type: 'reference'
+---
+
+## Description {#description}
+
+The `system.content_addressed_garbage_collection_log` table contains per-round records of the
+content-addressed (CA) MergeTree garbage collector. For every garbage-collection round it stores a
+`Start` row and a `Finish` row (like `system.part_log` stores events per data part), with the counts
+of objects marked and deleted, the round duration, the outcome, and a per-round `ProfileEvents`
+delta.
+
+Rounds are emitted both by the background GC scheduler (`trigger = 'Scheduled'`) and by the
+synchronous [`SYSTEM CONTENT ADDRESSED GC RUN`](/sql-reference/statements/system#content-addressed-garbage-collection)
+command (`trigger = 'Manual'`).
+
+The table is created only if the `content_addressed_garbage_collection_log` server setting is
+specified (it is enabled by default in the shipped `config.xml`).
+
+## Columns {#columns}
+
+- `hostname` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — Host name of the server executing the round.
+- `event_date` ([Date](/sql-reference/data-types/date)) — Event date.
+- `event_time` ([DateTime](/sql-reference/data-types/datetime)) — Event time.
+- `event_time_microseconds` ([DateTime64(6)](/sql-reference/data-types/datetime64)) — Event time with microseconds precision.
+- `event_type` ([Enum8](/sql-reference/data-types/enum)) — `Start` or `Finish` of a GC round.
+- `disk_name` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The content-addressed disk the round ran on.
+- `srid` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The `server_root_id` of the mount whose GC scheduler ran this round. Distinguishes concurrent mounters of the same shared pool; join on this column when correlating rounds against [`system.content_addressed_mounts`](/operations/system-tables/content_addressed_mounts).
+- `gc_id` ([String](/sql-reference/data-types/string)) — The GC scheduler instance id (which mounter ran the round).
+- `trigger` ([Enum8](/sql-reference/data-types/enum)) — `Scheduled` (background tick) or `Manual` (`SYSTEM` command).
+- `round` ([UInt64](/sql-reference/data-types/int-uint)) — The GC round number (`0` on a `Start` row).
+- `outcome` ([Enum8](/sql-reference/data-types/enum)) — `Unknown` (on a `Start` row), `Success` (led and completed), `NotALeader` (another replica holds the GC lease), or `Error` (the round threw).
+- `candidates_marked` ([UInt64](/sql-reference/data-types/int-uint)) — Objects retired (marked) this round.
+- `objects_deleted` ([UInt64](/sql-reference/data-types/int-uint)) — Objects physically deleted this round.
+- `objects_absent` ([UInt64](/sql-reference/data-types/int-uint)) — Retire candidates found already absent.
+- `objects_replaced` ([UInt64](/sql-reference/data-types/int-uint)) — `412`-saves (a resurrection won the race against the delete).
+- `objects_spared` ([UInt64](/sql-reference/data-types/int-uint)) — Candidates spared because their in-degree was greater than zero at recheck.
+- `manifests_deleted` ([UInt64](/sql-reference/data-types/int-uint)) — Owner-removed manifest bodies physically deleted this round, counted separately from blob deletes.
+- `entries_condemned` ([UInt64](/sql-reference/data-types/int-uint)) — Retired entries newly condemned this round (retired-cursor pipeline stage 1).
+- `entries_graduated` ([UInt64](/sql-reference/data-types/int-uint)) — Retired entries newly floor-passed and republished `delete_pending` this round (pipeline stage 2; deleted the next round).
+- `entries_redeleted` ([UInt64](/sql-reference/data-types/int-uint)) — Pending exact-token blob deletes executed this round (pipeline stage 3).
+- `fence_outs` ([UInt64](/sql-reference/data-types/int-uint)) — Expired mounts fenced out by this round's heartbeat floor.
+- `anomalies` ([UInt64](/sql-reference/data-types/int-uint)) — Fold clamps surfaced (and survived) this round. A steady non-zero value warrants a look at the round log details.
+- `duration_ms` ([UInt64](/sql-reference/data-types/int-uint)) — The round wall-clock duration (on a `Finish` row).
+- `error` ([String](/sql-reference/data-types/string)) — The exception text when `outcome = 'Error'`.
+- `ProfileEvents` ([Map(LowCardinality(String), UInt64)](/sql-reference/data-types/map)) — The per-round `ProfileEvents` delta (the `Cas*` counters and S3/disk events for this round).
+
+## Example {#example}
+
+```sql
+SELECT
+ event_type,
+ disk_name,
+ trigger,
+ outcome,
+ candidates_marked,
+ objects_deleted,
+ duration_ms
+FROM system.content_addressed_garbage_collection_log
+ORDER BY event_time_microseconds DESC
+LIMIT 2
+FORMAT Vertical;
+```
+
+## See Also {#see-also}
+
+- [`SYSTEM CONTENT ADDRESSED GC RUN`](/sql-reference/statements/system#content-addressed-garbage-collection) — run one GC round synchronously.
+- [`system.content_addressed_mounts`](/operations/system-tables/content_addressed_mounts) — live per-`server_root_id` mount and GC-health state.
+- [`system.content_addressed_log`](/operations/system-tables/content_addressed_log) — per-decision event log for the CA garbage collector and writer.
+- [`system.part_log`](/operations/system-tables/part_log) — the analogous per-part event log.
diff --git a/docs/en/operations/system-tables/content_addressed_log.md b/docs/en/operations/system-tables/content_addressed_log.md
new file mode 100644
index 000000000000..41b3a2a7ca25
--- /dev/null
+++ b/docs/en/operations/system-tables/content_addressed_log.md
@@ -0,0 +1,62 @@
+---
+description: 'System table containing a per-decision event log for the content-addressed (CA) MergeTree writer and garbage collector.'
+sidebar_label: 'content_addressed_log'
+sidebar_position: 32
+slug: /operations/system-tables/content_addressed_log
+title: 'system.content_addressed_log'
+doc_type: 'reference'
+---
+
+## Description {#description}
+
+The `system.content_addressed_log` table contains a per-decision event log for the content-addressed
+(CA) MergeTree storage engine: blob puts and dedup adoptions, root/ref transitions, in-degree changes,
+garbage-collector retire decisions and recheck verdicts, blob deletes, and dangling-access/corruption
+findings. It is a much finer-grained, per-event complement to
+[`system.content_addressed_garbage_collection_log`](/operations/system-tables/content_addressed_garbage_collection_log),
+which only records one `Start`/`Finish` row per GC round.
+
+## Columns {#columns}
+
+- `hostname` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — Host name of the server that emitted the event.
+- `event_date` ([Date](/sql-reference/data-types/date)) — Event date.
+- `event_time` ([DateTime](/sql-reference/data-types/datetime)) — Event time.
+- `event_time_microseconds` ([DateTime64(6)](/sql-reference/data-types/datetime64)) — Event time with microseconds precision.
+- `event_type` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The CA decision/event, e.g. `blob_put`, `blob_reuse_adopt`, `root_remove`, `indeg_zero`, `gc_retire_decision`, `gc_recheck_verdict`, `blob_delete`, `dangling_access`, `corrupt_dangle`.
+- `disk_name` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — The content-addressed disk / pool the event belongs to.
+- `namespace` ([String](/sql-reference/data-types/string)) — `roots/` (server/table); empty if not applicable.
+- `ref_name` ([String](/sql-reference/data-types/string)) — Part name / ref the event concerns; empty if not applicable.
+- `object_kind` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — One of `none`, `blob`, `manifest`, `root`, `snap`.
+- `object_hash` ([String](/sql-reference/data-types/string)) — Content hash (lowercase hex) of the object; empty if not applicable.
+- `token` ([String](/sql-reference/data-types/string)) — Incarnation token (`ETag`) involved; empty if not applicable.
+- `round` ([UInt64](/sql-reference/data-types/int-uint)) — GC round (`0` if not applicable).
+- `gen` ([UInt64](/sql-reference/data-types/int-uint)) — GC snapshot generation (`0` if not applicable).
+- `at_version` ([UInt64](/sql-reference/data-types/int-uint)) — Manifest `shard_version` of the driving journal record (`0` if not applicable).
+- `outcome` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — Decision outcome, e.g. `ok`, `adopt`, `resurrect`, `deleted`, `replaced`, `spared`, `absent`, `zeroed`, `skipped`.
+- `reason` ([LowCardinality(String)](/sql-reference/data-types/lowcardinality)) — Human-readable rationale for the decision. Templated across rows, so it is `LowCardinality`.
+- `thread_id` ([UInt64](/sql-reference/data-types/int-uint)) — OS thread that emitted the event.
+- `query_id` ([String](/sql-reference/data-types/string)) — Query id for correlation with [`system.query_log`](/operations/system-tables/query_log); empty if not applicable.
+- `detail` ([Map(LowCardinality(String), String)](/sql-reference/data-types/map)) — Structured event-specific facts, e.g. `prev_indeg`, `dropped_by`, `freed`, `cursor`, `code`, `site`.
+
+## Example {#example}
+
+```sql
+SELECT
+ event_time_microseconds,
+ event_type,
+ disk_name,
+ ref_name,
+ object_kind,
+ outcome,
+ reason
+FROM system.content_addressed_log
+ORDER BY event_time_microseconds DESC
+LIMIT 10
+FORMAT Vertical;
+```
+
+## See Also {#see-also}
+
+- [`system.content_addressed_garbage_collection_log`](/operations/system-tables/content_addressed_garbage_collection_log) — per-round GC event log.
+- [`system.content_addressed_mounts`](/operations/system-tables/content_addressed_mounts) — live per-`server_root_id` mount and GC-health state.
+- [`system.query_log`](/operations/system-tables/query_log) — correlate via `query_id`.
diff --git a/docs/en/operations/system-tables/content_addressed_mounts.md b/docs/en/operations/system-tables/content_addressed_mounts.md
new file mode 100644
index 000000000000..2e2eeb351a43
--- /dev/null
+++ b/docs/en/operations/system-tables/content_addressed_mounts.md
@@ -0,0 +1,70 @@
+---
+description: 'System table containing the live mount and GC-health state of every server mounted onto a content-addressed (CA) disk pool.'
+sidebar_label: 'content_addressed_mounts'
+sidebar_position: 31
+slug: /operations/system-tables/content_addressed_mounts
+title: 'system.content_addressed_mounts'
+doc_type: 'reference'
+---
+
+## Description {#description}
+
+The `system.content_addressed_mounts` table contains one row per mount slot discovered on every
+content-addressed (CA) disk configured on the node. A pool may be shared by several servers (or
+several `server_root_id` mounts on the same server), and this table lists every mount visible in
+the pool's backend at query time, not only the querying server's own mount — it exists for
+incident-time diagnosis of leases, epochs, and GC leadership across a shared pool.
+
+The table is read directly from the CA disk's backend on every query (there is no persisted log
+behind it); a transient backend error on one disk is skipped and does not blind the rest of the
+rows.
+
+## Columns {#columns}
+
+- `disk` ([String](/sql-reference/data-types/string)) — Name of the content-addressed disk.
+- `server_root_id` ([String](/sql-reference/data-types/string)) — Server root id owning the mount slot.
+- `server_uuid` ([UUID](/sql-reference/data-types/uuid)) — UUID of the server incarnation holding the lease.
+- `hostname` ([String](/sql-reference/data-types/string)) — Hostname recorded in the lease body.
+- `process_id` ([UInt64](/sql-reference/data-types/int-uint)) — Process id recorded in the lease body.
+- `writer_epoch` ([UInt64](/sql-reference/data-types/int-uint)) — Fenced writer epoch of the incarnation.
+- `renewal_sequence` ([UInt64](/sql-reference/data-types/int-uint)) — Lease renewal sequence number.
+- `started_at_ms` ([DateTime64(3)](/sql-reference/data-types/datetime64)) — Time when the lease started.
+- `expires_at_ms` ([DateTime64(3)](/sql-reference/data-types/datetime64)) — Time when the lease expires.
+- `min_active_build_sequence` ([UInt64](/sql-reference/data-types/int-uint)) — Oldest in-flight build sequence (`UINT64_MAX` means the mount said farewell).
+- `gc_fenced` ([UInt8](/sql-reference/data-types/int-uint)) — `1` if GC fenced this slot out (terminal).
+- `state` ([String](/sql-reference/data-types/string)) — One of `live`, `expired`, `terminated`, `fenced`, `corrupt`.
+- `is_leader` ([Nullable(UInt8)](/sql-reference/data-types/nullable)) — `1` if this server's GC scheduler currently holds this disk's leadership lease.
+- `pending_reclaim` ([Nullable(Int64)](/sql-reference/data-types/nullable)) — Cumulative two-phase deletion backlog observed by this process's GC on this disk (condemned entries minus executed exact-token deletes).
+- `last_success_age_seconds` ([Nullable(UInt64)](/sql-reference/data-types/nullable)) — Seconds since this disk's GC last led a round (`0` if it has never led or GC is not running here).
+- `wedged_namespace_count` ([Nullable(UInt64)](/sql-reference/data-types/nullable)) — Ref-append lanes currently wedged on this disk (an uncertain `PUT` exhausted its retry budget).
+
+:::note Local-only GC-health columns
+`is_leader`, `pending_reclaim`, `last_success_age_seconds`, and `wedged_namespace_count` are process-local
+facts about *this* server's own GC scheduler. They are populated **only** on the row whose `server_root_id` matches
+this server's own mount, and are `NULL` on every row describing another server's mount — stamping a local
+health fact onto a peer's row would misread as "the peer is the GC leader" during an incident. To see the
+peer's own view of these columns, query `system.content_addressed_mounts` on that server.
+:::
+
+## Example {#example}
+
+```sql
+SELECT
+ disk,
+ server_root_id,
+ state,
+ writer_epoch,
+ is_leader,
+ pending_reclaim,
+ last_success_age_seconds
+FROM system.content_addressed_mounts
+ORDER BY disk, server_root_id
+FORMAT Vertical;
+```
+
+## See Also {#see-also}
+
+- [`system.content_addressed_garbage_collection_log`](/operations/system-tables/content_addressed_garbage_collection_log) — per-round GC event log.
+- [`system.content_addressed_log`](/operations/system-tables/content_addressed_log) — per-decision event log for the CA garbage collector and writer.
+- [`SYSTEM CONTENT ADDRESSED GC RUN`](/sql-reference/statements/system#content-addressed-garbage-collection) — run one GC round synchronously.
+- [`SYSTEM CONTENT ADDRESSED DROP POOL MEMBER`](/sql-reference/statements/system#system-content-addressed-drop-pool-member) — permanently decommission a dead pool member's `server_root_id`.
diff --git a/docs/en/sql-reference/statements/system.md b/docs/en/sql-reference/statements/system.md
index 69b2b59cb0ae..926931f3bf5c 100644
--- a/docs/en/sql-reference/statements/system.md
+++ b/docs/en/sql-reference/statements/system.md
@@ -457,6 +457,80 @@ Wait until all asynchronously loading data parts of a table (outdated data parts
SYSTEM WAIT LOADING PARTS [ON CLUSTER cluster_name] [db.]merge_tree_family_table_name
```
+### SYSTEM CONTENT ADDRESSED GC RUN {#content-addressed-garbage-collection}
+
+Runs one garbage-collection round of the content-addressed (CA) MergeTree garbage collector synchronously and node-local: it reclaims content-addressed objects that are no longer referenced by any part. This is the on-demand counterpart of the background GC scheduler; it is useful for tests and diagnostics.
+
+```sql
+SYSTEM CONTENT ADDRESSED GC RUN [ON CLUSTER cluster_name] [disk_name]
+```
+
+When `disk_name` is given, the round runs on that content-addressed disk only; targeting a non-content-addressed disk raises an exception. When `disk_name` is omitted, one round runs on every content-addressed disk configured on the node; if none are configured, the command raises an exception.
+
+Each round is recorded in [`system.content_addressed_garbage_collection_log`](/operations/system-tables/content_addressed_garbage_collection_log) as a `Start` and a `Finish` row (with `trigger = 'Manual'`).
+
+The command returns one row per disk it ran on (multiple rows when `disk_name` is omitted), with columns `disk`, `acquired_lease`, `deferred`, `round`, `candidates_marked`, `objects_deleted`, `objects_absent`, `objects_replaced`, `objects_spared`, `manifests_deleted`, `entries_condemned`, `entries_graduated`, `entries_redeleted`, `fence_outs`, and `anomalies`, describing the outcome of that round.
+
+### SYSTEM CONTENT ADDRESSED GC REBUILD {#system-content-addressed-gc-rebuild}
+
+Disaster-recovery command for the content-addressed (CA) MergeTree garbage collector. It rebuilds a
+CA disk's `gc/state` baseline from scratch, by re-discovering the whole ref universe and re-folding
+manifest edges into a fresh generation. It writes only the GC plane (`gc/state` and the `gc/gen/*`
+artifacts) and never touches ref shards, manifests, or blobs, and it never deletes anything itself —
+but the rebuilt baseline drives every subsequent GC round's retire decisions, so this is a
+**destructive disaster-recovery tool**, not something to run routinely: a rebuild performed against
+a state that was not actually corrupted discards live bookkeeping, and an incorrect rebuild can make
+a later round delete objects that are still referenced.
+
+```sql
+SYSTEM CONTENT ADDRESSED GC REBUILD [FORCE] [ON CLUSTER cluster_name] disk_name
+```
+
+Unlike `SYSTEM CONTENT ADDRESSED GC RUN`, `disk_name` is **required**: the destructive
+baseline rebuild must never fan out across every content-addressed disk on the node from a bare
+command; targeting a non-content-addressed disk raises an exception.
+
+By default the command refuses to run when the disk's existing `gc/state` and every artifact it
+references decode successfully and are present — a rebuild would needlessly discard healthy live
+bookkeeping. Add `FORCE` to rebuild deliberately even though the existing state looks healthy. The
+command also refuses (regardless of `FORCE`) when another GC leader currently holds the disk's
+lease. In both refusal cases it raises an exception instead of returning a row.
+
+On success it returns one row with columns `disk`, `performed`, `round`, `generation`, `namespaces`,
+`shards`, `committed_refs`, `live_precommits`, `unowned_alive_manifests`, `edges`, and
+`clamped_shards`, describing the freshly rebuilt baseline.
+
+### SYSTEM CONTENT ADDRESSED DROP POOL MEMBER {#system-content-addressed-drop-pool-member}
+
+Permanently decommissions a dead member (`server_root_id`, or `srid`) of a content-addressed disk
+pool. It claims the member's mount slot as an administrative writer — fencing that `srid` from ever
+writing again — then drops every table namespace the member owned, drains eligible manifest debris,
+staging objects, and mountpoint objects belonging to it, and retires the mount slot once all drains
+are confirmed. This is a **destructive, irreversible** operation: only run it once the `srid` is
+confirmed permanently dead, since it fences the member out even if it later comes back online, and
+it deletes namespace and drain state that cannot be recovered.
+
+It is a writer operation, not GC: it emits ordinary ref-edge deltas rather than inventing GC
+transitions, and it does not synchronously reclaim shared blob content — removing the ref edges only
+makes the now-unreferenced blobs eligible for an ordinary GC round to reclaim later.
+
+```sql
+SYSTEM CONTENT ADDRESSED DROP POOL MEMBER 'srid' FROM DISK 'disk_name' [ON CLUSTER cluster_name]
+```
+
+Both `srid` and `disk_name` are required string literals (an `srid` is an opaque server-root path
+that may contain `/`, not a plain identifier, so it cannot be written unquoted).
+
+The operation is resumable: a rerun skips namespaces already marked `Removed` and reports them
+separately from namespaces newly removed by this invocation. Per-object drain failures are recorded
+as warnings and leave the slot in a terminated-but-not-fully-drained state that a later invocation
+can resume from, rather than raising an exception.
+
+The command returns one row with columns `server_root_id`, `namespaces_removed`, `namespaces_already_removed`,
+`committed_refs_removed`, `precommits_removed`, `manifest_debris_removed`, `staging_objects_removed`,
+`mountpoint_objects_removed`, `slot_removed`, and `warnings`. A non-empty `warnings` means some
+drain was not confirmed and the mount slot was left in place as a resume anchor.
+
## Managing ReplicatedMergeTree Tables {#managing-replicatedmergetree-tables}
ClickHouse can manage background replication related processes in [ReplicatedMergeTree](/engines/table-engines/mergetree-family/replication) tables.
diff --git a/docs/superpowers/RESUME-2026-07-15.md b/docs/superpowers/RESUME-2026-07-15.md
new file mode 100644
index 000000000000..0ba775d70107
--- /dev/null
+++ b/docs/superpowers/RESUME-2026-07-15.md
@@ -0,0 +1,119 @@
+---
+description: 'Cold-start resume instructions for the CAS unattended campaign after the 2026-07-15 reboot: entry points, the #49 stateless gate re-run, the draft-worktree integration pipeline for the codecs-v3 drafts, team topology, watchdog recreation, and standing rules.'
+sidebar_label: 'Campaign resume (2026-07-15)'
+sidebar_position: 99
+slug: /superpowers/resume-2026-07-15
+title: 'CAS campaign — cold-start resume instructions (2026-07-15 reboot)'
+doc_type: 'guide'
+---
+
+# CAS campaign — cold-start resume instructions {#resume}
+
+Written at the 2026-07-15 reboot checkpoint (`22a4f734a2f`..`0abd300ce2a`, branch `cas-gc-rebuild`).
+Audience: a FRESH Claude session with no memory of the prior one. Read top to bottom, then execute §3.
+
+## 1. Sources of truth, in priority order {#sources}
+
+1. `.superpowers/sdd/progress.md` — the ledger. The tail section `=== REBOOT CHECKPOINT 2026-07-15 ===`
+ is the authoritative state snapshot. Tasks listed complete there are DONE — never re-execute them.
+2. `git log` on `cas-gc-rebuild` — every claim in the ledger names its commit; trust commits over memory.
+3. Auto-memory (`~/.claude/projects/.../memory/MEMORY.md`) — background; verify against code before acting.
+4. `docs/superpowers/cas/BACKLOG.md` — the live pending list for CAS product work.
+
+State summary: all-tree part-files plan CLOSED (12/12, reviews Approved, milestone soak GREEN);
+codecs-v3 plans for phases 2/4/5/7 committed; reviewed code drafts for phases 2/4/7 parked in
+worktrees; the full stateless gate (#49) was dispatched and then killed 2 minutes in by the reboot.
+
+## 2. Standing rules (bind every step below) {#rules}
+
+- **NEVER `git push`** — under any pretext. New commits only; never amend/rebase; never commit to master.
+- Builds: `flock /tmp/cas_build.lock ninja -C > / 2>&1; echo "NINJA_EXIT=$?"`
+ — foreground, no `-j`, no `nproc`; analyze logs via a subagent (summary only). Git ops under `flock /tmp/cas_git.lock`.
+- Commit messages via file (`git commit -F`), trailer on every commit:
+ `Co-Authored-By: Claude Fable 5 ` + the session's `Claude-Session:` URL.
+- Explicit-path `git add` only (shared worktree — other sessions' dirty files must not be swept in).
+ Known foreign dirty files right now: `utils/ca-soak/scenarios/{BACKLOG.md,RUN_HISTORY.md,cards/s15_s18_shards_lifecycle.py}` — NOT ours, leave them.
+- After every milestone: 20-minute soak (phase-3 `--duration` mode, fresh cluster via `down -v`,
+ nothing heavy concurrent), then fix-or-backlog every finding.
+- Do not trust background-completion notifications — poll your own logs/artifacts on a cadence.
+- Do not trust the version banner's git hash (`GitHash.generated.cpp` regenerates at CONFIGURE time);
+ verify binary freshness by symbol greps.
+
+## 3. Immediate next step: re-run the #49 stateless gate {#gate-49}
+
+1. Verify/refresh the binary: build `clickhouse` at HEAD if not fresh, confirm `ci/tmp/clickhouse`
+ symlinks to it. Clean stale debris in `ci/tmp` from the killed run (old `test_result.txt`).
+2. Run the FULL CA-default lane (this full run is sanctioned — normally an empty selector is forbidden):
+ `python -m ci.praktika run "Stateless tests (arm_binary, content_addressed storage, parallel)"`
+ from repo root, foreground, output to `build/stateless49_full.log`; live-watch `ci/tmp/test_result.txt`.
+3. NO fixes mid-run. Afterwards, triage every failure by the `cas-test-triage` skill taxonomy:
+ (real CA bug → fix at source + regression test) / (genuinely unsupported → re-tag
+ `no-content-addressed-storage` with reason) / (env/config-sensitive → controlled CA-vs-plain repro)
+ / (flake/timeout). Known-orthogonal families that STAY tagged: projections (B5), partition-clone (B21),
+ BACKUP/RESTORE, experimental transactions, `non_replicated_deduplication_window`, replicated (B1).
+4. Watch specifically: stderr-cleanliness failures (siblings of the fixed `repointRef` WARNING class,
+ `6fb325d30e6`) and the T6/T8/T9 surfaces (`writeFile`/unlink marks/`moveDirectory`).
+5. Fix one-by-one with review; re-run the affected tests; commit per fix.
+
+## 4. Then: integrate the codecs-v3 drafts (the draft-worktree pipeline) {#codecs}
+
+Drafts live in git worktrees under `tmp/worktrees/` (plain directories — they survived the reboot).
+Each contains `DRAFT.md` (per-task summary, deviations with rationale, compile risks, commit split)
+and `DRAFT.patch` (generated with `git add -A && git diff --cached -M --binary`, so deletes/renames
+carry markers; `DRAFT.*` excluded from the patch itself).
+
+Integration contract (proven on T7/T8/T9): (1) read DRAFT.md fully — deviations need explicit
+accept/reject decisions; (2) apply `DRAFT.patch` onto mainline (`git apply --3way` is atomic;
+resolve failures by hand against the worktree, never regenerate blindly); (3) build + run the tests
+the draft names — drafts are UNBUILT, expect compile fixes and log every one; (4) commit using the
+draft's suggested split (one commit per plan task); (5) send the diff package
+(`review-package BASE HEAD` from the subagent-driven-development skill) to a reviewer; (6) fix
+findings, re-review, then the phase's exit criteria + a milestone soak.
+
+Order and gates:
+- **Phase 2** (`draft-codecs2`, 64 files) first — plan `docs/superpowers/plans/2026-07-15-cas-codecs-v3-phase2-control-plane.md`.
+ 8-commit split incl. the Step-0 `FormatSettings` pin as its own commit. Golden byte-precision is
+ compile-time-unverified — expect golden re-pins at first build (flagged in DRAFT.md).
+- **Phase 4** (`draft-codecs4`, 6-file delta) — apply ONLY after phase 2 (it is a delta on top of the
+ phase-2 patch; layering documented in its DRAFT.md).
+- **Phase 7** (`draft-codecs7`, 21 files) — mainline-independent; integrates before or after 2/4.
+- **Phase 3** (refsnaplog): parked until phase 2 is integrated AND soaked. **Phase 5** (runs): plan
+ committed (`1be66aed9f4`, scoping-spec level); its code draft needs an explicit controller decision
+ post-phase-2. **Phases 6/8**: JIT plans later (6 after 5; 8 strictly last).
+- After all integrated phases: **#48** codecs completion gate (20-min soak + fix bugs).
+
+## 5. Team topology (if resuming multi-agent) {#team}
+
+The prior session ran a controller + three teammates (all died with the reboot; nothing of theirs is
+unsaved). Roles to respawn as needed — or run single-session with ordinary subagents, which works fine:
+- **integrator** — owns mainline: applies drafts, builds, runs tests/soaks, commits. One at a time.
+- **reviewer** — verdicts strictly from pinned diffs (`git show :path`), never the live tree;
+ spec-compliance + quality verdicts; adversarial re-verification of implementer claims.
+- **planner/drafter** — writes JIT plans (gated by the controller before drafting) and code drafts in
+ fresh worktrees (`git worktree add tmp/worktrees/ HEAD`), NO builds in worktrees, no contrib.
+Known coordination hazards: message delivery between sessions is lossy (4 losses in one day) — poll
+artifacts (commits, report files, logs) instead of waiting for messages; message races are constant —
+re-sends must be idempotent; never run two agents writing the same checkout (git index races).
+
+## 6. Watchdog recreation {#watchdog}
+
+The prior watchdog was a recurring ~20-min cron (created via CronCreate; note: recurring crons expire
+after 7 days, and the reboot likely severed it). Recreate in the new session with CronCreate,
+recurring every 20 minutes, with this prompt (verbatim is fine):
+
+> UNATTENDED WATCHDOG TICK. Check the unattended work status and act ONLY if something is wrong:
+> (1) verify each running agent/build/test/soak shows fresh progress (log mtime+tail in build/ and
+> utils/ca-soak/logs/, report files in .superpowers/sdd/, commits); (2) if something shows no progress
+> artifact for >20 min with no legitimate reason, intervene: ping via SendMessage / TaskStop and
+> restart / take over inline; (3) check disk (df -h /, alert >85%), CPU starvation, docker debris;
+> no heavy builds concurrent with a running soak; (4) append one status line to the current worklog
+> (timestamp, what's running, verdict OK/INTERVENED+what). NEVER git push.
+
+## 7. Campaign queue after codecs {#queue}
+
+`#36` S36 MOVE PART/PARTITION scenario (both directions + multi-disk infra) → `#37` merge
+upload-retry systematic-debugging investigation (quality-flagged: 3 wrong takes previously; required
+behavior = merge retries the UPLOAD, never the whole merge) → `#38` scenarios 01-36 at prod scale →
+`#40` CAS unit-test ASan compatibility (incl. TSan verification of the rev.6 F3-1a race).
+Plus the BACKLOG headliner from the milestone soak: `[PART-REMOVAL-REPOINT]` (17 707 wasted repoints
+on `delete_tmp_*` refs ≈ 22% of the writer PUT class — `db28579459a`).
diff --git a/docs/superpowers/cas/01-architecture.md b/docs/superpowers/cas/01-architecture.md
new file mode 100644
index 000000000000..7a18361a40c0
--- /dev/null
+++ b/docs/superpowers/cas/01-architecture.md
@@ -0,0 +1,573 @@
+---
+description: 'What the content-addressed (CAS) MergeTree storage backend is, why it exists, its object model, pool layout, and a record of approaches tested and rejected with explanations.'
+sidebar_label: 'CAS Architecture'
+sidebar_position: 1
+slug: /superpowers/cas/architecture
+title: 'CAS MergeTree — Architecture'
+doc_type: 'reference'
+---
+
+# CAS MergeTree — Architecture {#cas-architecture}
+
+## What CAS is {#what-cas-is}
+
+Content-addressed (CAS) MergeTree is an opt-in storage backend that stores every MergeTree part as
+**content-addressed immutable objects** in a shared object-store pool. The core idea is borrowed from Git:
+
+> "Git for MergeTree" — files are blobs keyed by their content hash; parts are trees that name those blobs;
+> the only mutable state is a small set of **refs** (per-part pointers from a table's namespace to its
+> tree objects).
+
+Enabled per disk with `metadata_type = content_addressed`. A plain non-replicated `MergeTree` on such a disk
+needs no engine or DDL change. The feature coexists with zero-copy replication — see
+[REJECTED: zero-copy replication as a goal](#rejected-zero-copy-replication-as-a-goal).
+
+### Shared-nothing, not shared-state {#shared-nothing}
+
+The CAS pool is the **single source of truth**; its state is correct and self-describing at all times. Every
+coordination object (refs, build heartbeats, the GC-leader lease, fencing tokens) lives in the object store,
+and the only required mutual exclusion is a single `create-if-absent` compare-and-set per bucket. No external
+catalog, no auxiliary database, no write lock beyond the shard manifest CAS are required for correctness.
+
+This is a "shared-nothing" model — each server owns its ref namespace and writes to its own shard objects;
+the shared resource is the `blobs/` content space, addressed purely by hash. It is not "shared-state" in the
+sense of a globally-mutable shared catalog with serializable multi-writer access (that is the model used by,
+e.g., SharedMergeTree).
+
+### What it buys {#what-it-buys}
+
+- **Cross-replica dedup by reference, not byte copy.** A replicated write produces blobs on one server; every
+ other replica that "fetches" the part publishes a ref pointing at the already-present objects — zero bytes
+ moved across the network. A clone or mutation is similarly free when content is unchanged.
+- **Free carry-forward on mutation.** A `MUTATE_PART` that touches one column re-hashes only that column; the
+ 10 unchanged columns are carried forward by re-referencing their existing blob hashes from the source
+ manifest — no recompute, no re-upload.
+- **Self-describing, stale-cache-proof.** Immutable keys have infinite cache TTL by correctness; a local cache
+ never goes stale.
+- **Keeper load is flat in pool size.** Unlike zero-copy replication (whose per-part/per-blob Keeper state
+ scales with data and melts Keeper on large clusters), CAS holds only `O(active-writers)` ephemeral
+ coordination.
+
+### What it does NOT buy {#what-it-does-not-buy}
+
+Dedup of independently recomputed bytes: ClickHouse parts are not bit-reproducible across independent
+rebuilds (compression block boundaries, codec defaults, TTL `now()`). Cross-replica dedup works because
+one replica produces a part and the others *reference* it. Carry-forward applies only to **Wide** parts
+(the default threshold is 1 GiB on remote disks); **Compact** parts are a single blob and are always
+fully rewritten on mutation.
+
+---
+
+## Object model {#object-model}
+
+Four kinds of durable objects exist in the pool.
+
+### Blobs {#blobs}
+
+A blob is the content-addressed bytes of one file. Its identity `H` is a streaming, chunked `cityHash128`
+computed incrementally over the bytes as they are written (via `HashingWriteBuffer`, block size
+`DBMS_DEFAULT_HASHING_BLOCK_SIZE` = 2048), not a one-shot hash of a pre-existing buffer and not read from
+`checksums.txt`. The storage key is `blobs//`.
+
+Blobs are **pool-global and shared across servers**. The `blobs/` prefix is not rooted under any server
+identity; dedup spans replicas and tables by construction.
+
+### Ref shards (mutable) {#ref-shards}
+
+The only mutable objects. Each `(namespace, shard)` pair has one **RefShard** object at
+`cas/refs//`, where `` is an opaque namespace string composed by the caller (typically
+embedding the `server_root_id`, e.g. `srv1/`) — the key-building code (`rootShardKey` in
+`CasLayout.h`) does not hardcode a separate `` path segment itself. Its body is a JSON
+shard manifest containing:
+
+- `refs` — a map `part_name → PartManifestRef` (the current part-manifest pointer for that part; the
+ part's files — including `uuid.txt`, `txn_version.txt`, `metadata_version.txt` — are ordinary entries
+ in the pointed-to manifest, see "Part manifests (immutable)" below, not a separate per-ref sidecar).
+- `journal` — an append-only log of `+/-` ownership events; events are retained until the GC trim (gated by `gc_trim_min_events`/`gc_trim_body_soft_limit`) advances past the sealed fold cursor covering them (INV-JOURNAL-COVERAGE).
+- `incarnation` — a durable, strictly monotone pair `(writer_epoch, build_sequence)` stamped at
+ (re)creation and never changed for that object's life. Closes the ABA hazard: a fold cursor keyed by
+ `(ns, shard, incarnation)` can never silently skip a recreated shard's events.
+- `fence_round` — set by GC; a per-shard birth floor (THM-NO-RETURN) used GC-side. (The old writer-side
+ "refresh your retire view to ≥ fence_round before publishing" rule was removed in v3 — the writer now
+ point-reads each blob's `.meta` freshness state instead.)
+
+A publish is **one CAS `PUT`** updating `refs` and appending the journal record atomically. The active set
+for a table is exactly the union of refs across its shards.
+
+**Status:** DONE. `server_root_id`-rooted hot/cold split (Phase 1 of the layout redesign) is DONE. Per-shard
+`incarnation` field and cursor-key extension (D1) is also DONE as of 2026-07-03 (see
+[`#d1-registry-removal`](#d1-registry-removal)); this line previously said "TODO as of 2026-07-02", which
+is stale.
+
+### Part manifests (immutable) {#part-manifests}
+
+A PartManifest is an immutable protobuf object that records the complete file list of one part:
+
+```
+(file_name → blob_hash, size, placement) for each content-addressed file
+```
+
+Placement (`EntryPlacement`) currently has two kinds:
+- `Inline` — tiny files embedded in the manifest body (part-open cost = 1 GET).
+- `Blob` — large standalone file at `blobs/` (the common case for `.bin`, marks).
+
+A third kind, `pack_slice` — member of a pack object at `(pack_hash, abs_offset, length)` — was designed as
+a reserved placement but has no `EntryPlacement` enum value in the current code (not implemented as of
+2026-07-03).
+
+The per-part write-time files `uuid.txt`, `txn_version.txt`, and `metadata_version.txt` are ordinary
+manifest entries like any other part file (all-tree part-files migration, spec
+`2026-07-15-cas-all-tree-part-files-design.md`) — there is no separate mutable sidecar. A single-file
+change on an already-committed part (`metadata_version.txt` on `ATTACH`, `txn_version.txt` on
+commit/mutation) resolves through a committed-ref repoint: the transaction carries every unchanged
+entry forward and republishes one manifest with the changed/removed paths applied; a byte-identical
+rewrite is a zero-pool-mutation no-op (`repointRef`). Two parts sharing every file's bytes, including
+these three, still share one manifest by ordinary content-addressing; a differing byte in any of them
+— same as any other file — yields a distinct manifest.
+
+PartManifest bodies live at `cas/manifests////.proto` (as with ref
+shards, `` is the opaque namespace string that typically embeds `server_root_id`; `` is a
+6-digit zero-padded filename, e.g. `000001.proto`). The identity of a manifest is `ManifestId` = a
+`root_namespace` paired with a nested `ManifestRef` of `(writer_epoch, build_sequence, manifest_ordinal)` —
+not a type literally named `PartManifestId`. Every component is durable-monotone and never reused, so
+identity is unique by construction without relying on random-collision improbability.
+
+**Status:** DONE. Phase 3 `manifest_ordinal` reshape (replacing the earlier random `manifest_instance_id`)
+is DONE (developed on branch `cas-layout-hot-cold-split`; landed in the current CAS lineage — the layout below is what the code builds today).
+
+#### Projections in the manifest {#projections-in-manifest}
+
+A projection is **not** a separate part, ref, or sub-manifest. On disk it lives at
+`/.proj/`, and MergeTree hands the projection a **child**
+`IDataPartStorage` whose `part_dir` is `.proj`, sharing the parent part's transaction. So every
+projection file resolves to a **nested key in the parent part's single `PartManifest`**:
+`.proj/` (e.g. `p_sum.proj/data.bin`) sits in the same `blobs` map as the top-level
+`columns.txt`, `data.bin`, etc. This is the same flat-manifest-with-nested-keys generalization already
+used for detached parts (`detached//`) — **Approach A** in
+`specs/2026-06-03-cas-mergetree-projections-design.md §2/§3`. Two consequences fall out: merges rebuild
+projections (fresh projection blobs in the merged manifest, no carry-forward logic), and `part_id`
+naturally includes the projection blobs (ADD/DROP/MATERIALIZE PROJECTION yields a new part version, no
+false dedup).
+
+**Rejected alternatives** (§3): **B — nested sub-manifests** (each projection its own sub-`PartManifest`
++ `part_id` behind a `projections` footer): a format/version change and a recursive resolver, for
+identity projections never use (always read as children, rebuilt on merge) — YAGNI. **C — projections
+as a separate `projections/` ref namespace** lifecycle-linked to the parent: a second namespace + GC
+coupling for no benefit (projections are cloned/detached/dropped with their parent).
+
+### Incarnation identity (`server_root_id`, `writer_epoch`) {#incarnation-identity}
+
+The durable, per-`server_root_id` identity is built from three sticky objects under
+`gc/server-roots//`:
+
+| Object | Lifetime | Purpose |
+|--------|----------|---------|
+| `owner` | Permanent (`putIfAbsent` once, never rewritten) | Clock-free identity: binds `server_root_id` to one `server_uuid_hex` forever |
+| `epoch` | Permanent (CAS-bumped per writable open, never deleted) | Durable monotone `writer_epoch` counter; never reissued |
+| `mount` | Expirable lease | Active heartbeat / liveness; same-UUID reclaim only after expiry |
+
+`server_root_id` is a **required, validated, immutable config parameter** (not derived from `ServerUUID`).
+Every namespace, ref shard, and manifest is rooted under it. Changing it strands the old tree; the `owner`
+object makes the old `server_root_id` permanently bound to its `server_uuid_hex`, so uuid file regeneration
+fails loudly instead of silently switching roots.
+
+A superseded writer (its `writer_epoch` no longer matches the live mount) issues **no** mutable CAS/PUT —
+the write-path lease fence is a local in-memory check (no per-write S3 read) that trips the disk to
+`lost/fail-closed` on any supersession.
+
+**Status:** DONE (Phase 0 of the layout redesign; developed on branch `cas-layout-hot-cold-split`, landed in the current CAS lineage).
+
+---
+
+## Pool layout {#pool-layout}
+
+One pool = one disk root = one bucket/prefix. The layout reflects a **hot/cold split** introduced to fix the
+quadratic GC discovery that arose when manifests and ref shards lived under the same `roots/` prefix.
+
+```
+/
+ blobs/
+ / immutable blob bodies; pool-global, shared across servers
+ cas/
+ refs/
+ /
+ / RefShard objects — the only things GC hot-discovery LISTs
+ manifests/
+ /
+ ///.proto
+ PartManifest bodies — cold; cursor-paged sweep, not hot-listed
+ roots/
+ /
+ /_files/ verbatim (non-content-addressed) files: format_version.txt, etc.
+ gc/
+ server-roots/
+ /
+ owner sticky identity (server_uuid_hex); putIfAbsent once
+ epoch sticky durable-monotone writer_epoch counter
+ mount expirable heartbeat lease + build-watermark floor (see note below)
+ state GcState (lease, round, fold cursors, fence versions)
+ gen//attempt// per-round GC artifacts (fold seals, blob targets, retired sets, outcomes)
+ _pool_meta pool identity, format version, capability proof
+```
+
+The standalone `watermark` object and the `gc/checkpoint/` full-GC checkpoint path shown in earlier
+revisions of this diagram do not exist in the current code. The build-watermark floor (`min_active`) was
+merged into the `mount` object as part of the ack-floor GC redesign — there is no separate sticky
+`watermark` object anymore (not implemented as of 2026-07-03). `gc/checkpoint/` has no corresponding
+key-builder in `CasLayout.h` (not implemented as of 2026-07-03).
+
+### Why the split exists {#why-the-split}
+
+Before the hot/cold split, GC discovery ran a `LIST roots/` that returned ref shards, manifest bodies, and
+verbatim files interleaved. With `O(tens of thousands)` part manifests per namespace, each discovery round
+paged the entire backlog — `O(N²/page)` calls. After the split, GC discovery is:
+
+```
+GET gc/state + LIST cas/refs/
+```
+
+returning only ref-shard tokens — `O(total ref shards)`, independent of manifest backlog size.
+
+### Namespace mirroring and the `@cas@` boundary {#namespace-mirroring}
+
+A namespace (``) is the CAS mirror of a ClickHouse table directory. The mapping marks the
+content/verbatim boundary with a `@cas@` **suffix on the table-dir segment** (never a standalone
+segment): an Atomic table's `` maps to `store//@cas@` (`` = first 3 uuid chars,
+matching ClickHouse's `store/` fanout), a non-Atomic table's path maps to `data//@cas@`. A
+node is immutable **iff** it is content-addressed; `@cas@` is exactly that boundary. Detached parts
+(B181) fold into the same table namespace as `detached/`-prefixed refs, not a sibling namespace; no
+namespace may be a path-prefix of another (GC enumerates by prefix-LIST). Verbatim (loose) files and
+the logical-vs-physical `clickhouse-disks` view are covered in `05 §path-mapping` and
+`03 §verbatim-files`.
+
+### Key invariant: shared blobs, per-server trees {#shared-blobs-per-server-trees}
+
+`blobs/` is **not** rooted under any `server_root_id`. Content-addressed blob bodies are a global shared pool.
+Ref shards and manifests are per-`server_root_id`. Dedup spans replicas at the blob level; GC reachability
+and ownership are scoped per server to avoid cross-server locking.
+
+---
+
+## Write path overview {#write-path-overview}
+
+A MergeTree INSERT or merge produces a part locally, then publishes it to the pool:
+
+1. **Heartbeat** — a build heartbeat is durable in the pool before the first blob PUT, via the pool-wide
+ `gc/hb` pulse plus the writer's `mount` object build-watermark floor (there is no separate
+ `builds/` object in the current layout). GC uses this to distinguish live in-flight builds
+ from dead debris.
+2. **Upload by placement** — each content file is hashed (from `checksums.txt`) and placed into the pool:
+ - New content: `PUT If-None-Match:*` (idempotent; dedup hit → skip upload).
+ - Cold reuse of an existing blob: point-read the object's per-hash `.meta` freshness state
+ (`Core/CasBlobMeta.h`, `MetaState{Clean,Condemned}`) — condemned → **resurrect** with a fresh
+ `incarnation_tag`; else reuse as-is. (The `.meta` point-read replaced the old writer-side retire view.)
+3. **Precommit stage** — the build writes a precommit binding to a `/_precommits` namespace shard,
+ providing GC visibility into in-flight blobs (fold barrier holds the +1 edge until the manifest body
+ is present).
+4. **Publish** — one CAS PUT to the ref shard: updates `refs[part_name]` and appends `{+, name, T}` to
+ the journal atomically. Tokened deps are edge-protected (EDGE-BEFORE-OBSERVE) and not re-checked at
+ promote; non-tokened deps get a per-hash `.meta` point-read and copy-forward-if-condemned inline in the
+ blob-revalidation loop. (There is no writer-side retire-view refresh anymore — see `03-writer-protocol.md`.)
+5. **Drop** — symmetric: one CAS PUT removing `refs[part_name]` and appending `{-, name, T}`.
+
+A crash anywhere before the publish leaves uploaded objects as **debris** attributed to the build's
+heartbeat. GC's full-GC tier reclaims them after heartbeat expiry. Nothing dangles: no ref ever named
+the new objects before the publish CAS landed.
+
+**Status:** DONE for the core write/drop path. `pack_slice` placement is a reserved design idea, not
+encoded in the current `EntryPlacement` format (not implemented as of 2026-07-03).
+
+---
+
+## GC overview {#gc-overview}
+
+**Status (2026-07-03): stale.** The step-by-step protocol below (fold → retire → fence → recheck →
+delete) describes an earlier design. The code (`Gc` class, `CasGc.h`) now implements a **leader-paced
+ack-floor redesign** (spec 2026-07-02): one pass per round — heartbeat ack floor over
+`MountLease.observed_gc_round` → three-cursor merge fold → two-phase condemn/graduation deletion → a
+single `gc/state` CAS. The old `fence_round`/recheck terminology survives only as fossil comments/variable
+names in the current code. See `docs/superpowers/cas/04-gc-protocol.md` for the current protocol if it has
+been reconciled; otherwise treat the steps below as historical context for the no-return safety argument,
+which still holds under the new design.
+
+GC operates in two tiers that share one deletion tail.
+
+### Regular GC (historical description — see status note above) {#regular-gc}
+
+`O(delta)` — folds only journal-known nodes, never scans the full blob space:
+
+1. **Fold** — stream-merge journal `+/-` records per root shard into the snap shards (in-degree snapshot),
+ keyed by target content-hash prefix. A node whose in-degree transitions to 0 becomes a candidate.
+2. **Retire** — observe the current backend token for each candidate; append a durable retire record
+ `(kind, hash, observed_token)`. Retired ≠ dead: writers must resurrect rather than reuse.
+3. **Fence** — CAS-bump `fence_round` into every discovered ref shard. Writers with a retire view older
+ than this floor must refresh before publishing.
+4. **Recheck** — fold through the fence versions; per entry: `in-degree > 0` → spare; else
+ `DELETE If-Match token`.
+5. **Outcomes** — `deleted | absent | replaced (412 = resurrection won) | spared`. Drop retire entries on
+ confirmed outcomes.
+
+The key safety property: **no delete can ever be wrong** (the no-return argument). A publish before the
+fence is spared by the recheck (its journal record folds in). A publish after the fence saw the new
+`fence_round`, refreshed its retire view, and either resurrected the condemned object or aborted. In either
+path, the final delete finds in-degree > 0 or a different token. Exact-token deletes for an old
+incarnation can never affect a newer current incarnation — this relies on the backend enforcing
+`If-Match` (probed at startup; fail-closed if the backend silently ignores the header).
+
+### Shard incarnation and registry removal (D1) {#d1-registry-removal}
+
+**Status:** TLA+ gate GREEN (2026-07-01, `CaGcShardIncarnationCore.tla`, 724,944 states). Implementation
+DONE as of 2026-07-03 — `gc/registry` has been deleted from the code and discovery is `LIST(cas/refs/)`
+(verified by `gtest_cas_layout.cpp` and `gtest_cas_gc_ack_floor.cpp`, which assert `gc/registry` is
+absent). This section previously said "implementation TODO"; that is stale.
+
+Before D1: GC discovered namespaces from `gc/registry` and fenced the Cartesian product
+`registry × root_shards`, so per-round cost grew with every table ever created. Dropped namespaces were
+never deregistered, and their empty shard objects were never reclaimed.
+
+After D1:
+- Discovery = `LIST(cas/refs/)` — the set of `(ns, shard)` that physically exist.
+- `gc/registry` is deleted entirely.
+- A shard's `incarnation` field (stamped at creation, never mutated) closes the ABA hazard in the fold
+ cursor: a recreated shard at the same path carries a strictly greater incarnation, so an old sealed
+ cursor never silently skips its events.
+- `dropNamespace` appends a tombstone as the last journal event. GC reclaims the shard object with the
+ same exact-token `deleteExact` used for blobs, once the journal is folded past a completed fence.
+
+### Full GC {#full-gc}
+
+Rare. Finds **debris** (objects no journal ever knew — crashed builds) and repairs **drift** (snap
+diverging from truth). Debris candidates are range-read for their `build_id`, then checked against the
+writer's heartbeat (the `gc/hb` pulse plus the `mount` object's build-watermark; there is no separate
+`builds/` object) via GC-observed monotone `heartbeat_seq` change — no writer clocks. Both tiers
+share the same deletion tail (condemn → graduation → exact-token delete under the current ack-floor
+design; historically described as `retire → fence → recheck → deleteExact`, see status note above).
+
+---
+
+## Table-engine integration decisions {#integration-decisions}
+
+These are the MergeTree-integration decisions from the v3 shared design
+(`specs/content_addressed_shared_mergetree_design.md`) and the incarnation spec
+(`incarnation-tagged-cas.md`). They govern how SQL-level operations map onto the CAS object model.
+
+### DROP PART / DROP PARTITION supersession {#drop-supersession}
+
+A SQL `DROP PART`/`DROP PARTITION` is modeled as **removal supersession** under the uniform
+"commit = create ref(s)" rule (additive commit → a real part ref; removal → an **empty covering
+tombstone ref**). `MergeTreeData::dropPartImpl` already constructs a local Outdated empty covering
+part today; the CAS design **promotes that exact part into a persisted, Active, covering ref**: a
+data-less (zero-blob) tombstone manifest whose `MergeTreePartInfo` covers the dropped range, using
+the existing `ActiveDataPartSet`/`getActiveContainingPart` covering machinery to mark covered parts
+inactive. This is a persisted **part ref**, not a separate object type — **distinct from the D1
+shard-object tombstone** (`04 §d1-registry-removal`), which is a journal marker that lets GC reclaim
+an empty shard object.
+
+**DROP-PART mid-range race with a concurrent merge (documented weak guarantee):** a concurrent merge
+may produce a real covering part that carries the data, so `DROP PART` "deletes nothing and the part
+is merged into a bigger part" — the deliberately weak guarantee ClickHouse already documents
+(`ReplicatedMergeTreeLogEntryData::getVirtualPartNames` returns `{}` for `DROP_PART`). The persisted
+tombstone for a mid-range single part is therefore **conditional/reconciled, not unconditional**: if
+a real covering part wins the race the data legitimately survives under a new name, and the tombstone
+must not strand or double-cover.
+
+### FREEZE materializes real bytes {#freeze-materializes-bytes}
+
+`FREEZE` MUST materialize real bytes into the local `shadow/` directory — it **cannot** be
+reference-only. `freeze`/`freezeRemote`/`clonePart` are independent `IDataPartStorage` virtuals (not
+built on `createHardLinkFrom`), and `shadow/`'s documented contract is *filesystem-readable hardlinks
+consumed by external backup pipelines*. A reference-only FREEZE (emitting only a ref-set object)
+would silently break every filesystem-level backup. So FREEZE on this engine GETs/copies the
+referenced blobs into `shadow/` as ordinary files. A self-describing `snapshots/` ref-set may
+exist as an *additional* in-pool reachability root for restore-by-reference, but it does not replace
+the byte materialization.
+
+### Benign cross-replica divergence {#benign-cross-replica-divergence}
+
+Single-producer / non-divergence is an **optimization, not a correctness requirement** (the v3
+downgrade from v2's proposed `/merge_claims` lease + `manifest_hash` re-keying). The commit-time
+cross-replica check `checkPartChecksumsAndCommit → checkEqual(..., check_uncompressed_hash_in_compressed_files=true)`
+runs in **tolerant mode**: it compares the uncompressed hash of the compressed files, so two replicas
+with identical uncompressed content but different compressed bytes pass. In the content-addressed
+model that yields at worst **two manifests + two blob sets for one part name** — a benign P1
+dedup-MISS, not a correctness bug (reachability, refcounts, and query results are all correct, and
+both blob sets are GC'd when the part is later superseded; the duplicate lives only for the part's
+lifetime). A homogeneous cluster with deterministic compression produces identical bytes → identical
+blob hash → dedup with no check at all. This rationale settled the historical `manifest_hash`-on-znode
+idea (B1): it would have been a dedup optimization, not a safety fix — and it was **rejected outright**
+(2026-07-14) on layering grounds. Replication code stays disk-agnostic: CA adds **no field** to the
+Keeper part header, because a CA-specific field in `ReplicatedMergeTreePartHeader` would couple
+`ReplicatedMergeTree` to one disk implementation, grow the fork's upstream-contact surface, and add
+states to already-complex replication. Fetch-by-relink needs no Keeper field either — the manifest id
+travels in-band over the interserver handshake. If cross-replica manifest divergence ever needs
+*observability*, that is a pool-side fsck/`ca-inspect` concern (group live refs by part name across
+server namespaces), not a replication-protocol change.
+
+### Same-pool relink uses `pool_uuid`, not endpoint matching {#pool-uuid-relink}
+
+Fetch-by-relink (the CAS analogue of zero-copy replication: a replica publishes a ref to
+already-present shared blobs instead of downloading bytes) is only correct when the two replicas share
+one pool. Same-pool detection uses a **stable minted `pool_uuid`**, not endpoint+prefix string
+matching. `_pool_meta` carries `owner_server_id` + `claimed_at_unix` **and** a `pool_id` minted at
+first pool claim; `ContentAddressedMetadataStorage::getPoolUUID` exposes it (defined inline in
+`ContentAddressedMetadataStorage.h`; the field is assigned in `ContentAddressedMetadataStorage.cpp` via
+`pool_uuid = Cas::u128ToHex(cas_store->poolMeta().pool_id)`). The
+part-exchange handshake advertises `content_addressed_pool_uuid`, and the sender relinks **only** when
+the receiver's advertised `pool_uuid` equals its own (`DataPartsExchange.cpp:232`); anything else falls
+back to the byte fetch (correct on CAS — downloaded files content-address and dedup).
+
+Endpoint+prefix string matching was **rejected as unsafe**: DNS aliases, path-vs-vhost S3
+addressing, and trailing-slash normalization give **false negatives** (a needless byte fetch), while a
+shared proxy endpoint fronting two different buckets gives a **false positive** → a relink to blobs
+that are not in the receiver's pool → a dangling ref. `pool_uuid` fails closed on both. Adding it was a
+`_pool_meta` `CURRENT_VERSION` bump that fails closed on older readers. Source:
+`specs/2026-06-04-cas-mergetree-replication-design.md §3/§11`.
+
+### Logical-hash-collision → quarantine (fail-closed) {#hash-collision-quarantine}
+
+The one writer-side path that fails closed is a detected logical-hash collision or corruption. In
+`store_blob_safely`, when a writer reuses an existing logical-hash key it verifies the existing
+object's header hashes to the expected logical hash (`verify(existing_hdr.logical_hash == H)`); if it
+does not, the system **quarantines the key and raises a hard error** rather than silently papering
+over the mismatch. Silent reuse of a mis-verifying object would let corrupt or colliding content be
+served under a live ref.
+
+---
+
+## Approaches tested and REJECTED {#rejected-approaches}
+
+### REJECTED: Merkle `treeId` tree layer {#rejected-merkle-tree-layer}
+
+**What it was:** Content-addressed `trees/` objects forming a Merkle DAG of immutable folders, with tree
+hashes carrying `child_gen` inside the tree identity. Part identity was a tree hash `T`.
+
+**Why rejected:** `child_gen` inside tree identity forced ancestor rebuilds whenever a blob's generation
+floor moved (a GC reclaim at any child propagated upward through the whole tree chain). The `404→LIST`
+degraded read path for generation resolution added latency and complexity. The generation-in-key model
+required durable per-hash floors, epochs, per-build pins, a quiescence machinery, and Keeper as a required
+component.
+
+**What replaced it:** The incarnation-token design (spec `2026-06-10-ca-incarnation-store-design.md`). Part
+identity becomes a `ManifestId` (monotone composite, not a content hash); blobs remain one-key-per-hash;
+incarnation lives in the object body (not the key), so resurrecting a blob produces a distinct backend token
+without changing any parent's hash or key. The `404→LIST` path is gone.
+
+**Note:** The `trees/` key prefix and `ObjectKind::Tree` were removed entirely (2026-07-03) — not merely
+dead code: `ObjectKind` now has only `Blob`, and no `trees/` literal remains. Trees are manifest-internal.
+
+### REJECTED: EBR (Epoch-Based Reclamation) GC core {#rejected-ebr-gc}
+
+**What it was:** Writers hold a "pin epoch" (an ephemeral Keeper node). GC advances through epochs
+(`safe_epoch`, `commit_epoch`, `birth_epoch`), waiting for quiescence — all writers must advance past a
+GC epoch before reclamation can proceed. Generations on blob keys (`blobs//`) were the ABA guard.
+
+**Why rejected:** A stuck or wedged writer held down `safe_epoch` and stalled reclamation of **all** dropped
+parts across all writers — not just its own debris. Keeper was required for correctness. The quiescence
+requirement meant a long-running INSERT could delay GC for its entire duration.
+
+**What replaced it:** The incarnation-token design. Regular GC candidates are restricted to
+**journal-known** nodes (fresh uploads are structurally invisible — they have no journal record yet, so they
+can never be candidates). The publish gate against the fenced retire view is the writer-side safety;
+heartbeat-gated debris reclaim in full GC handles crashes. A wedged writer cannot stall regular reclamation
+at all — it has no pin and no `safe_epoch` to hold down.
+
+### REJECTED: Integer refcount (in-place mutable counter) {#rejected-integer-refcount}
+
+**What it was:** A per-blob mutable refcount stored in object metadata or a side object, incremented on
+reference and decremented on release.
+
+**Why rejected:** Mutable object metadata is write-once-per-upload on S3 (requires a copy to change).
+An in-place counter would require a round-trip CAS per write and per drop, adding per-object Keeper or S3
+conditional-write load proportional to data volume. More fundamentally, distributed decrement on concurrent
+writers and GC is a classic problem: a missed decrement leaks; a premature decrement dangles.
+
+**What replaced it:** The **source-edge set**: GC maintains an in-degree snapshot by folding the append-only
+`+/-` journal records from each ref shard. In-degree is never stored; it is computed from edges. The fold
+is idempotent (set semantics); losing or duplicating a record delays reclamation but never accelerates a
+delete.
+
+### REJECTED: Namespace registry as GC discovery authority {#rejected-namespace-registry}
+
+**What it was:** `gc/registry` was the authoritative list of all known namespaces. A writer's first publish
+into a namespace CAS-appended it to the registry. GC fenced `registry × root_shards` every round, minting
+fence-only manifests for absent shards to order first-publish against the fence.
+
+**Why rejected:** Three problems confirmed in code:
+1. `dropNamespace` never deregistered — the registry grew monotonically with every table ever created.
+2. GC fence cost was proportional to `(namespaces ever created) × root_shards`, not live namespaces.
+3. Empty shard objects were never reclaimed.
+
+The registry's one irreducible role — ordering a first publish's precommit `+1` against the fence — is
+subsumed by the existing precommit machinery combined with the `incarnation` field's strictly-increasing
+guarantee and strongly-consistent `LIST`. The registry is therefore removed entirely, not patched.
+
+**What replaced it:** D1 (shard incarnation + registry removal, spec
+`2026-07-01-cas-shard-incarnation-and-registry-removal-design.md`). TLA+ gate GREEN.
+
+### REJECTED: zero-copy replication as a goal to REPLACE {#rejected-zero-copy-replication-as-a-goal}
+
+Zero-copy replication is **not removed**. It is retained for backward compatibility. CAS is opt-in per disk
+(`metadata_type = content_addressed`) and coexists with zero-copy. The two mechanisms are mutually exclusive
+per disk and do not interfere.
+
+CAS does replace zero-copy replication's *function* (sharing part content without byte copies) with a more
+principled mechanism: content-addressed blobs rather than per-part Keeper refcount locks. But the
+replacement is opt-in, not a migration, and existing zero-copy tables and disks are unchanged.
+
+### REJECTED: deriving `server_root_id` from `ServerUUID` {#rejected-serveruuid-layout}
+
+**What it was:** The pool namespace root was derived from `ServerUUID` (the ClickHouse server identity uuid
+file). Layout identity was implicit.
+
+**Why rejected:** If the local uuid file is regenerated (lost), the namespace root silently switches to a
+new path, stranding the old tree. Nothing prevents two live servers from sharing the same pool path and
+writing to the same namespace tree. There was no explicit ownership marker.
+
+**What replaced it:** `server_root_id` — a required, validated, immutable config parameter. A single sticky
+`owner` object (`gc/server-roots//owner`) binds the `server_root_id` to one
+`server_uuid_hex` forever via `putIfAbsent`. uuid regeneration fails loudly at disk start instead of
+silently switching roots.
+
+---
+
+## Key invariants (summary) {#key-invariants}
+
+| Invariant | Statement |
+|-----------|-----------|
+| `INV-NO-DANGLE` | A live ref's transitive closure resolves through present keys. A ref naming an absent key is a surfaced storage exception, never a silent empty read. |
+| `INV-NO-LOSS` | A physical delete requires: durable retire entry; all-root-shard fence at recorded versions; fold-through-fence recheck showing in-degree 0; exact observed token. |
+| `INV-NO-RETURN` | Once incarnation `(kind, hash, token_old)` is retired and passes the post-fence zero-reachability recheck, that exact token can never again be a valid dependency of any publish. The logical key may return — only as a different token, which a stale delete for `token_old` cannot affect. |
+| `INV-OVER-COUNT-ONLY` | Every failure (lost fold, crashed leader, stale snap, duplicated records) delays reclamation; none accelerates a delete past the gates. |
+| `INV-S3-COMPLETE` | S3 alone is both the durable truth and sufficient coordination. Keeper is an accelerator. Total Keeper loss loses nothing and blocks nothing. |
+| `INC-MONO` | For a fixed `(ns, shard)`, every successive materialization of the shard object carries a strictly greater incarnation. ABA is closed by construction. |
+| `WriterEpochMonotoneUnique` | The `epoch`-object allocator never reissues a `writer_epoch` for a `server_root_id`, across crash/restart and across `mount` deletion. |
+
+The core invariants (`INV-NO-LOSS`, `INV-NO-DANGLE`, `INV-NO-RETURN`, `INV-JOURNAL-COVERAGE`,
+`MonotoneGC`) are model-checked by `CaIncarnationCore.tla`. The mount-safety invariants
+(`WriterEpochMonotoneUnique`, `SupersededWriterMakesNoMutation`) are model-checked by a focused Phase-0
+TLA+ model. `INV-NO-DANGLING` and `INV-NO-ORPHAN-EDGE` under D1 (registry removal + shard incarnation) are
+model-checked by `CaGcShardIncarnationCore.tla` (724,944 states, GREEN as of 2026-07-01).
+
+---
+
+## Backend contract {#backend-contract}
+
+Safety depends on the backend enforcing **exact-token conditional delete**: a `DELETE If-Match token` with
+a wrong token MUST fail (not silently succeed). This is probed at startup; fail-closed if the backend does
+not enforce it.
+
+| Backend | Token | v1 status |
+|---------|-------|-----------|
+| AWS S3 (versioning off) | `ETag` | Fully conforming, validated live 2026-07-03 (probe-gated at startup). `DeleteObject If-Match` GA Sep 2025. |
+| RustFS 1.0.0-beta.8 | `ETag` | Empirically verified 2026-06-11; leading open-source CI candidate. Known issue `rustfs#3231` (overwrite-leak of objects above the store's inline threshold, e.g. RustFS 128 KiB, in unversioned buckets) can produce metacache LIST storms and false 404s on HEAD — the only source of GC fold clamps observed to date. |
+| Azure Blob | `ETag` (write-sensitive) | Supported, probe-gated. Versioning/soft-delete must be off. |
+| GCS | `generation` | Binding specified; implementation deferred; fail-closed until probed. |
+| MinIO OSS (archived 2026-02) | — | `If-Match` on DELETE silently ignored — confirmed 2026-06-11. **Fail-closed.** |
+| Ceph RGW | — | No conditional delete documented. **Fail-closed.** |
+
+Bucket versioning must never be enabled on the CA prefix. Versioning-suspended buckets are also rejected
+(a delete inserts a null delete marker; the one-live-object model breaks). A startup probe verifies this
+functionally.
+
+**Status:** Probe logic DONE. GCS generation binding TODO. Versioned mode (token = `versionId`) is a future,
+explicitly separate feature.
diff --git a/docs/superpowers/cas/02-methodology.md b/docs/superpowers/cas/02-methodology.md
new file mode 100644
index 000000000000..d84efbf06d4c
--- /dev/null
+++ b/docs/superpowers/cas/02-methodology.md
@@ -0,0 +1,366 @@
+---
+description: 'The disciplined engineering methodology that drove CAS MergeTree architecture decisions: TDD, subagent-driven development, TLA+ as a pre-implementation gate, scenario/soak suite as an empirical oracle, and systematic debugging — with the concrete pivots each produced.'
+sidebar_label: 'CAS Methodology'
+sidebar_position: 2
+slug: /superpowers/cas/methodology
+title: 'CAS MergeTree — Engineering Methodology'
+doc_type: 'guide'
+---
+
+# CAS MergeTree — Engineering Methodology {#cas-methodology}
+
+**Status:** DONE (captures practice as of 2026-07-02).
+
+This document explains *how* the CAS MergeTree architecture was developed and why the big pivots happened.
+The *what* is in `01-architecture.md`; the protocols are in `03-writer-protocol.md` and `04-gc-protocol.md`.
+Sources: night logs 2026-06-04 and 2026-06-05, `UNATTENDED-WORKLOG-2026-06-18-b140-dangle.md`,
+`cas-unattended-work-log-2026-06-24.md`, `cas-gc-unattended-execution-log.md`, design reviews
+`reports/2026-06-07-ca-spec-review-milovidov*.md`, `reports/2026-06-07-ca-spec-review-distributed*.md`,
+and the model currency audit `models/MODEL_CURRENCY_REVIEW_2026-06-22.md`.
+(NOTE: most of these source documents were DELETED in the 2026-07-02 docs consolidation — their
+content is folded into this doc set; see `CONSOLIDATION-COVERAGE.md` for the mapping. The citations
+above are kept as the historical provenance record, not as live links.)
+
+---
+
+## 1. Overview {#overview}
+
+Four interlocking practices governed every phase of development:
+
+1. **Test-driven development (TDD)** — failing tests written before code at every task.
+2. **Subagent-driven development** — independent implementer → spec-review → quality-review subagents per
+ task; no single-threaded editing.
+3. **TLA+ as a pre-implementation gate** — no code task in a behavior-changing phase begins until the
+ relevant TLA+ model is green; the gate is not advisory.
+4. **Scenario/soak suite as an empirical oracle** — a 24-hour deterministic chaos soak (two replicas,
+ shared CA pool, seeded workload + seeded fault injection, quiesced checkpoints with `clickhouse-disks
+ ca-fsck` + `ca-gc-dryrun`) as the runtime invariant check.
+
+The design reviews (`reports/2026-06-07-ca-spec-review-milovidov*.md` and
+`reports/2026-06-07-ca-spec-review-distributed*.md`) played a fifth role: external adversarial scrutiny
+of specifications before TLA+ encoding.
+
+---
+
+## 2. Test-driven development {#tdd}
+
+**Practice.** Every implementation task begins with a failing gtest that exercises the property the task must
+establish. The test is committed first (RED state). The task is complete only when all new tests pass and the
+existing suite is green. A task-level quality review (separate subagent, fresh context) checks that the
+tests are non-trivial (not just passing vacuously) and that the implementation's invariants are preserved.
+
+**Why this mattered operationally.** The gtest suite caught multiple real bugs during the 2026-06-04 night
+run:
+
+- `ContentAddressedWriteBuffer` had no `cancelImpl` — on the insert-cancel path its inner hashing/temp-file
+ buffers were destroyed "neither finalized nor canceled". The test `02435_rollback_cancelled_queries` made
+ this deterministic; the bug had been masked by nondeterminism before.
+- The TOCTOU race in `LocalObjectStorage::listObjects`/`tryGetObjectMetadata` (concurrent sidecar deletion
+ between dir-listing and per-entry stat) was revealed by the multi-part transaction test batch; the fix is
+ universally correct (a real object store snapshots its listing).
+- In the 2026-06-18 B140 fix, a code review subagent independently caught that `Build::precommit` emitted
+ build-root shards keyed by `build_seq` beyond the table shard fan-out, so `shardsToVisit` had to be
+ extended to LIST them. The failing test (T7, `CasBuildRootDangle.SharedBlobSurvivesSourceDrop`) made the
+ gap visible before any integration test.
+
+**Oracles as the highest-value tests.** The most durable tests are *safety oracles* — tests that set up a
+dangerous interleaving and assert an invariant holds. Examples: `CasBuildRootDangle.SharedBlobSurvivesSourceDrop`
+(build-root protects a blob from GC mid-build); `PrematureReclaimCommitFailsClosed` (precommit reclaimed
+mid-build → commit ABORTs, never dangles); the S33 scenario card in the soak suite (concurrent GC leader
+scenario that exposes the concurrent-leader-leak, expected-FAIL until the fix lands).
+
+---
+
+## 3. Subagent-driven development {#subagent-driven}
+
+**Practice.** Each non-trivial implementation task is handed to a fresh implementer subagent with a
+self-contained brief (spec, plan, exact file paths, failing-test list, build/test commands). On completion,
+two independent review subagents assess it: a spec-compliance review and a code-quality review. The
+orchestrating agent synthesizes the findings, decides on changes-needed vs. approved, and dispatches a
+fixer if needed.
+
+**What this bought.** The pattern maintains independent scrutiny throughout. The 2026-06-27 Phase 1a review
+(`cas-gc-unattended-execution-log.md`) illustrates the value: the code review subagent found a real
+**blocker** — `RunFileReader::loadFooter` parsed untrusted footer length fields before verifying the footer
+CRC, giving heap over-read / UB under hardening on corrupt input. The implementer subagent had produced
+correct semantics but missed a security boundary. The review prevented the flaw from becoming load-bearing
+before it was caught.
+
+**Ground-truth gathering agents.** Before writing an implementation plan, parallel read-only exploration
+agents extract verbatim signatures, line numbers, and build constraints from the relevant source files. This
+eliminates placeholder-filled plans (plans that say "call the appropriate function") and makes the
+subagent's brief self-contained.
+
+**Simplification passes.** Phase B of the 2026-06-05 night run ran three `/simplify` rounds (GC cluster
+simplification, architecture reorg, codec-deserializer clarification). Each produced a review-approved
+commit. The architecture reorg (Phase B round 2) extracted `RefPayload`, `ObjectIO`, `GcLayout`, and
+`ContentAddressedWriteBuffers` into cohesive files; `ContentAddressedTransaction.cpp` shrank from 2505 to
+2243 lines. Simplification is not cosmetic — co-located related entities reduce the surface where
+invariants can be violated.
+
+---
+
+## 4. TLA+ as a pre-implementation gate {#tla-gate}
+
+**Practice.** Every phase that changes deletion semantics, the GC round protocol, or a safety invariant
+must have a TLA+ model that is GREEN before any code task begins. "GREEN" means: every liveness/safety
+stage HOLDS; every negative-control (`_sab_*`) VIOLATES its named invariant; no `UNEXPECTED PASS`. A
+failure to reproduce the named counterexample in the negative control is as bad as a safety violation —
+it means the model does not cover the case.
+
+This is a hard gate, not a documentation exercise. The gate discipline is explicit in the
+`cas-gc-unattended-execution-log.md`: *"no code task in a phase begins until that phase's TLA+ suite is
+GREEN."*
+
+### 4.1 Counterexample: the incarnation redesign {#tla-incarnation}
+
+**Before (EBR design, 2026-06-07):** the GC used an epoch-based reclamation (EBR) core. The generation was
+encoded in the **key** (`blobs//`), requiring a `404→LIST` degraded read path, durable per-hash
+floors, `child_gen` inside tree identity (ancestor rebuilds when a floor moved), and Keeper required for
+`safe_epoch` fencing.
+
+**What the reviews found (2026-06-07).** Two parallel adversarial reviews (`reports/2026-06-07-ca-spec-review-milovidov.md`
+and `reports/2026-06-07-ca-spec-review-distributed.md`) independently found the D6 write-ahead-intent
+mechanism (per-commit per-file persistent Keeper writes to track orphan builds) was unsafe and
+over-engineered:
+
+- The distributed-systems review: the D6 "in-degree==0 guard *alone*" was over-stated. In the
+ `decide→+`-not-yet-durable window the fold legitimately reads 0; the real protection is `safe_epoch > e_a`
+ plus the post-`+` resurrect-on-condemned step. An implementer reading "no rescue, in-degree alone" might
+ reclaim the orphan directly in R2, skipping the gate → loss. Separately, the intent key
+ `leases//` collided across writers building the same content — owner attribution broke.
+- The simplicity review (virtual Milovidov): D6 put `O(files)` **persistent** Keeper writes on every
+ commit — re-introducing the data-proportional Keeper traffic that made zero-copy replication painful.
+ *"CUT it; crash debris goes to the periodic Retention-guarded sweep."*
+
+D6 was cut entirely. Crash orphans move to a periodic condemn-not-delete reconcile. More fundamentally, the
+2026-06-10 incarnation redesign (`specs/2026-06-10-ca-incarnation-store-design.md`) moved the resurrection
+marker **into the object body** and deletion precision **into the backend token**, eliminating the generation
+in any key. The `404→LIST` path disappeared. A stuck writer can no longer stall pool-wide reclamation —
+there is no `safe_epoch` for it to pin.
+
+**The TLA+ gate here:** `CaIncarnationCore.tla` (the canonical core model) carries four invariants —
+`INV-NO-DANGLE-COMMITTED`, `INV-BUILDROOT-PROTECTS`, `INV-BUILDROOT-RECLAIM`, `INV-COMMIT-FAILCLOSED` —
+and model-checks them exhaustively before the C++ implementation follows the new design.
+
+### 4.2 Counterexample: source-edge set replacing integer refcount {#tla-source-edge}
+
+**Before:** the GC tracked blob liveness via an integer in-degree counter incremented/decremented per
+commit. The 2026-06-07 distributed review found a precise race: in the `decide→+`-not-yet-durable window
+the fold legitimately reads in-degree 0 for a live blob. If the guard was "in-degree==0 alone" the blob
+would be reclaimed, producing a dangle. The fix was not to patch the counter but to replace the model: the
+GC log records **source edges** (`+` and `-` deltas keyed by `(kind, hash, source_manifest)`) rather than
+bare counters. A fold computes the net in-degree from the edge multiset; the `BlobInDegreeMatchesActiveManifests`
+TLA+ invariant asserts the edge multiset is exactly the set of active manifests referencing the blob. With
+this structure, the `+`-not-yet-durable problem becomes a pending-fold problem rather than a guard problem:
+the fold barrier withholds any `+` until the body is confirmed present, closing the window.
+
+The TLA+ encoding of the edge multiset vs. bare integer is what made the invariant expressible and
+checkable at all. The model guided the implementation of `GcDelta` (coalesced `I1`/`I6` `+`-before-ref
+under lock, per-shard split, `event_id` dedup-on-fold) and the `foldDeltasIntoGeneration` function.
+
+### 4.3 Counterexample: the registry-removal two-coordinate proof {#tla-registry}
+
+**Before:** the GC discovered namespaces from a persistent `gc/registry`; the fence step CAS-bumped
+`fence_round` into every shard of every registered namespace — minting fence-only manifests for absent
+shards. Per-round cost was `O(namespaces-ever-created × root_shards)`. The registry grew monotonically
+because `dropNamespace` never deregistered.
+
+**Why obvious fixes were rejected.** The design spec `specs/2026-07-01-cas-shard-incarnation-and-registry-removal-design.md`
+enumerates the rejected alternatives:
+
+- Writer deregisters at `dropNamespace`: unsafe because the removal events (the `-1` in-degree edges) are
+ the only carrier of blob reachability updates; if the namespace vanishes from discovery before GC folds
+ that window past the fence, the `-1`s are lost → permanent blob leak.
+- GC hard-deregisters when "empty + settled": an empty-but-live namespace (table with no inserts) is
+ indistinguishable from a dropped one by emptiness alone. Inferring "empty ⇒ retire" would deregister a
+ live table between `CREATE` and the first `INSERT`.
+- Both of the above plus shard-object deletion on the current path-keyed cursor: deleting and later
+ recreating an object at the stable path `cas/refs//` is an ABA hazard. The fold cursor is
+ keyed by path and filters journal events by `transition_version`; a recreated shard resets
+ `shard_version` to 0 and an old sealed cursor silently skips the new incarnation's events → lost edges →
+ dangle or leak.
+
+**What the TLA+ model proved.** `CaGcShardIncarnationCore.tla` encodes two orthogonal coordinates:
+`incarnation` (a durable, monotone, never-reused tag stamped into the ref-shard object at creation) and GC
+`round` (the pool-global clock). The model checks `INV_NO_DANGLING` and `INV_NO_ORPHAN_EDGE` across
+724,944 distinct states. The three negative controls each break the invariant they target:
+`SabotageNewbornNoFloor` (round self-floor is irreducible), `SabotagePathKeyedCursor` (incarnation is
+irreducible — ABA), `SabotageDeleteBeforeFold` (fold-before-reclaim ordering).
+
+**The one-vs-two question.** The TLA+ run answered directly: two coordinates are necessary. Neither the
+incarnation alone nor the GC round alone is sufficient — the model shows a counterexample for each
+one-coordinate variant, and the two-coordinate design is clean. This answered a design question that had
+been deferred because the alternatives looked superficially viable.
+
+With the theorem proven, the registry is deleted entirely (`gc/registry`, `RootsRegistry`, `CasRootsRegistry`,
+`ensureRegistered`, `registered_cache`, the registry-fence sub-step). Discovery migrates to `LIST(cas/refs/)`.
+
+### 4.4 The B140-dangle TLA+ gate {#tla-b140}
+
+The B140 dangle (a blob GC-deleted while still referenced by a committed part) was pinned live in the soak
+via `system.content_addressed_log` (B170). The approved fix was a build-root/precommit redesign: protection
+becomes *reachability from a durable build root*, not a revocable per-blob hint. The fragile
+`cas_owner`/`protectedByLiveBuild` per-candidate machinery was scheduled for deletion.
+
+The TLA+ model (`models/CaBuildRootPrecommit.tla`) used a 2×2 flag matrix (BuildRoot ∧ FailClosedCommit):
+
+- Buggy (no build-root alone): `INV_NO_DANGLE_COMMITTED` violated in 481 states — the counterexample
+ matches the soak dangle exactly (write→adopt→BuildDie→GcDelete→Commit).
+- Fixed (build-root ∧ fail-closed): clean exhaustive 24,205 states.
+- **Sharp finding:** build-root *alone* still dangles (ordering window); fail-closed *alone* is clean only
+ vacuously. Both halves are independently necessary and jointly sufficient.
+
+The model validated the design before a line of C++ was written. The C++ implementation followed the proof
+(`UNATTENDED-WORKLOG-2026-06-18-b140-dangle.md`, T4→T10).
+
+### 4.5 Attempt-scoped generations and the retired-set hazard {#tla-attempt}
+
+The concurrent-leader leak (soak scenario S33) involved deposed GC leaders leaving stale retired-set
+artifacts visible to writers. An initial deviation from the spec proposed attempt-scoping only the four
+`gc/gen//…` write-once artifacts and leaving `retired`/`outcomes` under existing `(round, fence_seq)`
+keys, calling stale debris "bounded sweep, minor."
+
+The user review rejected this: `retired` is a **writer-facing publish-gate input** — `RetireView::refresh`
+LISTs the whole `gcRetiredPrefix()` and writers consult it on the commit path. A stale retired set written
+by a deposed leader under its own `(round, fence_seq)` survives in that LIST and **influences live writers**
+— directly violating *"no unadopted artifact may ever influence a decision."* There was also no existing
+sweep for stale `gc/retired/`+`gc/outcomes/` artifacts.
+
+The spec was followed exactly: all decision-bearing round artifacts (including `retired` and `outcomes`)
+move under `gc/gen//attempt//…`. This also simplifies cleanup — the `gc/gen//` retention prune
+reclaims retired/outcomes automatically, no separate sweep.
+
+---
+
+## 5. Scenario/soak suite as an empirical oracle {#soak-oracle}
+
+**Structure.** The soak harness (`utils/ca-soak/`) runs two `ReplicatedMergeTree` replicas sharing one CA
+pool on RustFS (or a conformant S3 backend). A seeded, deterministic Python ledger drives insert /
+merge-pressure / mutation / TTL-delete / truncate workers. A seeded chaos injector fires container
+kill/restart/pause faults at scheduled windows. At quiesced checkpoints (writers paused, replication
+queues drained, merges idle, GC at a fixpoint), the harness asserts:
+
+- SQL results match an independent model oracle on **both** replicas.
+- `clickhouse-disks ca-fsck` reports `dangling=0` (INV-NO-LOSS: every reachable object exists).
+- `ca-gc-dryrun` cross-checks: GC would delete only genuinely-unreachable objects.
+- `unreachable=N (M-F debris)` is a known-acceptable residual (abandoned builds — Full GC M-F, deferred).
+
+The `ProfileEvent` budgets (e.g. S3 operation counts, `S3WriteRequestsErrors`, `gate_revalidate`,
+`gate_resurrect`) provide a quantitative check that the protocol is operating within expected bounds.
+
+**What the soak revealed that tests did not.** Several bugs were first manifested in soak runs, not in
+gtests:
+
+- **B140 dangle (2026-06-18):** `reuseBlob` (the adopt operation) transferred the blob hash but not the
+ `cas_owner` protection, so the adopted blob's protection remained bound to the retired byte-writer. GC
+ reclaimed it under a still-in-flight adopter. The soak's `system.content_addressed_log` (B170 event
+ log) pinned the precise dangle event with its token and timing.
+- **Group-A resurrection-cap exhaustion (2026-06-05):** in an aggressive test GC cadence, the fixed
+ iteration cap of 8 in `resolveAndResurrectGeneration` was exceeded, producing `CORRUPTED_DATA` failures
+ in ordinary insert/mutation tests. The cap-8 choice was justified by the original design's generation
+ bound but not by the test's GC rate. The soak-like full-suite run surfaced 33 failures from this
+ single root cause; the fix (cap 8→256) was a one-line change.
+- **rustfs 412 vs. data loss (2026-06-20):** the soak reported 1.6M `S3WriteRequestsErrors` and 94
+ `fsck dangling` after a chaos window, appearing to be a Phase-6 regression. Forensic analysis of the
+ event log showed all 922,772 GC deletes had `indeg_at_recheck=0` and zero deleted-while-referenced.
+ The 1.6M errors were ~1.57M `PreconditionFailed` (412) — by-design CAS conditional-write contention on
+ root-shard manifests, not storage failures. The 94 dangling was a transient read-unavailability
+ false-positive (429/503 load shedding during chaos); the 180s settle budget was too short under the
+ write load. The soak distinguished Phase-6-is-correct from harness-needs-improvement.
+
+**Harness fragility as its own finding.** The 2026-06-19 VFS soak run needed four restarts to reach the
+chaos stage, each exposing a harness limitation rather than a product bug: TTL-band checkpoint
+over-strictness (fixed with bounded wait-out + re-quiesce), merge-aware quiescence (a 620s large-merge
+over S3 was misidentified as a hang), setup DROP timeout under accumulated data (fixed with fresh cluster
+on each run), and compose startup ordering. Fixing harness fragilities is itself a design output —
+the harness's behavior under load directly shapes what the soak can prove.
+
+**`ca-fsck`/`ca-gc-dryrun` as an independent read path.** The `ca-fsck` command opens the CA disk in a
+**read-only mode** that does not require the server to be running. It recomputes the full reachability
+graph independently (traversing roots → shard manifests → refs → trees → blobs), compares against the
+physical pool content, and reports `dangling` / `orphan` / `unreachable` counts. The `ca-gc-dryrun`
+previews what the next GC round would delete without executing the deletions. Together they constitute an
+independent check on both INV-NO-LOSS and GC-deletes-only-unreachable — the two safety properties the
+protocol is built around.
+
+---
+
+## 6. Systematic debugging {#systematic-debugging}
+
+**Root-cause-first.** Every incident follows a read-only diagnosis phase before any fix is written.
+The diagnosis names the exact failing invariant, the exact code path, and the exact interleaving that
+triggers it. Fixes never pre-empt a diagnosis.
+
+**Same-class defect hunts.** After a class of bug is fixed, a read-only hunt searches for structurally
+equivalent sites in the codebase. The 2026-06-05 hardening pass (H1–H5) illustrates: after the B85
+read-path generation-resolution fix and the B87 `moveFile`-on-attach-rollback fix, five same-class
+sites were found:
+
+- H1: `ContentAddressedMetadataStorage::getLastModified` called `resolvePartGenKeyForRead` (unchecked)
+ at four sites — a stale `active` hint could 404 and wedge `clearOldTemporaryDirectories` (DROP).
+- H2/H3: two sites threw `LOGICAL_ERROR` for benign concurrent-DROP/DETACH races where
+ `FILE_DOESNT_EXIST` (recoverable, matching plain-disk semantics) was correct.
+- H4: `replaceFile` lacked the `is_part_dir` directory-delegation that `moveFile` had just received —
+ defense-in-depth for a future regression.
+- H5: `ContentAddressedGC::listLivePartIds` did a LIST-then-`readSmallObject` without swallowing a
+ concurrently-dropped ref — an aborted GC round under DROP churn.
+
+The same-class hunt is disciplined: it uses grep + code reading with explicit scope (CA path only; non-CA
+sites noted but out of scope). Findings are triaged by severity (server-abort vs. recoverable vs.
+defense-in-depth) and fixed in a single reviewed commit before the next integration test.
+
+**Confirming "not a regression" before fixing.** The 2026-06-19 VFS path-mapping run found 68 unique
+failures in the full suite. Before writing any fix, a baseline determination ran the two failing test
+families (`transactions`, `text-index`) against the pre-refactor binary on the same host. Both families
+failed identically on the baseline — they were pre-existing CA gaps (B182, B183), not regressions. The
+refactor was declared regression-clean without any code change.
+
+---
+
+## 7. Design review as methodology {#design-review}
+
+Both the simplicity/performance review (virtual A. Milovidov persona) and the distributed-systems
+adversarial review (Lamport happens-before analysis) were run against the 2026-06-07 Merkle-store spec
+**before any TLA+ encoding and before any implementation**.
+
+The reviews produced five findings that changed the architecture:
+
+1. **D6 cut entirely** (both reviews independently): write-ahead lease intents re-introduced
+ data-proportional Keeper traffic. Removed; crash orphans moved to periodic condemn-not-delete reconcile.
+2. **Intent key collision** (distributed review): `leases//` collided across writers building
+ identical content. Moot after D6 was cut, but confirmed D6 was irreparable as specified.
+3. **In-degree-alone over-stated** (distributed review): the precise reclaim window where the fold
+ reads 0 for a live blob. Led directly to the source-edge multiset design.
+4. **`O(1)` memory for reconcile** (simplicity review, second pass): the §4.5 streaming merge-sort
+ (LIST ⋈ sorted stream, never materializing reachability) was adopted as the only framing that does not
+ OOM at 10¹¹ objects. The review gave explicit criteria: "one condemner, not two writers."
+5. **Keeper epoch cache: sole-writer rule must be normative** (distributed review): the cache is correct
+ only if nothing else ever writes the epoch znode; the condition must be a stated invariant (§3.2, §6.2)
+ with an assertion in the code, not just prose.
+
+The model currency audit (`models/MODEL_CURRENCY_REVIEW_2026-06-22.md`) performed a retrospective check:
+after several months of development, do the TLA+ models still correspond to the code? The audit found no
+old-design cruft (no EBR-epoch / `resurrect-by-GET` / `cas_owner` / `reuseBlob` / seal-TTL) in any live
+model. The real drift was around the B171 precommit-reachability mechanism (which replaced per-candidate
+watermark guards) and three models that were now stale against the shipped code — `CaBuildWatermark.tla`,
+`CaBuildWatermarkNum.tla`, and `CaResurrectLiveness.tla`. These were not old-design garbage but modeled a
+protection path the code had either removed or never shipped. The audit documented the dispositions (mark
+stale; repurpose the watermark floor lemma for precommit-ref reclaim liveness) rather than deleting the
+models, preserving the proof history.
+
+---
+
+## 8. Per-practice status summary {#status-summary}
+
+| Practice | Status | Notes |
+|---|---|---|
+| TDD | **DONE** — enforced throughout | Gtest sweep is the merge gate for every behavior-changing commit |
+| Subagent-driven development | **DONE** — enforced throughout | Two-review (spec + quality) pattern per task |
+| TLA+ gate (core models) | **DONE** — `CaIncarnationCore`, `CaBuildRootPrecommit`, `CaGcLeaseCore`, `CaGcRootLocalPartManifestCore`, `CaGcShardIncarnationCore` | See `06-tla-models.md` for full index |
+| TLA+ gate (D1 registry removal) | **DONE** — `CaGcShardIncarnationCore` green (724,944 states, two-coordinate proof) | Implementation landed 2026-07-01/02 (`gc/registry`/`RootsRegistry` deleted, discovery via `LIST(cas/refs/)`, shard incarnation stamped); status note updated 2026-07-03 |
+| Scenario suite | **DONE** (S01–S35 cards) | S33 concurrent-leader scenario now PASSES as a real regression guard (attempt-scoped generation landed 2026-06-28); see `08-testing-and-soak.md §5.1` |
+| 24h deterministic soak | **DESIRABLE** — blocked on conformant backend | rustfs 412 vs. write-error confound needs harness-side fsck retry; currently 4-hour runs |
+| `ca-fsck`/`ca-gc-dryrun` | **DONE** — shipped, used in every soak checkpoint | |
+| Model currency review | **DONE** — 2026-06-22 audit; dispositions recorded | Three stale models identified; `CaGcCore.tla` HISTORICAL |
+| Full GC (M-F) | **TODO** — deferred; soak's `unreachable=N` residual is this | Spec slot reserved in `CaIncarnationCore.tla` |
diff --git a/docs/superpowers/cas/03-writer-protocol.md b/docs/superpowers/cas/03-writer-protocol.md
new file mode 100644
index 000000000000..7934b6c6d13b
--- /dev/null
+++ b/docs/superpowers/cas/03-writer-protocol.md
@@ -0,0 +1,795 @@
+---
+description: 'The CAS MergeTree writer protocol: mount startup, writer_epoch/build_sequence identity, the build→precommit→upload→promote write path, mutable vs immutable files, renames, the fold barrier, and resource requirements.'
+sidebar_label: 'CAS Writer Protocol'
+sidebar_position: 3
+slug: /superpowers/cas/writer-protocol
+title: 'CAS MergeTree — Writer Protocol'
+doc_type: 'reference'
+---
+
+# CAS MergeTree — Writer Protocol {#cas-writer-protocol}
+
+This document covers the end-to-end write path for the content-addressed (CAS) MergeTree disk layer.
+It is part of the consolidated CAS documentation set in `docs/superpowers/cas/`.
+For the pool layout and object model see `01-architecture.md`; for the S3 op-count budget see `07-s3-budget.md`
+(cross-referenced inline); for GC see `04-gc-protocol.md`.
+
+Status stamps in each section: **DONE** = implemented + soak-validated, **TODO** = known gap, **REJECTED** = tried and dropped (with reason), **DESIRABLE** = not yet planned but clearly useful.
+
+---
+
+## Mount startup {#mount-startup}
+
+**Status: DONE** (Phase 0, `CasStore.cpp`, `CasServerRoot.cpp`).
+
+`Store::open` runs a strict ordered startup protocol before any ordinary data write is permitted.
+All these operations are "bootstrap-control" writes that establish the right to write; they run
+before the write fence gates ordinary mutations.
+
+### `server_root_id` and `server_id` {#server-root-id}
+
+Every `Store` is parameterized by:
+
+- `server_root_id` — a relative path string that identifies the writer's namespace inside the pool
+ (e.g. `srv1/uuid`). Two ClickHouse nodes must never share the same `server_root_id` on the same
+ pool; the mount-lease enforces this at runtime.
+- `server_id` (`UInt128`) — the server's UUID, loaded from the server UUID file. It is the
+ persistent identity anchor; a new process start with the same UUID is the same logical server.
+
+### Startup sequence {#startup-sequence}
+
+Steps run in strict order; failure at any step aborts the open:
+
+1. **Validate `server_root_id`** — syntactic check (clean relative path, no `..`).
+
+2. **Owner anchor — identity** (`claimOwnerOrThrow`, `CasServerRoot.cpp`).
+ The pool key `gc/server-roots//owner` carries an `OwnerObject{server_uuid}` encoded as
+ protobuf. Rules:
+ - Present + matching UUID → ok (idempotent restart).
+ - Present + foreign UUID → `CORRUPTED_DATA`: a different server owns this `server_root_id`.
+ - Absent + non-empty data subtree → `CORRUPTED_DATA`: the identity was lost over existing data;
+ re-claiming is forbidden.
+ - Absent + empty subtree → `putIfAbsent` the owner object (fresh claim, handles a concurrent
+ race by re-reading and comparing).
+
+3. **Durable-monotone `writer_epoch`** (`allocateWriterEpoch`, `CasServerRoot.cpp`).
+ The pool key `gc/server-roots//epoch` holds a `ServerEpoch{next_writer_epoch}`. On each
+ open, the current value is CAS-bumped and the current value is returned as the epoch for this
+ process. This is a strictly-increasing counter: even a crash and restart allocates a fresh,
+ strictly-higher epoch. The allocated value replaces the random `process_epoch` mint.
+ - A missing `epoch` over a non-empty subtree → `CORRUPTED_DATA` (reset hazard).
+ - Fresh empty root: first epoch handed out is 1 (0 is reserved as the "no epoch" sentinel;
+ `UINT64_MAX` is the retired sentinel).
+
+4. **Mount lease — liveness + merged heartbeat** (`claimMountAwaitingExpiry` wrapping `claimMount`,
+ `CasServerRoot.cpp`).
+ The pool key `gc/server-roots//mount` holds a
+ `MountLease{server_uuid, writer_epoch, hostname, pid, started_at_ms, seq, expires_at_ms, min_active, gc_fenced}`
+ (the `observed_gc_round` field was removed in v3 — the writer-side retired-view ack floor is gone; see §merged-heartbeat).
+ - Foreign UUID → `ForeignOwner`, open aborts.
+ - Same UUID + same epoch → adopt (idempotent restart, e.g. `seq+1` refresh).
+ - Same UUID + different epoch + lease live → `Store::open` bounds-waits (up to the lease TTL plus a
+ poll margin) for the stale lease to lapse, then reclaims it (S13 crash-recovery self-remount); only
+ if a genuinely live second incarnation keeps renewing through that whole wait does open abort with
+ `LiveDoubleStart`.
+ - Same UUID + different epoch + lease already expired → reclaim with a fresh body immediately.
+ The `MountLeaseKeeper` background thread renews the lease every `mount_renew_period` and on
+ failure latches the local write fence (`tripMountLost`). The **local write fence** (checked by
+ `mayMutate` before every `mutateShard`) enforces that a superseded writer does not race the
+ live one without any S3 read. A GC fence-out (`gc_fenced`, `04 §heartbeat-floor`) lands as a
+ foreign-touch on the renewal and trips `tripMountLost` permanently for this incarnation.
+
+ The **build watermark is carried in this same body** — there is no separate watermark object.
+ `min_active` is the orphan-sweep floor (`04 §manifest-cleanup`). (The old `observed_gc_round` GC-ack
+ floor is gone — the writer no longer advertises an ack; condemned-detection is a per-hash `.meta`
+ point-read. See §merged-heartbeat below.)
+
+---
+
+### Merged heartbeat: lease + watermark {#merged-heartbeat}
+
+**Status: DONE** (`cas-gc-ack-floor-fence`, `CasStore.cpp`, `CasServerRoot.cpp`).
+
+The standalone per-server watermark object and its `WatermarkKeeper` are **removed**: the build
+watermark folded into the mount lease, so one `SingleWriterSlot` subclass and one PUT per renewal
+carry the lease extension and `min_active` together (net **−1 PUT** per renewal versus two
+heartbeats). `min_active` (the orphan-sweep floor, `04 §manifest-cleanup`) and `gc_fenced` (a
+terminal GC fence-out of an expired lease) are the two fields the lease body carries besides plain
+liveness (`server_uuid`, `writer_epoch`, `hostname`, `pid`, `seq`, `expires_at_ms`) — this liveness
+and watermark role is unrelated to condemned-detection and unaffected by the point-read protocol
+below.
+
+**Condemned detection is a per-hash `.meta` point-read, not a writer-side retired view** (spec
+`2026-07-09` §meta-protocols v3, `Core/CasBlobMeta.h`). The earlier `Store::syncRetiredView` /
+`observed_gc_round` / `view_gate` machinery — a writer-side cached copy of the GC retired list,
+refreshed by a dedicated poller thread on its own cadence and drained against every in-flight
+`mutateShard` before being swapped in — is **removed**. `MountLease` (`CasServerRoot.h`) no longer
+has an `observed_gc_round` field at all.
+
+Each blob/tree hash instead has a small durable meta descriptor:
+
+```cpp
+enum class MetaState : uint8_t { Clean = 0, Condemned = 1 };
+struct BlobMeta { uint8_t version; MetaState state; uint64_t condemn_round; uint64_t size; };
+```
+
+`MetaState::Clean` means referenceable, body present; `MetaState::Condemned` means GC has marked the
+hash in-degree 0 (the body may still be physically present — a writer may resurrect it by CAS). GC
+always writes a `Condemned` meta BEFORE it ever deletes the body, so an absent meta reads exactly as
+live as a `Clean` one.
+
+The writer consults this per-hash meta with a single GET (`loadMeta`) at the two places that decide
+whether an observed incarnation may be adopted or must be displaced — `Build::observeAndAdmit`
+(dedup-hit / adopt path) and `Build::uploadFromSource` (fresh-upload / condemned-displacement path),
+both in `CasBuild.cpp`. A `Clean` (or absent) meta is adopted for free, no bytes moved; a `Condemned`
+meta throws `ABORTED` so the caller re-uploads from its own source (INV-1) rather than ever reading
+the dying object. `putMetaIfAbsent` seeds a `Clean` meta the first time a hash is observed with none
+yet (a Conflict just means a racing writer already created the same steady state); `casMeta`
+token-flips a stale `Condemned` meta back to `Clean` once a resurrect or copy-forward has displaced
+the body with a fresh, verified incarnation.
+
+This is a **point-read, not a linearization point**: the body's own `incarnation_tag` plus the
+exact-token delete remain the real safety core (INV-1, INV-NO-RETURN). The `.meta` descriptor is
+only a freshness hint that lets the common dedup-hit path skip straight to "adopt" without ever
+reading the body, instead of the writer having to trust a possibly-stale cached view.
+
+---
+
+## Writer identity: `writer_epoch` and `build_sequence` {#writer-identity}
+
+**Status: DONE** (`CasStore.cpp`, `CasBuild.cpp`, `CasRootShardCodec.h`).
+
+Every write carries a three-level identity:
+
+- **`server_id`** (`UInt128`) — which server owns the pool namespace. Persistent across restarts.
+- **`writer_epoch`** (`uint64_t`) — allocated durably at each `Store::open` from the pool's sticky
+ `epoch` counter. Strictly monotone, never reused. GC compares epochs for equality: a stale-epoch
+ object belongs to a dead incarnation and is immediately condemnable. Masked to 52 bits in the
+ watermark codec to stay within the JSON 2^53 integer interop bound (still collision-safe for
+ equality-only use).
+- **`build_seq`** (`uint64_t`) — allocated from a strictly-increasing in-memory counter per
+ `(server, epoch)`. The `Store` maintains an `active_build_seqs` set; `minActive` is the minimum
+ over in-flight builds. Strict monotonicity is load-bearing: `min_active` is a scalar floor and a
+ non-monotone sequence would allow the floor to drop, re-protecting a finished build's condemned
+ objects (a space-leak bug confirmed by TLA+ `CaBuildWatermarkNum`).
+
+These three values identify every `Build` and every `ShardIncarnation` (the `(writer_epoch, build_sequence)` pair stamped on newborn root shards). Together they implement the **no-manifest-id-reuse** property: `ManifestRef{epoch, build_seq, manifest_ordinal}` is globally unique by construction.
+
+The `build_id` (`UInt128`, random) is a separate per-build random identifier used in precommit
+bindings and event log attribution. It is not related to epoch/build_seq monotonicity.
+
+---
+
+## Write path: build → precommit → upload → promote {#write-path}
+
+**Status: DONE** (`ContentAddressedTransaction.cpp`, `CasBuild.cpp`, `CasStore.cpp`).
+
+A part write goes through four logical phases:
+
+```
+writeFile() × N → precommitAdd() → putBlob() × N → promote()
+ (spill+hash) (intent in shard) (upload to pool) (commit binding)
+```
+
+### Phase 1: spill and hash (`writeFile`) {#phase-write-file}
+
+The `ContentAddressedWriteBuffer` (defined in `ContentAddressedWriteBuffers.h`) spills each file's
+bytes to a unique local temp file under the scratch directory (`local_scratch_path`) while computing
+the `cityHash128` hash via `HashingWriteBuffer`. On `finalize`, the buffer hands `(hash_hex, size, temp_path)`
+to the transaction via an `OnFinalized` callback. The temp file remains on local storage until the
+upload phase (it is the re-readable source for `Build::putBlob`). Each file results in one
+`PendingBlob{hash, temp_path, size}` record.
+
+**Scratch directory**: the disk-local path configured via `local_scratch_path` on
+`ContentAddressedMetadataStorage`. It must be on a filesystem large enough to hold one full part's
+worth of staged blobs at once (one part, not the whole table). Temp files are deleted after upload.
+
+**Mutable files** (`txn_version.txt`, `metadata_version.txt`, `uuid.txt`) are separated from
+content blobs: they are written to a per-ref sidecar (`RefSidecar`) rather than the part manifest.
+See §Mutable vs immutable files below.
+
+### Phase 2: precommit (`precommitAdd`) {#phase-precommit}
+
+Before uploading any blob to the pool, the build publishes its **intent** into the root shard.
+
+`Build::stageManifest` constructs and uploads the part manifest body to
+`cas/manifests////.proto` (a `putIfAbsentStream`, no
+preliminary HEAD). It mints the `ManifestRef{epoch, build_seq, ordinal}`. The manifest body
+encodes the full closure of blob hashes the part will reference.
+
+`Build::precommitAdd` appends a `create-precommit` `RootOwnerEvent` to the target root shard via
+one `mutateShard` CAS:
+
+```
+{old_binding = none,
+ new_binding = {Precommit, final_ref_name, build_id, manifest_ref}}
+```
+
+This event — once folded by GC — gives every blob in the manifest's closure a GC-understood
+reference (in-degree ≥ 1). GC structurally cannot condemn a blob reachable from a live precommit.
+**No body HEAD is performed before the precommit CAS** — a missing body is a legal non-activating
+intent (the fold barrier, §below, handles it).
+
+The precommit stamping also carries the birth `ShardIncarnation{writer_epoch, build_seq}` for
+newborn shards (the shard did not exist before this call); this is used by GC's
+create-ordering fence (`fence_round`).
+
+**S3 budget**: 1 `putIfAbsentStream` (manifest body) + 1 `casPut` (shard CAS, which is a GET +
+CAS-PUT loop; the loop runs at most `MAX_CAS_ATTEMPTS = 100` times but in practice once). See
+`07-s3-budget.md` for the full precommit budget.
+
+### Phase 3: upload blobs (`putBlob`) {#phase-upload}
+
+After the precommit is durable, the transaction uploads each `PendingBlob` by calling
+`Build::putBlob(id, source)`:
+
+```
+BlobSource::write_payload → re-reads the staged temp file (never materializes whole blob in RAM)
+uploadFromSource → putIfAbsentStream (streaming) → Done (fresh upload)
+ → 412 → HEAD to check condemned/live
+ → live: observeAndAdmit (free)
+ → condemned: putOverwrite (re-stream from source)
+```
+
+The `BlobSource` is re-invokable: `write_payload` re-reads the local temp file on each attempt.
+This is the load-bearing invariant: on the condemned-displacement path (`putOverwrite`) the body
+is materialized into memory only once (the rare case), not on the common path.
+
+**INV-1 (revival-from-source)**: a condemned or vanished object is NEVER read/GET-ed to revive it.
+Revival = a fresh re-upload from the writer's own source bytes. `Build::resurrect` (which did a
+GET-from-existing) was deleted; `uploadFromSource` is the sole sourced revival primitive.
+
+**INV-1 exception — manifest-trust adoption (opt §4, 2026-07-14; supersedes the earlier copy-forward
+design of `2026-07-02-cas-copy-forward-condemned-evidence.md`):**
+a **tokenless W-EVIDENCE dep** (`adoptEvidence` — every call site adopts entries of a COMMITTED
+source manifest: `republishRef` part moves, the fetch receiver, part/file copies) names a blob that
+is referenced by a live committed owner right now — resolving it is a reference transfer, not a
+resurrection of garbage. `adoptEvidence` stamps `DepEntry.adopted = true` on exactly these entries
+(`CasBuild.cpp`). At promote, `Build::promote` no longer reads/copies/re-wraps that blob: it TRUSTS
+the durable manifest edge for an `adopted && !tokened` leaf (`isTrustedAdopt`), emitting a
+`reason="manifest-trust"` adoption event (`CasBlobAdoptTrusted`) with NO per-file `HEAD`/`GET`. The
+trust is sound because the source pins the blob (a live committed owner ⇒ in-degree ≥ 1 ⇒ GC, the
+sole deleter, cannot condemn it) AND this build's own precommit edge is durable and RE-PROVED LIVE at
+the promote owner-liveness gate BEFORE the trust fires (EDGE-BEFORE-OBSERVE; the deleted per-file
+occupancy probe is what the lever removes). `copyForwardFromCondemned` / `isCopyForwardableTokenless`
+are DELETED. The trade-off (D4 relink trust model = ordinary ReplicatedMergeTree interserver trust):
+a genuinely-absent adopted blob is no longer caught at promote — it becomes an **fsck dangling
+finding** (`CasFsck` reachable-but-absent → `report.dangling`) instead of a promote `ABORTED`. This
+scenario is unreachable in production (source-pin + durable-edge), verified by an independent
+EDGE-BEFORE-OBSERVE consult + per-test reachability proof (`opt-task-5-report.md`). A **non-adopted /
+pending / cross-algo leaf is NOT trusted** — it fails closed `LOGICAL_ERROR` (dep-set fail-close, no
+probe). `StrictValidate`/fsck are unchanged. Also incidentally fixes the S13 soak-run-3 liveness
+brick (`republishRef -> promote ABORTED (condemned)` left the table readonly forever): the adopt now
+trusts instead of aborting.
+
+**Condemned-detection commit gate (recreate a retired incarnation, do not adopt it):** a writer
+checks each observed blob incarnation against its per-hash `.meta` descriptor — a single `loadMeta`
+GET, §merged-heartbeat — rather than any in-memory retired list. A `Condemned` meta is a **condemned
+incarnation and is never referenced**. This recreate is performed by the existing `Build::putBlob`
+cold-reuse rule — the `PreconditionFailed`-then-condemned branch in `uploadFromSource` re-uploads
+from source with a fresh token (INV-1), on the *retried* build; it is not a new gate code path. At
+`promote`, this same point-read gate re-runs only for the non-tokened (evidence/copy-forward)
+dependency subset (see the blob-leaf revalidation step, §phase-promote below); a **tokened dep is
+edge-protected (EDGE-BEFORE-OBSERVE) and gets zero promote-time re-validation** — the common
+`putBlob` case. Unknown/condemned non-tokened leaves fail closed `ABORTED`; copy-forwardable
+tokenless-evidence deps take the copy-forward path above. This closes the condemned-adoption gap: a
+plain `putIfAbsent` HEAD-hit would otherwise adopt the condemned incarnation that a GC round is about
+to delete. The write path gains **zero** extra S3 operations in the common (not-condemned) case
+beyond the one `.meta` GET.
+
+**Shard-mutation queue (flat combining, spec `2026-07-03-cas-shard-mutation-queue.md`):**
+`Store::mutateShard` serializes intra-server writers per `(namespace, shard)` through a
+leader-caller group commit: callers enqueue `(scope, closure)`; the caller finding the queue
+leaderless FLUSHES batches (one read → apply-all in order with per-closure snapshot isolation →
+ONE `casPut`, `shard_version` bumped per closure so `transition_version`s stay dense) until its own
+item completes, then passes the baton. The carve runs AFTER the flush's first read, so the S3
+read latency IS the batching window — a slower pool makes batches larger (the old positive
+feedback loop of conflicts under load is now negative). Scope rule: at most ONE mutation per ref
+name per flush (per-ref durable histories stay bit-identical to the unbatched protocol; `precommit
+→ promote` of one part can never co-batch anyway — promote awaits the precommit flush, INV-2);
+`WholeShard` closures (trim, fence, dropNamespace, reclaim) flush SOLO; create-if-absent flushes
+solo so birth stamps apply exactly as before. Failure semantics: a throwing closure rolls back only
+its own edits and fails alone; the hard limit degrades a batch to solo re-flushes so exactly the
+offender gets `LIMIT_EXCEEDED`; a CAS conflict (cross-writer only now — e.g. the GC leader's trim
+from another replica) replays the carved batch. Bounded by construction: every queued item is a
+blocked caller thread.
+
+**Dedup** (HEAD-before-PUT, `CasBlobHeadFirst`): if the dedup cache signals the blob is present, or
+the blob is large (≥ `dedup_head_first_min_bytes`), a HEAD is issued first. A present HEAD →
+`observeAndAdmit` (free, no body upload). A stale/absent HEAD → falls through to `uploadFromSource`.
+
+**Envelope header**: every blob carries a fixed-length `EnvelopeHeader` prefix (padded to
+`blob_header_len` bytes) encoding `{kind, hash_algo, logical_size, logical_hash, domain_id, incarnation_tag, build_id, provenance, intended_ref}`.
+The `incarnation_tag` is a fresh random `UInt128` minted per upload (W-FRESH-TAG). The
+`build_id` links this object to the live build for watermark-based GC protection.
+
+**Multipart for large blobs**: large blobs use `putIfAbsentStream`, which internally uses S3
+multipart upload when the blob exceeds the SDK's single-PUT threshold. The streaming path does not
+materialize the whole blob; only the rare condemned-displacement path (`putOverwrite`) materializes
+the body in RAM. **TODO** (B172): move blob staging to an S3 staging area + server-side copy so
+the network upload is done only once even on the condemned-displacement path (currently blobs are
+re-read from the local temp file for each retry).
+
+### Phase 4: promote (`promote`) {#phase-promote}
+
+Once all blobs are uploaded, `Build::promote` performs the **fail-closed commit**:
+
+1. Read and validate the manifest body (one streaming GET; absent or mismatched → `ABORTED`,
+ never commits a dangle).
+2. Check `RefMatchesBody` and `ManifestNamespaceMatches`.
+3. Verify the precommit binding is still the live owner (not removed by abandon or GC reclaim).
+4. **Blob-leaf revalidation**, non-tokened deps only: a tokened dep is edge-protected
+ (EDGE-BEFORE-OBSERVE — its precommit closure was durable before `putBlob` ever observed it, so a
+ condemnation in the `putBlob`→`promote` window cannot graduate) and is **not** re-checked here.
+ Every non-tokened leaf (a tokenless W-EVIDENCE adopt) gets one mandatory `.meta` point-read:
+ absent/`Clean` → validated; `Condemned` + copy-forwardable → verified copy-forward (§phase-upload
+ above); `Condemned` + not copy-forwardable, or absent from the pool → fail closed `ABORTED`.
+ **No writer-side view-refresh runs before this step** (spec
+ `2026-07-09-cas-writer-gc-simplification` D5, TLA+ Gate A): promote-time view freshness is not
+ load-bearing given the edge protection above. `fence_round` survives only as a **GC-side** birth
+ floor stamped once at shard creation (`THM-NO-RETURN`); there is no writer-side refresh of it.
+5. Append a `promote` `RootOwnerEvent` to the shard — a pure owner MOVE, no blob delta:
+ ```
+ {old_binding = {Precommit, final_ref_name, build_id, manifest_ref},
+ new_binding = {Committed, final_ref_name, manifest_ref}}
+ ```
+ The `RootRef` entry (in `root.refs`) carries `published_at_ms`; a part's own files (including
+ `uuid.txt`/`txn_version.txt`/`metadata_version.txt`) are ordinary entries in the manifest this
+ ref points to, not a separate per-ref payload (`01 §part-manifests`).
+6. Clear `precommitted = false`. The precommit binding is gone; the committed binding now carries
+ the object's in-degree.
+
+The promote CAS is one `mutateShard` call. If the precommit binding was already removed (GC
+reclaimed an abandoned build, or a concurrent abandon), `promote` throws `ABORTED` and the caller
+retries the whole part write from scratch. This is the fail-closed invariant:
+
+> **INV-COMMIT-FAILCLOSED**: a committed ref is published iff the full manifest closure is verified
+> present at the promote CAS. An abandoned/reclaimed precommit → abort and retry, never a dangle.
+
+### Publish-gate obligations from the incarnation model {#publish-gate}
+
+The `CaIncarnationCore.tla` model imposes three obligations on the dependency re-validation. Since
+EDGE-BEFORE-OBSERVE, these apply **only to the non-tokened (evidence/copy-forward) dependency
+subset** — the blob-leaf revalidation, step 4 above. A **tokened** dep (the common `putBlob` case) is
+edge-protected and gets **zero** promote-time re-validation against any of MR-1/MR-2/MR-3: its
+precommit closure was durable before `putBlob` observed it, which already discharges these
+obligations at observe time (see `06-tla-models.md §caincarnationcore` for the model detail):
+
+- **Consult durable deleted-token history, not only a cached retired set (MR-1).** A non-tokened
+ dependency's hash is condemned if its `.meta` reads `Condemned` **or** it appears in the durable
+ deleted-token history: a physically-deleted token whose condemned meta was already consumed by the
+ delete must still be rejected. The `.meta` point-read alone is insufficient by itself for a
+ fully-deleted object; the copy-forward gate's fail-closed absent-object handling covers this case.
+- **Re-validate the dependency's CURRENT physical state (MR-2 / F1).** The gate must confirm the
+ dependency is still present and still carries the originally-observed token — not merely that the
+ observed token was not condemned when first seen. Any displacement (a resurrect/copy-forward that
+ minted a fresh `incarnation_tag`, or a GC delete) makes the old token stale; referencing it would
+ dangle. This is why the copy-forward path re-reads and re-verifies the object at promote time
+ rather than reusing a previously-observed token, and why `putBlob`'s condemned-displacement path
+ re-uploads from source rather than reusing the old token.
+- **Bottom-up tree publish (MR-3 / F2).** A tree/manifest that references child objects may be
+ published only when every direct child is present and non-condemned at publish time. Publishing
+ over an absent or condemned child dangles as soon as GC folds the tree's edges. The writer builds
+ and commits children before their parents.
+
+**S3 budget**: 1 GET (manifest body) + 1 `casPut` (shard CAS). See `07-s3-budget.md`.
+
+---
+
+## Mutable vs immutable files {#mutable-vs-immutable}
+
+**Status: DONE** (M5.3, `ContentAddressedTransaction.cpp`, `ContentAddressedMetadataStorage.cpp`).
+
+Part files split into two classes:
+
+| Class | Files | Storage | Dedup |
+|---|---|---|---|
+| **Immutable content** | column data, indexes, `checksums.txt`, etc. | Content-addressed blob (`blobs/`) + part manifest | Yes (shared across parts with identical content) |
+| **Mutable per-ref** | `txn_version.txt`, `metadata_version.txt`, `uuid.txt` | Per-ref sidecar object next to the ref | No (private copy per ref) |
+
+The mutable files are excluded from the `ManifestRef` identity computation (`computePartId`
+excludes them by name) so two logically-different parts with identical column content map to the
+same manifest. The mutable files are stored inline in a versioned `RefSidecar` object
+(`store///refs/.meta`) and read by overlaying the sidecar on top of the
+manifest-resolved files. `removeDirectory` deletes the sidecar with the ref.
+
+**Rename / `republishRef`**: renaming a part (e.g. the tmp→final rename in
+`ContentAddressedTransaction::moveDirectory`) publishes the **final** ref directly at the lock-free
+rename (`DiskObjectStorageTransaction::moveDirectory` dispatches eagerly for CA, not inside the
+under-lock `commitTransaction`). This was the fix for B151 (manifest publish held the exclusive
+`data_parts` lock for seconds under S3 throttling). The `published` flag on the staging prevents
+`commit()` from re-publishing an already-published ref.
+
+**REJECTED — publish at precommit (tmp-then-final double-write)**: considered but rejected because
+it would add a second manifest PUT per part (op-count regression). The eager `moveDirectory`
+dispatch achieves the same lock-free property with one PUT.
+
+This section covers only the **storage substrate** for the mutable per-ref files. The writer-side
+MVCC transaction machinery that *drives* rewrites of these files (the transaction gate, the
+mutable-only commit branch, `replaceFile` routing, rollback, and the multi-part disk transaction) is
+`§transactions-mvcc` below.
+
+### Verbatim (non-content-addressed) files {#verbatim-files}
+
+**Status: DONE** (`CasLayout.h`, `CasStore.h`; sources `specs/2026-06-19-ca-vfs-contract.md`,
+`specs/2026-06-19-ca-vfs-path-mapping-design.md`).
+
+A "verbatim" file is a loose, non-content-addressed file mirrored at its ClickHouse path — no
+manifest, no journal, no dedup. There are exactly **two** verbatim-file locations:
+
+1. **Loose in the mountpoint** — `roots//` via `mountpointObjectKey` /
+ `putMountpointObject`/`getMountpointObject`/`removeMountpointObject`. Examples: the startup write
+ probe, and anything written outside a `@cas@` archive. **GC never scans these** (GC deletes only
+ content and folds only registered namespaces); they are owned by their path and removed only by
+ `removeMountpointObject`.
+2. **Inside a `@cas@` archive** — `…@cas@/_files/` via `namespaceFileKey`, e.g.
+ `format_version.txt`.
+
+The earlier `_disk` magic namespace has been **eliminated**: loose files are plain mountpoint
+objects, not entries under a special `_disk`/`genericNamespace`. See `05 §path-mapping` for the full
+CH-path → CAS-namespace mapping and the logical-vs-physical view contract.
+
+---
+
+## The fold barrier {#fold-barrier}
+
+**Status: DONE** (`CasBuild.cpp`, `CasGc.cpp`).
+
+A create-precommit event (`new_binding = {Precommit, ...}`) is a **non-activating** reservation in
+the GC fold: it records the intent and protects the referenced objects from condemnation, but it
+does NOT contribute a `+1` to the blobs' in-degree until the manifest body is present and expanded.
+The fold barrier is the mechanism that delays in-degree activation until the body exists:
+
+- **Absent body during precommit fold** → pending-tolerance: do not `markExpanded`, record no
+ partial edges, skip. The objects are not yet protected by in-degree, but they are protected by the
+ precommit owner binding (GC will not delete an object with `InDeg = 0` if a live precommit holds
+ it — the watermark guard backs this).
+- **Body present** → normal `walk(manifest_tree, ...)` expansion: `addRootEdge` + per-child
+ `addTreeEdge`/`addPackEdge` + `markExpanded`. This is the activating `+1`.
+
+The promote (owner move precommit → committed) emits NO blob delta because the activating `+1`
+was already contributed by the create-precommit fold once the body was present.
+
+**Inline closure on the precommit journal `Add` record** (B199-S2, `CasRootShardCodec.h`): to
+close the "displaced-before-expansion" leak (GC reclaims a staged tree before the precommit is
+folded), the `RootOwnerEvent.new_binding` carries an inline closure of the build's staged tree
+entries. During the precommit fold, staged nodes are sourced inline (no I/O); adopted nodes fall
+back to `readTree`. This prevents the S2 space-leak class by construction. **TODO**: TLA+ gate for
+B199-S2 before full implementation (model the inline-closure expansion + abandon/reclaim path).
+
+---
+
+## Build lifecycle and GC protection {#build-lifecycle}
+
+**Status: DONE** (watermark in the merged heartbeat: `CasStore.cpp`, `CasServerRoot.cpp`; precommit protection: `CasBuild.cpp`, `CasGc.cpp`).
+
+A `Build` is in one of three states at any time:
+
+1. **In-flight** (`alive = true`): the build is between `startBuild` and `publish`/`abandon`.
+ Protected by two mechanisms:
+ - **Watermark**: `build_seq` ≥ `min_active` of the live server with matching epoch → GC skips
+ condemning the build's in-flight objects. `min_active` is carried in the mount-lease body and
+ refreshed by the merged heartbeat beat (§merged-heartbeat).
+ - **Precommit edge**: after `precommitAdd`, every object in the manifest closure has a
+ GC-understood build-root edge (in-degree ≥ 1 once the fold barrier activates).
+2. **Finished** (`publish` or `abandon` ran, or dtor): `retireBuildSeq` removes `build_seq` from
+ `active_build_seqs`; `min_active` advances.
+3. **Crashed** (process gone, epoch stale): GC's frozen-`seq` detection — `seq` unchanged across
+ K = 2 consecutive GC passes → `serverLive = false` → objects become condemnable.
+
+**`abandon`**: if the build made a precommit (`precommitted = true`), `abandon` appends a
+precommit-removal event `{old = precommit, new = none}` to the shard (one `mutateShard`). GC folds
+this removal, drops the precommit owner, and eventually reclaims the objects. The manifest body is
+NOT deleted by the writer on abandon (it is a live precommit input; writer-deleting it would strand
+the fold barrier or lose the activating `+1`). GC's `reclaimAbandonedPrecommit` handles the body
+after the sealed decrement. Never-precommitted staged manifests are best-effort deleted by the
+writer on abandon.
+
+---
+
+## Renames and detached parts {#renames}
+
+**Status: DONE** (`ContentAddressedTransaction.cpp`).
+
+| Rename type | Mechanism | Notes |
+|---|---|---|
+| `tmp→final` (INSERT/merge/mutation) | Eager `moveDirectory` → `publish` final ref + set `published` flag | Lock-free; `commit` skips already-published refs (B151 fix) |
+| `final→detached/` (DETACH) | `republishRef` (ref rename in the shard) | HEAD-free: records tokenless evidence dep + `precommit`, merged gate re-proves at publish |
+| `detached/→final` (ATTACH) | `adoptEvidence` + `precommit` + `promote` | Standard write path |
+| Committed-source `createHardLink` | `adoptEvidence` (tokenless, no HEAD before precommit) | Pre-precommit GETs on the source are forbidden (INV-2) |
+
+**INV-2 (precommit-first / reachability-before-content)**: the build-root precommit is published
+BEFORE any pool content GET/HEAD/PUT of the build's content. `republishRef` and `createHardLink`
+were audited and fixed to comply (B190 / B190-revival-consolidation). The full write ordering
+(`publishStaging`, B188) is even stronger: blobs stage into LOCAL temp files, then `stageManifest`
+makes the EDGE-BEARING body durable, then `precommitAdd` lands the owner event, and only then do
+blob uploads reach the pool. GC consequence (2026-07-02 brainstorm): a writer dying mid-upload
+leaves blobs whose edges ALREADY exist in a folded (or foldable) body — `reclaimAbandonedPrecommit`'s
+`-1` transitions them to zero and the normal condemn pipeline reclaims them. There is therefore NO
+structural "orphan blob" class: an object in `blobs/` with no edge-bearing body in history should be
+impossible, and `fsck` may treat one as an anomaly signal, not expected debris.
+
+**Fresh-fetch detached landing** (`FETCH PARTITION`/`FETCH PART` into `detached`): a fresh
+(non-cloned) fetch downloads bytes and writes them into `detached/tmp-fetch_/…`, which parses
+into the table's detached namespace (`kDetachedRefPrefix`). Unlike DETACH (which clones from an
+already-committed active ref), the fetch's files exist only as the transaction's `recorded` blobs, so
+the commit must **publish a `detached` ref by folding those `recorded` blobs** — the same publish as
+`republishCommittedPartIntoDetached`, but sourcing from the in-transaction `recorded` set rather than
+a re-keyed source manifest. The subsequent `detached/tmp-fetch_` → `detached/` rename is a
+**detached→detached re-key** (`rekeyDetachedPartDir`, the same operation that stages
+`attaching_`), re-keying `/` → `/` within the shared detached ref's
+manifest/sidecar. This is separate from the `final↔detached` rows above: it is the staging→final
+landing of a *freshly-fetched* part. Source: `specs/2026-06-04-cas-mergetree-fetch-partition-design.md §3.3`.
+
+---
+
+## Transactions and MVCC (writer-side) {#transactions-mvcc}
+
+**Status: DONE** (B39 gate + single-part; B67 layer 2 multi-part; `ContentAddressedTransaction.cpp`,
+`ContentAddressedMetadataStorage.{h,cpp}`, `StorageMergeTree.cpp`). Sources:
+`specs/2026-06-04-cas-mergetree-transactions-design.md`,
+`specs/2026-06-04-cas-mergetree-multipart-transaction-design.md`.
+
+This is the writer-side layer that makes MergeTree transactions (`BEGIN`/`COMMIT`/`ROLLBACK`, implicit
+transactions, snapshot isolation, mutation/`DELETE`-in-transaction) work on a CAS disk. The MVCC
+**engine** — snapshot/CSN assignment, `TransactionLog`, visibility, the in-memory `DataPartsLock`
+serialization — is storage-agnostic and unchanged; the CAS-side job is solely to satisfy the per-part
+`txn_version.txt` storage contract. Since the all-tree part-files migration (spec
+`2026-07-15-cas-all-tree-part-files-design.md`), `txn_version.txt` (`VersionMetadata` /
+`VersionMetadataOnDisk`) — along with `uuid.txt` and `metadata_version.txt` — is written through the
+same content path as any other part file: an ordinary manifest entry, not a separate per-ref sidecar.
+
+### The transaction gate is decoupled from append {#txn-gate}
+
+Ordinary S3-backed MergeTree gets transactions from
+`MetadataStorageFromDisk::supportWritingWithAppend` returning `true`. CAS is content-addressed and
+cannot append, so it inherits the base `false` — which historically also blocked transactions.
+
+The fix is a **distinct capability**: `IMetadataStorage::supportsTransactionalMutableFiles` (base
+returns `false`; `ContentAddressedMetadataStorage` overrides to `true`,
+`ContentAddressedMetadataStorage.h:84`). `StorageMergeTree::supportTransaction`
+(`StorageMergeTree.cpp:178`) now also accepts a disk whose metadata storage
+`supportsTransactionalMutableFiles`, rather than gating purely on `supportWritingWithAppend`.
+
+**Decision — do NOT make CAS `supportWritingWithAppend` return `true`.** Transactions provably do not
+need append: `txn_version.txt` is rewritten via tmp + `replaceFile`, never `WriteMode::Append`; the
+only append user, `MergeTreeDeduplicationLog`, already has a no-append rewrite fallback that CAS's
+dedup window relies on. Flipping `supportWritingWithAppend` would defeat that dedup-log fallback and
+disarm the `DiskObjectStorageTransaction` append guard (CAS's content-addressed write branch cannot
+append). So a *narrow* new capability, not the easy reuse of the append flag.
+
+### `replaceFile`/`moveFile` mutable-file routing — superseded by the atomic single-write path {#txn-replacefile}
+
+Since Task 5's `supportsAtomicFileWrites` capability (`ContentAddressedMetadataStorage::
+supportsAtomicFileWrites() == true`), `VersionMetadataOnDisk::storeInfoToDataPartStorage` takes a
+single atomic `writeFile` for `txn_version.txt` — no `.tmp` file, no `replaceFile` call. The tmp
+(`txn_version.txt.tmp`, `Rewrite`) + `replaceFile(tmp, txn_version.txt)` dance this section used to
+describe only runs on disks that lack atomic file writes.
+
+Consequently, `ContentAddressedTransaction::moveFile`'s legacy mutable-per-part-file destination
+routing (`isMutablePerPartFile(dst->file)`: re-stage the committed bytes under the destination via a
+force-fresh resolve, mark the source removed) is dead in production for CA — no `.tmp` file is ever
+created to rename into place. The branch is still present in code (Task 9's mechanical sweep removes
+it along with the rest of the mutable-file concept) but unreached by any current CA write path.
+
+### CSN fill-in / removal-TID rewrites on a committed part — an ordinary standalone-write repoint {#txn-mutable-only}
+
+Filling in the **creation CSN** on `COMMIT`, and **locking/unlocking the removal TID**
+(`tryLockRemovalTID`/`unlockRemovalTID`) when a `DELETE`/mutation/`DROP`/`TRUNCATE`-in-transaction
+marks an **already-committed** part for removal, both rewrite `txn_version.txt` on a part whose ref is
+already published — via a fresh autocommit transaction. Since `txn_version.txt` is now an ordinary
+manifest entry (Task 6), this is an ordinary **committed-ref standalone write** (§4 above, Task 4),
+not a special mutable-only case: `writeFile` stages the one changed entry, and `publishStaging`'s
+repoint branch carries every OTHER committed entry forward and republishes one manifest via
+`repointRef` — never recomputing over an empty set, so the part is never clobbered. A byte-identical
+rewrite resolves through `repointRef`'s byte-equal check with zero pool mutations (Task 3).
+
+The pre-all-tree mechanism this section used to describe — a dedicated "mutable-only" `publishStaging`
+branch that skipped manifest staging entirely and merged into the ref's separate `RootRef.mutable_files`
+payload via `updateRefPayload` — is superseded for these three files. That branch
+(`ContentAddressedTransaction.cpp`'s `!st.build && st.entries.empty() && st.content_removed.empty()`
+guard) still exists to serve the now-legacy `mutable_files`/`mutable_removed` fields until Task 9's
+schema-deletion sweep removes them, but nothing in the current write path populates those fields for
+`uuid.txt`/`txn_version.txt`/`metadata_version.txt` anymore.
+
+The whole-part INSERT path is unaffected either way: it stages the full content set, so the repoint
+branch does not trigger — `publishStaging`'s `Build` path runs as normal.
+
+**Fail closed**, restated for the current shape: if a transaction stages a removal mark or a changed
+entry for a part with no existing ref and no `Build`, `publishStaging` throws `LOGICAL_ERROR` rather
+than publish a standalone one-file/empty manifest — a rewrite or removal on a non-existent part never
+fabricates or clobbers a part.
+
+### Rollback and the MVCC-on-CAS lifecycle {#txn-rollback}
+
+An **uncommitted** INSERT inside an open transaction still publishes a CAS ref (its blobs stay
+GC-reachable — a `WriteSession` pins them until the ref lands), and the part is made *logically
+invisible* by its `txn_version.txt` creation CSN, exactly like the existing precommitted-part model (a
+part physically present but invisible until commit). **`ROLLBACK`** removes the ref
+(`removeRecursive`/`removeDirectory` unlinks it); the blobs then become **GC-eligible** — see
+`04-gc-protocol.md` for the ROLLBACK→unreferenced→sweepable edge, which is the same
+publish-a-ref-then-drop-a-ref path GC already handles.
+
+**Rollback-reload hardening.** The rollback path stats an in-flight part via
+`VersionMetadataOnDisk::removeTmpMetadataFile` → `getLastModified`. On a ref-less in-flight part CAS
+must resolve this via the in-flight read-your-writes overlay (B59, `09-read-protocol.md §8`) rather
+than throw `FILE_DOESNT_EXIST`, so the rollback completes and removes the in-flight ref/blobs.
+
+**Concurrency argument.** The engine serializes per-part `txn_version.txt` writes under
+`DataPartsLock` *before* calling the disk; CAS's per-part sidecar objects do not contend across parts;
+the mutable-only branch re-validates **no** blobs (it adds none), so no new CAS-level lock is
+introduced. The `gc_lock` in the content commit still guards blob re-validation (§write-path).
+Force-fresh (`allow_stale = false`) `resolveRef` on the mutable-file read side guarantees
+read-your-writes for the transaction version (`09-read-protocol.md §9.1`).
+
+### The multi-part disk transaction (B67 layer 2) {#txn-multipart}
+
+This is a **multi-part *disk* transaction — NOT S3 multipart upload.** A transactional merge/mutation
+runs one `DiskObjectStorageTransaction` that spans several parts, so a single
+`ContentAddressedTransaction` must hold and commit writes for more than one part.
+
+Two things make one transaction span parts:
+
+- **Deferred rename window.** `preparePartForCommit` with `rename_in_transaction=true` adds the
+ merge-output part with `need_rename` and defers the `tmp_merge_X → X` rename to
+ `Transaction::renameParts`. In the window between add and rename the part's logical name is the final
+ `X` while the transaction is still keyed to `tmp_merge_X` — a `txn_version.txt`/CSN write landing here
+ targets `X` while the transaction holds `tmp_merge_X`.
+- **Covered source parts.** On commit, `addNewPartAndRemoveCovered → lockRemovalTID` rewrites
+ `txn_version.txt` on each covered SOURCE part (so a rollback can un-cover them); these rewrites can
+ land on the same transaction as the merge output.
+
+The transaction is therefore keyed by a **per-part staging map** rather than a single
+`(table_uuid, part_name)`: `parts` maps a `(namespace, ref)` key to a `PartStaging`
+(`ContentAddressedTransaction.cpp`) holding that part's own `recorded` blobs, `recorded_mutable`
+(sidecar files), `recorded_mutable_removed`, and pending blobs. The former single-part assertion in
+`rememberTarget` is gone — multiple keys are legal; every staging op (`recordBlob`/`writeFile`/
+`unlinkFile`/`replaceFile`/the in-flight read helpers) parses the path and routes to
+`parts[{namespace, ref}]`. The single-part case (every INSERT) is exactly one map entry.
+
+The **rename re-key**: `moveDirectory(tmp_merge_X → X)` moves the staging entry from the source key to
+the destination key, **merging** into any destination entry that a deferred-window `txn_version.txt`
+write already created — so after the re-key part `X` carries both its content blobs and the mutable
+file. `commit` takes the `gc_lock` once, then iterates `parts`: content entries take the normal
+whole-part publish, mutable-only entries take the branch above.
+
+**§3.0 atomicity argument (why no new cross-part atomicity requirement).** On a local disk a
+transactional merge is **not** filesystem-atomic — it renames the output and rewrites each source
+`txn_version.txt` as individual ops. MVCC visibility is gated by the per-part **CSN/TID in
+`txn_version.txt`**, not by disk-op atomicity. So CAS may publish parts **one at a time**; a crash
+mid-`commit` leaves some refs published and some not, and those orphan refs are GC-reclaimed exactly as
+any uncommitted-then-abandoned build — identical to the local-disk story. No cross-part atomic flip is
+introduced. (Note: there is still no multi-ref atomic publish, so a publish that throws after earlier
+parts published leaves a *partial* commit at the disk layer; MVCC governs visibility, and the durable
+orphan refs are reclaimed by GC — B122.)
+
+---
+
+## Resource requirements {#resource-requirements}
+
+**Status: DONE** (scratch dir, backpressure); **TODO** (B172 S3 staging).
+
+### Scratch directory {#scratch-dir}
+
+Each `writeFile` call spills the file to a unique temp path under `local_scratch_path` (a
+server-local directory, configured per disk). The temp file is kept until after `putBlob` completes
+(the upload path re-reads it). Peak local disk usage is one full part's worth of staged files at
+once. Temp files are deleted after upload; crashed writes leave debris that the orphan sweep cleans.
+
+**DESIRABLE** (B172): move blob staging to an S3 staging area so no local disk is required. Blocked
+pending the server-side copy capability probe on all supported backends.
+
+### Memory {#memory}
+
+Blobs are streamed through the `putIfAbsentStream` path without materializing the full body in
+memory. The condemned-displacement `putOverwrite` path materializes the header + payload
+(`source.write_payload` into a `WriteBufferFromString`) — this is the only full-blob memory copy,
+and it fires only on the rare condemned-dedup edge, not on the common fresh-upload path.
+
+The `stageManifest` inline caps (OQ7, enforced fail-closed before any body write):
+- Max entries per manifest: 1,048,576
+- Max encoded manifest size: 256 MiB
+- Max total inline bytes per manifest: 16 MiB
+- Max single inline entry: 1 MiB
+
+### Manifest backpressure {#manifest-backpressure}
+
+**Status: DONE** (B164b, `CasStore.cpp`).
+
+When a root shard's encoded size crosses `manifest_soft_limit`, `mutateShard` introduces a linear
+write delay for Writer-origin mutations (GC mutations are not delayed). The delay is 0 at the soft
+limit and `manifest_max_delay_ms` near `manifest_hard_limit`. At or above `manifest_hard_limit`
+the mutation is rejected with `LIMIT_EXCEEDED`; with the flat-combining shard-mutation queue
+(the "Shard-mutation queue" part of §phase-upload above) a batch at the hard limit degrades to solo
+re-flushes so exactly the offending mutation gets `LIMIT_EXCEEDED` and its innocent co-batched
+neighbors proceed. This paces
+writers to give GC time to fold+trim the journal. One delay per **flush** (not per queued mutation,
+and not per CAS retry) — since 2026-07-03 a flush may batch several callers' mutations into one
+`casPut`.
+
+---
+
+## Write-path S3 budget {#write-path-s3-budget}
+
+See `07-s3-budget.md` for the full breakdown. Summary per part:
+
+| Operation | S3 ops | Notes |
+|---|---|---|
+| `stageManifest` | 1 `putIfAbsentStream` | Manifest body; no preliminary HEAD |
+| `precommitAdd` | 1 GET + 1 CAS-PUT (loop) | Shard `mutateShard`; typically 1 round |
+| `putBlob` per new blob | 1 `putIfAbsentStream` | Streaming; no HEAD on fresh upload |
+| `putBlob` dedup hit (cache) | 1 HEAD | Body PUT avoided (`CasBlobBodyPutAvoided`) |
+| `putBlob` dedup hit (large) | 1 HEAD | HEAD-before-PUT (`CasBlobHeadFirst`) |
+| `putBlob` condemned displacement | 1 HEAD + 1 `putOverwrite` | Rare; re-streams from source |
+| `promote` | 1 GET (manifest) + 1 GET + 1 CAS-PUT (shard) | GET = manifest body; shard `mutateShard` |
+| Mutable-file sidecar write | 1 PUT | Per ref; tiny object |
+
+**Key reductions** (tracked in `07-s3-budget.md`):
+- **Dedup cache** (`CasBlobDedupCacheHit`): LRU keyed by blob hash; a cache hit skips both HEAD
+ and body PUT.
+- **Adaptive HEAD-before-PUT** (`dedup_head_first_min_bytes`): HEAD large blobs before streaming to
+ avoid wasted multipart upload on a dedup hit.
+- **Precommit-first** (no pre-precommit HEAD): `precommitAdd` does not HEAD the manifest body;
+ the fold barrier accepts absent bodies.
+
+---
+
+## DONE / TODO / REJECTED / DESIRABLE summary {#status-summary}
+
+### DONE {#status-done}
+
+- Mount startup: owner anchor, durable `writer_epoch`, mount lease (with the merged build-watermark +
+ GC-ack heartbeat; the standalone watermark object and `WatermarkKeeper` are removed).
+- `build_seq` strictly-monotone active-set watermark; GC condemn guard.
+- Four-phase write path (spill+hash → precommit → upload → promote).
+- Precommit-first invariant (INV-2) enforced at `precommitAdd`, `republishRef`, `createHardLink`.
+- INV-1 (revival-from-source): `Build::resurrect` (GET-from-existing) deleted; `uploadFromSource`
+ is the sole revival primitive.
+- INV-COMMIT-FAILCLOSED: `promote` reads and validates the manifest body before committing.
+- Manifest body inline closure (B199-S2): precommit `Add` record carries staged tree entries inline
+ so the fold barrier never needs to read staged objects.
+- Fold barrier: non-activating precommit until body present; activating `+1` on expansion.
+- Lock-free publish via eager `moveDirectory` dispatch (B151 fix).
+- Mutable per-ref files (`txn_version.txt` etc.) in sidecar, not manifest.
+- Manifest size backpressure (B164b) with soft/hard limits and linear write delay.
+- `abandon` writes a precommit-removal event; does not writer-delete the live precommit body.
+- Dedup cache + adaptive HEAD-before-PUT (`CasBlobHeadFirst`, `CasBlobBodyPutAvoided`).
+
+### TODO {#status-todo}
+
+- **B172** — S3 staging area + server-side copy: eliminate local scratch dir requirement; allow
+ streaming merge+upload with hash-on-the-fly then server-side copy. Pure performance / footprint
+ improvement; correctness is independent.
+- **B199-S2 TLA+ gate** — `CaBuildRootPrecommit.tla` extension for the inline-closure fold barrier
+ before full implementation.
+- **Plain (non-replicated) mutation publish** — still runs under the `data_parts` lock (B151 fix
+ covers only replicated paths). Needs the precommit-hook approach or per-path analysis.
+- **`farewell` on graceful shutdown** — retire the epoch at clean shutdown so GC can reclaim the
+ server's in-flight objects immediately (currently GC waits for K=2 frozen-`seq` detection).
+
+### REJECTED {#status-rejected}
+
+- **`cas_owner` S3 metadata + `protectedByLiveBuild` guard** (pre-B171): per-object hint in S3
+ metadata. Replaced by precommit reachability. Flaw: adopt did not transfer ownership; protection
+ was revocable.
+- **Per-build heartbeat object** (`builds/` key, B167 Part B reverted): leaked on
+ successful publish (only `abandon` cleaned it up), breaking
+ `DeletedCandidateDoesNotReappear`. Replaced by the per-server watermark (now itself merged into the
+ mount-lease heartbeat, §merged-heartbeat).
+- **`resurrect` via GET-from-existing** (`Build::resurrect`, deleted B190): races with GC delete
+ in the HEAD→GET window; bodyless gate path had no fallback. Replaced by `uploadFromSource`.
+- **Body retention in RAM** (the `retained_blobs` draft): column blobs can be gigabytes; rejected
+ on memory grounds. Re-readable `BlobSource` (re-reads local temp file) is the correct approach.
+- **Server-side copy / `UploadPartCopy`** for bodyless re-stamp: dropped when the per-server
+ watermark made bodyless re-stamp unnecessary; MinIO has known `UploadPartCopy` gaps.
+- **Publish-at-precommit double-write** (tmp ref then final ref): would add a second manifest PUT
+ per part. Rejected on op-count grounds; eager `moveDirectory` achieves the same property.
+
+### DESIRABLE {#status-desirable}
+
+- **B172 S3 staging** (see TODO above) — would also eliminate the temp-file re-read on the
+ condemned-displacement path.
+- **`farewell` on graceful shutdown** — reduces GC lag after a clean server stop.
+- **Wedged-build watchdog** beyond the gate's local-heartbeat sanity — a build whose background
+ watermark renewer is starved (e.g. S3 retry storm) could be falsely judged dead; a dedicated
+ watcher thread on the renewer's health would close the residual window.
diff --git a/docs/superpowers/cas/04-gc-protocol.md b/docs/superpowers/cas/04-gc-protocol.md
new file mode 100644
index 000000000000..e192ec1f0153
--- /dev/null
+++ b/docs/superpowers/cas/04-gc-protocol.md
@@ -0,0 +1,711 @@
+---
+description: "Canonical reference for the content-addressed (CAS) MergeTree garbage-collection protocol: leader election, lease + advisory heartbeat, the one-pass ack-floor round (heartbeat floor → three-cursor merge → two-phase graduation → single gc/state CAS), orphan removal, ref removal, shard-object reclaim, incarnation, registry removal (D1), attempt-scoped generations, snap prune, and concurrent-leader safety."
+sidebar_label: "GC protocol"
+sidebar_position: 4
+slug: /superpowers/cas/gc-protocol
+title: "CAS MergeTree — GC Protocol"
+doc_type: reference
+---
+
+# CAS MergeTree — GC Protocol {#gc-protocol}
+
+**Status summary:** see per-section stamps. The regular round is a **single pass**: a heartbeat ack floor, one three-cursor merge that verifies/graduates/condemns in the same fold, two-phase graduation of deletions, and one `gc/state` CAS. It is **DONE** (implemented on branch `cas-gc-ack-floor-fence`; soak-validated, including a live-AWS S3 run, 2026-07-03 — the night soak that ran under this round found and fixed the clamp-suppression gap below). The ack floor replaces the former per-round all-shard fence and fold-through-fence recheck (see the History note in §3). Attempt-scoped generations (concurrent-leader safety) and the source-edge-set in-degree (H1b fix) are **DONE** (2026-07-01). Shard incarnation + registry removal (D1) is **DONE** (TLA+ gate GREEN 2026-07-01, all five implementation phases landed). Snap prune and advisory heartbeat are **DONE**.
+
+Cross-links: `06-tla-models.md` for formal proofs · `07-s3-budget.md` for per-operation cost.
+
+> **⚠️ Narrative-vs-code drift (retired-in-snapshot, 2026-07-11).** The prose below still describes the
+> pre-refactor **three-cursor** merge and a separate **retired-list run** published via
+> `gc/state.retired_refs`. As of the retired-in-snapshot refactor
+> (`specs/2026-07-10-cas-retired-in-snapshot-design.md`, DONE) this is superseded: condemned state now
+> rides the **source-edge run** as `kCondemned` sentinel rows (at the zero-sentinel key), the merge is
+> **two-cursor** (prior run + deltas), and the fold seal carries a per-gc-shard `condemned_summary`
+> (`condemned_total`, `pending_total`, `oldest_nonpending_condemn_round`, total over `gc_shards`) that
+> `graduationDue` reads zero-I/O. `RetiredSet`/`retiredKey`/`retired_refs`/magic `CART` are deleted.
+> Graduation is round-paced (writer condemned-detection is a per-hash `.meta` point-read), not ack-floor
+> `min_ack`. The section-by-section prose refresh is tracked in `ROADMAP.md` (doc-debt TODO); read the
+> code (`Core/CasGc.cpp`, `Core/CasBlobInDegree.cpp`) and the spec as authoritative until then.
+
+---
+
+## 1. Overview {#overview}
+
+The GC subsystem is the only entity authorized to **delete** content-addressed objects. It is the symmetric counterpart of the write path: writers add reference edges; GC detects and collects objects whose edge count reaches zero. The central guarantee is:
+
+> **No committed reference to content ever becomes unreadable**, because no object is deleted while any session pin or committed reference names it. (`INV_NO_LOSS`, `INV_NO_DANGLE` — proved in `CaGcRootLocalPartManifestCore.tla`, cross-link `06-tla-models.md §root-local-manifest`.)
+
+GC is **not** on the write critical path. It runs as a background lease-holder loop (`CasGcScheduler`) and is *work-dedup only* — every round step is idempotent and split-brain-safe; a stale leader can only duplicate work, never mis-delete. The safety proof (`CaGcRootLocalPartManifestCore.tla`) explicitly makes no leadership uniqueness assumption.
+
+### Object graph {#object-graph}
+
+```
+dynamic root namespace (per-table)
+ └─ root shard (RootShard, mutable CAS object)
+ └─ part manifest (PartManifest, immutable, root-local, single-owner)
+ └─ blob (content-addressed, deduplicated across all tables)
+```
+
+- **Blobs** are content-addressed under `blobs//`. Only blobs are deduplicated across namespaces.
+- **Part manifests** are immutable root-local objects under `roots//_manifests////.proto`. Each manifest has at most one structural owner at any time.
+- **Root shards** (`cas/refs//`) are mutable CAS objects. Each carries a `RootOwnerEvent` journal, committed `RefRecord`s, and a `fence_round` birth floor (stamped once at shard creation — see §7; it is no longer bumped per round).
+- **Precommit owners** are journal entries naming a manifest before its committed publish. They protect blobs during the upload window.
+
+---
+
+## 2. Leader election, lease, and advisory heartbeat {#leader-election}
+
+**Status: DONE** (B160 advisory heartbeat landed and soak-validated; `CaGcLeaseCore.tla` proves safety — see `06-tla-models.md §lease-core`).
+
+### 2.1 Lease structure {#lease-structure}
+
+GC leader election is a **clock-free, observation-window steal** over the durable `gc/state` object. The `GcState` carries:
+
+```
+GcState {
+ lease { owner: UInt128, seq: uint64 } // current leader identity + monotone sequence
+ round: uint64 // monotone GC round counter
+ snap_generation: uint64 // pointer to the authoritative in-degree run set
+ snap_attempt: uint64 // attempt id that produced snap_generation (attempt-scoped gen)
+ snap_pruned_through: uint64 // retention cursor for old gc/gen generations
+ retired_refs: { gc_shard → retired_list_object_key } // current retired-list runs (ack-floor)
+ gc_shards: uint64 // immutable creation-time blob-target shard count
+}
+```
+
+`retired_refs` maps each blob-target gc-shard to the object key of the **current** retired-list run (the outstanding-candidate set); it is published in the same `gc/state` CAS that advances `round`, so a reader that observes round K can always load the retired list of version K (the publish-order invariant, §3.6). The former per-round `fence_seq` epoch and the `fence_version` map are **removed** with the fence phase (see the History note below).
+
+The steal is a single **atomic CAS** on `gc/state` that bumps `lease.owner` and `lease.seq` together. Safety of every round step is independent of who holds the lease; the CAS-steal merely prevents redundant work.
+
+A follower steals only when it has observed the incumbent's `(owner, seq)` **unchanged** across its observation window AND the heartbeat counter is also frozen (see §2.2). This prevents a false steal when a leader is alive but mid-round.
+
+### 2.2 Advisory heartbeat (B160) {#advisory-heartbeat}
+
+**Problem solved:** a GC round can take longer than the follower's tick interval. A follower that sees `gc/state.seq` frozen (the leader only bumps it once per round) falsely concludes the leader is dead and steals — causing ~70–80% of GC rounds to abort in two-replica soaks. (The one-pass ack-floor round is far cheaper than the old fence/recheck round, but the false-steal risk is structural, not cost-driven, so the heartbeat remains load-bearing.)
+
+**Mechanism:** a separate lightweight heartbeat thread writes `gc/hb` (a small `{owner, hb_seq}` object) every `H` seconds independently of round progress. The steal decision is extended:
+
+```
+incumbent_alive =
+ gc/state.(owner,seq) changed since last observation // existing
+ OR ( gc/hb.owner == gc/state.owner // not a stale ex-leader
+ AND gc/hb.hb_seq advanced since last observation ) // new heartbeat pulse
+steal ONLY IF NOT incumbent_alive
+```
+
+A live-but-slow leader's heartbeat advances within the observation window → no false steal. A dead leader's pulse also freezes → steal proceeds via the same atomic `gc/state` CAS.
+
+**Fallbacks (never-worse):** if `gc/hb` is absent (fresh pool) or has a different owner (displaced ex-leader still pulsing), the protocol falls back to the existing `gc/state.seq` observation — exactly today's behavior. No degradation.
+
+**TLA+ reference:** `CaGcLeaseCore.tla` + `_sab_noheartbeat.cfg` (produces a false-steal counterexample without the heartbeat) + `_heartbeat.cfg` (proves no false steal with heartbeat). See `06-tla-models.md §lease-core`.
+
+---
+
+## 3. The GC round {#gc-round}
+
+**Status: DONE** (one-pass ack-floor round implemented on `cas-gc-ack-floor-fence`; soak-validated live on AWS S3, 2026-07-03. `CaGcAckFloorCore.tla` + `CaGcAckFloorZombie.tla` prove the ack-floor safety obligations — see `06-tla-models.md §ackfloor-core`; the fold/manifest machinery it reuses stays proved by `CaGcRootLocalPartManifestCore.tla`).
+
+The regular round is **one pass**:
+
+```
+heartbeat ack floor → discover → fold (three-cursor merge) → pre-CAS deletes of previously-published pending → outcome logs → retired-list publish → single gc/state CAS → post-CAS cleanup
+```
+
+There is **no fence phase, no recheck phase, and no crash-resume step**. The three-cursor merge (§3.4) verifies old candidates, graduates the safe ones, and condemns new ones in a single streaming fold; the ack floor (§3.2) is what makes a delete safe without an all-shard write fence. Physical deletion is two-phase (§3.5): condemnation → `delete_pending` at the first pass whose floor passes the entry → exact-token delete the next pass. No cascade step exists (blob decrements are emitted directly from manifest fold).
+
+> **History (superseded fence/recheck round).** Through 2026-07-02 the round was
+> `discover → fold → retire → fence → recheck → exact-token delete → trim`, with a per-round
+> **fence** (a CAS write to every present root shard bumping `fence_round`, recorded in
+> `GcState.fence_version`) and a **recheck** (re-folding the `(folded_cursor, fence_version]` window
+> per fenced shard). Both phases were O(universe) GET+CAS-PUT every round — ~2.4 M requests at
+> 100k tables × 8 root shards (`07 §gc-budget`) — and the recheck's per-candidate
+> `inDegreeInGeneration` re-reads were the quadratic hot spot that started the investigation. They
+> were replaced by the causal ack floor + three-cursor merge. See
+> `specs/2026-07-02-cas-gc-ack-floor-fence-redesign.md` for the full rationale.
+
+### 3.1 Discovery {#discovery}
+
+The GC leader discovers the live namespace universe by `LIST(cas/refs/)` — the set of `(ns, shard)` root-shard objects that physically exist. Each object is self-identifying (carries its `incarnation` — see §6.1).
+
+**Registry removal (D1, DONE):** historically discovery read from `gc/registry`, an append-only authority. D1 (§7) replaces this with the LIST-based discovery and deletes the registry (`discoverUniverse` now LISTs `cas/refs/` directly). The token-diff optimization (skip body re-read when the listed shard token matches the previously folded token) is retained as a read accelerator.
+
+This one LIST sweep is the round's only universe-proportional operation (O(universe/1000) LIST requests); everything else is O(delta) + O(servers). Discovery runs **after** the heartbeat floor (§3.1a), so any fenced-out writer's last commits are durable before the sweep enumerates them.
+
+### 3.1a Heartbeat fence pass {#heartbeat-floor}
+
+Before discovery, GC runs one **heartbeat fence pass** (`computeHeartbeatFloor`, `CasServerRoot.cpp`) so a superseded (expired) writer's last commits are made durable-or-fenced before the sweep enumerates them. It is the round's first step and its only use of a wall clock:
+
+1. **Enumerate:** LIST `gc/server-roots/` + GET each mount (O(servers), single-digit counts).
+2. **Classify** each heartbeat, using the injected `now_ms_fn` and `skew_margin_ms = mount_lease_ttl_ms / 2` (`live | terminated | expired`):
+ - *terminated* (graceful-shutdown stamp) → excluded: its own final write is causal proof no further mutations exist.
+ - *live* (`expires_at_ms + skew_margin > now`) → left alone.
+ - *expired* (`now > expires_at_ms + skew_margin`, no terminated stamp) → **fence-out**: one token-guarded `putOverwrite` that preserves the body, sets `gc_fenced`, and bumps `seq`. Success ⇒ the sleeper's next renewal permanently fails (`tripMountLost`; only a full re-open with a fresh view can resume writing). `PreconditionFailed` ⇒ it renewed concurrently ⇒ re-GET and reclassify as live.
+
+The result is a `HeartbeatFloor{live, terminated, fenced_now, already_fenced, fenced_srids}` (counts + one `GcFenceOut` audit event per fenced srid). Fence-outs must complete before discovery, so every fenced writer's last commits are durable before the sweep.
+
+#### Fence vs decommission — a dead member's footprint outlives the fence {#fence-vs-decommission}
+
+The fence pass settles **liveness only**: a fenced member can no longer write, but its footprint —
+frozen watermark authority, stale precommits, staging debris, roots objects, and the mount/owner/epoch
+slot itself — stays in the pool indefinitely (a long-absent member pinning shared floors is the safe
+default). Erasing that footprint is a **deliberate operator action**:
+`SYSTEM CONTENT ADDRESSED DROP POOL MEMBER '' FROM DISK ''` (also
+`clickhouse-disks ca-drop-member`), specified in
+[`specs/2026-07-13-cas-pool-member-decommission-design.md`](../specs/2026-07-13-cas-pool-member-decommission-design.md).
+Decommission impersonates the dead member as a **WRITER** (it claims the victim's mount via
+`Store::openForDecommission` and refuses a live one) — `GC` still never invents a ref transition.
+Drain-phase failures are per-object tolerated (warned + continued) but any warning keeps the slot
+**terminated, not deleted**, so a re-run re-drains; only a fully clean run retires the slot
+(farewell stamp, then `epoch → owner → mount` deletion, mount last for crash-safe resume).
+Known limitation (fail-closed): a mid-retirement crash on a victim that still has namespace debris
+(`ref-log`/`_snap` objects not yet physically reclaimed by GC) is refused on re-run by the
+`serverRootSubtreeEmpty` gate until GC's namespace-cleanup catches up — an availability narrowing,
+never data loss.
+
+**SUPERSEDED (v3 freshness-meta):** this pass used to ALSO compute a writer-ack floor `min_ack = min(observed_gc_round)` over live heartbeats, and graduation was gated on it. The `observed_gc_round` field was removed (`cas_format.proto` reserved 10 — the writer-side retired-view ack floor is gone). **Graduation now paces on GC rounds** — a retired entry graduates one round after it is condemned (via `new_round`), not on heartbeat acks — and the writer's condemned-detection is a per-hash `.meta` point-read (§3.4, `03 §merged-heartbeat`). `CaGcAckFloorZombie.tla` (`06 §area-11`) models the historical ack-floor ordering; only its GC-round-pipeline half stays current.
+
+### 3.2 Fold {#fold}
+
+Fold reads the incremental diff from each root shard's `RootOwnerEvent` journal and emits blob-edge deltas into the per-shard in-degree runs.
+
+**Input:** each shard's journal window `(folded_cursor, current_shard_version]`. Events outside this window are skipped.
+
+**Event semantics** (from the single ordered `RootOwnerEvent` stream):
+
+| Event | Blob delta | Part-manifest cleanup |
+|-------|------------|----------------------|
+| `new_binding` only (first publish or precommit) | `+1` per blob entry (if manifest body present) | none |
+| `old_binding` only, true removal | `−1` per blob entry | record `ManifestId` for cleanup |
+| `old_binding == new_binding` (promote precommit, pure owner move) | **Δ = 0** | none |
+| `old_binding ≠ new_binding` (repoint) | `−1` old + `+1` new | cleanup for old `ManifestId` |
+
+**Fold barrier (missing-body precommit):** GC does NOT advance the fold cursor past a `RootOwnerEvent` that leaves a live precommit whose manifest body is absent or fails `RefMatchesBody`/`ManifestNamespaceMatches` validation. The cursor advances only when the body activates (emitting its `+1` deltas) or the precommit is abandoned (emitting no deltas). This is the fold barrier that makes promotion always of an activated manifest and keeps Δ = 0 correct for promotions. A stuck missing-body precommit is bounded by the writer-epoch watermark-based precommit reclaim path.
+
+In the ack-floor round the fold does **no internal CAS**: it sets `snap_generation` / `snap_attempt` in memory and the single round CAS (§3.6) commits them. The fold also loads the prior retired list from `gc/state.retired_refs` (GET each gc-shard run; a referenced-but-missing run is `CORRUPTED_DATA` — the integrity of destructive bookkeeping, not a data-plane 404) and feeds it as the third cursor of the merge (§3.4).
+
+**Output artifacts** (all under the attempt namespace — see §5):
+
+```
+gc/gen//attempt//fold_seal
+gc/gen//attempt//blob_target// // new in-degree snapshot run
+gc/gen//attempt//part_manifest_cleanup//
+gc/gen//attempt//retired// // current retired list (published via retired_refs)
+gc/gen//attempt//outcomes// // per-entry delete/spare outcomes
+```
+
+The `FoldSeal` records per-`(ns, shard)` coverage: `folded_token`, `folded_cursor`, and the produced run `RunRef`s (key + footer checksum). It is written **write-once** before the round CAS; it is a deterministic artifact and any colliding write must be byte-equal or `CORRUPTED_DATA`. Completion seals are a retired concept — the fold seal alone resolves cursors now.
+
+**Cursor advance:** the single `gc/state` CAS that advances `snap_generation` (§3.6) also advances `folded_cursor` per shard and publishes `retired_refs` and `round` — all atomically. A crash before the CAS leaves the prior generation and the prior retired list authoritative and the new attempt as invisible garbage; the next pass re-runs under a fresh attempt (§3.6 crash-resume).
+
+### 3.2a Snapshot-run reads: streaming + reference-parent runs {#snapshot-run-reads}
+
+**Status: DONE** (T2 streaming reads + T0 reference-parent runs, 2026-07-02; `specs/2026-07-02-cas-gc-snapshot-streaming-design.md`). The in-degree snapshot run (the per-`(generation, shard)` `RunKind::SourceEdge` artifact, §3.3) is read at **O(block) resident memory**, and an empty-delta shard reads nothing at all.
+
+**Streaming reads (T2).** Every run consumer — the prior cursor of the three-cursor merge and the preview helpers `zeroInDegree` / `inDegreeInGeneration` — reads a run through `RunFileReader` in **streaming mode**, never materializing the whole run. Opening a run is a fixed request profile:
+
+1. `head(key)` — the object size.
+2. one ranged `get` of the tail suffix (`min(object_size, kRunHardCapBlockSize + 64 KiB)`) — carries the footer (the `footer_len` trailer + the CRC'd sparse block index).
+3. `getStream(key, {header_end, data_end})` — a forward-only body stream over a **write-once** object, positioned at the first block; the 13-byte header is drained from its front so it needs no extra request.
+
+So a linear scan is **exactly three requests** (`head` + tail `get` + body `getStream`), plus **one exact-footer ranged `get`** for a run whose footer exceeds the tail probe — a large run is four requests. Resident state is the footer index plus **one** current block (`cur_block.size() <= kRunHardCapBlockSize`); the whole-run member is gone. A `seek` after open costs one extra ranged `get` per touched block (the sparse index locates it); after a `seek`, every subsequent block comes via ranged `get` — the pure-linear fold path never seeks. The block-by-block ranged-`get` alternative for full scans was rejected (an 8 GB run is ~32 000 blocks = a per-shard request storm); it survives only as the `seek` implementation.
+
+`getStream` is contractually for **write-once objects only** (runs, seals): their bytes cannot change under an open stream. Mutable objects (root shards, `gc/state`, mounts) stay on `get`. Fail-closed is unchanged: per-block CRC and the footer CRC verify exactly as the borrowed-memory path does; a short/truncated stream read or any CRC failure is `CORRUPTED_DATA`, never a partial parse. `RunFileWriter` is untouched, so output runs are byte-identical and `putDeterministicArtifact` semantics are unaffected.
+
+**Reference-parent runs (T0).** When a gc-shard's delta bucket is empty for a pass, the fold **neither reads nor writes that shard's run**: the new `fold_seal` carries the parent generation's `RunRef` for that shard verbatim (key + checksum + shard + generation). `RunRef` gains explicit `shard` (which gc-shard the run belongs to) and `generation` (whose key namespace physically holds the object) fields — both additive proto — so per-shard association and retention never parse key paths. This is deterministic by construction (same inputs ⇒ same refs), so seal determinism and crash-replay adoption are unchanged.
+
+A shard with an empty delta AND an empty retired list is **pure ref-carry: zero run I/O**. A shard with an empty delta but a non-empty retired list still runs the merge with empty deltas — settlement of retired entries must happen every pass — so the ref-carry shortcut requires **both** buckets empty. A fully idle round (no journal changes anywhere, no retired entries) therefore touches **zero** run objects: no GET, no PUT.
+
+**Consumers resolve runs through seal refs, never by key construction.** Because the run for generation `G` may physically live under an older generation's key namespace, no consumer builds a run key from `(generation, shard)`. The fold reads the parent seal's `blob_target_runs` and resolves each shard's run through the seal's `RunRef`s; `previewDeletes` resolves `gc/state → adopted seal → refs`. See §4.3 for how retention keeps a referenced older-generation run alive.
+
+### 3.3 In-degree representation: source-edge set {#indegree-source-edge-set}
+
+**Status: DONE** (H1b fix, 2026-07-01; `CaGcIndegRefoldCore.tla` validated the fix — see `06-tla-models.md §indeg-refold`).
+
+**The persisted per-`(generation, shard)` artifact is `(blob_hash → set of active source edges)`, not an integer refcount.** A source edge is identified by `(ManifestId, path)`; its id is `sourceEdgeId(ManifestId, path)` (a stable deterministic 16-byte id). The run key is `(blob_hash BE-16 ++ source_id BE-16)` — lexicographic order is `(blob_hash, source_id)`.
+
+Fold is **idempotent**: activating a source edge inserts it into the set (union); removing one deletes it from the set (set-difference). Re-folding either operation is a no-op. This structural idempotency eliminates the entire class of integer-refcount underflow bugs (H1b, H2 — see below under REJECTED).
+
+`in_degree(blob) = |edge_set(blob)|`, computed transiently within a round from the run; never persisted as a separate counter.
+
+`zeroInDegree` = blobs whose edge set became empty **this generation** (an explicit zero-transition marker row is written; prior-generation zero markers are dropped). Only blobs with an explicit zero-marker are GC candidates; blobs that never appeared as edge targets are never candidates (`INV_JOURNAL_COVERAGE` — a blob is only a candidate if GC ever saw it as a reference target).
+
+**REJECTED: integer refcount.** The implementation previously persisted `(blob_hash → int64 count)` and folded as `prior_count + Σ ±1`. This is **not** idempotent: re-folding a removal (H1b — a fence-window removal whose cursor was not advanced past the fence) drives the count to −1, triggering a fail-closed `CORRUPTED_DATA` throw and wedging GC. The model `CaGcRootLocalPartManifestCore.tla` never caught this because it uses set-based idempotent in-degree — the implementation diverged from the model. The source-edge-set design is what the big model actually proves; the change makes the implementation faithful to it. See `06-tla-models.md §indeg-refold` for the focused model that validated the fix.
+
+### 3.4 The three-cursor merge (verify, graduate, condemn) {#three-cursor-merge}
+
+The heart of the ack-floor round. Per gc-shard, one streaming pass extends the existing two-cursor `foldDeltasIntoGeneration` (prior snapshot run + sorted incoming edge deltas) with a **third cursor over the prior retired run** — all three inputs sorted by `blob_hash`. The single pass emits the new snapshot run, the new retired run, and the delete list. It **verifies old candidates, graduates the safe ones, and condemns new ones at once**, replacing the separate retire + fence + recheck phases.
+
+Let `min_ack` be the floor latched in §3.1a and `condemn_round = state.round + 1`. Per blob, at the merge point:
+
+| In-degree | Retired? | Action |
+|-----------|----------|--------|
+| `> 0` | yes | **spare** — drop the entry; emit a B170 recheck-verdict event with the recovered in-degree. |
+| `= 0` | retired, `condemn_round < min_ack` | **graduate** — mark the entry `delete_pending` and re-publish it (two-phase, below). |
+| `= 0` | retired, `condemn_round ≥ min_ack` | keep the entry unchanged (not yet provably seen by every live writer). |
+| `= 0` | not retired | **condemn** — `HEAD` the blob to capture its current token (absent ⇒ nothing to delete, skip); append `(hash, token, condemn_round)` to the retired output. |
+
+Because `min_ack ≤ round − 1` always (acks cannot exceed the last published round), an entry condemned in this pass structurally cannot graduate in the same pass — the two-round pipeline falls out of the arithmetic, with no explicit rule.
+
+**Clamp suppression (`suppress_destructive`, 2026-07-03):** the table above is overridden pass-wide the instant the fold recorded **any** clamp anomaly (§absent-at-head): a `suppress_destructive` flag is threaded `Gc::runRegularRound` → `foldDeltasIntoGeneration` / `ShardReducer::reduce` → `settleEntry`. While set, no entry graduates to `delete_pending` and no already-`delete_pending` entry is re-delivered for `deleteExact`; every retired entry (pending or not) is carried unchanged to `still_retired`. Condemning and sparing are unaffected. This restores the ack-floor lemma "landed before the cut ⇒ folded before graduation," which a clamped shard's frozen cursor otherwise violates — found live in the night soak as 31 dangling blobs. Deletes resume on the first clamp-free pass.
+
+**Condemn fail-closed (absent-at-condemn):** a blob already absent when its `HEAD` returns 404 records **nothing** — there is no token to condemn. GC must **never fabricate a token** on a 404: a synthetic token would let a stale exact-token delete match a future unrelated incarnation reusing the same hash, violating `INV_NO_RETURN`.
+
+**Recovery wins over everything, `delete_pending` included** (fail-closed spare): a retired entry whose in-degree recovered to `> 0` is spared even if it was already `delete_pending`. A pending entry observed with recovered in-degree is structurally impossible (floor-passed ⇒ every live writer sees it condemned ⇒ no new reference), so it is spared *and* logged loudly.
+
+The retired run is an **observation-bearing artifact**: the token is the value this pass's `HEAD` observed; two leaders may observe different tokens if a re-incarnation happened between their HEADs. The first durable write wins (read-if-present, not byte-equal-or-`CORRUPTED_DATA`). Once an entry is condemned, GC writes a per-hash `Condemned` `.meta` marker (`writeCondemnedMeta`, `CasGc.cpp`) on the bounded `gc_meta_pool_size` job pool — the writer's dedup/reuse gate point-reads it via `loadMeta` (`CasBlobMeta.h`) and treats `Condemned` as `ABORTED` → re-upload-from-source (`INV-1`, `CasBuild.cpp`), rather than referencing the condemned blob. There is no round-indexed `RetireView`; the meta is a 2-state freshness marker (`Clean` / `Condemned`), not a linearization point.
+
+**Missing manifest-body policy (inherited from fold, unchanged):** a missing/invalid committed-or-promoted new-binding body **clamps** the affected shard (fail-closed, surfaced to `fsck`, **not** spare-by-default); a missing precommit body is non-activating; an old-binding removal uses edges already sealed at fold and never reads a deleted body. Clamped coverage is now first-class (`ShardCoverage classification = 4`) and forbids the token-diff Skip: a barrier-clamped shard whose listed token never changes (the missing precommit body arrives via a manifest PUT, not a shard rewrite) must still be re-read, or its edges are lost forever. This was a real regression found while porting the merge.
+
+### 3.5 Two-phase graduation (`delete_pending`) {#two-phase-graduation}
+
+Physical deletion is **two-phase**, and this is what makes a deposed (zombie) leader's fresh decisions harmless. The single-CAS round removed the old protocol's incidental zombie ejection (a deposed leader used to fail one of the ~4 per-phase CASes long before reaching a delete). A deposed leader running the one-pass round could otherwise graduate from a *stale* retired list — an entry spared by the new leader and re-condemned at `r' ≥ min_ack` would look floor-passed under its old `r`.
+
+The fix: graduation is two-phase.
+
+1. A pass that first floor-passes an entry marks it `delete_pending = true` and **keeps it in the published list** (writers still see it condemned → recreate).
+2. `deleteExact` executes only for entries that were **already published as pending in the previous list version**, strictly **before** this pass's CAS (§3.6, "pre-CAS deletes").
+
+Pending is terminal — no spare is possible (floor-passed ⇒ every live writer sees the entry ⇒ no new reference), so re-executing pending deletes is safe at **any** staleness; a zombie's fresh decisions never survive its failing CAS. Crash-safety is leak-free: a crash before the CAS leaves the prior list still pending → the next pass re-issues. Physical deletion therefore lags condemnation by one extra pass (condemn K → pending at the first floor-pass → deleted the next pass) — immaterial in practice. This is strictly more conservative than the TLA+ `GComplete` (which deletes at graduation) and implements the model's drop-on-confirmed-outcome discipline (`06 §ackfloor-zombie`).
+
+### 3.6 Deletes, publish, and the single CAS {#deletes-publish-cas}
+
+The round tail is a fixed sequence of one destructive phase, artifact publication, and exactly one CAS.
+
+**Pre-CAS deletes (the single content-delete site).** For each entry that was `delete_pending` in the *previous* published list, `deleteExact(blobKey, token)` uses `If-Match`:
+
+- `Deleted` → done.
+- `TokenMismatch` (412) → a writer recreated the blob → done (the fresh incarnation is a live object; a future round re-condemns it if unreferenced).
+- `NotFound` → already deleted (crash-resume replay) → done.
+
+Each emits a B170 `BlobDelete` + `GcRecheckVerdict` outcome (`Deleted` / `Replaced` / `Absent`). Spared entries emit `Spared` + a verdict event (and a loud WARNING if the spared entry was `delete_pending` — structurally impossible). Manifest-body cleanup (`mfCleanup`, delete-after-adopted-decrements) rides this phase unchanged. These deletes are justified by **previously published** state only, so replay under a fresh attempt is idempotent — which is why no separate crash-resume step exists.
+
+**`created_delete_marker → LOGICAL_ERROR` (per-delete versioning guard):** if a `deleteExact` outcome reports `created_delete_marker = true`, the backend created a versioning tombstone instead of a real delete — the bucket is versioning-enabled, which the startup probe (`01 §backend-contract`) should have rejected. GC throws `LOGICAL_ERROR` fail-closed: a delete-marker regime could resurrect an exact-token-deleted object (`INV_NO_RETURN`).
+
+**Outcome logs & retired-list publish (publish-order invariant).** Outcome logs (observation-bearing ⇒ `putIfAbsent`-adopt) and the per-gc-shard retired-list runs (written **always**, even empty, at `retiredKey(generation, attempt, round, shard)` via `putIfAbsent` + byte-adopt) are durable **before** the CAS. A reader that observes round K can therefore always load retired list K. This subsumes the old retire-visibility barrier / `ViewableRound`.
+
+**The single `gc/state` CAS.** One lease-token CAS publishes, atomically: `round := K`, the adopted `(snap_generation, snap_attempt)`, `retired_refs` (the new per-gc-shard run keys), the per-shard folded tokens/cursors, and `snap_pruned_through`. Superseded-generation prune (§4.2) runs before the CAS (zombie-safe). Failure ⇒ `ABORTED "retry next round"`. This is the **only** CAS per round (the old protocol used ~4: fold-adopt, retire, fence, seal).
+
+**Post-CAS cleanup.** Manifest-body deletes for adopted decrements, `reclaimDroppedShards`, trim (§3.8), and the orphan manifest sweep cursor pass all run after the CAS, exactly as before.
+
+**Crash-resume (no explicit step).** Attempt-scoped write-once artifacts mean a crashed pass leaves only never-adopted debris (pruned by retention). A new leader (or the same one) re-runs the pass under a fresh attempt; already-executed deletes land on the `NotFound` branch; only the adopted attempt is reader-visible. The former `tryResumeIncompleteRound` and completion seals are gone.
+
+### 3.7 Part-manifest cleanup {#manifest-cleanup}
+
+Part manifests (`_manifests/...`) are deleted when their owning reference is removed. The part-manifest cleanup bundle records `ManifestId`s whose owner was removed and whose blob decrements were sealed into the generation. The delete is an exact-token `deleteExact` issued in the post-CAS cleanup phase (§3.6), gated by the same fold ordering as blob deletes — delete-after-adopted-decrements, one window now (the old recheck double-window skip is gone).
+
+A part manifest has at most one structural owner at any time (`SingleManifestOwner`). An owner removal that leaves no successor is a true removal → the manifest body is debris after its blob decrements are sealed.
+
+**Orphan part-manifest sweep** (pre-precommit debris): manifest bodies written before `PrecommitAdd` that were never activated have no owner. A bounded background sweep (one namespace, one eligible build prefix per round) enumerates and deletes them by exact token after confirming they are absent from the namespace's sealed owner view. Eligible build prefixes are those whose `writer_instance_id`/`build_sequence` the watermark confirms can no longer write (e.g. `min_active > build_sequence`). See `CaGc.cpp::orphanPartManifestSweep`.
+
+### 3.8 Trim {#trim}
+
+After the round CAS is durable, `trim` removes journal records from root shards that are at or below the committed `folded_cursor` (there is no separate completion seal any more; `trim` reads the fold seal). This is `INV_JOURNAL_COVERAGE`: only records durably represented in the committed snap generation may be trimmed.
+
+**Lazy-trim gate (B12):** trim does not compact a shard's journal on every pass it could. A shard is compacted only when the trimmable-event count reaches `gc_trim_min_events` (default 256; 0 = eager, always compact) **OR** the shard's encoded body size reaches `gc_trim_body_soft_limit` (default 8 MiB, a backstop that bounds journal growth even if the count gate never fires) — plus an explicit one-round maintenance-compaction bypass. Below both gates the shard is skipped so its listed token stays stable for the discovery token-diff Skip.
+
+**B140-dangle HISTORY:** the original implementation stored snap edges and `folded_cursor` as two separately-durable objects with no enforced coherence. A lease-steal between the snap write and the `gc/state` CAS could leave `folded_cursor` ahead of the actual snap coverage → `trim` over-trimmed → a live part's edge was permanently lost → in-degree undercount → data loss (`fsck dangling`). The fix (v2, `2026-06-18`) embeds the fold cursor inside the snap codec so the two are always co-durable. The `CaB140DangleMerge.tla` model proved the fix closes the dangle. See `06-tla-models.md §b140-dangle`.
+
+### 3.9 Resident-snap incremental GC and the durable-vs-resident cursor {#resident-snap-checkpoint}
+
+**Status: SUPERSEDED** (2026-07-02 ack-floor one-pass rewrite). This section originally described a
+resident, incrementally-folded `GcSnap` kept in the long-lived per-leader GC object and persisted only
+at a checkpoint gated by `gc_checkpoint_records` / `gc_checkpoint_rounds`, with two cursors
+(resident vs. durable `folded_cursor`) reconciled at checkpoint time. That machinery does not exist in
+the current implementation: there is no resident `GcSnap`, no checkpoint gate, and no
+resident/durable cursor split. Every round now does a full attempt-scoped fold plus exactly one
+`gc/state` CAS — see §3.2/§3.6 for the current fold + publish model and §5 for the attempt-scoped
+artifact layout that replaced incremental checkpointing.
+
+---
+
+## 4. Snap prune {#snap-prune}
+
+**Status: DONE** (B174 snap prune, `gc_snap_generations_to_keep`, soak-validated; `CaGcRootLocalPartManifestCore.tla` is unaffected — prune is pure space reclamation).
+
+### 4.1 Delete-time and retire-404 node pruning {#node-pruning}
+
+**Status: SUPERSEDED.** This section originally described `GcSnap::forget(kind, hash)` (P9), which
+removed a node from a persisted `known` candidate-node set the instant GC confirmed it was gone
+(delete-time prune + condemn-404 prune), to stop ~46k re-`HEAD`s per idle round against nodes GC had
+already deleted. That machinery — `GcSnap`, its `known` set, and `forget` — does not exist in the
+current implementation (superseded by the source-edge-set model, §3.3). The source-edge-set model has
+**no persisted node registry to forget from**: GC candidates are derived transiently, per generation,
+from the explicit zero-transition marker rows the fold emits (§3.3) — a blob that is not a
+zero-transition candidate this generation is never re-`HEAD`ed, with no separate forgetting step
+needed. The re-`HEAD`-storm problem P9 solved no longer applies under this model.
+
+### 4.2 Generation retention {#generation-retention}
+
+**Status: DONE** (B174).
+
+GC writes one `gc/gen//` tree per round (the full folded in-degree run set + seals) and historically never deleted old ones. Measured at 12h soak: `gc/` was 82% of pool storage (2822 generations retained, ~3.7 MiB/shard/generation), dwarfing actual data 8:1.
+
+**Mechanism:** `PoolConfig::gc_snap_generations_to_keep` (default 3; 0 = unlimited for forensics). The prune step runs inside each round's cascade, before the `gc/state` CAS. It advances a `GcState::snap_pruned_through` cursor forward in bounded batches (`MAX_PRUNE_GENERATIONS_PER_ROUND = 64`):
+
+```
+prune_floor = adopted_generation - keep
+for g in (snap_pruned_through, prune_floor]:
+ for each shard in [0, gc_shards):
+ deleteExact(gcGenKey(g, shard), head_token) // Absent/Replaced tolerated (idempotent)
+```
+
+**Safety:** pruning runs before the `gc/state` CAS, when `gc/state` still names the prior generation. `prune_floor = adopted_generation - keep` is strictly below the committed generation and `keep−1` above it. If the subsequent CAS fails (lease lost), the already-issued deletes are harmless (the winning leader's floor is even higher). The cursor is not durably advanced on CAS failure, so the next round re-attempts idempotently.
+
+**Scope:** prunes only GC's internal `gc/gen/` bookkeeping. Does NOT touch data objects (blobs, manifests, refs).
+
+### 4.3 Ref-aware retention + hand-off delete {#ref-aware-retention}
+
+**Status: DONE** (T0 reference-parent runs, 2026-07-02; `specs/2026-07-02-cas-gc-snapshot-streaming-design.md`). Reference-parent runs (§3.2a) mean the live adopted seal may reference a run object that physically lives under an **older** generation's key namespace. Wholesale generation prune (§4.2) must not delete such a run out from under the live seal.
+
+**Retention skip (generation granularity).** `pruneSupersededGenerations` takes the set of generations still referenced by the adopted seal's `blob_target_runs` and **skips** any such generation in its wholesale-delete loop (one log line per retained generation). The skip is at **generation granularity**: the prune cursor is a monotone high-water mark over every generation it *visits*, retained ones included. The loop starts at `snap_pruned_through + 1`, runs `g <= prune_floor`, and on a referenced generation does `continue` without deleting — but `g` still increments, and after the loop `snap_pruned_through = g − 1`. Consequence: once a retained generation is behind the cursor, the wholesale prune **never revisits it**.
+
+**Post-CAS hand-off delete.** Because the cursor advances past a retained generation, the run that finally REPLACES a shard's parent-ref must clean up the whole superseded generation, not just its one carried run object. In `runRegularRound`, for every parent ref whose `generation <= snap_pruned_through` (already behind the cursor) and whose generation no NEW live ref references, the round wholesale-deletes that generation's entire `gc/gen//` prefix **post-CAS** (best-effort; `NotFound` / `TokenMismatch` tolerated). A single-run hand-off would permanently leak the rest of that generation's prefix (fold seal, attempt subtree, retired/outcome sets, other shards' runs), so the whole-prefix delete is the one that matches the generation-granularity skip.
+
+**Leak-freedom.** Every formerly-referenced generation is eventually fully reclaimed by exactly one of two disjoint paths: (1) the ref moves off it **before** the cursor reaches it → the normal wholesale prune reclaims it when it ages past `keep`; or (2) the cursor passes it while it is still referenced (skipped) → the round that finally moves the ref off it hand-off deletes its whole prefix post-CAS. The single-crash window between the CAS and the hand-off strands at most one generation's prefix and is **fsck-visible best-effort**, like the rest of post-CAS cleanup (§3.6); a plain retry does not re-attempt it (the cursor already advanced), so fsck is the backstop — no permanent leak.
+
+---
+
+## 5. Attempt-scoped generations (concurrent-leader safety) {#attempt-scoped-generations}
+
+**Status: DONE** (concurrent-leader-leak fix, 2026-07-01; `CaGcRootLocalPartManifestCore.tla` `_sab_deposedleaderwritesfinalgen.cfg` is the negative-control that proves the old design was unsafe).
+
+### 5.1 Problem: orphaned-seal wedge and divergent-run corruption {#concurrent-leader-problem}
+
+Before this fix, a GC leader deposed mid-round would still finish its fold and write `fold_seal(N+1)` via `putIfAbsent` to a **final** `gc/gen//fold_seal` key — which succeeds because the write-once slot is empty — then fail the lease-guarded `gc/state` CAS. `snap_generation` stays at `N`, but `fold_seal(N+1)` is now durable. Every later round recomputes `N+1`, re-folds, hits the orphaned seal's divergent bytes, and throws `ABORTED "concurrent leader"` — **forever** (GC wedges).
+
+Worse: if a divergent leader's blob in-degree run also landed at the final key, the live leader's seal records a checksum of different bytes → wrong in-degree → either over-pin (leak) or over-delete (dangle, `INV_NO_DANGLE` violation).
+
+Root cause: **an irreversible, reader-visible artifact was published at a final generation key before the leader re-confirmed its authority.**
+
+### 5.2 Fix: accepted-attempt generation {#attempt-scoped-design}
+
+Each round mints exactly one **attempt** `a = lease.seq`. All of that round's artifacts live under an attempt-scoped prefix:
+
+```
+gc/gen//attempt//fold_seal
+gc/gen//attempt//blob_target//
+gc/gen//attempt//part_manifest_cleanup//
+gc/gen//attempt//retired//
+gc/gen//attempt//outcomes//
+```
+
+No artifact is written to a final `gc/gen//…` key outside an `attempt/` prefix before its adopt CAS. Two competing leaders for the same generation write to **different** attempt prefixes (`lease.seq` is unique per steal). Divergent-run and divergent-seal collisions are structurally impossible.
+
+**Round lifecycle (one pass, one CAS):**
+
+1. **Fold + merge (hidden).** The leader latches the ack floor, then writes all attempt artifacts under `gc/gen//attempt//` — the fold seal, the new in-degree snapshot runs, the retired-list runs, and the outcome logs from the three-cursor merge (§3.4). Nothing is reader-visible. Competing leaders write under their own `a`.
+2. **Pre-CAS deletes.** Executed only for entries **previously published** as `delete_pending` (§3.5) — justified by prior committed state, so idempotent across attempts.
+3. **The single adopt CAS.** One lease-token `gc/state` CAS sets `snap_generation = G_f`, `snap_attempt = a`, `retired_refs`, `round := K`, and the folded cursors atomically. Commits iff the leader still holds the lease; a deposed leader's CAS fails and its entire attempt is unadopted garbage. This replaces the old two CASes (fold-adopt + completion advance) and the intervening fence/retire CASes.
+
+**Artifact-class rule** (how concurrent writes to the same attempt key are reconciled):
+
+- **Deterministic artifacts** (in-degree runs, fold seal): byte-reproducible from their inputs. Any producer finding one present must find it **byte-equal → adopt** (deterministic replay) or **divergent → `CORRUPTED_DATA`** (fail-closed, impossible under correct operation).
+- **Observation-bearing artifacts** (retired-list runs, outcome logs): carry HEAD-observed tokens that two observers may legitimately differ on (a re-incarnation between their HEADs). **First-durable-write wins** (read-if-present). A later producer reads the present artifact and uses it as authority; it never recomputes-and-compares.
+
+**Publish-order invariant (subsumes `ViewableRound`):** the retired-list runs (and the snapshot runs) for round K are durable **before** the single `gc/state` CAS that publishes `round := K` and `retired_refs`. Because the refs and the round land in the *same* CAS, a reader that observes round K can always load the complete retired list of version K — there is no window where the round advances ahead of its retired-token set. This is the operational statement of `INV_ONLY_ADOPTED_VIEWABLE`; the old requirement of "advance the round only after every shard's retired set is durable" is now enforced by the single-CAS structure itself.
+
+**Crash-resume (no explicit resume step):** the former `tryResumeIncompleteRound` is removed. One CAS per round means a crash leaves only never-adopted attempt-scoped debris (pruned by retention). The next pass re-runs from scratch under a fresh attempt; already-executed pre-CAS deletes replay idempotently on the `NotFound` branch because they were justified by previously-published state.
+
+**Pruning** (space reclamation, not safety-load-bearing):
+
+1. *Generation retention* (§4.2) deletes `gc/gen//` wholesale when `g < retention_floor` — all attempts of obsolete generations.
+2. *Attempt orphan sweep* looks at `gc/gen//attempt/*`, skips `snap_attempt`, deletes attempts with `seq < min_live_lease_seq`. Bounded per round, fail-open, exact-token.
+
+**TLA+ reference:** `CaGcRootLocalPartManifestCore.tla` `_sab_deposedleaderwritesfinalgen.cfg` — a deposed leader writes artifacts to a final gen key; this **must** produce a counterexample (`INV_ONLY_ADOPTED_VIEWABLE` or an R0 safety violation). The design config holds all R0 invariants. See `06-tla-models.md §root-local-manifest`.
+
+---
+
+## 6. Orphan removal {#orphan-removal}
+
+### 6.1 Blob orphan removal {#blob-orphan-removal}
+
+**Status: DONE** (via the normal round — the three-cursor merge condemns zero-in-degree blobs and, after the ack floor passes them, graduates and deletes them, §3.4–3.6).
+
+A blob becomes an orphan when all manifests referencing it are removed (their edges are folded out). The fold emits explicit zero-transition markers for such blobs; the three-cursor merge (§3.4) condemns them (capturing the current token) and, once the ack floor passes them, graduates them to `delete_pending`; the pass after that deletes them exactly (§3.5–3.6).
+
+The only path to blob deletion is through this pipeline. There is no direct delete bypassing the merge + two-phase-graduation gates.
+
+### 6.2 Part-manifest orphan removal {#manifest-orphan-removal}
+
+**Status: DONE** (owner-driven cleanup in part-manifest cleanup bundle; orphan sweep for pre-precommit debris is DONE).
+
+A part manifest is orphaned when its owner (committed ref or precommit) is removed. The fold emits its `ManifestId` into the part-manifest cleanup bundle; the post-CAS cleanup phase (§3.6) issues `deleteExact` for it after the adopted decrements confirm no concurrent re-attachment.
+
+Pre-precommit manifest bodies (written before `PrecommitAdd`) have no owner. The orphan sweep handles them (§3.7).
+
+### 6.3 In-degree via source-edge set, not integer refcount {#source-edge-not-refcount}
+
+**REJECTED: integer refcount.** See §3.3. The key invariant is that **in-degree is never persisted as authority** — it is a transient quantity derived from the active source-edge set within a round. Persisting it as a separate counter and carrying it forward as `prior_count + Σ±1` is fragile (H1b: fence-window removal re-folded next round → underflow; H2: drop-then-repoint double-counts a −1). The source-edge set is idempotent by construction.
+
+The `CasBlobInDegree.h` API reflects this: `foldDeltasIntoGeneration` takes `std::vector` where each `BlobDelta{blob_hash, source_id, remove}` is an edge activation or removal, and the fold is an idempotent set merge.
+
+### 6.4 Additional reachability roots {#reachability-roots}
+
+Beyond committed part refs and precommits, two integration paths contribute reachability roots the
+MARK walk must honor (source `specs/content_addressed_shared_mergetree_design.md`):
+
+**Patch parts / lightweight deletes are first-class roots.** A patch is its own ref class: a **patch
+manifest** that references the base part's data columns (the columns it patches) plus its own delta
+blobs. A patch ref is a first-class reachability root, exactly like a regular part ref. While a patch
+is active, its ref keeps alive **both** its own delta blobs **and** the base blobs it references
+(transitive reachability through the patch manifest) — a base part's blobs cannot be swept while a
+live patch references them, even if the base part is otherwise superseded, provided the MARK walk
+treats patch manifests as roots and follows their references into the base. When a patch is
+materialized into the base (a merge), the patch ref is released like any other ref, and its
+uniquely-owned delta blobs become sweepable. (Residual-risk note: patch-on-content-addressed-base is
+flagged as not yet fully model-checked in the v3 design's residual-risk list.)
+
+**Stateless / ref-less reader GC fence.** The per-process `isSharedPtrUnique` (`use_count() == 1`)
+fence is a valid cross-node fence only for replicas that hold a `/parts` ref. It does **not** cover a
+stateless compute node that attaches by reading the catalog and registers no `/parts` ref — such a
+reader is invisible to the MARK union, and the owning replicas can release their refs after
+`old_parts_lifetime` (480 s) mid-SELECT, making the manifest/blobs unreachable and swept out from
+under the read. The fix is **ephemeral reader state included in the MARK union**: an ephemeral Keeper
+node (or equivalent) created at query start naming the parts/snapshot it reads, **auto-released on
+session end / crash** so a crashed reader cannot leak the pin (grain: one pin per part, or a single
+per-snapshot pin for huge scans). A coordinator-free deployment may instead size grace ≥ the longest
+ref-less SELECT, but the self-bounding ephemeral pin is preferred. This is **distinct** from the
+**lost-replica timeout** (a crashed-but-not-dropped replica's `/parts` refs outlive its process and
+pin blobs forever — a genuine leak needing a separate long-dead-replica reaping rule).
+
+> Note: under the current per-server-owned-namespace model (`01 §shared-blobs-per-server-trees`) each
+> server owns its own ref namespace, which narrows but does not eliminate the ref-less-reader window;
+> the ephemeral-pin design remains the documented mechanism for the shared-pool cross-node read case.
+
+---
+
+## 7. Ref removal and shard-object reclaim (D1) {#d1-registry-removal}
+
+**Status: TLA+ gate GREEN (2026-07-01); implementation phases 1–5 DONE** (Tasks 2–6: `ShardIncarnation` stamped on `RootShard`, `discoverUniverse` LIST-based over `cas/refs/`, `RootsRegistry` deleted, the newborn self-floor in `CasBuild.cpp`, and `Gc::reclaimDroppedShards` tombstone reclaim are all in code). `CaGcShardIncarnationCore.tla` `_design.cfg` holds `INV_NO_DANGLING` + `INV_NO_ORPHAN_EDGE` across 724,944 distinct states. See `06-tla-models.md §shard-incarnation`.
+
+### 7.1 Problem: `dropNamespace` never deregisters {#drop-namespace-problem}
+
+Before this fix, `dropNamespace` (`CasStore.cpp:1392-1452`) tombstoned each touched shard (appends removal `RootOwnerEvent`s, clears refs via `mutateShard`), but left the namespace in `gc/registry`. GC discovered from `registry.namespaces × shardsToVisit` and fenced the full cartesian product — so per-round GC cost was proportional to **every table ever created**, not the live ones. The empty shard objects and registry entries accumulated without bound (S30 scenario). A scalability defect, not a correctness bug.
+
+### 7.2 Rejected fixes {#d1-rejected}
+
+- **Writer deregisters at `dropNamespace`:** unsafe. The removal `RootOwnerEvent`s carry the `−1` blob-in-degree edges; GC folds them only in the shard's fold window `(folded_cursor, current_shard_version]`. If the namespace vanishes from discovery before GC folds that window, the `−1`s are lost → blobs keep phantom in-degree → permanent leak.
+- **GC infers "empty ⇒ retire":** a freshly-created table with no inserts yet is indistinguishable from a dropped empty one.
+- **Path-keyed cursor + delete-and-recreate:** ABA hazard. A recreated shard resets `shard_version` to 0; the old sealed `folded_cursor = K` silently skips events at versions `1..≤K` → lost edges → dangle or leak.
+
+### 7.3 Design: incarnation + LIST-based discovery {#d1-design}
+
+Two orthogonal coordinates replace all five overlapping counters:
+
+**Coordinate 1 — `incarnation`.** A durable, monotone, never-reused value stamped into a `RootShard` at its (re)creation and immutable for that object's life. Source: `(writer_epoch, build_sequence)` of the build that first creates the shard, both already durable-monotone-never-reused. On delete + recreate, the new create stamps a strictly-greater coordinate. **`INC-MONO`**: for a fixed `(ns, shard)`, every successive materialization carries a strictly greater incarnation.
+
+**Coordinate 2 — GC `round`.** The pool-global clock (unchanged). Writer-visible condemnation is a per-hash meta point-read (`BlobMeta.state`, §3.4), not a round-indexed floor a writer refreshes: a writer that observes `Condemned` via `loadMeta` re-uploads from source (`INV-1`) rather than referencing the blob. `BlobMeta.condemn_round` is carried in the meta body but is only an ABA guard against a stale spare-then-recondemn race, never a writer-visible floor. `round` itself stays pool-global and cannot be per-`server_root`.
+
+The fold cursor is keyed by `(ns, shard, incarnation)` instead of `(ns, shard)`. A recreated shard draws a strictly-greater incarnation → the old sealed cursor never matches → fold always processes a new incarnation from zero. ABA is closed by construction.
+
+**Discovery replaces the registry:** `discoverUniverse` becomes `LIST(cas/refs/)`. The registry (`gc/registry`, `RootsRegistry`, `rootsRegistryKey`) is deleted entirely. `listNamespaces` (used for FREEZE shadow-tree enumeration) migrates to a `LIST` over `cas/refs/`.
+
+**Newborn namespace ordering (no separate object):** the registry's irreducible role was ordering a first publish that dedup-references an existing blob against a concurrent last-drop-and-GC. D1 replaces this with the existing **precommit machinery**: a first publish creates the ref-shard object at its canonical path carrying a precommit binding (with the fresh incarnation). The shard is LIST-discoverable immediately. The precommit gate (fold barrier + watermark reclaim) provides create-ordering without a separate pending-newborns object. **THM-NO-RETURN (central):** with the registry removed, for a first publish that dedup-references blob `B`, either (a) the newborn shard is in GC's LIST universe and GC folds its precommit `+1` before condemning `B`, or (b) the writer's meta point-read (`loadMeta`) observes `B` as `Condemned` and re-uploads from source (`INV-1`) rather than referencing the condemned blob. No interleaving can dangle a live ref. This is the theorem the TLA+ gate proved.
+
+**Shard-object reclaim:** `dropNamespace` appends, as its last journal event, an explicit **tombstone** marker (`RootOwnerEvent` variant meaning "namespace dropped, no owner"). GC, during fold, when a shard is empty (no refs), its last journal event is the tombstone, and its journal has been folded past a completed fence, issues `deleteExact(rootShardKey(ns, shard), token)`. Any writer append (revive) changes the token → `deleteExact` returns `TokenMismatch` → the delete is refused; the object survives with new content. An idle-but-live shard (all parts dropped, table alive) has no tombstone → not reclaimed → still discovered. The tombstone is the drop signal.
+
+**TLA+ validation:** `CaGcShardIncarnationCore.tla` `_design.cfg`. Three negative controls confirm irreducibility: `SabotageNewbornNoFloor` (round self-floor is irreducible), `SabotagePathKeyedCursor` (incarnation is irreducible — ABA without it), `SabotageDeleteBeforeFold` (fold-before-reclaim ordering is irreducible). **The one-vs-two coordinates question is answered: two** — neither coordinate alone suffices.
+
+---
+
+## 7a. GC baseline guard + raw rebuild {#gc-rebuild}
+
+**Status: DONE (2026-07-03).** Spec: `2026-07-03-cas-gc-rebuild-design.md`. Answers the question every
+prior section assumes away: what happens when `gc/state` itself is lost or corrupted (an operator
+`mc rm`, a botched migration, backend corruption) over a pool whose per-shard journals have already
+been **trimmed** past the point a from-scratch fold could recover? Without a guard, a fresh
+`runRegularRound` would fold only the surviving journal tails, under-count every blob referenced by
+trimmed history, and mass-delete live data — the one class of event `INV_NO_LOSS` cannot survive
+without an explicit baseline.
+
+**The guard (Part 1 — ships ahead of the rebuild, fails closed by default).** Before folding, the
+round classifies `gc/state` as healthy or not: healthy means it decodes AND, when it claims a
+baseline (`snap_generation > 0`), the fold seal for that `(generation, attempt)` is present AND every
+run/retired-list it references is HEAD-present. A `gen == 0` state additionally requires that **no**
+shard journal proves trimmed history (`journal.empty() && shard_version > 0`, or a surviving
+journal's earliest record has `transition_version > 1`) — a fresh-pool-shaped state sitting over
+trimmed journals is exactly the disaster this guard exists to catch, not a legitimately-empty pool.
+Any journal proving trim with no adopted seal under live state throws `CORRUPTED_DATA` and aborts
+the round — never a silent under-count.
+
+**The rebuild algorithm (Part 2 — `Gc::rebuildBaseline(bool force)`).** Reuses the round's own
+bricks; no new scanner or merge class:
+
+1. **Health check** (the same classification above) — refuses with `performed=false` unless `force`
+ is set, so an operator never discards live bookkeeping by accident.
+2. **Lease** — `acquireOrRenewLease` (Part 1's protocol, unchanged): refuses if another leader (a
+ regular round, or a concurrent rebuild) currently holds it.
+3. **Numbering** — mint a `generation` strictly above every surviving `gc/gen//` prefix (so
+ `putDeterministicArtifact` can never collide with debris from the lost era).
+4. **Universe replay** — `discoverUniverse` (the same `LIST(cas/refs/)` sweep the regular round
+ uses) drives, per shard: committed refs (`root.refs` is the authoritative committed state) →
+ `foldManifestEdges(+1)`; live precommits (journal replay: apply `old_binding` erases / `new_binding`
+ inserts, keep what remains `Precommit`) → edges when the body is present, else clamp the shard's
+ cursor below that transition (the fold-barrier semantics from §3.2).
+5. **`foldDeltasIntoGeneration`**, attempt-iterated per gc-shard bucket (empty priors, budget-bounded
+ memory — the same primitive the regular round's three-cursor merge uses), builds the rebuilt
+ in-degree runs and the fold seal.
+6. **Zero-edge condemnation** — a blob with a rebuilt in-degree of zero has no transition to trigger
+ the normal condemn path (the fold discovers candidates by transition-to-zero, and a rebuild-time
+ zero-edge blob never transitions). The rebuild condemns it directly: every physically-present blob
+ outside the rebuilt edge set enters the retired list with a **head-captured exact token at the
+ minted round**. Graduation still waits for every mount to ack past that round — the normal
+ two-phase floor (§3.5), not a shortcut.
+7. **Round mint** — `round = max(heartbeat-floor max_ack, max shard fence_round seen, the old
+ state.round, max_gen) + 1`. Minting above every surviving mount ack (not just the old round) closes
+ a skew hazard: a stale mount's ack could otherwise float a fresh condemnation past its own floor
+ before that mount re-observes the rebuilt list.
+8. **Single CAS** — one `gc/state` write publishes the new `(round, generation, attempt, retired_refs)`
+ atomically, same as a regular round. A refused rebuild (any step above) writes nothing; `gc/state`
+ is untouched.
+
+**Over-protection, not under-protection (design delta 2 — a deliberate, bounded leak class).** A
+build alive across trim has no journal evidence (the evidence was trimmed); its manifests look
+unowned. The rebuild cannot tell "unowned because dead" from "unowned because trimmed-but-live", so
+it includes edges for every **unowned** manifest that is **not provably build-dead** (the live
+watermark fact) — i.e. it over-protects. An unowned manifest that later dies with no journal evidence
+leaks its blob's edges until a future rebuild reclaims it. This is documented, bounded (one
+rebuild-to-rebuild window, `fsck`-visible as `unaccounted`), and strictly preferred to the
+alternative: `INV_OVER_COUNT_ONLY` — over-count, never under-count — is the invariant a
+disaster-recovery tool must never violate.
+
+**Refusals (all fail-closed, all write nothing):**
+- gc/state and every referenced artifact are already healthy (bypass with `FORCE`).
+- another GC leader holds the lease.
+- a **committed** ref names a missing or invalid manifest body — real data loss the rebuild refuses
+ to bless; the refusal names the offending owner so `fsck` forensics can follow up before anyone
+ reaches for `FORCE`.
+
+**The two command forms** (§8/§testing tie-in: `08-testing-and-soak.md §gc-rebuild-runbook`):
+- `SYSTEM CONTENT ADDRESSED GC REBUILD [FORCE] []` — on any live replica; synchronous, one
+ rebuild attempt, throws with `report.refusal` on refusal, `LOG_INFO`s the report's counters on
+ success.
+- `clickhouse-disks --disk ca-gc-rebuild [--force]` — when no server is up; requires the disk to
+ be opened `true` (same rule as `fsck`/`ca-gc-dryrun`: this tool must never
+ claim a live server's mount), prints the report as `key=value` pairs, exits nonzero on refusal.
+
+---
+
+## 8. Safety invariants {#safety-invariants}
+
+The following invariants are the formal safety obligations. Sources: `CaGcRootLocalPartManifestCore.tla` (primary big model), supplemented by focused models for specific sub-problems.
+
+| Invariant | Meaning | Proved in |
+|-----------|---------|-----------|
+| `INV_NO_LOSS` | No committed ref to content becomes unreadable | `CaGcRootLocalPartManifestCore.tla` |
+| `INV_NO_DANGLE` | No live edge addresses a deleted object | `CaGcRootLocalPartManifestCore.tla` |
+| `INV_NO_RETURN` | A dead token (exact-token-deleted object) never returns | `CaGcRootLocalPartManifestCore.tla` |
+| `INV_JOURNAL_COVERAGE` | Trim only removes records at or below the committed `folded_cursor` | `CaGcRootLocalPartManifestCore.tla` |
+| `INV_OVER_COUNT_ONLY` | GC state may over-count, never under-count | `CaGcRootLocalPartManifestCore.tla` |
+| `INV_ONLY_ADOPTED_VIEWABLE` | Only artifacts under `(snap_generation, snap_attempt)` are consulted by any decision path | `CaGcRootLocalPartManifestCore.tla` (attempt-scoped gen config) |
+| `INV_NO_DANGLING` (D1) | No live ref addresses a deleted shard object | `CaGcShardIncarnationCore.tla` |
+| `INV_NO_ABA` (D1) | Incarnation-keyed cursors never skip a new incarnation's events | `CaGcShardIncarnationCore.tla` |
+| `INV_INDEG_NONNEG` | Blob in-degree never goes negative (source-edge set cannot) | `CaGcIndegRefoldCore.tla` `_fix.cfg` |
+
+**Ordering bias:** all ordering choices (write `+` before ref; write ref before session removal; remove ref before writing `−`) are biased to **over-count** on crash, never under-count. An over-count delays reclamation; an under-count could delete a live object. This is `INV_OVER_COUNT_ONLY`.
+
+---
+
+## 9. Concurrent-leader safety summary {#concurrent-leader-summary}
+
+Three mechanisms together make the round split-brain-safe with no additional locking:
+
+1. **Attempt-scoped generations (§5):** unadopted artifacts are invisible to every decision path. Two concurrent leaders for the same generation write to different attempt prefixes. Only the fold-adopt CAS #1 selects the authoritative attempt; any leader whose adopt fails has its entire fold attempt as inert garbage.
+
+2. **Exact-token delete:** `deleteExact(key, observed_token)` uses `If-Match`. A re-incarnation between `retire` and `deleteExact` changes the token → 412 → `Replaced` outcome → no delete. A deposed leader's delete is idempotent: the winner observes `NotFound`/token-mismatch and records the outcome.
+
+3. **GC lease (work-dedup only):** the lease prevents redundant parallel work and reduces S3 churn, but is not a safety primitive. The TLA+ proof makes no leadership uniqueness assumption. Any number of concurrent GC leaders produce correct outcomes.
+
+**REJECTED: `gc_lock` (in-process mutex).** The original PoC serialized every commit against the whole sweep with a shared in-process mutex. This blocked writers and limited GC to one process. The current design replaces it with the above three mechanisms; the mutex has been removed.
+
+---
+
+## 10. S3 budget {#s3-budget}
+
+GC is the dominant S3 cost center on a pool with steady ingest/drop churn. Detailed per-phase breakdown is in `07-s3-budget.md`. Key figures:
+
+| Phase | Dominant cost |
+|-------|--------------|
+| Heartbeat floor | O(servers) GETs (LIST `gc/server-roots/` + GET each); rare fence-out PUT |
+| Discover | 1 LIST sweep of `cas/refs/` (token-diff: skip GET if token unchanged) |
+| Fold + three-cursor merge | 1 GET per changed root shard; 1 GET per prior retired run; 1 HEAD per newly-condemned candidate; 1 PUT per in-degree run + 1 PUT per retired run + 1 PUT fold seal |
+| Deletes | `deleteExact` per previously-published `delete_pending` entry (free on AWS) |
+| Snap prune | `snap_shards × (HEAD + DELETE)` per pruned generation (steady state: 1 generation/round) |
+| Round CAS | 1 `gc/state` CAS-PUT (the only CAS per round) |
+| Heartbeat | merged into the writer's mount-lease beat (`03 §merged-heartbeat`): +1 `gc/state` GET, −1 PUT per beat |
+
+The ack-floor round is **O(delta) + O(servers)** requests plus the single LIST sweep — no O(universe) GET or PUT phase exists. This is the whole point of replacing fence+recheck; the per-round cost dropped from ~2.4 M requests to ~2 000–3 000 at the 100k-tables example. See `07-s3-budget.md §gc-budget` for the full breakdown.
+
+**Node-forgetting impact (SUPERSEDED):** this line originally credited a `GcSnap::forget` node-forgetting mechanism (P9) with eliminating ~46k re-`HEAD`s per idle round against previously-deleted candidates. That mechanism does not exist under the current source-edge-set model (§3.3, §4.1) — there is no persisted node registry to prune, so the re-`HEAD` storm it solved does not arise in the first place (candidates are derived transiently from zero-transition markers each generation, §3.3). The `gc/` prefix's reduction from 82% of pool storage to a bounded sawtooth is generation retention (§4.2, B174), not node-forgetting.
+
+---
+
+## 11. Testing {#testing}
+
+**Status: comprehensive gtest suite exists; soak S04/S33/S03/S11 all drain to `fsck unreachable=0, dangling=0, gc_residual=0` with the current binary.**
+
+Key test files:
+- `src/Disks/tests/gtest_cas_gc_ack_floor.cpp` — the `CasGcAckFloor` protocol suite (condemn → pending → delete pipeline, `NoOpRoundDoesNotMutateRefShards`, stale-ack-holds-the-floor, pre-ack publish spares, expired-mount fence-out, recreated-blob-delete-is-`TokenMismatch`-ok); ported from the old `gtest_cas_gc_fence_recheck.cpp`
+- `src/Disks/tests/gtest_cas_mount.cpp` — `CasHeartbeatFloor` (`computeHeartbeatFloor` classification + fence-out), `CasHeartbeat` (merged keeper)
+- `src/Disks/tests/gtest_cas_store.cpp` — `CasStoreBeat` (ack advances only after view load; drain blocks ack while a mutation is in flight; a `gc/state` read failure leaves the ack unchanged)
+- `src/Disks/tests/gtest_cas_blob_indegree.cpp` — `CasThreeCursorMerge` (spare / graduate→pending / condemn rules, the `condemn_round = min_ack−1 / = min_ack` boundary)
+- `src/Disks/tests/gtest_cas_gc_undercount_repro.cpp` — H1b regression; GREEN after source-edge-set fix
+- `src/Disks/tests/gtest_cas_gc_snap.cpp` — node-forgetting / snap-prune unit tests
+- `utils/ca-soak/scenarios/` — adversarial scenario suite (S01–S35); S30/S34/S35 test D1 create/drop churn; S33 tests concurrent GC leaders
+
+**Known blind spot:** tests running with `gc_shards=1` (the default) do not exercise sharded bugs. All new fold/discovery tests added for D1 must also run with `gc_shards > 1`.
+
+---
+
+## 12. DONE / TODO / REJECTED / DESIRABLE {#status}
+
+| Area | Status | Note |
+|------|--------|------|
+| One-pass ack-floor round (floor → three-cursor merge → single CAS) | **DONE** | `cas-gc-ack-floor-fence`; soak-validated live on AWS S3, 2026-07-03 |
+| Heartbeat ack floor + expired-mount fence-out (`computeHeartbeatFloor`) | **DONE** | Round's only clock (injected `now_ms_fn`) |
+| Merged heartbeat (mount lease ∪ build watermark, `observed_gc_round` ack) | **DONE** | `WatermarkKeeper` removed; −1 PUT/beat |
+| Two-phase graduation (`delete_pending`) | **DONE** | Zombie-safe pre-CAS deletes; deletion lags condemn by one pass |
+| Clamp-suppressed passes (`suppress_destructive`) | **DONE** | 2026-07-03 night-soak safety fix; no graduation/redelete while any shard is clamped (§three-cursor-merge) |
+| `CaGcAckFloorCore.tla` + `CaGcAckFloorZombie.tla` | **DONE** | 7 sabotages + order invariant; `delete_pending` proved load-bearing |
+| Exact-token delete (single content-delete site) | **DONE** | |
+| Source-edge-set in-degree (H1b fix) | **DONE** | 2026-07-01; faithful to big TLA+ model |
+| Per-round all-shard fence (`fence_round` bump, `fence_version`) | **REJECTED** | Replaced by ack-floor; was O(universe) CAS-PUT/round |
+| Fold-through-fence recheck phase | **REJECTED** | Replaced by the three-cursor merge; was O(universe) GET + quadratic `inDegreeInGeneration` |
+| Ack-floor soak validation (spare-then-recondemn under kill; SIGSTOP holds floor; O(delta) request guard) | **TODO** | On `utils/ca-soak`; the round's regression guard against reintroducing a universe sweep |
+| Delta-runs + compaction for the snapshot (bytes O(edges)/pass) | **DESIRABLE** | Next dominant cost; deferred O(buffer) streaming work |
+| Attempt-scoped generations (concurrent-leader safety) | **DONE** | 2026-07-01; `_sab_deposedleaderwritesfinalgen` confirmed |
+| Advisory heartbeat (B160, false-steal fix) | **DONE** | `CaGcLeaseCore.tla` proved |
+| Snap prune — node forgetting (P9) | **SUPERSEDED** | `GcSnap::forget`/`known` set removed with the source-edge-set model (§3.3); no persisted node registry to forget from (§4.1) |
+| Snap prune — generation retention (B174) | **DONE** | 3 gen default; sawtooth gc/ storage |
+| Part-manifest cleanup (owner-driven) | **DONE** | |
+| Orphan part-manifest sweep (pre-precommit debris) | **DONE** | Bounded per round |
+| Shard incarnation (D1 phase 1) | **DONE** | `ShardIncarnation` stamped on `RootShard` (Task 2) |
+| LIST-based discovery + registry deletion (D1 phase 2) | **DONE** | `discoverUniverse` over `cas/refs/`; `RootsRegistry` deleted (Task 3–4) |
+| Newborn = precommit-shard (D1 phase 3) | **DONE** | Newborn self-floor in `CasBuild.cpp` (Task 5) |
+| Shard tombstone reclaim (D1 phase 4) | **DONE** | `Gc::reclaimDroppedShards` (Task 6) |
+| S30 soak validation (D1 phase 5) | **DONE** | Task 6 commit message cites the S30 scenario |
+| Distributed `gc_shards > 1` parallel GC | **DESIRABLE** | Attempt-scoped gen is its prerequisite; shard claim/scheduler not yet built |
+| Integer refcount persisted artifact | **REJECTED** | H1b underflow; replaced by source-edge set |
+| `gc_lock` in-process mutex | **REJECTED** | Blocks writers; replaced by exact-token + attempt-scoped gen |
+| TLA+ leadership uniqueness assumption | **REJECTED** | Safety proof makes no leadership assumption; see `INV_ONLY_ADOPTED_VIEWABLE` |
+| Keeper as durable state for GC | **REJECTED** | Keeper is optional accelerator only; all GC durable state is in S3 |
+| Registry as authority for namespace universe | **REJECTED** → D1 | Monotone growth; being replaced by LIST-based discovery |
+| "empty ⇒ deregister" inference | **REJECTED** | Indistinguishable from idle-but-live namespace |
+| Path-keyed fold cursor without incarnation | **REJECTED** | ABA hazard on drop+recreate |
+
+## Absent-at-HEAD is not durably-absent {#absent-at-head}
+
+Named rule (2026-07-03 forensics). An object store may answer `HEAD -> 404` for an object that
+exists: the night-soak clamp era was caused by RustFS returning FALSE 404s on live manifest bodies
+while its metacache was degraded by the rustfs#3231 leak storm (proof: the clamping manifests'
+`+1` folds read the bodies seconds BEFORE the clamps; no deletion event exists for them; the
+release fold at the storm's end read them again). Consequences, both already enforced:
+
+- the FOLD treats an unreadable body as a **clamp** (cursor freeze), never a guess and never a
+ wedge (`gc_fold_clamp` event carries namespace/shard/manifest/reason since 2026-07-03);
+- a clamped pass is **destruction-suppressed** — no graduations, no pending deletes — because
+ landed events behind a clamp are exactly the references a false 404 would otherwise let the
+ floor delete over.
+
+The rule generalizes: no DESTRUCTIVE decision may treat a single point-in-time absence observation
+as durable truth; only exact-token deletes over entries that survived the full condemn -> floor ->
+graduate pipeline qualify. (Upstream: false-404-under-load is worth a RustFS report of its own,
+tied to rustfs#3231 degradation.)
diff --git a/docs/superpowers/cas/05-formats-and-backend.md b/docs/superpowers/cas/05-formats-and-backend.md
new file mode 100644
index 000000000000..a4022543b1a2
--- /dev/null
+++ b/docs/superpowers/cas/05-formats-and-backend.md
@@ -0,0 +1,654 @@
+---
+description: 'Object kinds, the one-header envelope format, codecs, deterministic-artifact upload, layout keys, the Cas::Backend abstraction with exact-token deletes, the rustfs testbed, AWS/GCS/Azure support status, and schema-evolution stance for the CAS MergeTree feature.'
+sidebar_label: 'Formats and backend'
+sidebar_position: 5
+slug: /superpowers/cas/formats-and-backend
+title: 'CAS MergeTree — Formats and Backend'
+doc_type: 'reference'
+---
+
+# CAS MergeTree — Formats and Backend {#title}
+
+**Status:** covers generation-1 decisions: the envelope format (`DONE`, blob only — see below), the
+encoding taxonomy and proto rename (`DONE`), `putDeterministicArtifact` (`DONE`, GC-internal artifacts
+only), the `Backend` seam (`DONE`), layout keys (`DONE`); the standalone `Tree` object kind, its
+inline-data section, and the Merkle `treeId` rule (**removed 2026-07-03** — superseded by
+the rev. 15 `PartManifest` redesign, then excised from the code entirely), schema-evolution rollout
+machinery (`TODO`/deferred — Part IV); GCS/Azure binding (`TODO`); Merkle `treeId` tree-layer
+intermediate object (`REJECTED`).
+
+Sources: `specs/2026-06-07-ca-merkle-store-design.md`, `specs/2026-06-08-ca-merkle-store-requirements.md`,
+`plans/2026-06-24-cas-2b-envelope-one-header.md`, `plans/2026-06-24-cas-2a-merkle-tree-id.md`,
+`specs/2026-06-26-cas-proto-rename-design.md`, `specs/2026-06-24-cas-schema-evolution-framework-design.md`,
+`specs/2026-06-15-ca-rustfs-overwrite-leak-mitigation-design.md`,
+`specs/2026-06-15-ca-head-after-put-etag-design.md`, `specs/2026-06-19-ca-vfs-contract.md`.
+Grounded against `Core/CasEnvelope.{h,cpp}`, `Core/CasBackend.h`, `Core/CasLayout.h`,
+`Core/CasToken.h`, `Core/CasObjectStorageBackend.{h,cpp}`.
+
+---
+
+## Object kinds {#object-kinds}
+
+**Status: DONE, but see removed-Tree note below**
+
+Three object kinds were designed; a fourth (`Pack`) was removed (see Rejected section). **As of the
+rev. 15 root-local part-manifest redesign, `Tree` was no longer produced on the write path**, and it
+was excised from the code entirely on 2026-07-03: `CasManifestCodec.h` states explicitly "No
+`Subtree`: there are no nested tree objects in this design; a directory is a path prefix, not a
+placement." Part contents are described by a `PartManifest` (protobuf; see `Core/CasManifestCodec.h`)
+whose entries are either `EntryPlacement::Inline` (bytes embedded in the manifest body) or
+`EntryPlacement::Blob` (content-addressed blob). `ObjectKind::Tree` and the `CATR` magic are **removed
+2026-07-03** from `CasEnvelope.h`/`CasFormat.h` — `ObjectKind` now has `Blob` as its sole enumerator.
+`Cas::Layout` correspondingly has no `treeKey` builder; `objectKey` only ever addresses `Blob` (the
+`ObjectKind::Tree` LOGICAL_ERROR thrower it used to carry was removed along with the enumerator).
+
+| Kind | Mutable? | Content-addressed? | Encoding |
+|------|----------|--------------------|----------|
+| **Blob** | no | yes | 256-B header (`CABL`) + raw file bytes |
+| **Tree** (removed 2026-07-03 — see above) | no | yes | 256-B header (`CATR`) + catalog + inline-data section |
+| **Part manifest** (`PartManifest`, part of a root-shard manifest body) | no | no (referenced by ref, not by content hash) | protobuf — `clickhouse.cas.format` package |
+| **Root shard** (manifest/journal) | yes | no | protobuf — `clickhouse.cas.format` package |
+| **GC/control objects** (`gc/state`, mount/heartbeat, `pool-meta`, retired list, etc.) | yes | no | protobuf |
+
+The decisive split: **content-addressed ⇒ binary; mutable ⇒ protobuf.** JSON was used for small
+control objects in earlier iterations and is now fully abandoned (`specs/2026-06-24-cas-schema-evolution-framework-design.md`
+§ "JSON abandoned"). The JSON codec family (`JsonObjectWriter`, `require*`, `parseJsonDocument`,
+`tolerateUnknownKeys`) is deleted; every mutable object uses the protobuf path.
+
+Rationale for binary on the hashed path: blobs are raw file bytes (cannot be protobuf's 2 GB
+limit or overhead); trees are the hot read path and embed raw inline bytes; canonical serialization
+is required for reproducible identity. Protobuf's skip-unknown is the additive-evolution engine on
+the mutable side, where identity-stability is not required.
+
+---
+
+## Encoding taxonomy and the proto rename {#encoding-taxonomy}
+
+**Status: DONE**
+
+One normative proto file: `src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Core/Proto/cas_format.proto`,
+package `clickhouse.cas.format` (renamed from `cas_root_shard.proto` / `DB.Cas.Proto` — see
+`specs/2026-06-26-cas-proto-rename-design.md`). The rename is behavior-preserving: protobuf wire
+format is independent of package/message names (field numbers + wire types only). C++ consumers
+alias `namespace Proto = ::clickhouse::cas::format;` to leave call sites unchanged.
+
+The CMake target name `clickhouse_cas_proto` is unchanged.
+
+Every object in the pool carries a self-describing header with three invariants:
+
+- **magic** — 4-byte ASCII type tag (`CABL` blob, `CARS` root-shard, `CAPM` pool-meta,
+ `CAGT` gc-state, `CAHB` heartbeat/mount, `CAGO` gc-outcomes, `CAMT` per-hash
+ freshness meta — see [§per-hash freshness meta](#per-hash-meta)). Sanity check only — no CRC;
+ integrity comes from S3 ETag and content-addressing for hashed objects.
+ (`CATR` (tree) was removed 2026-07-03 — see the removed-`Tree` note above. The standalone `CAWM`
+ watermark object was removed when the build watermark merged into the mount-lease heartbeat — see
+ `03 §merged-heartbeat`; its `min_active` field now lives in the `CAHB` mount body — the `observed_gc_round`
+ ack field was later removed in v3, `cas_format.proto` reserved 10.)
+- **`writer_version`** — forensic: which build wrote this object.
+- **`compatibility_version`** (`min_reader_version` in earlier plans) — functional write-down-to-floor:
+ a reader must fail-closed (`UNKNOWN_FORMAT_VERSION`) if `compatibility_version > G_BUILD`.
+
+For **hashed objects** (blob/tree) the trio lives in the binary envelope header described below.
+For **mutable objects** a `CasHeader` protobuf message (field 1 of each object message) carries the
+same three things with no binary prefix, making the objects fully `protoc`-decodable.
+
+---
+
+## The one-header envelope format {#envelope}
+
+**Status: DONE** (`plans/2026-06-24-cas-2b-envelope-one-header.md`)
+
+Every blob and tree object begins with the same **256-byte fixed-length header** (`blob_header_len = 256`),
+padded uniformly for both kinds. A constant payload offset means every blob in the pool starts at
+the same offset — a constant shift to locate content, no per-object header parse on the read path.
+
+### Binary core layout (94 bytes, hole-free) {#envelope-core}
+
+```
+offset size field
+[0,4) 4 magic "CABL" (blob) or "CATR" (tree) — encodes kind; no separate kind byte
+[4,6) 2 writer_version u16 LE
+[6,8) 2 compatibility_version u16 LE (formerly min_reader_version)
+[8,9) 1 hash_algo u8 (1 = cityHash128)
+[9,10) 1 flags u8 (FLAG_HAS_CRITICAL_EXTENSION = 0x01)
+[10,14) 4 header_len u32 LE
+[14,22) 8 logical_size u64 LE
+[22,38) 16 logical_hash u128 LE
+[38,54) 16 domain_id u128 LE
+[54,70) 16 incarnation_tag u128 LE
+[70,86) 16 build_id u128 LE
+[86,94) 8 header_hash u64 LE (CityHash64 over [0,94) with this field zeroed)
+```
+
+`CORE_HEADER_LEN = 94`, `HEADER_HASH_OFFSET = 86`. The core is **never grown**: new fields go into
+the `[94, header_len)` TLV extension area. `header_len` is padded to an 8-byte multiple;
+`pad_to_header_len = 256` for every object in the pool (set by `CasBuild::uploadFromSource`).
+
+The **incarnation zone** (`domain_id`, `incarnation_tag`, `build_id`) is **excluded from
+`logical_hash`**: the header may differ between incarnations of the same logical object, and a test
+asserts that `logical_hash` is unchanged when the incarnation zone varies. `logical_hash` is
+`cityHash128` of the **payload** for blobs, or the Merkle tree id for trees (see §5 below).
+
+Decode validation: bad magic → `CORRUPTED_DATA`; wrong kind for the expected magic → `CORRUPTED_DATA`;
+`compatibility_version > G_BUILD` → `UNKNOWN_FORMAT_VERSION`; `header_hash` mismatch → `CORRUPTED_DATA`.
+An unknown non-critical TLV is skipped; an unknown critical TLV → `UNKNOWN_FORMAT_VERSION`.
+
+The prior format had a `CHCA` magic with a separate `kind` byte and a zero-pad word at `[12,16)`.
+Both were reclaimed by shifting fields left to form the 94-byte hole-free core. The old `FORMAT_VERSION`
+byte is replaced by the `writer_version`/`compatibility_version` pair.
+
+Code: `Core/CasEnvelope.h`, `Core/CasEnvelope.cpp`.
+
+### Why the incarnation tag lives in the body, not provider metadata {#incarnation-in-body}
+
+The `incarnation_tag` (and the rest of the incarnation zone) is carried **in the object body**, not
+in provider metadata, and the choice is not close for a CAS meant to run across object stores
+(source `incarnation-tagged-cas.md`). On S3, user-defined metadata is **write-once per upload** and
+changing it **requires copying the object**; S3 documents an **8 KB PUT-header cap** and that the
+**ETag reflects the object contents, not its metadata**. A body-carried incarnation tag therefore
+gives a backend-independent way to force a new physical incarnation — a genuinely different backend
+token (ETag) — while keeping the logical content address stable. Putting the tag in metadata would
+change no bytes, would not reliably change the S3 overwrite/ETag token, would require a copy to
+modify, and would be subject to provider header limits. GCS `generation`/`metageneration` and Azure
+ETag concurrency are the per-provider alternatives; the body-carried tag is the portable primitive
+that works uniformly. The incarnation zone is excluded from `logical_hash` (see above) precisely so
+that minting a fresh tag re-incarnates the physical object without rekeying it.
+
+---
+
+## Codecs and determinism {#codecs-and-determinism}
+
+**Status: DONE**
+
+### `putDeterministicArtifact` — byte-reproducible sealed artifacts {#deterministic-artifact}
+
+A blob or tree produced by a given build is **deterministic**: same logical content → identical
+payload bytes → identical `logical_hash` key. This is the core dedup property. Determinism
+requirements:
+
+- **Blobs** — raw file bytes; the hash is the pool-wide streaming content-hash convention: chunked
+ `CityHash128` chained via `HashingWriteBuffer`/`HashingReadBuffer` per `DBMS_DEFAULT_HASHING_BLOCK_SIZE`
+ = 2048-byte blocks (`Core/CasBuild.cpp`, `poolContentHash`), hex-formatted via `getHexUIntLowercase` —
+ **not** a one-shot `CityHash128` of the whole payload; a one-shot hash diverges from the streaming hash
+ for any payload larger than one hash block (this caused a live false `CORRUPTED_DATA` in the 2026-07-03
+ soak). Identity is independent of the header incarnation zone.
+- **Trees** — the catalog is sorted by entry name (byte-wise) and uses a fixed field layout with no
+ nondeterministic fields (`mtime`, `uid`, and any host-dependent attribute are excluded). Two trees
+ with the same logical content therefore hash identically, enabling recursive directory dedup.
+- **Canonical serialization** — same rule as Git tree objects: entries sorted by name, fixed field
+ order, explicit `kind` byte for domain separation, no optional fields in the identity-sensitive
+ input.
+
+Blob upload (`Build::putBlob`, `Core/CasBuild.cpp`) derives the content hash **before** serializing/
+uploading the payload (the caller supplies the already-hashed `BlobId`) and uses it as the object key.
+On `putIfAbsent`/`putIfAbsentStream` the object-storage backend returns `PreconditionFailed` if the key
+already exists — a dedup hit — and no bytes are transferred. This is why the write path is
+`O(new-content)`, not `O(all-content)`.
+
+`putDeterministicArtifact(Backend&, key, bytes)` (`Core/CasBlobInDegree.cpp`) is a distinct, narrower
+helper: it is used only for **GC-internal** deterministic artifacts (fold seals, run files) where a
+replay is expected to reproduce byte-identical output. It calls `putIfAbsent`, and on
+`PreconditionFailed` re-`get`s the existing bytes and throws `CORRUPTED_DATA` if they differ from what
+was about to be written (divergent bytes at a supposedly-deterministic key); a byte-equal collision is
+treated as an idempotent no-op adoption. It is not part of the blob/tree content-addressed write path.
+
+**Blob-hash-over-payload domain:** the blob hash covers only `[header_len, EOF)` — the raw payload
+bytes — not the header. A header TLV or incarnation-zone change never rekeys a blob. A test in the
+gtest suite (`CasEnvelope.IncarnationZoneDoesNotAffectPayloadOrId`) asserts this.
+
+### Tree codec and inline-data section {#tree-codec}
+
+**Not implemented as of 2026-07-03:** the standalone `Tree` object kind described in this section was
+superseded by the rev. 15 root-local part-manifest redesign. There is no `CasTreeCodec.{h,cpp}` in the
+tree; part contents are described by `PartManifest` (`Core/CasManifestCodec.h`), whose entries carry
+`EntryPlacement::Inline` or `EntryPlacement::Blob` directly — see `§object-kinds` above.
+
+The tree payload layout is **catalog-first, inline-data-last** (not interleaved):
+
+```
+[ catalog: N × (name, kind, content_hash, size, placement, offset, length) ]
+[ inline-data: concatenated bytes of Placement::Inline files ]
+```
+
+A directory listing reads only the catalog and never fetches the inline-data section. Reading one
+inline file is a seek to its `(offset, length)`. `size`/`placement`/`offset`/`length` live in the
+catalog but are **outside identity** and may evolve freely.
+
+`Placement::Inline` is used for eager small part files (`checksums.txt`, `columns.txt`, `count.txt`,
+`serialization.json`, `metadata_version.txt`, `partition.dat`, small `primary.idx`) — they ride the
+single tree GET at part open. `Placement::Blob` is used for lazy per-column data (`.bin`, `.mrk`),
+preserving column selectivity.
+
+Code: `Core/CasTreeCodec.{h,cpp}` (not implemented as of 2026-07-03 — no such file exists; superseded by
+`Core/CasManifestCodec.{h,cpp}`, see above).
+
+---
+
+## Per-hash freshness meta (`.meta`, `CAMT`) {#per-hash-meta}
+
+**Status: DONE** (v3 freshness-meta, spec `2026-07-09 §meta-protocols`).
+
+Alongside each blob body the pool may carry a small per-hash **freshness meta** object
+(`Core/CasBlobMeta.{h,cpp}`), tagged with the on-disk magic `"CAMT"` (fixed-length body). It records a
+2-state `MetaState` — `Clean = 0` (referenceable; body present) or `Condemned = 1` (GC marked in-degree
+0; body STILL present, a writer may resurrect by CAS) — plus a `condemn_round` (an ABA guard after
+spare→re-condemn) and `size`. There is **no** `Tombstone` state (the raw-body/terminal-tombstone and
+per-incarnation-key variants were rejected — see the freshness-meta v3 plan).
+
+Role: it is the **writer's freshness point-read**, replacing the old writer-side retire-view. On a
+dedup/reuse hit the writer `loadMeta`s the hash and treats `Condemned` as `ABORTED` → re-upload from
+source (INV-1); otherwise it adopts the existing blob. GC keeps it current (`writeCondemnedMeta` on
+condemn, plus spare/delete transitions) on a bounded pool (`gc_meta_pool_size`). It is a freshness
+marker only, **not** the linearization point: delete-exactness stays with the in-body incarnation tag +
+exact-token BODY delete (see [§incarnation-in-body](#incarnation-in-body); `CaIncarnationCore`). Ops
+surface (`CasBlobMeta.h`): `loadMeta`, `putMetaIfAbsent`, `casMeta`, `deleteMetaExact`.
+
+---
+
+## Merkle `treeId` identity rule {#merkle-tree-id}
+
+**Status: not implemented as of 2026-07-03** — superseded by the rev. 15 root-local part-manifest
+redesign. There is no `merkleTreeId` function and no `CasTreeCodec.{h,cpp}` in the tree; a `PartManifest`
+is addressed by its journal-assigned `ManifestRef` (`writer_epoch`/`build_sequence`/`manifest_ordinal`),
+not by a content hash of its entries. The description below documents a design that was implemented in
+an earlier generation and is retained for historical context only.
+
+`treeId = CityHash128( "CAMT" || u8(1) || u32(entry_count) || per-entry[(u16 name_len || name || u8 node_kind || u128 child_hash)] )`
+
+where:
+- entries are sorted by name (byte-wise); duplicate names rejected (`BAD_ARGUMENTS`).
+- `node_kind`: `0` = file (Inline or Blob), `1` = subtree — RFC 6962-style domain separation (prevents a file hash and a subtree hash under the same name from colliding; Bitcoin's CVE-2012-2459 arose from its absence).
+- `child_hash` = `logical_hash` for files, `treeId` of the child tree for subtrees.
+- `file_size`, `placement` (Inline vs Blob), and `inline_bytes` are **excluded** — identity is
+ independent of storage layout. An inline file and a standalone blob with the same content produce
+ the same `treeId`.
+
+The domain tag `"CAMT"` and rule version `u8(1)` live only inside this (historical, never-persisted)
+hash input. (Note: the 4-byte tag `CAMT` has since been REUSED as a real on-disk magic for the
+unrelated per-hash freshness-meta object — see [§per-hash freshness meta](#per-hash-meta); that reuse
+is independent of this treeId hash-domain tag.) The rule is **frozen by convention**: changing it only loses cross-boundary dedup
+(logically identical trees get different ids → stored twice — a benign duplicate, never a
+correctness problem). Readers never recompute `treeId`; the writer always computes it directly from
+the collected entries before serializing the catalog (`CasBuild::stageTree` uses `merkleTreeId(entries)`).
+
+The old rule was `CityHash128(encodeTree(...))` — identity from the serialized catalog bytes. This
+was fragile: any catalog encoding change would rekey all trees, breaking dedup. The Merkle rule
+decouples identity from serialization entirely.
+
+Code: `Core/CasTreeCodec.{h,cpp}` (`merkleTreeId`); `Core/CasBuild.cpp` (`stageTree`). (Not implemented
+as of 2026-07-03 — see status note above; the current write path is `CasBuild::stageManifest`.)
+
+---
+
+## REJECTED: Merkle `treeId` tree-layer intermediate object {#rejected-tree-layer}
+
+**Status: REJECTED** (plan `plans/2026-06-24-cas-2a-merkle-tree-id.md` was superseded by the
+identity-rule change in §5 above; the intermediate tree-layer *object* was never proposed or
+implemented)
+
+The rejected design that *was* considered and discarded is a separate **tree-layer object** that
+would have sat between blobs and the part-level tree — analogous to Git's "tree of trees" nesting
+with explicit intermediate objects stored in the pool. The rationale for rejection:
+
+1. **No benefit over inline subtrees.** The CAS tree codec already supports recursive subtrees
+ (`Placement::Subtree` entries pointing to child tree ids). A separate `trees/` object layer adds
+ key-space complexity without any protocol benefit.
+2. **`treeId` decoupling suffices.** The actual requirement was to decouple identity from
+ serialization so encoding changes don't rekey objects. That is achieved by the Merkle rule over
+ logical inputs (§5) — no intermediate object is needed.
+3. **GC complexity.** Every new object kind adds a GC edge type and a retire/delete path. The
+ decision was to keep the object model to exactly two content-addressed kinds: blob and tree.
+
+The Merkle `treeId` *rule* (§5) was implemented in an earlier generation but is **not implemented as of
+2026-07-03** (superseded by the rev. 15 root-local part-manifest redesign — see `§merkle-tree-id`).
+What remains REJECTED regardless is the idea of an additional intermediate tree-layer object as a
+distinct `ObjectKind`.
+
+---
+
+## Layout keys {#layout-keys}
+
+**Status: DONE** (`Core/CasLayout.h`)
+
+All keys are built by `Cas::Layout` from a pool prefix. The naming follows the protocol spec §4
+layout exactly.
+
+```
+/
+ blobs// content blob (immutable)
+ cas/refs// root-shard manifest (Phase 1 hot/cold split)
+ cas/manifests////.proto part-manifest body
+ roots// per-server mutable state mirror
+ _files/ verbatim namespace files
+ gc/
+ state GC lease/round state (retired-in-snapshot 2026-07-10: no retired_refs)
+ hb advisory GC-leader heartbeat (B160)
+ gen//attempt//
+ fold_seal per-gc-shard coverage + condemned_summary (retired-in-snapshot)
+ blob_target// source-edge run; condemned state rides here as kCondemned rows
+ part_manifest_cleanup//
+ outcomes// (retired// removed 2026-07-10: no separate retired-list run)
+ checkpoint/
+ server-roots//
+ owner / epoch / mount (mount body carries min_active; observed_gc_round removed in v3)
+ _pool_meta pool identity + format version
+```
+
+The `completion_seal` and the standalone `_watermark` object are **gone**: the ack-floor round has one CAS (no completion phase), and the build watermark merged into the `mount` body (`03 §merged-heartbeat`). Each gc-shard's `retired//` run is the **current** outstanding-candidate set for that shard — `RetiredEntry` per entry now carries `condemn_round` (the ack-floor graduation gate, GC-only) and `delete_pending` (the two-phase-graduation flag, `04 §two-phase-graduation`), both additive proto fields. `gc/state` gained `retired_refs` (gc-shard → the current run's object key) and **dropped** the per-round `fence_version` map with the fence phase. The refs and the round number are published in one CAS, so a reader that observes round K can always load the complete retired list of version K (the publish-order invariant).
+
+**Key design choices:**
+
+- `` = first two hex characters of the id. `Layout::blobKey` is the canonical builder for
+ content blobs. `objectKey(layout, kind, hash)` dispatches on `ObjectKind`, which has `Blob` as its
+ sole enumerator — the `ObjectKind::Tree` LOGICAL_ERROR thrower and `Layout::treeKey` were removed
+ 2026-07-03 along with the enumerator (see `§object-kinds`).
+- Namespaces are opaque strings to the core; the wiring composes e.g. `/`.
+ `_files`, `_manifests`, `_precommits` are reserved segments. Numeric-only shard keys and `_files`
+ cannot collide.
+- Phase 1 (hot/cold split) relocated root-shard manifests from `roots//` to
+ `cas/refs//`. GC discovery LISTs `casRefsPrefix()` — only ref shards, no verbatim
+ files interleaved.
+- Part-manifest bodies live under `cas/manifests/` keyed by `(writer_epoch, build_sequence,
+ ordinal)`, making them enumerable by namespace without a full-pool LIST.
+
+### CH-path → CAS-namespace mapping (the `@cas@` contract) {#path-mapping}
+
+**Status: DONE** (`PartPathParser.{h,cpp}`, `ContentAddressedMetadataStorage.cpp`, `CasLayout.h`).
+Sources: `specs/2026-06-19-ca-vfs-contract.md`, `specs/2026-06-19-ca-vfs-path-mapping-design.md`.
+The rules below are grounded live in code.
+
+**The `@cas@` archive-suffix marker.** `kCasArchiveSuffix = "@cas@"` is a **suffix on a table-dir
+segment** (`…/@cas@`), **never a standalone path segment**. It marks where the mirrored
+ClickHouse path ends and the content-addressed archive begins — like a `.zip` extension in
+`foo.zip/inner/file`. `@` is S3-safe and never occurs in ClickHouse uuids, part names, detached
+prefixes, projection names, or column files, so it cannot collide with real path data. The
+**mutability invariant** is exactly this boundary: a node is immutable **iff** it is
+content-addressed (has a hash); trees/subtrees/blobs/pack-slices have a hash → immutable; namespaces,
+refs, and verbatim/overlay files have none → mutable. `@cas@` is the content/verbatim boundary.
+
+**Atomic vs non-Atomic mirroring** (`mirroredArchiveNamespace`):
+
+| Database engine | Reported path | CAS namespace |
+|---|---|---|
+| Atomic | bare `` | `store//@cas@` — `` = first 3 chars of the uuid, matching ClickHouse's `store/` fanout |
+| non-Atomic | full `data//` | `data//@cas@` — the `@cas@` suffix appended verbatim (no `store/` fanout) |
+
+**`@cas@`-scoped shard classification.** Root-shard parsing under `roots/`/`cas/refs/` applies
+**only inside a `@cas@` archive** (namespace's last segment ends with `@cas@`, or is the reserved
+`_precommits` segment). A numeric-tailed loose file with **no `@cas@` ancestor** (e.g.
+`roots/srv1/clickhouse_access_check_123`) is an **opaque ordinary file and is never mis-parsed as a
+`(namespace, shard)`**. This closes the old hazard where `roots/srv1/foo/7` would be read as
+namespace `srv1/foo`, shard `7`.
+
+**Non-nesting namespace invariant.** No registered namespace may be a path-prefix of another, because
+GC enumerates a namespace's shards by prefix-LIST and a nested namespace would let a parent's LIST
+sweep a child's keys. Live-vs-shadow diverge at their first segment (`store/…@cas@` vs
+`shadow//store/…@cas@`) so neither is a prefix of the other. FREEZE publishes **one ref per
+frozen part** under `shadow//…/refs/` (not a shared container ref) deliberately, to avoid
+a shared-`detached`-style read-modify-write on a shared ref under concurrent freeze — the B66a torn-read
+hazard (source: `plans/2026-06-04-cas-mergetree-freeze.md §3.2/§7d`; see also `08 §scenario-table` S18). **Detached parts (B181)** are
+handled by folding them **into the table's own namespace** as refs keyed by `kDetachedRefPrefix =
+"detached/"` (a `detached/PART` ref versus a live `PART` ref) — one namespace per table, no
+live-vs-detached name collision (the old sibling `detachedNamespace` is gone). The `TABLE/detached`
+container dir surfaces the table's refs filtered to the `detached/` prefix (stripped for display).
+The non-nesting property is thus preserved via ref-name prefixing rather than a separate namespace.
+
+**Reserved segments.** `_files`, `_manifests`, `_precommits` are reserved segments under a namespace;
+numeric-only shard keys and `_files` cannot collide.
+
+**Logical vs physical view.** `clickhouse-disks` `cd`/`ls`/`read` present the **logical** ClickHouse
+view — `@cas@` stripped (`stripCasArchiveSuffix`), files reconstructed from the manifest/trees. Raw
+`aws s3 ls` shows the **physical** archive — the same paths with `@cas@` on table dirs. The two
+correspond segment-for-segment but are deliberately different renderings (logical vs physical), not
+byte-identical listings. A raw subtree `rm roots//…` is **destructive offline maintenance,
+NOT equivalent to `dropNamespace`**: it bypasses the journal and the fenced index prune and may leave
+index/GC leftovers that a repair/prune step must reconcile.
+
+---
+
+## `Cas::Backend` abstraction {#cas-backend}
+
+**Status: DONE** (`Core/CasBackend.h`)
+
+`Backend` is a token-aware storage seam with 9 pure-virtual operations: `get`, `getStream`, `head`,
+`putIfAbsent`, `putIfAbsentStream`, `putOverwrite`, `casPut`, `deleteExact`, `list` (plus the
+capability query `supportsListTokens`). It has three concrete implementations:
+
+- `ObjectStorageBackend` — production, wraps `IObjectStorage` (S3, GCS, Azure).
+- `CasInMemoryBackend` — in-process fake for unit tests, mints monotonic tokens.
+- `CasInstrumentedBackend` — wraps any backend with operation counting for test assertions.
+
+The token contract is documented in `CasBackend.h`:
+
+> a token must uniquely identify the byte-content of the incarnation it labels — i.e.
+> `head(k).token == prior get(k).token` MUST imply the bytes are unchanged.
+
+This is load-bearing for the `readShardDecoded` cache: a token match skips a re-`get`+decode,
+so a backend whose token could repeat across different content would serve stale manifests.
+
+### Exact-token deletes {#exact-token-deletes}
+
+`deleteExact(key, token)` removes **only the incarnation whose token matches** and must return
+`TokenMismatch` with the object untouched if the token does not match the current incarnation.
+Backends that silently ignore the condition are rejected by `Cas::Probe` at pool open.
+
+This is the safety-critical primitive of the ABA-prevention protocol: the GC captures a delete token
+by `HEAD`ing the object when the three-cursor merge condemns it (`04 §three-cursor-merge`), then
+issues `deleteExact` only after the entry has graduated through the ack floor to `delete_pending` and
+that pending state was published by a prior round. A write that arrived after the condemn observation
+carries a new token; the exact-token delete mismatches (`TokenMismatch`) and the object is left
+alone.
+
+`Cas::Backend` exposes `casPut(key, bytes, expected_token)` for the root-manifest CAS path (commit
+a manifest iff the expected current token or expected absence matches — corresponding to model
+`WCreate`/`WReuse`).
+
+### `get` NoSuchKey → `std::nullopt` contract {#get-nullopt}
+
+`ObjectStorageBackend::get` wraps its ranged read so that a concurrent-delete `S3Exception`
+(`NoSuchKey` / 404) **during the GET** returns `std::nullopt` (the object vanished between HEAD and
+GET = absent), matching `CasInMemoryBackend`. This removes the raw-`Code 499`-escape class for all
+callers: a legitimate live read in the HEAD→GET delete window (e.g. `Store::readTree` for a SELECT
+or merge) surfaces cleanly as absence rather than propagating a raw S3 error. Callers expecting
+absence check for `std::nullopt`; callers expecting presence get a handled `FILE_DOESNT_EXIST`
+instead of an unhandled `Code 499`. A unit test drives a backend that throws `NoSuchKey` mid-GET and
+asserts `get` returns `nullopt`, not an exception. (Source
+`specs/2026-06-21-ca-revival-consolidation-design.md`; note this is a read-window contract only —
+revival never GETs, per INV-1 in `03-writer-protocol.md`.)
+
+### `supportsListTokens` capability {#list-tokens}
+
+`supportsListTokens()` returns true iff the backend surfaces per-key incarnation tokens through
+`list`. When true, GC `discover` may skip reading the body of an unchanged root shard (listed token
+== persisted folded token ⇒ no change). When false, GC must read every root-shard body. S3 ETags
+are content-derived and returned in LIST responses → true for `ObjectStorageBackend`.
+
+---
+
+## Exact-token semantics: ETag vs Generation tokens {#etag-vs-generation}
+
+**Status: ETag path DONE; GCS generation binding DEFERRED**
+
+`CasToken.h` defines three token types:
+
+```cpp
+enum class TokenType : uint8_t
+{
+ ETag = 1, /// S3-family / Azure
+ Generation = 2, /// GCS (binding deferred; fail-closed until probed)
+ Emulated = 3, /// test backends
+};
+```
+
+**ETag (S3/Azure):** content-derived. The S3 `PutObject` and `CompleteMultipartUpload` responses
+include an ETag in the response body; `WriteBufferFromS3` captures it and exposes it via
+`getResultObjectETag()`. This enables eliminating the post-write HEAD that was ~73 % of all S3
+HEADs (`specs/2026-06-15-ca-head-after-put-etag-design.md`): the CA backend records the
+PUT-response ETag directly as the dependency token (`TokenType::ETag`) — model `WCreate` — instead
+of issuing a follow-up `HEAD`.
+
+The dedup-reuse path (`PreconditionFailed` on `putIfAbsent`) still needs an observation token; its
+`HEAD` remains (model `WReuse`).
+
+**GCS generation token:** GCS uses a per-object generation number rather than a content-derived
+ETag. The generation number is NOT content-based — it increments on every overwrite, including
+overwriting with the same bytes. This breaks the `Token ⟹ content precondition` that the
+`readShardDecoded` cache depends on. GCS generation tokens are defined in `CasToken.h` but the
+binding is deferred and fail-closed until `Cas::Probe` validates the actual semantics at pool open.
+
+**Azure:** uses ETags; follow the same code path as S3.
+
+**Non-content-based tokens — the protocol gap:** the CAS protocol requires that equal token ⟹
+equal content (for the decode cache, not just for the delete gate). A backend whose token can repeat
+across different content (e.g. a GCS-style generation number that increments on overwrite) must NOT
+be used as a CAS pool without a workaround. The current design records this as a `TODO` in the
+`Probe` component.
+
+---
+
+## AWS / GCS / Azure support status and gaps {#cloud-support}
+
+**Status: AWS S3 DONE; Azure partial; GCS DEFERRED**
+
+| Provider | Conditional write | Exact-token delete | LIST returns token | `supportsListTokens` | Notes |
+|---|---|---|---|---|---|
+| AWS S3 | `If-None-Match: *` via `WriteSettings` | `removeObjectIfTokenMatches` (ETag) | yes | `true` | Full production path |
+| MinIO / RustFS | same S3 API | same | yes | `true` | Used in soak/test harness |
+| Azure Blob | ETags; Azure conditional PUT | yes | yes | `true` | Code path identical to S3 |
+| GCS | `x-goog-if-generation-match: 0` (dialect-mapped from `If-None-Match: *`) | generation-exact delete (dialect-mapped `If-Match`) | NO — list tokens disabled (XML LIST bodies carry MD5 ETags) | `false` on generation stores | **Validated live 2026-07-03** via `http_client = gcs_hmac` (GOOG4-HMAC + conditional dialect); versioned buckets refused by the probe; conditional writes single-PUT only (`gcs_max_conditional_put_bytes`) |
+
+**LIST consistency requirement:** the protocol's GC `discover` step LISTs `cas/refs/` and expects
+the list to be consistent: an object PUT before the LIST boundary must appear in the list. S3 and
+GCS both offer strong LIST consistency (as of 2021). Azure has strong consistency for Blob Storage.
+RustFS offers strong consistency in the single-node configuration used in the soak harness.
+
+An object-storage backend with **eventual LIST consistency** (an older S3 model, or some
+third-party implementations) would allow a freshly written root shard to be invisible to the GC
+discovery scan — causing it to be missed in the fold and potentially retired. The protocol assumes
+**strong LIST consistency** as a hard requirement. `Cas::Probe` should verify this at pool open
+(tracked as `TODO`).
+
+---
+
+## The rustfs testbed {#rustfs}
+
+**Status: DONE as a soak harness; overwrite-leak workaround DONE**
+
+RustFS is used as the S3-API backend in the `utils/ca-soak` 24-hour soak harness instead of MinIO,
+to expose object-storage behavior under realistic CA write patterns.
+
+**Overwrite-leak defect (`specs/2026-06-15-ca-rustfs-overwrite-leak-mitigation-design.md`):**
+RustFS `1.0.0-beta.8` does not reclaim the previous data directory on an un-versioned overwrite
+(`xl.meta` is repointed, but the old `/` data directory leaks). The CA manifest pattern
+overwrites `roots/` (root-shard manifests) on every commit; ~74 GB grew in ~1 h from 18 live keys.
+
+Mitigation: a **scope-limited orphan reaper** sidecar (`utils/ca-soak/scripts/orphan_reaper.sh`)
+runs every 300 s, scoped to `roots/` only (blobs/trees are immutable and have exactly one data dir).
+For each manifest object dir it keeps `xl.meta` and any `/` dir whose mtime is within a 120 s
+grace window, and the single newest dir; deletes the rest. Blobs/trees cannot be touched because
+they are outside `roots/` and have one data dir per key.
+
+This is a **test-harness workaround** for a RustFS defect. The CA server code is unchanged.
+Production deployments use real S3 (or a correctly-versioned object store) and have no equivalent
+issue.
+
+---
+
+## Schema-evolution stance {#schema-evolution}
+
+**Status: Generation-1 freeze DONE; rollout machinery DEFERRED (pre-release)**
+
+(`specs/2026-06-24-cas-schema-evolution-framework-design.md`, Parts I–V)
+
+### Generation-1 freeze {#gen1-freeze}
+
+The following byte-level decisions are frozen (irreversible after first release):
+
+- Single header for blob (and, until removed 2026-07-03, tree); magic `CABL` (`CATR` removed along
+ with `ObjectKind::Tree`); exact core size 94 bytes; `blob_header_len = 256`.
+- Version field width: 2 bytes (`uint16` LE) for `writer_version` and `compatibility_version`.
+- Merkle `treeId` rule: `CityHash128("CAMT" || u8(1) || sorted(name, node_kind, child_hash))`
+ (not implemented as of 2026-07-03 — superseded by the rev. 15 `PartManifest` redesign; see
+ `§merkle-tree-id`).
+- Blob hash is the streaming chunked convention (`§codecs-and-determinism`), over payload only
+ (not the header).
+- Catalog-first / inline-data-last tree layout (not implemented as of 2026-07-03 — see `§tree-codec`).
+- `Placement::Pack` fully removed (was never produced; no reserved slot — YAGNI).
+- Format id set (`CABL`/`CARS`/… — `CATR` removed 2026-07-03).
+- Single error code `UNKNOWN_FORMAT_VERSION` for future format and unknown critical TLV.
+
+### Write-down-to-floor discipline {#write-down-to-floor}
+
+The writer targets the pool floor (`min G_build` over members) and emits the field-set valid at
+that level, so an old reader is never handed something it would silently mis-skip:
+
+```
+serialize(compatibility_version):
+ if (compatibility_version < 10) emit the <10 field-set
+ else if (compatibility_version < 20) emit the <20 field-set
+ else emit the freshest
+```
+
+A **safe-additive change** (new protobuf field number) needs no branch: old readers skip the
+unknown field. A **breaking change** (semantics of an existing field changes, or a new binary format
+section) uses a branch and waits for the floor to rise. The ladder is pruned after a completed
+upgrade cycle.
+
+For trees, `treeId` was designed to be Merkle over logical children — so a breaking serialization
+change would not rekey objects (not implemented as of 2026-07-03; see `§merkle-tree-id`). Old builds
+refuse to parse the new tree (fail-closed); objects dedup correctly across the boundary.
+
+### Deferred rollout machinery (Part IV) {#deferred-rollout}
+
+The following are designed but **not yet built** (pre-release, no breaking change exists yet):
+
+- **`max_content_addressable_pool_format` setting** — per-server cap on write generation; staged rollout.
+- **Durable roster** (`pool-meta` extension): `{members: {server_id: {path, G_build}}}` — one GET
+ to read all members, CAS to update own entry. Floor = `min(G_build)` over members. Write a format
+ `V` iff `min_reader(V) ≤ floor AND V ≤ setting`. Collocated with `RootsRegistry`.
+- **B200 — deliberate decommission** — **IMPLEMENTED 2026-07-15, without the roster** (grammar
+ landed as `SYSTEM CONTENT ADDRESSED DROP POOL MEMBER '' FROM DISK ''`; spec
+ `2026-07-13-cas-pool-member-decommission-design.md`). Never auto-removes for inactivity;
+ a long-absent member pins shared floors until deliberately dropped (safe default). Roster
+ forward-hook stays Part IV: when the durable roster lands, decommission additionally removes
+ the member's roster entry.
+
+These are designed so the door is provably open, but nothing in frozen bytes depends on them.
+
+### gc-snap conversion deferred {#gc-snap-defer}
+
+`gc-snap` keeps its existing versioned-binary+zstd codec (not converted to protobuf). Reasoning:
+it is GC-internal durable state (not a cross-implementation interchange format), already binary,
+already versioned with `GC_SNAP_VERSION` + magic, and the conversion is the highest-risk
+lowest-interchange-value change. The B165 OOM concern (not materializing the whole body for large
+snaps) is addressable within the binary codec. GC-local objects (`gc-state`, `retired-set`) are
+converted off JSON for cleanup but their protobuf is not an interchange contract.
+
+---
+
+## DONE / TODO / REJECTED / DESIRABLE summary {#status-summary}
+
+| Item | Status | Notes |
+|---|---|---|
+| One 256-B header for blob (`CABL`), 94-byte hole-free core | DONE (envelope format only; `Tree`/`CATR` removed 2026-07-03 — see below) | `CasEnvelope.{h,cpp}` |
+| `compatibility_version` (formerly `min_reader_version`) + `gateOnRead` | DONE | `CasFormat.{h,cpp}` |
+| Blob hash over payload, not header | DONE (streaming chunked hash, not one-shot — see `§codecs-and-determinism`) | test asserts this |
+| Standalone `Tree` object kind / `Layout::treeKey` | **removed 2026-07-03** | superseded by `PartManifest` (`CasManifestCodec.h`), rev. 15 redesign, then excised from the code (`ObjectKind` now has `Blob` as its sole enumerator) |
+| Merkle `treeId` rule (2a) | **not implemented as of 2026-07-03** | no `merkleTreeId`/`CasTreeCodec` in tree; superseded by rev. 15 |
+| Catalog-first / inline-data-last tree layout | **not implemented as of 2026-07-03** | no `CasTreeCodec`; superseded by rev. 15 |
+| `Placement::Pack` removed | DONE | no code, no on-disk data |
+| proto rename → `cas_format.proto`, `clickhouse.cas.format` | DONE | `CasObjectStorageBackend` |
+| JSON codec family deleted | DONE | `CasCodecUtil.h` |
+| PUT-response ETag capture (no post-write HEAD on S3) | DONE | `WriteBufferFromS3`, `CasObjectStorageBackend` |
+| `Backend` seam + exact-token `deleteExact` | DONE | `CasBackend.h` |
+| `Cas::Layout` key builders | DONE | `CasLayout.h` |
+| AWS S3 full support | DONE | production path |
+| rustfs soak harness + orphan-reaper workaround | DONE | `utils/ca-soak/scripts/` |
+| GCS generation-token binding | **DONE 2026-07-03** | `http_client = gcs_hmac`: GOOG4-HMAC signer + wire-boundary conditional dialect (response `ETag := generation`); full live validation cycle green (spec `2026-07-03-cas-gcs-generation-binding-design`) |
+| LIST consistency probe in `Cas::Probe` | TODO | see §10 |
+| Durable roster + `max_content_addressable_pool_format` setting | DEFERRED | Part IV, pre-release |
+| gc-snap → protobuf | DEFERRED | lowest-priority; keep binary+zstd |
+| B200 deliberate decommission | **DONE 2026-07-15** | implemented WITHOUT the roster (`SYSTEM CONTENT ADDRESSED DROP POOL MEMBER` + `ca-drop-member`); roster entry removal = Part IV forward-hook |
+| Merkle tree-layer intermediate object | REJECTED | see §6 |
+| JSON for control objects | REJECTED | abandoned 2026-06-24 |
+| `CHCA` single magic with kind byte | REJECTED | replaced by per-kind `CABL`/`CATR` magic |
diff --git a/docs/superpowers/cas/06-tla-models.md b/docs/superpowers/cas/06-tla-models.md
new file mode 100644
index 000000000000..25b63a1c3cc5
--- /dev/null
+++ b/docs/superpowers/cas/06-tla-models.md
@@ -0,0 +1,1188 @@
+---
+description: 'Index of every TLA+ formal model in the CAS MergeTree corpus: what each proves, the counterexamples and sabotages that drove design decisions, and code-currency status.'
+sidebar_label: 'CAS TLA+ model index'
+sidebar_position: 6
+slug: /superpowers/cas/tla-models
+title: 'CAS MergeTree — TLA+ model index'
+doc_type: 'guide'
+---
+
+# CAS MergeTree — TLA+ model index {#cas-tla-model-index}
+
+This document indexes every TLA+ model in `docs/superpowers/models/` for the content-addressed (CAS)
+MergeTree feature. For each model it records: the file(s), what the model proves (key invariants), the
+counterexamples or sabotage traces that drove concrete design decisions, and a code-currency note.
+
+The `.tla` and `.cfg` source files are **not modified**; this doc superseded the older per-model prose
+files (`*_README.md`, `INDEX.md`, `RESULTS.md`, `MODEL_CURRENCY_REVIEW_2026-06-22.md`). All
+bounded-model-checking runs use TLC v2.19 / OpenJDK 21; Apalache 0.58.0 is used for
+inductive-invariant checking.
+
+> **Directory index.** The complete, up-to-date inventory of `docs/superpowers/models/` — including
+> models added after this doc's last full refresh (the ref snapshot+log family `CaRef*`,
+> `CaRetiredInRun*`, `CaGcCondemnMarkerGate`, and the rev.6 `CaCasMountCore` configs) and the list
+> of removed obsolete models — is `docs/superpowers/models/README.md`. When this doc and the README
+> disagree on a model's config set or currency, the README (audit date 2026-07-21) wins.
+
+---
+
+## Area 1 — Incarnation-token GC core {#area-incarnation}
+
+The canonical model of the fold → retire → fence → recheck → exact-token-delete → cascade → trim round.
+Replaces the superseded EBR/epoch design (`CaGcCore.tla`; see §Area 8).
+
+### `CaIncarnationCore.tla` — canonical GC core {#caincarnationcore}
+
+**Files:** `CaIncarnationCore.tla`, `CaIncarnationCore_stage*.cfg`, `CaIncarnationCore_sab_*.cfg`,
+`CaIncarnationCore_hunt_*.cfg`, `CaIncarnationCore_reval_stage2.cfg`
+
+**What it proves.** The safety invariants `INV_NO_DANGLE` (no manifest ref points to an absent object),
+`INV_NO_LOSS` (every reachable object stays present), `INV_NO_RETURN` (a present object's token is
+never in the deleted-token history `deadTok`), and `INV_JOURNAL_COVERAGE` (the trim base never advances
+past the fold cursor) hold across the full adversarial interleaving: concurrent writers and GC leaders,
+split-brain, debris classification, full-GC exact-cut, `WResurrect`/`WOverwrite`, tree expansion and
+atomic cascade, namespace registry + manifest creation (B91), evidence staleness + re-observation (B91).
+
+| Stage | Configs | Adds | Distinct states | Result |
+|---|---|---|---|---|
+| 1 core | `stage1` | publish/drop, fold/retire/fence/recheck, in-flight deletes | 20.9M | PASS |
+| 2 resurrect/evidence | `stage2` | `WResurrect`, `WEvidence`, `WResolveEvidence` | 155K | PASS |
+| 3 trees/cascade | `stage3` | expansion markers, atomic cascade, shared-child survival | 52.8M | PASS |
+| 4a debris/full-GC cut | `stage4_small` | heartbeat-gated debris, two-shard full-GC cut | 16.8M | PASS |
+| 4b journaled-delete + tree rebuild | `stage4_journaltree` | journaled tail + `FGCommit` tree rebuild | 35.6M | PASS |
+| 5 split/overwrite | `stage5_small` | split-brain + `WOverwrite` | 64.4M | PASS |
+| 6a registry | `stage6_registry` | namespace registry + manifest creation (B91) | 3.7M | PASS |
+| 6b evidence staleness | `stage6_evstale` | amended `W-EVIDENCE`, `WEvObserve` (B91) | 10.1M | PASS |
+| 6c cross smoke | `stage6_cross_smoke` | registry × evidence-staleness interaction | 440K | PASS |
+| liveness | `stage2_live` | `NoLeakForever` under `FairSpec` | — | bound-artifact lasso (MaxRound=2 budget, not a design bug) |
+
+Large-bound hunt (2026-06-11, `W-REVALIDATE` mode): 782M distinct states BFS + 8.3B deep random
+visits at enlarged bounds — 0 violations found.
+
+**Sabotages (negative controls — all must produce a counterexample):**
+
+| Config | Rule removed | Violated invariant | Trace summary |
+|---|---|---|---|
+| `sab_nofence` | fence does not write to manifests (horn 2 missing) | `INV_NO_DANGLE` | Post-fence publish not blocked → GC deletes the committed object |
+| `sab_norecheckfold` | recheck does not require fold-through-fence (horn 1 missing) | `INV_NO_DANGLE` | Pre-fence publish missed by the recheck cursor → object deleted before the late publish is folded |
+| `sab_noretireview` | W-PUBLISH-GATE retire-view check removed | `INV_NO_DANGLE` | Stale reuse dep re-published after the fence; GC deletes the object under the stale ref |
+| `sab_unconddelete` | exact-token delete replaced by unconditional delete | `INV_NO_DANGLE` | Stale delete message kills the live token-2 incarnation written by `WResurrect` |
+| `sab_reusedtag` | W-FRESH-TAG / token distinctness removed | `INV_NO_RETURN` | Resurrect reuses the condemned token-1; delete lands, marks token-1 dead; incarnation still present |
+| `sab_cascade` | cascade deferred as a separate pipeline step | `INV_NO_LOSS` | Round-2 GC re-expands a now-live tree's stale `pendCasc` strip, deleting a live child |
+| `sab_cutoverclaim` | full-GC cursor jumps past unincorporated incorporated-state | `INV_NO_DANGLE` | `FGCommit` skips the add record; recheck fires as if the fold covered it; delete lands |
+| `sab_noreobserve` | W-REVALIDATE re-observation conjunct removed (reval mode) | `INV_NO_DANGLE` | Stale dep on a deleted object passes the weakened publish gate |
+| `sab_noregistry` (B91) | namespace registration skipped; no publish-floor | `INV_NO_DANGLE` | Newborn namespace's publish floor stays 0; a publish lands before the registry fence captures it |
+| `sab_foldtimeuniverse` (B91) | GC fences the FOLD-TIME registry universe instead of commit-time | `INV_NO_DANGLE` | A namespace registered between the fold's read and the registry-fence CAS is never fenced → its exact-token delete dangles |
+| `sab_noevreobserve` (B91) | stale evidence admitted without re-observation | `INV_NO_LOSS` | A stale carry-forward dep on a deleted object reaches publish; bytes lost |
+
+**Design decisions driven by this model:**
+
+- The fence writes to EVERY manifest (not just manifests with activity) — established by `sab_nofence`.
+- Recheck requires the fold cursor to have advanced through the fence position — established by `sab_norecheckfold`.
+- Deletes are exact-token — established by `sab_unconddelete`.
+- Cascade is atomic with the delete landing (not a deferred pipeline step) — established by `sab_cascade`.
+- The registry fence must use the COMMITTED (not fold-time) universe — found as a real C++ hole
+ (`commit 724eb5363ff`) during the B91 refresh; `sab_foldtimeuniverse` is the permanent negative
+ control for this shape.
+- `adoptTree` must cold-reuse (`observeAndAdmit`), not accept tokenless evidence — found as a real C++
+ blind-adopt hole (`commit a247e29c125`) during the B91 refresh.
+- The `GFenceShard` fence write must be a monotone-max CAS (never lowers the durable fence under
+ split-brain) — model refinement MR-4.
+
+**Model-refinement findings for the publish gate (MR-1 / MR-2·F1 / MR-3·F2):** three refinements the
+model forced on the writer's publish gate. The literal names below are in `CaIncarnationCore.tla`.
+
+- **MR-1 — publish gate consults durable `deadTok[h]`, not only in-flight `retired`.** A
+ token-bearing dependency `(h, t)` is condemned at view `v` iff
+ `CondemnedAtView(h, t, v) == RetiredHit(h, t, v) ∨ t ∈ deadTok[h]`. Consulting only the live
+ `retired` set is unsafe: the `Land` action consumes the retired entry on delete, so after the
+ message lands a stale unpublished dependency on `(h, t)` would pass `DepOK` even though the token
+ is physically dead. `deadTok[h]` is the durable history of tokens that have stopped being current.
+
+- **MR-2 / F1 — displaced-token push + current-state re-validation.** Any action that makes a token
+ stop being current MUST push the displaced token into `deadTok[h]`: in-place overwrite
+ (`WResurrect`, `WOverwrite`) and physical delete (`Land`). Correspondingly, in `W-REVALIDATE` mode
+ (`EnableReval`) the publish gate re-validates a dependency's **current physical state**
+ (`present[d] ∧ tokOf[d] = observed_token`) in the same CAS, not merely the originally-observed
+ token. Negative control: `sab_reusedtag` re-issues a condemned token and violates `INV_NO_RETURN`;
+ `sab_noreobserve` drops the re-observation conjunct and dangles.
+
+- **MR-3 / F2 (`TreeDepsOK`) — bottom-up tree publish.** A tree ref may be published into a manifest
+ only when **all direct children are present and non-condemned at publish time**
+ (`TreeDepsOK(w, h) == h ∈ TreeHashes ⇒ ∀c ∈ Children[h] : present[c] ∧ ~CondemnedAtView(c, tokOf[c], wView[w])`,
+ called from `WPublish`). Publishing a tree over an absent/condemned child immediately dangles once
+ the fold (`GFold`) expands the tree edges. This is one-level closure in the model (nested subtrees
+ not modeled); the writer's bottom-up build discipline covers the transitive case in code.
+
+**P9 extension (2026-06-17):** `GForget` prunes absent zero-in-degree nodes from `everEdged` to
+eliminate the retire-404-HEAD storm. Frame argument: no safety invariant reads `everEdged`, so
+`GForget` preserves all invariants independent of scope. Re-verified at stages 1–3 (clean). Liveness
+lasso unchanged.
+
+**Code currency:** CURRENT as the **architecture-independent safety spine**; its concrete structure
+is superseded (audited 2026-07-22). Every proven guard maps to a faithful code counterpart —
+incarnation token + exact-token delete + `INV_NO_RETURN` (`CasGc.cpp:419,507-512`), fold → retire →
+fence → recheck → delete → cascade → trim, no destructive decision on a not-fully-folded snapshot
+(realized by `suppress_destructive`), retired entries drop only on a confirmed outcome — so no proven
+invariant is at risk. **But its concrete representation is superseded by the `cas-gc-rebuild`
+architecture**: the model embeds the journal *inside the manifest* (`man[s].log`) and appends the
+fence as a *record in that journal* gating writers on `man[s].fence`, whereas the shipped code uses
+separate ref-log objects (`RefTxnId`), source-edge runs, write-once fold seals, and heartbeat-floor
+fencing — there is no manifest-embedded journal or manifest fence record. So read this model as the
+canonical *invariant* proof, not a structural mirror of the code (the concrete layer is tracked by
+`CaGcRootLocalPartManifestCore.tla` + the ack-floor/round-defer models). Other standing discrepancies:
+(1) heartbeat/debris/full-GC-cut actions (`WHb*`, `GDebrisRetire`, `FGRead`, `FGCommit`) are specified
+in the model but deferred in code; (2) `CaBuildRootPrecommit.tla` is the live precommit safety
+mechanism, only obliquely modeled here; (3) the `EnableReval=FALSE` dead-token-oracle gate has no
+production code path.
+
+---
+
+### `CaIncarnationProofCore.tla` — Apalache inductive invariant {#caincarnationproofcore}
+
+**Files:** `CaIncarnationProofCore.tla`, `Apalache.tla`, `CaIncarnationProofCore_tlc.cfg`,
+`CaIncarnationProofCore_tlc2h.cfg`, `run_apalache.sh` — **removed 2026-07-22** (stale AND
+unverifiable here — no Apalache binary; full text in git history). It modeled a superseded fragment
+of the core and could not be re-checked, let alone re-derived, in this environment. If an inductive
+proof is wanted, install Apalache and re-derive `IndInv` against the current `CaIncarnationCore.tla`.
+
+**What it proved.** An inductive invariant `IndInv` (19 conjuncts) for the pre-B91, `W-REVALIDATE`
+token-only fragment (single leader, no trees/debris/evidence/split-brain). Apalache 0.58.0 verified
+base case (`Init => IndInv`, 1 s) and step check (`IndInv => IndInv'`, 45–73 s) — both `NoError`. This
+is stronger than TLC bounded-checking: the step check quantifies over ALL states satisfying `IndInv` at
+fixed constant sizes (`|Writers|=2`, `|Shards|=1`, `|Hashes|=2`, `MaxToken=3`, `MaxRound=2`,
+`MaxLog=4`), regardless of trace depth. The prepared CTI journal (12 iterations) is the input for a
+future parametric TLAPS proof.
+
+The heart conjunct is `InflightCurrentUnreferenced` (a delete message in flight for a still-current
+token implies no folded root edge and no unfolded add past the cursor). Found as irredundant by the
+negative control `IndInv_NoICU`; dropping it produces a CTI.
+
+Negative controls: dropping `InflightHeld` or `InflightVsRefs` each leave the step check green
+(documented redundancy — both are corollaries of ICU + the fence-discipline family). Removing the
+`W-REVALIDATE` re-observation conjunct from `WPublish` (`NextNoReval`) produces a counterexample
+breaking `NoDangle` — machine-checking the F1 re-observation requirement.
+
+**Code currency:** STALE and REMOVED 2026-07-22. Predated the B91 amendments (namespace registry,
+evidence staleness, `ViewableRound`); the canonical current-design coverage is
+`CaIncarnationCore.tla` (bounded TLC, CURRENT). A stale inductive proof of a superseded fragment
+that cannot be re-verified in this environment is false comfort, so it was removed rather than kept
+as speculative groundwork.
+
+---
+
+## Area 2 — Build-root / precommit protection (B140, B171, B199-S2) {#area-precommit}
+
+### `CaBuildRootPrecommit.tla` — precommit-first + fail-closed commit {#cabuildRootPrecommit}
+
+**Files:** `CaBuildRootPrecommit.tla`, `CaBuildRootPrecommit_buggy.cfg`,
+`CaBuildRootPrecommit_buildrootonly.cfg`, `CaBuildRootPrecommit_failclosedonly.cfg`,
+`CaBuildRootPrecommit_fixed.cfg`, `CaBuildRootPrecommit_inlineclosure.cfg`,
+`CaBuildRootPrecommit_lazyleak.cfg`, `CaBuildRootPrecommit_inlineclosure_b2.cfg`,
+`CaBuildRootPrecommit_b2_witness.cfg`
+
+**What it proves.** The 2×2 necessity/sufficiency matrix for the B140/B171 fix — build-root structural
+reachability (`UseBuildRoot`) and fail-closed commit (`FailClosedCommit`) — and the B199-S2
+inline-closure liveness fix (`InlineClosure`).
+
+| `UseBuildRoot` | `FailClosedCommit` | `InlineClosure` | Spec | Result | States |
+|---|---|---|---|---|---|
+| F | F | T | `Spec` | `INV_NO_DANGLE_COMMITTED` violated | ~505 (to CE) |
+| T | F | T | `Spec` | `INV_NO_DANGLE_COMMITTED` violated | ~3031 (to CE) |
+| F | T | T | `Spec` | clean (safety) | 2193 |
+| T | T | T | `Spec` | **clean** (all 4 invariants) | 45161 |
+| T | T | T | `FairSpec` | **clean** (safety + `INV_NO_LEAK` HOLDS) | 45161 |
+| T | T | F | `FairSpec` | `INV_NO_LEAK` **VIOLATED** — S2 leak reproduced | 71953 (to CE) |
+| T | T | T (b1+b2) | `FairSpec` | **clean** (safety + `INV_NO_LEAK` HOLDS) | 310993 |
+
+**B140-dangle counterexample (buggy cfg, 6 states):** `WriteBlob(bld1,b1)` → `AdoptBlob(bld2,b1)` [owner stays `bld1`] → `BuildDie(bld1)` [owner gone, `OwnerProtected(b1)=FALSE`] → `GcDelete(b1)` [in-degree 0, unprotected] → `Commit(bld2,t1)` [no presence re-check, blind publish] → **committed manifest references absent `b1`**. This is adopt-without-ownership-transfer → owner retires → GC deletes → blind commit dangles.
+
+**Why build-root alone is insufficient (buggy `buildrootonly` counterexample):** `WriteBlob(bld1,b1)` → `GcDelete(b1)` (no precommit yet, so in-degree 0) → `Precommit` → blind `Commit` → dangle. The precommit must exist **before** the blob can reach in-degree 0 AND the commit must re-check presence.
+
+**B199-S2 inline-closure leak (`lazyleak`, 6 states + stutter):** `Precommit(bld2,t2)` while tree object `t2` **absent** → lazy path records `closure[bld2]={}` (empty) → `b1` never enters `everSnapped` → `GcDelete` can never fire on `b1` → `b1` leaks forever. Inline closure (IC=TRUE) closes this by construction: the writer records the closure at precommit time, from the staged structure it already holds in memory.
+
+**Reachability witnesses (non-vacuity):** all four negated witnesses are reachable: `W_GcDeleteReached`, `W_BuildRootProtectReached`, `W_LiveFrozenReclaimDeleteReached`, `W_PrematureReclaimAbortReached`. The two-blob run confirms shared blobs are spared and unique blobs are reclaimed.
+
+**Design decisions driven by this model:**
+
+- Build-root reachability (structural, not revocable liveness hint) is required — adopter builds must
+ precommit before relying on the adoption.
+- Fail-closed commit (presence re-check on the whole closure) is independently necessary — it catches
+ the ordering-window case where the blob is deleted before the precommit edge exists.
+- Both halves are necessary and jointly sufficient.
+- The precommit must record its closure **inline at precommit time** — not lazily from a tree-object
+ read at GC fold time (which can 404 if the tree is already condemned/deleted).
+
+**Code currency:** CURRENT for the safety conclusion; two named mechanisms have DRIFTED (audited
+2026-07-22) — the model's proven conclusion (build-root reachability + fail-closed commit jointly
+close the adopted-blob dangle) still holds, but the code realizes it differently than the two
+bullets above describe:
+
+- The "record its closure **inline at precommit time**" bullet is **not** how the shipped writer
+ works. `precommitAdd` carries only the manifest ref, never a blob list
+ (`CasPartWriteTxn.cpp:879-890`); GC learns the closure by reading the manifest body **lazily at
+ fold time** (`foldManifestEdges`, `CasGc.cpp:789-796`). Shipped closes the same leak by a
+ different, unmodeled discipline: a missing-body `+1` fold **clamps** the per-table cursor (a
+ barrier that re-reads next round) instead of recording an empty closure, and GC is the **sole
+ deleter** of the manifest body, deferring its exact-token delete until after the `-1` edges fold
+ (`mf_cleanup`, `CasGc.cpp:815-816`). So `InlineClosure=TRUE` models a mechanism the code does not
+ use; the lazy path it labels the S2 leak is the shipped path, made safe by the clamp barrier.
+- "Fail-closed commit (presence re-check on the whole closure)" is realized as an
+ **owner-liveness** fail-close, not a per-blob presence HEAD: `promote` aborts unless the precommit
+ is still the live owner (`CasPartWriteTxn.cpp:963-968`); tokened leaves are skipped as
+ edge-protected and tokenless leaves trusted. This is *stronger* (live owner ⇒ in-degree ≥ 1 ⇒
+ present, GC being the sole in-degree-respecting deleter), so it is sound — but it is not the
+ model's `∀c: present[c]` gate.
+
+Not a CODE-RISK — the safety conclusion holds via these stronger/different mechanisms. The model
+should be re-annotated (or its `InlineClosure`/presence-gate arms recast to the lazy-fold +
+clamp-barrier + GC-owned-deletion + owner-liveness discipline) so it structurally mirrors the code;
+that recast is a scoped follow-up, not done in this pass. The flat (one-level) model does not cover
+nested subtrees; that recursion fix is validated by the C++ gtest in
+`src/Disks/tests/gtest_cas_gc_leak.cpp`.
+
+---
+
+## Area 3 — GC lease heartbeat (B160) {#area-lease}
+
+### `CaGcLeaseCore.tla` — GC leader lease / advisory heartbeat {#cagcleasecore}
+
+**Files:** `CaGcLeaseCore.tla`, `CaGcLeaseCore_heartbeat.cfg`,
+`CaGcLeaseCore_safety_noheartbeat.cfg`, `CaGcLeaseCore_sab_noheartbeat.cfg`
+
+**What it proves.** `NoEpochCollision` (no two leaders commit a retire at the same fence epoch — the
+atomic single-CAS steal + `fence_seq` epoch isolation is safe regardless of timing) and `NoFalseSteal`
+(no steal fires against an alive mid-round incumbent when the advisory heartbeat is enabled).
+
+| Config | `EnableHeartbeat` | Invariants | States | Result |
+|---|---|---|---|---|
+| `_heartbeat` | TRUE (fix) | `NoEpochCollision`, `NoFalseSteal` | 8,633 | PASS |
+| `_safety_noheartbeat` | FALSE | `NoEpochCollision` | 10,777 | PASS |
+| `_sab_noheartbeat` | FALSE (sabotage) | `NoFalseSteal` | 618 (to CE) | VIOLATED |
+
+**B160 counterexample (`sab_noheartbeat`, 7 states):** `Tick, Tick` → `Create(L2)` (L2 mid-round, `seq` frozen) → `ObserveOrSteal(L1)` (records frozen obs) → `Tick` (full window; heartbeat OFF, `hb` frozen) → `ObserveOrSteal(L1)` (owner, seq, AND hb all frozen → L1 steals from the alive, mid-round L2 → `falseSteal=TRUE`). With heartbeat ON, the second `Tick` bumps L2's `hb`; L1 backs off.
+
+**Design decisions driven by this model:**
+- Safety is independent of the heartbeat — the epoch-fence CAS alone prevents double-commit.
+- The advisory heartbeat is the minimal addition that eliminates false steals from a mid-round leader
+ whose `seq` is frozen for the round's duration.
+
+**Code currency:** CURRENT. Untouched by B171.
+
+---
+
+## Area 4 — Mount ownership and server-root identity {#area-mount}
+
+### `CaCasMountCore.tla` — mount ownership safety gate {#cacasmountcore}
+
+**Files:** `CaCasMountCore.tla`, `CaCasMountCore_stage1.cfg`, `CaCasMountCore_rev6_observe.cfg`,
+`CaCasMountCore_sab_adoptwedge.cfg`, `CaCasMountCore_sab_epochreset.cfg`,
+`CaCasMountCore_sab_fenceresurrect.cfg`, `CaCasMountCore_sab_foreigntakeover.cfg`,
+`CaCasMountCore_sab_wallclockreclaim.cfg`, `CaCasMountCore_witness_reclaim.cfg`,
+`CaCasMountCore_witness_observedreclaim.cfg`,
+`CaCasMountCore_witness_recoveryafterobservedreclaim.cfg`,
+`CaCasMountCore_witness_remountafterfence.cfg`
+
+**What it proves.** The sticky-owner / durable-monotone-epoch / TTL-lease mount protocol over a shared
+server-root, extended (2026-07-14) with the lease-boundary-exclusivity mechanism the shipped code
+uses: `NoTwoServerUuidsOwnSameServerRoot` (owner is sticky), `ForeignUuidNeverAutoTakesOver` (mount is
+never held by a non-owner), `WriterEpochMonotoneUnique` (the durable epoch counter is a monotone
+ceiling, never reset), `GlobalSupersededWriterMakesNoMutation` (a fenced/superseded actor makes no
+mutation — now a *knowledge* witness, not a per-write body-read guard), plus the observation-based
+reclaim safety of the exclusivity extension.
+
+The exclusivity extension is the load-bearing current mechanism: reclaiming an expired mount is
+**observation-based** — the reclaimer must observe a *stable holder token* over a full `TTL + Drift`
+window measured on its **own** monotonic clock (never trusting the wall-clock timestamp in the foreign
+mount body), and the reclaim installs the **successor's own body**. This matches the shipped
+`claimMountAwaitingExpiry` (`CasServerRoot.cpp:395-470`, threshold `ttl + ttl/20 + cadence` on
+`mono_ms_fn` only) and the same-uuid/different-epoch reclaim that installs `makeMountBody(our_uuid,
+our_epoch, …)` (`CasServerRoot.cpp:331-332`). The durable epoch is a separate CAS-bumped
+`ServerEpoch` object that fails closed rather than reset (`allocateWriterEpoch`,
+`CasServerRoot.cpp:151-195`). The per-write path is a **pure local** check
+(`CasMountRuntime::mayMutate` = `!lost ∧ now < deadline`, `CasMountRuntime.cpp:62-66`) — the old
+per-write epoch/body-read guard was correctly removed.
+
+| Config | Kind | Invariant / witness | Result |
+|---|---|---|---|
+| `stage1` | positive | sticky-owner / epoch-monotone / superseded-no-mutation | PASS |
+| `rev6_observe` | positive (main green gate) | observation-based reclaim safety | PASS |
+| `sab_foreigntakeover` | sabotage | `ForeignUuidNeverAutoTakesOver` | VIOLATED |
+| `sab_epochreset` | sabotage | `WriterEpochMonotoneUnique` | VIOLATED |
+| `sab_wallclockreclaim` | sabotage — reclaim trusts the foreign wall-clock timestamp | reclaim-exclusivity safety | VIOLATED |
+| `sab_adoptwedge` | sabotage | reclaim/adopt exclusivity | VIOLATED |
+| `sab_fenceresurrect` | sabotage | fence-out exclusivity | VIOLATED |
+| `witness_reclaim`, `witness_observedreclaim`, `witness_recoveryafterobservedreclaim`, `witness_remountafterfence` | reachability | the reclaim/fence-recovery states are reachable | VIOLATED (expected) |
+
+**Design decisions driven by this model:**
+- An expired-mount reclaim branch must still check `owner = uuid`, and must wait out `TTL + Drift` on
+ the reclaimer's **own** clock observing a stable token — never trust the foreign body's wall-clock
+ timestamp (`sab_wallclockreclaim` is the permanent negative control for that mistake).
+- The reclaim installs the successor's own body, not a copy of the foreign one.
+- The epoch object must never be reset; the durable counter is a strict monotone ceiling.
+- The per-write guard is a pure-local liveness check; a superseded actor is blocked by latching
+ `lost` (knowledge), not by a per-write shared-state read.
+
+**Code currency:** CURRENT (model faithful; audited 2026-07-22). Note: the earlier
+`sab_supersededwrites` config and the `SupersededWriterMakesNoMutation` per-write invariant were
+retired when the per-write body-read guard was removed; the exclusivity extension (observation-based
+reclaim, `Drift`, `sab_wallclockreclaim`, the `witness_observedreclaim*` / `witness_remountafterfence`
+reachability checks) is the current mechanism above.
+
+---
+
+## Area 5 — B140 dangle: faithful reproduction and fix proof {#area-b140}
+
+Three models formed a deliberate progression from initial reproduction to fix proof. The fix-proof
+model (`CaB140DangleMerge.tla`) is the kept survivor; the two reproduction models were removed
+during the 2026-07 model audit (sections kept as the record).
+
+### `CaB140DangleMerge.tla` — faithful B140 reproduction + fix proof {#cab140danglemerge}
+
+**Files:** `CaB140DangleMerge.tla`, `m_both_buggy.cfg`, `m_cursorskip.cfg`,
+`m_trimonly.cfg`, `m_merged.cfg`
+
+**What it proves.** The 2×2 necessity/sufficiency of the trim-gate + cursor-in-snap fix for the
+trim-before-durable dangle across a GC lease handoff.
+
+| `TrimGated` | `CursorInSnap` | Config | Result | States |
+|---|---|---|---|---|
+| F | F | `m_both_buggy` | `INV_NO_LOSS` violated | 0.71M (to CE) |
+| T | F | `m_cursorskip` | `INV_NO_LOSS` violated | 0.85M (to CE) |
+| F | T | `m_trimonly` | `INV_NO_LOSS` violated | 0.34M (to CE) |
+| T | T | `m_merged` | **clean** | 5.33M |
+
+**B140 counterexample (17 states):** L1 folds `add t2` (edge `t2→b1`) into in-memory wip → `GTrim` uses L1's in-memory cursor (trim-before-durable) → L1 loses lease, wip discarded → L2 rebuilds from empty committed snap → L2 GAP-skips trimmed `add t2` → `t2→b1` never enters any durable snap → L2 retires and deletes `b1` while live `t2` still references it.
+
+**Cursor-skip counterexample:** `GCommitCursor` publishes cursor=1 while committed edges still point at empty gen-0 → `GTrim` (gated by committed cursor) trims `add t2` → the cursor ran ahead of the edges, so the gate over-trims. This shows trim-gate alone is insufficient: the cursor it trusts must be coherent with the committed edges, which is exactly what cursor-in-snap guarantees.
+
+**Design decisions driven by this model:**
+- The committed snap is one atomic write-once object carrying its own fold cursor (no separate `GCommitCursor` step independent of edge commit).
+- The journal may be trimmed only up to the committed snap's cursor.
+- Neither half alone closes the dangle; both are necessary and jointly sufficient.
+
+**Code currency:** CURRENT (as history record). The live B140 protection model is `CaBuildRootPrecommit.tla`.
+
+---
+
+### `CaB140DangleFaithful.tla` — faithful refutation of Phase-1 mechanism {#cab140danglefaithful}
+
+**Files:** `CaB140DangleFaithful.tla`, `CaB140DangleFaithful_shared.cfg` — **removed 2026-07-22**
+(full text in git history).
+
+**What it proved.** Clean over 9.1M states — the Phase-1 B140 fix (faithful producers: no marker-retaining strip, no field-mixed generation adoption) does not exhibit the dangle under the original Phase-1 mechanism. Superseded `CaB140Dangle.tla` as the faithful producer model.
+
+**Code currency:** historical record only (a refutation about the long-gone Phase-1 mechanism); files removed 2026-07-22.
+
+---
+
+### `CaB140Dangle.tla` — Phase-1 B140 reproduction (superseded as producer) {#cab140dangle}
+
+**Files:** `CaB140Dangle.tla`, `CaB140Dangle_adopt.cfg`, `CaB140Dangle_blob.cfg`,
+`CaB140Dangle_loss.cfg`, `CaB140Dangle_producer.cfg`, `CaB140Dangle_safe.cfg` — **removed
+2026-07-21** (full text in git history).
+
+An initial Phase-1 reproduction with unfaithful producers (marker-retaining strip, field-mixed generation adoption). Superseded as a producer model by `CaB140DangleFaithful.tla`.
+
+**Code currency:** SUPERSEDED as producer; files removed 2026-07-21.
+
+---
+
+## Area 6 — Build watermark and resurrect liveness (B167) {#area-watermark-resurrect}
+
+These three models were **stale against shipped code** — they modeled a per-candidate blob-guard
+(`protectedByLiveBuild`) that B171 removed and a condemn-time `HeartbeatGuard` never implemented
+(deferred M-F Full GC). Their safety role fully migrated into `CaBuildRootPrecommit.tla`. All three
+were **removed on 2026-07-21**; these sections stay as the record of the investigation and as
+documentation of the B167 livelock shape.
+
+### `CaResurrectLiveness.tla` — abstract resurrect-liveness (B167) {#caresurrectliveness}
+
+**Files:** `CaResurrectLiveness.tla`, `CaResurrectLiveness_guard.cfg`,
+`CaResurrectLiveness_noguard.cfg` — **removed 2026-07-21** (full text in git history).
+
+**What it proves.** The abstract `HeartbeatGuard` boolean is load-bearing for resurrect liveness:
+guard ON → `<>published` holds (4 states); guard OFF → livelock lasso (7 states).
+
+**B167 livelock (noguard lasso):** `present=T, condemned=T` (dedup hit on stale incarnation) → `GcDelete` → `BuildUpload` (fresh incarnation, `freshOwned=T`) → `GcCondemn` (guard OFF: GC re-condemns the build's OWN fresh incarnation) → `GcDelete` → ... loop, `published` never TRUE. The key insight: the stale-incarnation upload→publish span is NOT atomic; GC can re-condemn in the gap.
+
+**Design decision:** writer-side re-upload alone is starvable; a guard that blocks GC from condemning a freshly-owned incarnation is required.
+
+**Code currency:** STALE vs shipped; files removed 2026-07-21. Modeled the deferred M-F `HeartbeatGuard` (condemn-time guard on live `build_id`). The shipped protection is precommit-first reachability (`CaBuildRootPrecommit.tla`).
+
+---
+
+### `CaBuildWatermark.tla` — concrete watermark oracle (B167) {#cabuildwatermark}
+
+**Files:** `CaBuildWatermark.tla`, `CaBuildWatermark_guard.cfg`, `CaBuildWatermark_noguard.cfg`,
+`CaBuildWatermark_staleactive.cfg`, `CaBuildWatermark_unsounddetect.cfg`,
+`CaBuildWatermark_crash.cfg` — **removed 2026-07-21** (full text in git history).
+
+**What it proves.** The concrete `min_active` scalar watermark oracle converges (safety: `Inv_ProtectedNeverCondemned`, `Inv_NoDangle` hold in all five configs; liveness: `<>(published=Builds)` holds with guard ON and crashes are leak-free). Three negative controls show the three independent failure modes each reproduce the B167 starvation lasso: no guard, stale active set (floor advances past in-flight builds), unsound crash detection (false-positive `gcDead`).
+
+**Design decisions driven by this model:**
+- `build_seq` must be allocated from a **monotone counter** (not just unique) — `CaBuildWatermarkNum` finding.
+- The active-set floor (`min_active`) must be the exact minimum of in-flight build sequences.
+- Crash detection must be sound (frozen-seq-across-K-passes discipline; false-positive death is fatal to liveness).
+
+**Code currency:** STALE vs shipped; files removed 2026-07-21. The per-candidate blob-guard (`protectedByLiveBuild`) was removed by B171 (replaced by precommit-first reachability). The watermark floor lemma (`monotone build_seq`) survives for precommit-ref reclaim (`Gc::prefixEligible` / the `BuildPrefix` watermark floor in `CasGc.cpp`), not blob protection.
+
+---
+
+### `CaBuildWatermarkNum.tla` — numeric watermark validation {#cabuildwatermarknum}
+
+**Files:** `CaBuildWatermarkNum.tla`, `CaBuildWatermarkNum_correct.cfg`,
+`CaBuildWatermarkNum_confused.cfg`, `CaBuildWatermarkNum_nonmonotonic.cfg` — **removed 2026-07-21**
+(full text in git history).
+
+**What it proves.** Safety (`Inv_ProtectedNeverCondemned`, `Inv_NoDangle`) of the concrete numeric floor with two servers, real `epoch` watermarks, and real `build_seq` allocation. Key finding: monotone `build_seq` allocation is load-bearing — uniqueness alone is insufficient; a non-monotone allocation lets `min_active` be pulled back below a finished build's seq, re-protecting a condemned blob (a leak). The `_confused` config (wrong server's watermark) violates `Inv_ProtectedNeverCondemned`.
+
+**Code currency:** STALE vs shipped (same as `CaBuildWatermark.tla`); files removed 2026-07-21. The monotone-`build_seq` floor lemma survives for precommit-ref reclaim.
+
+---
+
+## Area 7 — Root-local part-manifest GC (streaming sharded redesign) {#area-partmanifest}
+
+### `CaGcRootLocalPartManifestCore.tla` — root-local part-manifest GC R0 gate {#cagcrootlocalpartmanifestcore}
+
+**Files:** `CaGcRootLocalPartManifestCore.tla`, `CaGcRootLocalPartManifestCore_stage*.cfg`,
+`CaGcRootLocalPartManifestCore_sab_*.cfg`, `CaGcRootLocalPartManifestCore_witness_*.cfg`,
+`CaGcRootLocalPartManifestCore_live.cfg`
+
+**What it proves.** The full root-local part-manifest GC protocol (spec
+`2026-06-26-cas-gc-streaming-sharded-redesign-design.md` rev.15), including precommit + missing-body
+states, owner transitions, orphan sweep, mutable manifests, token-diff discovery (Phase 2), lazy trim
+(Phase 3, all-shard fresh fence only), target-sharded reducers (Phase 4), retire-token optimization
+(Phase 5), and attempt-scoped generation visibility (Phase 6). Invariants: `INV_NO_DANGLE`,
+`INV_NO_LOSS`, `INV_NO_RETURN`, `INV_JOURNAL_COVERAGE`, `NoManifestIdReuse`, `RefMatchesBody`,
+`ManifestNamespaceMatches`, `SingleManifestOwner`, `CommittedManifestBodyRequired`,
+`CommittedNoMissingBlob`, `NoCommittedDangle`, `BlobInDegreeMatchesActiveManifests`,
+`FoldedEdgesAreActive`, `ManifestActivationMatchesEdges`; action property `MonotoneGC`. Liveness:
+`OrphanManifestDebrisDrains` and `NoLeakForever` under `FairSpec`.
+
+**Positive stages (all HOLD):**
+
+| Stage | Config | Distinct states | Wall |
+|---|---|---|---|
+| 0 type/journal coverage | `stage0` | 19,846 | 0s |
+| 1 identity + body validation | `stage1` | 402,034 | 2s |
+| 2 owner transitions + precommit + promote | `stage2` | 68.6M | 7m11s |
+| 3 full GC pipeline | `stage3` | 365.6M | 27m45s |
+| 4 manifest cleanup + orphan sweep + mutable | `stage4` | 27.4M | 3m29s |
+| 5 token-diff discovery | `stage5_tokendiff` | 8.3M | 28s |
+| 5 lazy trim (Phase 3) | `stage5_lazytrim` | 338.8M | ~21m |
+| 5 target-sharded reducers (Phase 4) | `stage5_sharding` | 983.9M | ~65m |
+| 5 retire-token optimization (Phase 5) | `stage5_retiretoken` | 3.5M | <1m |
+| liveness | `live` | 17.8M | 30m10s |
+| Phase 6 attempt-scoping | `stage6_attemptscoping` | 11.7M | — |
+
+**28 negative controls — all produce their named counterexample (no unexpected pass):**
+
+Selected critical sabotages:
+
+| # | Config | Rule removed | Violated |
+|---|---|---|---|
+| 3 | `sab_splitpromote` | promote = two CAS with a gap, no fail-closed | `INV_NO_DANGLE` |
+| 5 | `sab_commitskipblobreval` | committed publish skips blob revalidation | `INV_NO_DANGLE` |
+| 7 | `sab_noorphansweep` | omit pre-precommit debris sweep | `OrphanManifestDebrisDrains` |
+| 11 | `sab_deletebodybeforedecrements` | delete body before decrements durable | `NoLeakForever` |
+| 12 | `sab_cutoverclaim` | cursor past unsealed deltas | `INV_NO_DANGLE` |
+| 14 | `sab_nofence` | skip global fence | `INV_NO_DANGLE` |
+| 25 | `sab_lazyfenceunsafe` | reuse stale parent fence position | `INV_NO_DANGLE` (24.5M states) |
+| 26 | `sab_reducerownsfence` | target reducer fences only its own shard | `INV_NO_DANGLE` |
+| 27 | `sab_crosssharddisplacement` | scatter drops displaced old-binding `-1` deltas | `INV_NO_LOSS` |
+| 28 | `sab_staletokenoverdelete` | stale stored token triggers destructive `Land` | `INV_NO_LOSS` |
+
+Full table in `CaGcRootLocalPartManifestCore_RESULTS.md`.
+
+**Key design decisions driven by this model:**
+
+- The fence is always all-shard fresh (reusing a stale parent fence position is load-bearing-unsafe —
+ `sab_lazyfenceunsafe` is a permanent negative control for this shape).
+- The target-sharded fold uses ONE global coordinator fence (`GCoordFence` over every root shard) —
+ independent per-shard fences by a reducer leave another shard's fence stale-low.
+- The scatter must emit paired `-1` old-binding deltas; inferring the old target from the new ref alone
+ under-counts in-degree for a cross-shard surviving ref.
+- The retire-token source (`storedTok`) must never over-delete: the destructive `Land` gate uses the
+ STORED token only if it matches the exact current `tokOf` (the stale-disjunct sabotage shows
+ `INV_NO_LOSS` not `INV_NO_RETURN` is the violation when a live re-incarnated object is over-deleted).
+- Deposed-leader GC attempts must never be reader-visible: only the ADOPTED attempt's artifact can
+ be consulted (Phase 6 `INV_ONLY_ADOPTED_VIEWABLE`).
+
+> **Note — fence-era controls document a SUPERSEDED mechanism.** The all-shard fence and its negative
+> controls (`sab_nofence` #14, `sab_lazyfenceunsafe` #25, `sab_reducerownsfence` #26) prove the
+> *fence-based* create-ordering that the ack-floor redesign (Area 11) replaced with a causal
+> acknowledgement floor. They remain valid as evidence that fencing only changed shards or reusing a
+> stale fence position was unsound *within that mechanism* — which is exactly why the mechanism, not a
+> patch of it, was replaced. This model's section is kept as historical evidence and is **not**
+> deleted; the live round protocol and its proofs are `CaGcAckFloorCore.tla` +
+> `CaGcAckFloorZombie.tla` (Area 11). The fold, manifest-cleanup, orphan-sweep, source-edge, and
+> attempt-scoping results above are unaffected by the redesign and stay CURRENT.
+>
+> **Audit note (2026-07-22): the `EnableSharding` arm no longer runs — a regression, not just
+> supersession.** All three sharding configs — the POSITIVE stage `stage5_sharding` (historically
+> 983.9M states, now crashes at 158) and the sabotages `sab_reducerownsfence` (#26) and
+> `sab_crosssharddisplacement` (#27) — throw a TLC `RuntimeException` (`CHOOSE m ∈ {}` at `TheM`,
+> `.tla:113`, reached from the `GReduceShard`/scatter path): a both-empty edge (`e.old = e.new = {}`)
+> reaches the `IF e.old # {} THEN TheM(e.old) ELSE TheM(e.new)` extraction and applies `CHOOSE` to an
+> empty set. So the sharded scatter/reduce machinery currently has NO working positive gate in this
+> model, and its two fence-era sabotages crash rather than cleanly violate. The sharding correctness
+> is still exercised by the C++ `gtest_cas_gc_shard_plan.cpp`; fixing the model's sharding arm (guard
+> or repair the both-empty edge in the scatter) is proof-model surgery folded into the deferred
+> fence/recheck-and-sharding realignment (see §area-ackfloor), not an unattended edit. The
+> non-sharding stages (`stage0..4`, `stage5_tokendiff`/`_lazytrim`/`_retiretoken`,
+> `stage6_attemptscoping`, `live`) and their sabotages are unaffected.
+
+**Code currency:** CURRENT for the fold/manifest/attempt-scoping machinery the ack-floor round reuses;
+the fence/recheck phases it models are SUPERSEDED by Area 11 (controls kept as historical evidence).
+The largest and most comprehensive model in the corpus.
+
+### `SkipParksDeadPrecommit` — dangling-precommit orphan gate (C++ fix LANDED) {#skipparksdeadprecommit}
+
+**Files:** `CaGcRootLocalPartManifestCore.tla` + `CaGcRootLocalPartManifestCore_sab_skipparksdeadprecommit.cfg`
+(bug) / `_fix_skipparksdeadprecommit.cfg` (fix).
+
+**What it proves.** The `DANGLING-PRECOMMIT` manifest orphan found in the `utils/ca-soak` S30 churn
+scenario (2026-07-07): an abandoned precommit (activated body, never promoted, never removed) sits on a
+content-static ref-shard; the token-diff `Skip` parks the shard, so the fold-visit
+`reclaimAbandonedPrecommit` never re-runs, and the precommit — provably dead once the watermark advances
+past its `build_sequence` — orphans its manifest forever (the orphan sweep spares it because
+`activeManifestKeys` keeps the un-removed precommit binding live). The liveness property
+`LiveDeadPrecommitReclaimed` (a present, body-present, still-bound, watermark-dead abandoned precommit is
+eventually reclaimed, under `WF` of `GReclaimDeadPrecommit`) is **VIOLATED** with
+`SabotageSkipParksDeadPrecommit = TRUE` (the shipped bug — the static shard is parked forever) and
+**HOLDS** with `= FALSE` (the fix: `CanSkipShard` force-Reads a shard holding a watermark-dead live
+precommit, keyed on the same durable death fact `reclaimAbandonedPrecommit` uses). TLC v2.19: bug cfg
+violated (174,024 distinct states); fix cfg no error (800,072 states). `GReclaimDeadPrecommit` refines
+`WAbandonPrecommit` (introduces no new reachable state), so the fix only restricts WHEN reclaim fires.
+All four `EnableTokenDiff = TRUE` sibling cfgs re-verified with the added constant — no counterexample
+masked; `sab_skipchangedshard` still violates `INV_NO_DANGLE`.
+
+**C++ fix (LANDED, branch `cas-gc-rebuild`).** `Gc::computeDiscoverDecisions` overrides a would-be `Skip`
+to Read when the sealed `ShardCoverage`'s minimal live precommit `isPrecommitDead` vs the namespace mount
+watermark (`ProfileEvents::CasGcPrecommitRevisitForced`); `Gc::fold` stamps the minimal live precommit
+into the coverage; the shared `isPrecommitDead` helper is used by both the guard and
+`reclaimAbandonedPrecommit`. Unit test `CasDanglingPrecommit.*` (deterministic: reclaim after the
+watermark advances; idempotency; Skip preserved for live/no precommit; double-removal idempotency). S30
+regression: pre-fix left 1 `_manifests` orphan (FAIL); post-fix ×4 seeds all PASS with residual 0
+(`reclaimAbandonedPrecommit` observed firing live in one seed via the normal Read path — the specific
+force-Read/parked-static-shard timing is a rare race exercised deterministically only by the unit test).
+
+**Follow-up (out of scope, backlogged):** the write-path `PROMOTE-OVER-COMMITTED-LEAK` (a distinct
+writer-side leak reachable via a `republishRef` crash re-drive) and `ABANDON-RETIRE-ORDERING`.
+
+**Code currency:** CURRENT (the C++ fix matches this gate).
+
+### `AtMostOneCommittedManifestPerRef` — promote-over-committed gate (C++ fix LANDED) {#atmostonecommittedmanifestperref}
+
+**Files:** `CaGcRootLocalPartManifestCore.tla` + `CaGcRootLocalPartManifestCore_stage2.cfg`.
+
+**What it proves.** The `PROMOTE-OVER-COMMITTED-LEAK` write-path bug (2026-07-08): `Build::promote`
+overwrote `refs[ref]` with a Δ=0 owner-move without releasing a pre-existing *different* committed manifest
+`T_old`, leaving two committed bindings for one ref and orphaning `T_old` (owner↔refs divergence);
+reachable via a `republishRef` crash re-drive (RENAME/DETACH-ATTACH). The model's `WPromote`/
+`WPublishCommitted` ALREADY enforce `RefFreeFor(ref, m)` (a ref owns ≤1 committed manifest) — the shipped
+C++ simply diverged. The new invariant `AtMostOneCommittedManifestPerRef ==
+\A r \in Refs : Cardinality({m \in ManifestIds : owner[m] = r}) <= 1` makes that property TLC-checked; it
+**HOLDS** in `stage2` (68.5M distinct states) under the enforced `RefFreeFor`. It is logically equivalent
+to the pre-existing `SingleManifestOwner` (same ref→manifest direction) — a dedicated, named regression
+gate rather than new coverage. The bug itself is reproduced deterministically by the C++ RED test
+(`CasPromoteRepublish.*`), so no `SabotagePromoteOverwritesCommitted` negative control was added (it would
+force editing all 47 sibling cfgs for marginal added assurance).
+
+**C++ fix (LANDED, branch `cas-gc-rebuild`).** `Build::promote` fail-closes with `ABORTED` when
+`refs[final_ref_name]` already names a different committed `manifest_ref` (a same-manifest re-promote and an
+absent ref proceed); `ContentAddressedTransaction::republishRef` is idempotent on the destination (skip the
+publish + `dropRef(src)` when dst is already committed with the same path-sorted `entries`; `ABORTED` on a
+different-content conflict), so RENAME/DETACH-ATTACH re-drives no longer leak; and `Build::abandon` retires
+its build_seq only after the precommit-removal CAS (`ABANDON-RETIRE-ORDERING`, closing the double-removal
+window that the just-landed dangling-precommit fix made more frequent). Unit tests: `CasPromoteRepublish.*`.
+
+**Code currency:** CURRENT (the C++ fix matches this gate).
+
+---
+
+## Area 8 — In-degree re-fold undercount (B-indeg fix) {#area-indeg-refold}
+
+### `CaGcIndegRefoldCore.tla` — in-degree re-fold undercount {#cagcindegrefoldcore}
+
+**Files:** `CaGcIndegRefoldCore.tla`, `CaGcIndegRefoldCore_fix.cfg`, `CaGcIndegRefoldCore_sab.cfg` —
+**removed 2026-07-22** (models a superseded integer-accumulation design; full text in git history).
+
+> **Removed — the shipped in-degree design is no longer a non-idempotent integer stream.** The
+> `cas-gc-rebuild` fold computes in-degree by an **idempotent two-cursor presence-set merge** keyed
+> by `(blob_hash, source_id)` (`CasBlobInDegree.cpp:380-389`: "prior present + activate ⇒ present;
+> any remove ⇒ absent"); the surviving-edge count is a `uint64_t` that structurally cannot go
+> negative, and there is no `merged < 0 → CORRUPTED_DATA` guard. This model existed precisely to
+> catch the integer-underflow that the idempotent edge-set recompute cannot express — but the code
+> adopted exactly that idempotent recompute, so the hazard is now structurally impossible and the
+> model describes a design the code abandoned (its three code citations had all drifted). Not a
+> CODE-RISK: the property it guarded holds by construction. Retired for the same reason as the EBR
+> `CaGcCore.tla` — a superseded-design model.
+
+**What it proved.** The minimal model of the bug H1b: the completion-seal cursor must be persisted at
+`max(foldCursor, fenceVersion)` (past what recheck already folded), not at `foldCursor` (the
+pre-window fold-time cursor). If the seal persists `foldCursor`, the next round's fold reconstructs its
+parent cursor from that seal and re-folds the fence-window removal, driving the integer in-degree
+counter to `-1` → `INV_INDEG_NONNEGATIVE` violated (in the C++, `merged < 0` → `CORRUPTED_DATA`).
+
+Invariant: `INV_INDEG_NONNEGATIVE` (`∀ b : indeg[b] >= 0`). Bounds: `Blobs={b1}`, `MaxLog=4`,
+`MaxRound=3`.
+
+| Config | `SabotageCompletionCursorAtFold` | Result |
+|---|---|---|
+| `_fix` | FALSE (fix: cursor = `max(foldCursor, fenceVersion)`) | PASS |
+| `_sab` | TRUE (bug: cursor = `foldCursor`) | `INV_INDEG_NONNEGATIVE` VIOLATED |
+
+**Why this model is necessary (not caught by `CaGcRootLocalPartManifestCore`):** the large model
+recomputes in-degree from a folded EDGE SET — set-difference recompute is idempotent, so re-folding is
+a no-op there. The C++ accumulates in-degree as a NON-idempotent INTEGER delta stream; re-folding an
+already-absorbed removal drives the counter negative. This minimal model targets that concrete
+implementation class directly.
+
+**Design decision:** the `Seal` action must advance `persistedCursor` to `fenceVersion` (not remain at
+`foldCursor`) so the next round's `parentCursor` starts past the events recheck already consumed.
+
+**Code currency:** REMOVED 2026-07-22 — superseded integer-accumulation design (see the note above).
+
+---
+
+## Area 9 — Shard incarnation and registry removal (D1) {#area-shard-incarnation}
+
+### `CaGcShardIncarnationCore.tla` — registry removal gate (D1 Phase 0) {#cagcshardincarnationcore}
+
+**Files:** `CaGcShardIncarnationCore.tla`, `CaGcShardIncarnationCore_design.cfg`,
+`CaGcShardIncarnationCore_sab_newbornnofloor.cfg`, `CaGcShardIncarnationCore_sab_pathkeyedcursor.cfg`,
+`CaGcShardIncarnationCore_sab_deletebeforefold.cfg`, `CaGcShardIncarnationCore_sab_incarnationreuse.cfg`
+
+**What it proves.** With the namespace registry removed (proven load-bearing in `CaIncarnationCore` via
+`sab_noregistry`), two replacement coordinates keep the safety invariants: (1) a durable never-reused
+per-`(ns,shard)` incarnation (`sInc`, from a per-shard high-water `sIncMax`), and (2) a newborn shard
+born fenced to the current `gcRound` (self-floor). Invariants: `INV_NO_DANGLING` (no committed ref to
+an absent/dead-token blob) and `INV_NO_ORPHAN_EDGE` (no folded edge outlives its shard object).
+
+| Config | Flags | Result | States |
+|---|---|---|---|
+| `_design` | none | ✅ No error | 5,872,030 |
+| `_sab_newbornnofloor` | drop round self-floor, keep incarnation | ❌ `INV_NO_DANGLING` violated | — |
+| `_sab_pathkeyedcursor` | drop incarnation from cursor, keep round | ❌ `INV_NO_DANGLING` violated (ABA) | — |
+| `_sab_deletebeforefold` | delete before journal fully folded | ❌ `INV_NO_ORPHAN_EDGE` violated | — |
+| `_sab_incarnationreuse` | recreate draws ≤ `sIncMax` (same-path ABA) | ❌ `INV_NO_DANGLING` violated | — |
+
+**Design decisions driven by this model:**
+
+- Neither coordinate alone suffices: the pool-global round closes the publish-race the registry
+ previously closed; the per-shard incarnation prevents ABA confusion of a delete+recreate at the same
+ path. The design's two-coordinate model is minimal.
+- Per-shard monotonicity is the invariant, not global uniqueness. Cross-shard incarnation collision is
+ safe (the fold cursor keys by `(shard, incarnation)`, so the same value in two shards is not ABA);
+ same-shard reuse across a delete+recreate IS dangerous.
+- Reclaim ordering is load-bearing: delete must wait until the journal is fully folded (tombstone
+ included).
+- The registry can be deleted — the spec's `pending-newborns` ephemeral fallback is NOT needed.
+
+**Code currency:** CURRENT (D1 Phase 0 gate, 2026-07-01).
+
+---
+
+## Area 11 — Ack-floor GC fence redesign {#area-ackfloor}
+
+These two focused models prove the one-pass ack-floor round that replaced the per-round all-shard
+fence and the fold-through-fence recheck (spec `2026-07-02-cas-gc-ack-floor-fence-redesign.md`; GC
+protocol `04 §gc-round`). They are the formal gate for that redesign; the fold/manifest machinery
+the round reuses stays proved by `CaGcRootLocalPartManifestCore.tla` (Area 7).
+
+> **Note — the writer-heartbeat half is superseded by v3 freshness-meta (2026-07-10), the GC-side round
+> half is not.** Both models foreground writer `observed_gc_round` heartbeats and a `min_ack` floor
+> (`condemn_round < min_ack`) as the graduation gate. The freshness-meta redesign
+> (`docs/superpowers/plans/2026-07-10-cas-freshness-meta-v3.md`) removed exactly that half:
+> `CasRetireView.{h,cpp}` (the writer-side retired-view download this ack floor fed) is deleted;
+> `Core/Proto/cas_format.proto` reserves the wire field (`reserved 10; // was observed_gc_round; the
+> writer-side retired-view ack floor was removed (v3 Task 6)`); and `Gc::graduationDue` (`CasGc.cpp`)
+> now paces graduation on GC rounds alone, per its own comment — "graduation itself paces on GC rounds
+> via `new_round`, not on heartbeat acks". The GC-side round pipeline these models also cover —
+> `GBegin`/`GFold`/`GComplete`, the two-step commit gate, `graduationDue`'s `condemn_round < current_round`
+> check, disaster-recovery `GRebuild`, the clamp-suppression guard (§area-clamp-suppression) — is
+> UNCHANGED and stays CURRENT. Only the writer-heartbeat/`min_ack` half of what these models prove is
+> historical; the models are kept as the record of the mechanism they gated, not deleted.
+
+### `CaGcAckFloorCore.tla` — ack-floor round core {#ackfloor-core}
+
+**Files:** `CaGcAckFloorCore.tla`, `CaGcAckFloorCore_stage1.cfg`, `CaGcAckFloorCore_sab_*.cfg`,
+`CaGcAckFloorCore_witness_*.cfg`. Run wrapper: `run_ackfloor.sh`.
+
+**What it proves.** Writers advertise `observed_gc_round` through heartbeats; GC graduates a retired
+entry only when `condemn_round < min_ack` computed over heartbeats that are **live OR
+expired-but-not-fenced**; a fenced heartbeat can never renew or land a commit again. Commits are
+two-step (`WPrepare` = gate evaluation, `WLand` = CAS response) so the in-flight window is a real
+interleaving, and the pass is three steps (`GBegin` / `GFold` / `GComplete`) so a commit landing
+between the fold cut and the deletes is a real interleaving. The model also includes disaster-recovery
+rebuild (`GRebuild`, spec `2026-07-03-cas-gc-rebuild-design.md`, with a `lostRefs` ghost variable for a
+sabotaged rebuild's dropped edges), sourceless copy-forward (`WCopyForward`), and an honest fold clamp
+(`clampedL` / `clampedEver`) with a suppression guard in `GComplete`. Invariants: `INV_NO_DANGLE` (no
+landed, folded, or rebuild-lost ref points at an absent blob), `INV_NO_RETURN` (no ref binds a deleted
+incarnation), `INV_ACK_LE_VIEW` (the honest ack never runs ahead of the installed view).
+
+Bounds: `Writers = {w1, w2}`, `Blobs = {b1}`, `MaxRound = 4`, `MaxTok = 4`.
+
+**Eleven sabotages (negative controls — each breaks exactly one load-bearing rule and MUST produce a
+counterexample):**
+
+| Config | Rule broken |
+|---|---|
+| `sab_ignorefloor` | graduate ignoring the floor entirely (`condemn_round ≥ min_ack`) |
+| `sab_ackwithoutread` | ack advances without installing the retired view |
+| `sab_ackbeforedrain` | ack advances while an old-view commit is still in flight (no drain) |
+| `sab_sleeperrearm` | the floor excludes expired-UNFENCED heartbeats (assumes dead without a fence-out) |
+| `sab_skipshard` | the fold cut leaves one landed ref unconsumed |
+| `sab_adopttoken` | the commit gate references a visibly-retired token instead of recreating |
+| `sab_openbeforeload` | a fresh mount starts with an unloaded (round-0) view and mutates before loading |
+| `sab_rebuilddropedge` | rebuild loses one committed owner's folded ref |
+| `sab_rebuildkeepretired` | rebuild carries the old retired entries into the new baseline |
+| `sab_rebuildlowround` | rebuild mints a round below surviving mount acks |
+| `sab_clampnosuppress` | graduation ignores a declared clamp — reproduces the 2026-07-03 night incident |
+
+**Six witnesses (negated reachability — a TLC "violation" means the state IS reachable):**
+`W_DeleteHappens` (condemn → graduate → delete), `W_SpareHappens` (condemn → recover → spare),
+`W_RecreateHappens` (recreate → `TokenMismatch` on the pending delete), `W_CopyForwardHappens`
+(sourceless copy-forward taken), `W_RebuildHappens` (raw rebuild taken), `W_ClampHappens` (an honest
+clamped pass occurred).
+
+**Design decisions driven by this model:**
+- Graduation is gated purely by the causal floor (`condemn_round < min_ack`) — no wall clock on the
+ happy path.
+- The ack must not advance without loading the retired list, and not before the drain completes.
+- An expired heartbeat must be fenced-out (not merely assumed dead) before it drops out of the floor.
+- The commit gate must recreate a listed token, never adopt it.
+- Rebuild must discard the old retired list and mint a round above every surviving mount ack (see
+ §area-clamp-suppression for the clamp/rebuild/copy-forward extension detail).
+
+**Code currency:** MIXED — the graduation gate this model checks is SUPERSEDED; the rebuild and
+clamp-suppression parts still MATCH (audited 2026-07-22, deeper than the "minor drift" this note used
+to claim). The model's `GComplete` graduation predicate is `e.r < minAckL`, a **writer-ack floor**
+(`MinAck` over `wAck` of the live/expired `FloorSet`) — but the shipped `Gc::graduationDue` paces on
+`condemn_round < current_round` alone, with **no** writer-ack input (`CasGc.cpp:1867`; the merge gate
+is `!suppress_destructive && e.condemn_round < current_round`, `CasBlobInDegree.cpp:431`). So the
+model's entire writer-heartbeat apparatus — `WOpen`/`WBeat` advertising a round into `wAck`,
+`FloorSet`/`MinAck`/`minAckL`, and the `sab_sleeperrearm` / `sab_ackbeforedrain` / `sab_ackwithoutread`
+sabotages and `INV_ACK_LE_VIEW` that test it — models a graduation input the code no longer has. What
+still MATCHES and gives the model its unique current value: clamp suppression (`~clampedL` guarding
+graduation ↔ `suppress_destructive`, `CasGc.cpp:1360`), disaster-recovery `GRebuild` (↔
+`Gc::rebuildBaseline`, `CasGc.cpp:1909`: empty retired list, over-protect unowned-alive, round minted
+above surviving numbers), the three-step cut + two-step commit window, and exact-token delete. Not a
+CODE-RISK: round-pacing is a stricter safety condition than the ack floor, and the current graduation
+is independently proven by `CaGcRoundDeferCore` + `CaGcCondemnMarkerGate` + the two-phase
+`CaGcAckFloorZombie`. To realign, trim the ack apparatus and re-base graduation on the latched round,
+keeping the `GFenceOut`/`WExpire` fence-out only as *liveness*; this is a scoped proof-model rewrite
+(false-green risk — kept for a careful pass with adversarial review), not done here.
+
+---
+
+### `CaGcAckFloorZombie.tla` — two-leader `delete_pending` gate {#ackfloor-zombie}
+
+**Files:** `CaGcAckFloorZombie.tla`, `CaGcAckFloorZombie_stage1.cfg`,
+`CaGcAckFloorZombie_sab_eagerdelete.cfg`, `CaGcAckFloorZombie_witness_delete.cfg`. Run wrapper:
+`run_ackfloor_zombie.sh`.
+
+**What it proves.** The two-phase graduation (`delete_pending`, Task-9 amendment; `04
+§two-phase-graduation`) is load-bearing when **two** leaders' passes fully interleave. Each leader
+latches `(round, retired list, fold-cut in-degrees)` at `GBegin`, deletes pre-publish, and its
+publish CAS succeeds only if `(round, retired)` are unchanged since the latch (the `gc/state` token
+guard) — so a deposed leader's merge output evaporates but its deletes do not. Restricting pre-CAS
+deletes to entries **already published pending by a previous round's CAS** keeps a deposed leader's
+arbitrarily-stale snapshot from deleting a live blob.
+
+Bounds: `Writers = {w1, w2}`, `Leaders = {l1, l2}`, `Blobs = {b1}`, `MaxRound = 5`, `MaxTok = 4`.
+Honest stage (`SabotageEagerZombieDelete = FALSE`) is clean (~2 M distinct states) on `INV_NO_DANGLE`
++ `INV_NO_RETURN`. `sab_eagerdelete` (a pass ALSO deletes its FRESH graduations — the pre-amendment
+single-phase behavior) yields the `INV_NO_DANGLE` counterexample, proving `delete_pending`
+load-bearing. `witness_delete` shows the pending-delete path is reachable.
+
+**Order invariant surfaced by this model (the implementation must never lose it):** the ack floor is
+latched **no later than the fold cut**. A floor read *after* the cut would see acks advertised by
+writers whose in-flight commits landed after the cut (invisible to this pass's in-degrees), and a
+fresh graduation could then go pending over a live reference. Pinned as a comment at the
+`computeHeartbeatFloor` call site and in the spec's TLA+ section.
+
+**Code currency:** CURRENT (minor drift; partially superseded — v3 freshness-meta, see the note above).
+The two-phase `delete_pending` graduation and the deposed-leader `gc/state` CAS guard are GC-side
+mechanics and stay CURRENT (`cas-gc-ack-floor-fence`); graduation no longer depends on a writer
+heartbeat ack.
+
+---
+
+## Area 10 — Superseded EBR/epoch GC core {#area-ebr}
+
+### `CaGcCore.tla` — EBR epoch/generation design (superseded) {#cagccore}
+
+**Files:** `CaGcCore.tla`, `CaGcCore_stage*.cfg` — **removed 2026-07-21** (superseded design; this
+section stays as the historical record, full text in git history).
+
+The original GC core, based on Epoch-Based Reclamation (EBR) with a monotone `epoch_current` counter,
+per-writer `O_W` pins, and a `+`/`-` event fold. Checked at stages 1–4 (full adversarial: expiry gap,
+split-brain, total Keeper wipe); all four stages PASS (`INV_NO_LOSS`, `INV_NO_DANGLE`, `INV_NO_ABA`,
+`TypeOK`). **Superseded** by `CaIncarnationCore.tla` (incarnation-token design, 2026-06-10+). Four
+counterexamples found during EBR model development encoded the four load-bearing rules:
+
+- **CE-1** (`INV_NO_LOSS`): flush-`+`-then-advance must cover the decide→`+`-durable window. A writer
+ advancing `O_W` before its `+` is durable allows GC to condemn and delete the epoch while the writer
+ still holds an unpublished ref.
+- **CE-2** (`INV_NO_LOSS`): the reuse decision must target an epoch the writer currently observes
+ (`e ≥ O_W[w]`); reusing an epoch the writer already advanced past means its live lease no longer
+ covers the dependency.
+- **CE-3** (modeling artifact): a shared `plusDurable` boolean collapsed all `+(e)` into one flag;
+ a drop removed another writer's in-flight reuse pin. Fixed by per-writer `pin` sets.
+- **CE-4** (real design constraint, `INV_NO_LOSS`): failing on `Disconnected` alone does not close the
+ `[t_expire, t_aware]` session-expiry gap. A self-fence on a local elapsed-time deadline strictly
+ inside `T_session` is required.
+
+**Code currency:** SUPERSEDED. Replaced by the incarnation-token design; files removed 2026-07-21.
+
+---
+
+## Summary table {#summary-table}
+
+| Model | Area | Status | Key invariant(s) | Sabotages | Design decisions |
+|---|---|---|---|---|---|
+| `CaIncarnationCore.tla` | GC core | **CURRENT as safety spine**; concrete structure superseded (manifest-embedded journal/fence vs ref-log/source-edge-run) | `INV_NO_DANGLE`, `INV_NO_LOSS`, `INV_NO_RETURN`, `INV_JOURNAL_COVERAGE` | 11 | fence, recheck, exact-token delete, atomic cascade, registry fence-time universe, evidence re-observation |
+| `CaIncarnationProofCore.tla` | GC core (Apalache) | **REMOVED 2026-07-22** (stale pre-B91; unverifiable — no Apalache) | `IndInv` (19 conjuncts) inductive at fixed bounds | 5 negative controls | `W-REVALIDATE` is load-bearing (F1 machine-checked); `InflightCurrentUnreferenced` is irredundant |
+| `CaBuildRootPrecommit.tla` | Precommit/B140/B199-S2 | **CURRENT conclusion**; 2 mechanisms drifted (inline-closure → lazy-fold+clamp-barrier; presence-gate → owner-liveness) | `INV_NO_DANGLE_COMMITTED`, `INV_BUILDROOT_PROTECTS`, `INV_COMMIT_FAILCLOSED`, `INV_NO_LEAK` | 2 + 1 liveness | build-root + fail-closed commit jointly necessary; inline closure at precommit time |
+| `CaGcLeaseCore.tla` | Lease/B160 | **CURRENT** | `NoEpochCollision`, `NoFalseSteal` | 1 | advisory heartbeat eliminates false steals; safety independent of heartbeat |
+| `CaCasMountCore.tla` | Mount | **CURRENT** (rev.6 observation-reclaim; audited 2026-07-22) | `NoTwoServerUuids…`, `ForeignUuid…`, `WriterEpochMonotoneUnique`, `GlobalSupersededWriter…` | 5 (incl. `sab_wallclockreclaim`) | sticky owner, monotone epoch, observation-based reclaim on own clock (not foreign wall clock), pure-local per-write guard |
+| `CaB140DangleMerge.tla` | B140 fix proof | **CURRENT** (history record) | `INV_NO_LOSS` | 2×2 matrix | trim-gate + cursor-in-snap jointly necessary |
+| `CaB140DangleFaithful.tla` | B140 history | **REMOVED 2026-07-22** (dead-mechanism refutation) | `INV_NO_LOSS` | — | Phase-1 mechanism clean with faithful producers |
+| `CaB140Dangle.tla` | B140 history | **REMOVED 2026-07-21** (unfaithful producers) | — | — | Phase-1 investigation record |
+| `CaResurrectLiveness.tla` | Resurrect/B167 | **REMOVED 2026-07-21** (stale, deferred M-F guard) | `<>published` | 1 | upload→publish span not atomic; heartbeat guard load-bearing |
+| `CaBuildWatermark.tla` | Watermark/B167 | **REMOVED 2026-07-21** (blob-guard removed by B171) | `Inv_ProtectedNeverCondemned`, `Inv_NoDangle`, liveness | 3 | monotone `build_seq`, exact min active set, sound crash detection |
+| `CaBuildWatermarkNum.tla` | Watermark numeric | **REMOVED 2026-07-21** (blob-guard removed by B171) | `Inv_ProtectedNeverCondemned`, `Inv_NoDangle` | 2 | monotone `build_seq` (not just unique), per-server scoping |
+| `CaGcRootLocalPartManifestCore.tla` | Part-manifest GC R0 | **CURRENT** (fold/manifest/attempt-scoping); fence/recheck phases SUPERSEDED by Area 11 | `INV_NO_DANGLE/LOSS/RETURN`, 10 more; liveness | 28 | all-shard fresh fence (superseded), single coordinator fence (superseded), scatter deltas, stale-token-no-over-delete, attempt-scoped visibility |
+| `CaGcIndegRefoldCore.tla` | Indeg re-fold | **REMOVED 2026-07-22** (superseded integer-delta design) | `INV_INDEG_NONNEG` | 1 | seal cursor at `max(foldCursor, fenceVersion)`, not `foldCursor` |
+| `CaGcShardIncarnationCore.tla` | Registry removal D1 | **CURRENT** | `INV_NO_DANGLING`, `INV_NO_ORPHAN_EDGE` | 4 | two-coordinate replacement (incarnation + round self-floor) for registry; per-shard monotonicity |
+| `CaGcAckFloorCore.tla` | Ack-floor round core | **MIXED**: graduation gate (writer-ack floor) SUPERSEDED by round-only pacing; `GRebuild` + clamp-suppression still MATCH | `INV_NO_DANGLE`, `INV_NO_RETURN`, `INV_ACK_LE_VIEW` | 11 | causal floor gates graduation; ack after drain + view load; expired ⇒ fence-out; recreate not adopt; rebuild discards retired list + mints round above all acks; clamp suppression gates graduation |
+| `CaGcAckFloorZombie.tla` | Ack-floor two-leader | **CURRENT** (minor drift; writer-heartbeat half superseded — v3) | `INV_NO_DANGLE`, `INV_NO_RETURN` | 1 | `delete_pending` two-phase graduation load-bearing; floor latched ≤ fold cut (order invariant) |
+| `CaGcCore.tla` | EBR GC core | **REMOVED 2026-07-21** (superseded) | `INV_NO_LOSS`, `INV_NO_DANGLE`, `INV_NO_ABA` | 4 CEs during dev | EBR design record; replaced by incarnation-token |
+| `CaGcRoundDeferCore.tla` | GC round DEFER/skip-unchanged | **CURRENT** | `NoOverDelete`, `NoDangle`; `EventuallyFolded` | 2 | a due graduation force-folds first (no destructive decision on a not-fully-folded snapshot); deferral bounded (`deferCount < MaxDefer`) |
+| `CaEdgeBeforeObserve.tla` | Writer/GC simplification Gate A | **CURRENT** for no-tokened-reval + order + K1; **K3Head/K3AdoptCheck DRIFTED** (tokenless leaf now manifest-trusted, no promote probe) | `NoOverDelete`-shaped safety (implicit) | 4 | with EDGE-BEFORE-OBSERVE + same-pass decided-delete, promote-time revalidation of TOKENED leaves is redundant; K1/K3Head/K3AdoptCheck + the order itself stay load-bearing |
+| `CaMetaDescriptor.tla` | Writer/GC simplification Gate B (meta descriptor, v1) | **REMOVED 2026-07-22** (`INV-META-BODY` linearizer framing false vs shipped advisory meta) | `INV-META-BODY` | 7 | create bottom-up (body, then meta); delete top-down (meta at captured etag, then body at condemn-time token) |
+| `CaMetaDescriptorRaw.tla` | Gate B raw-body / terminal-tombstone | **REMOVED 2026-07-21** (rejected by v3) | `INV_NO_LOSS`, `INV_NO_DANGLE`, `INV_META_BODY` | 5 | raw immutable bodies force a terminal tombstone + writer-waits-on-GC coupling; rejected in favor of keeping the in-body incarnation tag |
+| `CaMetaIncarnationKey.tla` | Gate B Option B (per-incarnation body keys) | **REMOVED 2026-07-21** (rejected) | `INV_NO_DANGLE` (implicit) | 1 | removes the tombstone/wait but reintroduces the already-rejected generation-in-key design (404→LIST, manifest carries incarnation) |
+| `CaManifestSweepWindow.tla` | Orphan-sweep vs removal-fold wedge | **REMOVED 2026-07-22** (gtest covers) | `INV_FOLD_PROGRESS` | 1 | the orphan sweep must skip a committed body with a pending (unsealed) removal — the removal-fold still needs the body to emit its decrement |
+
+---
+
+## Running the models {#running-models}
+
+All models run from `docs/superpowers/models/`. TLC jar expected at `../../../tmp/tla2tools.jar`
+(v2.19). Shell wrappers: `run_tlc.sh`, `run_gc_partmanifest.sh`, `run_mount.sh`,
+`run_apalache.sh`. Apalache binary at `../../../tmp/apalache/bin/apalache-mc` (v0.58.0+).
+
+```bash
+# incarnation core — main staged suite
+./run_tlc.sh CaIncarnationCore_stage1.cfg
+# ... stages 2, 3, 4_small, 4_journaltree, 5_small, 6_registry, 6_evstale
+# sabotages (all MUST exit non-zero):
+for c in nofence norecheckfold noretireview unconddelete reusedtag cascade \
+ cutoverclaim noreobserve noregistry foldtimeuniverse noevreobserve; do
+ ./run_tlc.sh CaIncarnationCore_sab_$c.cfg && echo "UNEXPECTED PASS: $c"
+done
+
+# build-root / precommit
+for cfg in buggy buildrootonly failclosedonly fixed inlineclosure lazyleak inlineclosure_b2; do
+ java -XX:+UseParallelGC -cp ../../../tmp/tla2tools.jar tlc2.TLC -workers auto \
+ -config CaBuildRootPrecommit_$cfg.cfg CaBuildRootPrecommit.tla
+done
+
+# part-manifest GC
+./run_gc_partmanifest.sh stage3
+./run_gc_partmanifest.sh stage4
+./run_gc_partmanifest.sh live
+# ... all stage5_* and sab_* configs
+
+# shard incarnation / registry removal
+java -XX:+UseParallelGC -cp ../../../tmp/tla2tools.jar tlc2.TLC -workers auto \
+ -config CaGcShardIncarnationCore_design.cfg CaGcShardIncarnationCore.tla
+
+# ack-floor GC round core — positive stage + witnesses
+./run_ackfloor.sh CaGcAckFloorCore_stage1
+for w in delete spare recreate copyforward rebuild clamp; do ./run_ackfloor.sh CaGcAckFloorCore_witness_$w; done # each MUST report reachable
+# sabotages (each MUST violate):
+for s in ignorefloor ackwithoutread ackbeforedrain sleeperrearm skipshard adopttoken openbeforeload \
+ rebuilddropedge rebuildkeepretired rebuildlowround clampnosuppress; do
+ ./run_ackfloor.sh CaGcAckFloorCore_sab_$s && echo "UNEXPECTED PASS: $s"
+done
+
+# ack-floor two-leader delete_pending gate
+./run_ackfloor_zombie.sh CaGcAckFloorZombie_stage1 # clean
+./run_ackfloor_zombie.sh CaGcAckFloorZombie_sab_eagerdelete # MUST violate INV_NO_DANGLE
+./run_ackfloor_zombie.sh CaGcAckFloorZombie_witness_delete # pending-delete reachable
+
+# in-degree re-fold
+java -XX:+UseParallelGC -cp ../../../tmp/tla2tools.jar tlc2.TLC -workers auto \
+ -config CaGcIndegRefoldCore_fix.cfg CaGcIndegRefoldCore.tla
+java -XX:+UseParallelGC -cp ../../../tmp/tla2tools.jar tlc2.TLC -workers auto \
+ -config CaGcIndegRefoldCore_sab.cfg CaGcIndegRefoldCore.tla # MUST violate INV_INDEG_NONNEGATIVE
+
+# Apalache inductive invariant (proof core)
+# (CaIncarnationProofCore.tla + Apalache.tla + run_apalache.sh were removed 2026-07-22 — stale
+# pre-B91 fragment, unverifiable without an Apalache binary. To revive: install Apalache and
+# re-derive IndInv against the current CaIncarnationCore.tla.)
+```
+
+## Clamp + destruction-suppression extension (2026-07-03) {#area-clamp-suppression}
+
+`CaGcAckFloorCore` models the fold-clamp mechanism honestly: `GFold` may hold back one landed ref
+— the abstraction of a per-shard cursor frozen at an unreadable manifest body (a false 404, a
+bodiless precommit) — but DECLARES it (`clampedL`), unlike `SabotageSkipChangedShard`, which holds
+one back silently and remains the lethal lying-fold counterexample. `GComplete`'s graduation is
+guarded by `~clampedL` (the implementation's clamp-suppressed pass: no graduations, no pending
+deletes while any shard is clamped); held refs stay in `landed`, so a later clamp-free pass
+consumes them — the clamp release needs no extra machinery. `SabotageClampNoSuppress` removes only
+the guard and reproduces the 2026-07-03 night incident (31 dangling blobs) as an `INV_NO_DANGLE`
+counterexample; `W_ClampHappens` witnesses a reachable honest clamped pass. Honest stage-1 is
+clean at 83.9M distinct states (the held ref persisting across passes multiplies configurations;
+the hold is bounded to one ref — any dangle of this class needs a single held `+1`, and the
+suppression rule reads only the boolean declaration).
+
+The same revision adds disaster-recovery rebuild (`GRebuild`, spec
+`2026-07-03-cas-gc-rebuild-design.md`) and sourceless copy-forward (`WCopyForward`) to the model.
+`GRebuild` recomputes the baseline from owner state at idle: the retired list restarts empty
+(over-protect — everything re-condemns through the normal pipeline) and the round is minted above
+every mount's advertised ack, so no stale ack can float a fresh condemnation past the floor before
+its writer re-observes. A `lostRefs` ghost set records any owner ref a sabotaged rebuild failed to
+re-emit (`sab_rebuilddropedge`); `INV_NO_DANGLE` is checked over `landed ∪ folded ∪ lostRefs` so a
+dropped edge is caught even though the rebuild itself doesn't retain it. `sab_rebuildkeepretired`
+and `sab_rebuildlowround` are the other two rebuild sabotages; `W_RebuildHappens` and
+`W_CopyForwardHappens` are the corresponding reachability witnesses.
+
+---
+
+## Area 12 — Resurrect re-upload orphan (2026-07-07) {#area-resurrect-reupload-orphan}
+
+### `CaGcResurrectReuploadOrphan.tla` — resurrect-replaced incarnation orphan {#cagcresurrectreuploadorphan}
+
+**Files:** `CaGcResurrectReuploadOrphan.tla`, `CaGcResurrectReuploadOrphan_bug.cfg`,
+`CaGcResurrectReuploadOrphan_fix.cfg` — **removed 2026-07-22** (covered by the deterministic
+`CasGcLeak.ResurrectReplaced*` gtests, see the currency note; full text in git history).
+
+**What it proves.** A focused reproduction of the `RESURRECT-REUPLOAD-ORPHAN` leak found in the
+ca-soak S30 create/drop churn scenario (root-caused via `system.content_addressed_log`; see
+`utils/ca-soak/scenarios/BACKLOG.md`). The liveness property `NoLeakForever` — a present, ever-edged,
+unreferenced incarnation is eventually deleted or referenced again, under weak fairness of the GC
+fold/delete — is **VIOLATED** with `FixReCondemnCurrentToken = FALSE` (shipped behavior) and **HOLDS**
+with `TRUE` (the fix). TLC v2.19, 194 distinct states; the bug counterexample is a lasso: a condemned
+token is replaced by a resurrect re-upload (new token at the same content-hash key), the old token's
+exact-token delete finds the newer token and skips (`replaced`), and the fold — being touch-gated for
+fresh condemn and hash-keyed for settling the prior entry — never re-condemns the replaced token, which
+then stutters present forever.
+
+**Why the canonical model missed it (model-vs-code faithfulness gap).** `CaIncarnationCore`'s `GRetire`
+condemns by **(hash, CURRENT token)** — guard `~\E e \in retired : e.h = h /\ e.t = tokOf[h]` — so after
+a resurrect changes the token it re-condemns the new incarnation, and `NoLeakForever` (checked at
+`stage2_live`) genuinely holds there (its one lasso was a `MaxRound=2` bound artifact, §Area 1). In
+other words the canonical model already encodes the CORRECT algorithm. The shipped C++ `closeBlob`
+(`CasBlobInDegree.cpp` ~L225–251) diverged: it keys the "already retired?" decision on the **hash only**
+(a prior retired entry for the hash takes the `settleEntry` branch and NEVER reaches the fresh-condemn
+path for the current token), and the fold only visits blobs touched in the current window. No model was
+faithful to that drifted code. This model is that faithful variant; the fix (`FixReCondemnCurrentToken`)
+re-condemns the current token when settling a prior entry whose token differs from the present
+object — i.e. it makes the code match `CaIncarnationCore`'s already-proven `GRetire`.
+
+**Why a focused model (not a flag in `CaIncarnationCore`).** A `SabotageRetireByHash` flag was tried in the canonical model (key the retire decision on hash, not (hash, current token)). It does NOT reproduce the *permanent* orphan: `CaIncarnationCore`'s `GRetire` is **un-touch-gated** — it re-condemns any eligible blob (`present ∧ everEdged ∧ InDeg=0`) every retiring phase — so once the stale entry drops, a later round simply re-condemns the replaced token. At every `MaxRound` both the sabotaged and the unsabotaged run violate `NoLeakForever` identically (the last-round bound artifact the doc already notes), so the property cannot distinguish them. The C++ orphan is permanent ONLY because the fold is **touch-gated** (`closeBlob` visits a blob only when it has edge deltas this window; once the resurrect-replaced token's edges are folded-and-gone the fold never revisits it). The canonical model abstracts that away (idealized always-eventually-condemn GC) — which is the DEEPER reason it misses this class. A faithful port would need to add a touch-gating dimension to `GFold`/`GRetire` plus a state invariant (not round-capped liveness); that is a real structural change, so the focused model is the gate for now.
+
+**Code currency:** the C++ fix is CURRENT and **LANDED** (branch `cas-gc-rebuild`):
+`CasBlobInDegree.cpp` `closeBlob` re-condemns the CURRENT token when settling a prior retired entry
+whose token differs from the present object (keyed on `(hash, current token)`, matching
+`CaIncarnationCore`'s `GRetire`), with a `blob_retire_replaced` CA-log event + `CasGcRetireReplaced`
+counter. The model files were removed 2026-07-22: at 194 distinct states TLC explored essentially
+the single scenario that the deterministic gtests
+(`CasGcLeak.ResurrectReplacedIncarnationReclaimed`, `ResurrectReplacedReclaimIsIdempotent`,
+`ResurrectReplacedTokenIsCondemnedInMeta`, `src/Disks/tests/gtest_cas_gc_leak.cpp`) pin directly,
+so as a regression gate the model added nothing beyond the unit tests; its lasting lesson (the
+model-vs-code faithfulness gap above) is prose. Documented follow-up (non-blocker): add a
+touch-gating dimension to `CaIncarnationCore` `GFold`/`GRetire` so the canonical model can
+reproduce this class directly.
+
+---
+
+## Area 13 — Writer/GC simplification, freshness-meta, and round-defer (2026-07-06 → 2026-07-10) {#area-writer-gc-simplification}
+
+Four models gate the writer/GC simplification effort (spec
+`2026-07-09-cas-writer-gc-simplification-design.md`) and the adjacent GC round-defer and orphan-sweep
+work. Two further models explored and rejected alternate freshness-meta designs, kept as historical
+record (the doc's convention for rejected models, e.g. `CaB140Dangle.tla` / `CaGcCore.tla`).
+
+### `CaGcRoundDeferCore.tla` — GC round DEFER (skip-unchanged) core {#cagcrounddefercore}
+
+**Files:** `CaGcRoundDeferCore.tla`, `CaGcRoundDeferCore_stage1.cfg`,
+`CaGcRoundDeferCore_witness_deferthenfold.cfg`, `CaGcRoundDeferCore_sab_graduate_on_stale.cfg`,
+`CaGcRoundDeferCore_sab_unbounded_defer.cfg` (spec `2026-07-06-cas-gc-round-skip-unchanged-design.md`,
+Phase 4 Lever A). Mirrors `CaGcAckFloorCore.tla`'s round shape (`GBegin`/`GFold`/`GComplete`, a monotone
+`min_ack` abstraction).
+
+**What it proves.** A round that would make no destructive decision may DEFER (re-adopt the sealed
+in-degree snapshot) instead of rebuilding it, but a due graduation must force-fold first: `GComplete`
+may physically delete a blob only when the unfolded delta carrier holds no pending add/remove touching
+it (`NoOverDelete`) — the mirror of the 2026-06-27 concurrent-leader leak on the unfolded `+1` side.
+Deferral itself is bounded (`deferCount < MaxDefer`, `EventuallyFolded`) so an unfolded delta is never
+permanently skipped. `stage1` is clean (8,445 distinct states); `sab_graduate_on_stale` (drop the
+unfolded-covers-`b` delete guard) violates `NoOverDelete`; `sab_unbounded_defer` (drop the bound)
+violates `EventuallyFolded` (a permanent-skip liveness lasso); `witness_deferthenfold` confirms a real
+DEFER-then-FOLD sequence is reachable (non-vacuity).
+
+**Code currency:** CURRENT (gate for the GC round DEFER/skip-unchanged mechanism).
+
+---
+
+### `CaEdgeBeforeObserve.tla` — writer/GC simplification Gate A {#caedgebeforeobserve}
+
+**Files:** `CaEdgeBeforeObserve.tla`, `CaEdgeBeforeObserve_reduced.cfg`,
+`CaEdgeBeforeObserve_sab_late_edge.cfg`, `CaEdgeBeforeObserve_sab_no_adopt_check.cfg`,
+`CaEdgeBeforeObserve_sab_no_k3_head.cfg`, `CaEdgeBeforeObserve_sab_no_k3_adopt_check.cfg` (Gate A of
+`2026-07-09-cas-writer-gc-simplification-design.md`, Phase A).
+
+**What it proves.** With the writer order `precommit (closure durable) → adopt/observe → promote` and
+GC's `condemn → graduate(floor) → same-pass decided delete` pipeline with per-pass d-recheck, the
+promote-time revalidation of TOKENED leaves is redundant, while the dedup-adoption check (K1), the
+tokenless presence HEAD (K3Head), the tokenless condemned check (K3AdoptCheck), and the ORDER itself
+stay load-bearing. `reduced` (no tokened revalidation) holds; the four sabotages each reproduce a
+dangle: `sab_late_edge` (adoption allowed before the durable closure — the pre-B188 order),
+`sab_no_adopt_check` (K1 removed), `sab_no_k3_head` (absent tokenless leaf published blind),
+`sab_no_k3_adopt_check` (condemned tokenless leaf adopted at promote).
+
+**Code currency:** CURRENT for the load-bearing half; the tokenless-leaf half has DRIFTED (audited
+2026-07-22). The parts that still match the code: no promote-time revalidation of tokened leaves
+(`CasPartWriteTxn.cpp:985-986` skips them as edge-protected), the edge-before-observe order
+(fail-closed at `CasPartWriteTxn.cpp:331-336`), and K1 (the adopt path point-reads the per-hash
+meta; `Condemned` ⇒ `ABORTED` ⇒ re-upload a fresh incarnation, `CasPartWriteTxn.cpp:298-320`). But
+**K3Head and K3AdoptCheck no longer describe shipped code**: a tokenless (`adoptEvidence`) leaf is
+now **trusted via its durable manifest edge** — no promote-time presence HEAD, no `loadMeta`, no
+copy-forward (`CasPartWriteTxn.cpp:926,987-1004`); only a no-dep / pending-upload leaf fails closed.
+The model's leaf `he` (a pre-existing unowned blob GC can condemn, so promote must HEAD +
+condemned-check it) is superseded by an in-degree-pinned source (in-degree ≥ 1, not condemnable),
+with fsck as the genuinely-absent backstop. So `sab_no_k3_head` / `sab_no_k3_adopt_check` are valid
+negative controls for the MODEL's logic but gate a promote-time probe the code no longer performs.
+Not a CODE-RISK — the safety argument moved to the in-degree invariant + manifest-trust + fsck.
+Recast the tokenless leaf as source-pinned (trusted, no promote probe): scoped follow-up, not done
+in this pass.
+
+---
+
+### `CaMetaDescriptor.tla` — writer/GC simplification Gate B, meta descriptor (v1) {#cametadescriptor}
+
+**Files:** `CaMetaDescriptor.tla`, `CaMetaDescriptor_reduced.cfg`,
+`CaMetaDescriptor_sab_a_meta_first.cfg`, `CaMetaDescriptor_sab_b_body_first.cfg`,
+`CaMetaDescriptor_sab_c_blind_adopt.cfg`, `CaMetaDescriptor_sab_d_uncond_body.cfg`,
+`CaMetaDescriptor_sab_e_no_claim_sweep.cfg`, `CaMetaDescriptor_sab_f_birth_adopt.cfg`,
+`CaMetaDescriptor_sab_g_fresh_head.cfg`, `run_meta.sh` — **removed 2026-07-22** (its central invariant
+is contradicted by the shipped code; full text in git history).
+
+> **Removed — the shipped meta is an advisory freshness hint, not the lifecycle linearizer this
+> model assumes.** The model's headline invariant `INV-META-BODY` (meta present ⇒ body present, with
+> the meta as *the* linearization point and delete-top-down = meta-first) is directly contradicted
+> by the code: `Formats/CasBlobMetaFormat.h` states the marker "is only a point-read hint, **not the
+> linearization point** for blob lifetime … reads of the blob never consult the meta," the body's
+> in-body `incarnation_tag` + exact-token delete is the real linearizer, GC deletes the **body
+> first** (`CasGc.cpp:419`) and drops the meta only advisorily afterwards, and absent reads
+> identically to `Clean` (no tombstone). So a `Condemned` meta legitimately outlives its deleted
+> body — a state the model forbids. Keeping a model whose headline invariant holds in the model but
+> is false in the code is misleading (false comfort). The meta's real behavior is already gated by
+> CURRENT models: the writer adopt-gate point-read (`Condemned` ⇒ re-upload) by `CaEdgeBeforeObserve`
+> (K1), and the GC condemn-marker graduation gate by `CaGcCondemnMarkerGate`; the settled v3
+> freshness argument is discharged in the design against `CaIncarnationCore`. Not a CODE-RISK — the
+> meta is advisory and never authority for deletion. Also removed alongside the two earlier-rejected
+> meta variants (`CaMetaDescriptorRaw`, `CaMetaIncarnationKey`) and the false-green
+> `CaMetaAbsenceClean`.
+>
+> (The model additionally had a config defect the audit surfaced: its eight `.cfg` files omit
+> `CHECK_DEADLOCK FALSE`, so a spurious terminal-state deadlock in a shallow BFS branch halted TLC
+> before the sabotage counterexample was reached — five of the seven negative controls silently
+> reported a deadlock instead of the intended violation. Confirmed by re-running with deadlock
+> checking off: `reduced` GREEN, all seven sabotages VIOLATE. Moot now that the model is removed,
+> but recorded because the same omission should be checked in any surviving small model.)
+
+**What it proved.** `INV-META-BODY` (meta present ⇒ body present) across create-bottom-up
+(body then meta, If-None-Match) / delete-top-down (meta at the captured etag, then body at the
+condemn-time token) discipline, with a resurrect modeled as two steps (meta CAS, then body re-upload)
+and a no-claim sweep sabotage modeled as two steps (observe, then blind delete) so the crash/race
+windows are explorable. Seven sabotages (a–g: meta-before-body, body-before-meta, blind adopt over
+condemned, unconditional body delete after losing the meta-delete CAS, no-claim debris sweep, birth-
+completion adopting the orphan body instead of resurrect-from-source, GC deleting the body at whatever
+token it currently holds instead of the condemn-time token) all dangle as required.
+
+**Code currency:** REMOVED 2026-07-22 — the shipped meta is advisory (absent ≡ `Clean`, body-first
+delete, never the linearization point), so the model's `INV-META-BODY` linearizer framing is false
+against the code (see the note above). Its create-bottom-up and exact-token-body-delete results did
+match code, but the advisory-meta behavior is covered by `CaEdgeBeforeObserve` (K1) +
+`CaGcCondemnMarkerGate`.
+
+---
+
+### `CaMetaDescriptorRaw.tla` — Gate B raw-body / terminal-tombstone (REJECTED by v3) {#cametadescriptorraw}
+
+**Files:** `CaMetaDescriptorRaw.tla`, `CaMetaDescriptorRaw_reduced.cfg`,
+`CaMetaDescriptorRaw_sab_meta_first.cfg`, `CaMetaDescriptorRaw_sab_blind_adopt.cfg`,
+`CaMetaDescriptorRaw_sab_adopt_tomb.cfg`, `CaMetaDescriptorRaw_sab_del_notomb.cfg`,
+`CaMetaDescriptorRaw_sab_resurrect_tomb.cfg`, `run_metaraw.sh` — **removed 2026-07-21** (full text
+in git history).
+
+**What it proves.** A Gate B variant that drops the envelope to RAW, immutable, write-once bodies and
+makes the per-hash meta the SOLE three-state linearizer (`clean, condemned, tombstone`), with the meta
+etag as the only conditional authority. The v2 correction models GC's body delete as a non-atomic
+two-step (`GcClaimTombstone` then `GcDeleteBody`) and makes `tombstone` terminal — a writer observing
+`tombstone` waits for `absent` and re-creates fresh, never `tombstone → clean`
+(`SabResurrectFromTombstone` re-enables the un-tombstone race and breaks `INV_NO_LOSS`/`INV_NO_DANGLE`).
+`reduced` is clean; the five sabotages (meta-before-body, blind adopt, adopt-over-tombstone,
+delete-without-tombstone-claim, resurrect-from-tombstone) each dangle.
+
+**Code currency:** SUPERSEDED. Raw immutable bodies (fixed etag) cannot let a resurrect displace the
+body by itself, which is exactly what forced the terminal-tombstone handshake — a writer↔GC liveness
+coupling. REJECTED by the v3 design (`docs/superpowers/plans/2026-07-10-cas-freshness-meta-v3.md`,
+superseding `docs/superpowers/plans/2026-07-10-cas-meta-descriptor-raw-body.md`), which keeps the
+settled one-key-per-hash + in-body `incarnation_tag` + exact-token BODY delete instead. Files
+removed 2026-07-21; this section stays as the explored-and-rejected record.
+
+---
+
+### `CaMetaIncarnationKey.tla` — Gate B Option B, per-incarnation body keys (REJECTED) {#cametaincarnationkey}
+
+**Files:** `CaMetaIncarnationKey.tla`, `CaMetaIncarnationKey_reduced.cfg`,
+`CaMetaIncarnationKey_sab_reuse.cfg`, `run_inckey.sh` — **removed 2026-07-21** (full text in git
+history).
+
+**What it proves.** An alternative to the terminal-tombstone fix ("Option B"): per-incarnation body keys
+(`blobs/xx/.`), a tombstone-free two-state meta (`clean`/`condemned`) pointing at the
+current incarnation, and manifest records of `(hash, incarnation)`. GC deletes the condemned
+incarnation's body by its exact key; a resurrect writes a fresh incarnation key, so GC's delete can
+never hit the writer's live body — no cross-object atomicity, no tombstone, no wait. The sabotage
+`SabResurrectReuseIncarnation` (resurrect reuses the condemned incarnation instead of minting a fresh
+one) reintroduces the shared-key race and dangles, proving fresh-incarnation-on-resurrect load-bearing
+within this design.
+
+**Code currency:** SUPERSEDED/REJECTED. This is the generation-in-key design already rejected once
+before (see `docs/superpowers/cas/01-architecture.md` §"Approaches tested and REJECTED": EBR's
+`blobs//` and Merkle `child_gen`): it forces a `404 → LIST` read path and propagates the
+incarnation up into the manifest/parent, breaking the pure-content manifest and FUSE-readiness. Files
+removed 2026-07-21; this section stays as the explored-and-rejected record.
+
+---
+
+### `CaManifestSweepWindow.tla` — orphan-sweep vs removal-fold wedge {#camanifestsweepwindow}
+
+**Files:** `CaManifestSweepWindow.tla`, `CaManifestSweepWindow_reduced.cfg`,
+`CaManifestSweepWindow_sab_sweep_committed.cfg`, `run_sweepwindow.sh` — **removed 2026-07-22**
+(covered by the deterministic gtest, see below; full text in git history). (Task 0 of
+`docs/superpowers/plans/2026-07-10-cas-freshness-meta-v3.md`; the wedge gate for the committed-removal-
+scoping debt behind `gtest_cas_orphan_manifest_sweep.cpp::PendingCommittedRemovalBodyIsSkipped`).
+
+**What it proves.** `INV_FOLD_PROGRESS`: when a COMMITTED manifest ref is dropped, its `-1` removal is
+appended to the shard journal but not yet sealed by the GC fold; a promoted build has retired its
+`build_seq`, so the prefix is watermark-eligible for the orphan-manifest sweep. The sweep must NOT
+delete the committed body in the `dropRef → fold-seal` window — the removal-fold still needs the body
+present to emit its decrement. `reduced` is clean; `sab_sweep_committed` (sweep ignores the
+pending-committed-removal protection) deletes the body, so the fold can never decrement and
+`INV_FOLD_PROGRESS` is violated forever.
+
+**Code currency:** the wedge fix is CURRENT (`8606ab382aa`); the model files were removed
+2026-07-22 — the interleaving space is small enough that the deterministic gtest
+(`gtest_cas_orphan_manifest_sweep.cpp::PendingCommittedRemovalBodyIsSkipped`, plus ten sibling
+sweep tests) covers the same scenarios, so the model added no assurance beyond the unit test.
diff --git a/docs/superpowers/cas/07-s3-budget.md b/docs/superpowers/cas/07-s3-budget.md
new file mode 100644
index 000000000000..f6d98590a2d6
--- /dev/null
+++ b/docs/superpowers/cas/07-s3-budget.md
@@ -0,0 +1,425 @@
+---
+description: 'Precise per-protocol-part S3 op-count breakdown for the content-addressed (CAS) MergeTree feature: write, read, and GC budgets with reduction history.'
+sidebar_label: 'S3 Op-Count Budget'
+sidebar_position: 7
+slug: /superpowers/cas/s3-budget
+title: 'CAS S3 Op-Count Budget'
+doc_type: 'reference'
+---
+
+# CAS S3 Op-Count Budget {#cas-s3-budget}
+
+**Status:** DONE (write + GC budgets measured; read budget partially modeled). Sources: measured
+`system.content_addressed_log` / `ProfileEvents` from soak runs (annotated as **measured**);
+per-code-path counts derived from `CasBuild.cpp`, `CasStore.cpp`, `CasGc.cpp`,
+`CasObjectStorageBackend.cpp` (annotated as **code-derived**); projections from the cost model
+(`specs/2026-06-08-s3-ops-cost-model.md`) annotated as **modeled/uncertain**.
+
+Request-price reference (AWS S3 Standard, us-east-1):
+
+| Tier | Operations | Price / 1 M |
+|---|---|---|
+| Read | `GET`, `HEAD` | ~$0.40 |
+| Write | `PUT`, `LIST`, `COPY` | ~$5.00 |
+| Delete | `DELETE` | free |
+
+---
+
+## 1. Write Budget — per Part {#write-budget}
+
+A "part" is one INSERT, MERGE, or MUTATION output. Let `F` = number of distinct blob files
+(data columns, marks, primary index) with size ≥ `dedup_head_first_min_bytes` (default 1 MiB),
+`f` = total blob count (including small blobs), `D` = blob dedup-hit count (content already
+present in the pool from a prior part), `f - D` = novel blobs to upload. One tree object covers
+the file-path listing; one part-manifest object captures the part metadata.
+
+### 1.1 Baseline write path (no dedup cache, no ETag optimization) {#write-budget-baseline}
+
+This was the state prior to the B168 P0/P1/P2 optimizations and the head-after-put ETag fix.
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Blob: novel upload | `PUT` (`If-None-Match:*`) | `f - D` | One conditional create per novel blob (**code-derived**). |
+| Blob: dedup hit (412 path) | `PUT` (`If-None-Match:*`) → 412 | `D` | Body streamed then discarded on collision (**measured**: 64% hit rate, P0 instrumentation run). |
+| Blob: dedup follow-up HEAD | `HEAD` | `D` | `observeAndAdmit` HEAD after a 412 to read the existing token (**code-derived**, `CasBuild.cpp:200`). |
+| Blob: head-after-put | `HEAD` | `f - D` | After each novel PUT: a follow-up HEAD to read the object ETag as the admission token, because `WriteBufferFromS3` did not capture the response ETag (**measured**: ~73% of all HEADs pre-fix, `CasObjectStorageBackend.cpp:147`). **ELIMINATED by ETag fix (B168 #1).** |
+| Tree: novel upload | `PUT` (`If-None-Match:*`) | 1 | One tree object (content-addressed; dedup applies). |
+| Tree: head-after-put (pre-fix) | `HEAD` | 0–1 | Same ETag path as blobs. **ELIMINATED by ETag fix.** |
+| Precommit | `PUT` (`If-Match`) | 1 | One `casPut` on the target root shard (appends create-precommit event, **code-derived**, `CasBuild.cpp:582`). |
+| Part-manifest | `PUT` (`If-None-Match:*`) | 1 | One unconditional content-addressed manifest object (`CasBuild.cpp:562`). |
+| Promote (commit) | `PUT` (`If-Match`) | 1 | One `casPut` on the target root shard (owner precommit → committed ref, **code-derived**, `CasBuild.cpp:657`). |
+| Promote: revalidate deps | `HEAD` | 0–`f+1` | **Superseded (EDGE-BEFORE-OBSERVE, `2026-07-09-cas-writer-gc-simplification`):** the bulk re-HEAD-of-all-deps at promote (the old `revalidateDeps`, no longer in code) is gone. Tokened deps are edge-protected and NOT re-checked at promote; only non-tokened deps get a HEAD + a per-hash `.meta` point-read GET (`Build::promote`, `CasBuild.cpp`). |
+
+**Baseline total** (pre-optimizations, no dedup): 2·`f` + 4 `PUT`s + `f` + `D` `HEAD`s (ignoring
+revalidate). At 64% dedup hit rate that is ≈ 3·`f` + 4 ops for a typical part.
+
+### 1.2 Optimized write path (current, after ETag fix + P1/P2 dedup cache) {#write-budget-optimized}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Blob: cache HIT, HEAD-first present | `HEAD` + `GET` | `D_cache` | Cache-hit blobs: 1 cheap HEAD (replaces a body-PUT + HEAD) **plus a per-hash `.meta` point-read GET** (v3 freshness-meta, `observeAndAdmit` — condemned? → resurrect, else adopt). `D_cache` ≈ `D` after warmup (**code-derived**). The same `.meta` GET is added on the 412 dedup-follow-up path below. |
+| Blob: cache HIT, HEAD-first 404 (stale) | `HEAD` + `PUT` | rare | Stale cache hit: HEAD 404, fall through to normal PUT. |
+| Blob: large-body cache MISS (P2 size trigger) | `HEAD` + `PUT` | `F - D` | Blobs ≥ 1 MiB and not in cache: HEAD-first check, then PUT on miss (**code-derived**, `CasBuild.cpp:119`). |
+| Blob: small-body cache MISS | `PUT` | `(f-F) - D_small` | Small blobs bypassing P2 with no cache hit go straight to conditional PUT. |
+| Blob: 412 dedup follow-up HEAD | `HEAD` | near 0 | Residual 412s on concurrent-writer races; negligible at steady state. |
+| Blob: head-after-put | `HEAD` | 0 | **ELIMINATED**: `WriteBufferFromS3` now returns the response ETag (**measured soak #6 t=343s**: HEAD/PUT ratio dropped to 1.20 from ~116:1, `CasObjectStorageBackend.cpp:127`). |
+| Tree | `PUT` | 1 | Unchanged. ETag from response; no follow-up HEAD. |
+| Precommit | `PUT` | 1 | One root-shard `casPut`. |
+| Part-manifest | `PUT` | 1 | One content-addressed manifest object. |
+| Promote | `PUT` | 1 | One root-shard `casPut`. |
+
+**Optimized total** (warm cache, 64% dedup hit rate, large blobs): ≈ `D` `HEAD`s + `(f-D)+3`
+`PUT`s per part. For `f=10` blobs and `D=6` dedup hits: 6 HEADs + 7 PUTs. That is a ~55%
+reduction from the baseline 30+ ops.
+
+### 1.3 Mutable files {#write-budget-mutable}
+
+`mutable_files` (e.g. `txn_version.txt`) are inlined into the root shard's `RootRef` protobuf
+(`CasRootShardCodec.cpp:121`) and written as part of the promote `casPut` — **0 extra S3 ops**
+(**code-derived**).
+
+### 1.4 Heartbeat renewal {#write-budget-watermark}
+
+Each writer renews **one merged heartbeat** per renewal interval: the mount lease and the build
+watermark (`min_active`) ride a single `PUT` (the GC-ack `observed_gc_round` was removed in v3 — see
+`04 §heartbeat-floor`), plus one `gc/state` `GET` to learn the current round (`03 §merged-heartbeat`). This is **−1 PUT** versus the
+former two separate heartbeats (mount lease + standalone watermark object) and **+1 GET**. The
+per-build `CasBuildPut` heartbeat was already removed earlier (B167c: redundant with the watermark).
+
+### 1.5 Manifest `casPut` contention (write-tier 412s) {#write-budget-cas-contention}
+
+Every `casPut` is an `If-Match` conditional PUT. On a conflict (concurrent writer or GC fence)
+it 412s, re-reads the shard (1 `GET`), and retries. Measured conflict rate: **35%** of
+`casPut` calls at `workers=2` (`CasRootCasConflict` 49 k / `CasRootCas` 93 k, P0 soak, at the
+then-widened `root_shards=64` soak config). Each conflict costs one extra `PUT` (412d) + one
+`GET` (**measured**).
+
+**Superseded by the flat-combining shard-mutation queue (2026-07-03).** `casPut` calls on the
+same `(namespace, shard)` are now grouped by a leader-caller batch builder (`Store::mutateShard`,
+`CasStore.cpp:971`) into one read + one `casPut` per flush instead of one independent CAS loop
+per mutation. This is now the primary contention lever, not shard fan-out: soak-validated ~2.3×
+CAS-write compression with the intra-server conflict rate dropping from ~257k/h to ~11/h. The
+shipped `root_shards` default was correspondingly changed to **32** (see §4), chosen to balance
+per-shard journal body size and GC discovery cost rather than to spread CAS contention, since the
+queue removes most of the contention argument for large shard counts (`CasStore.h:110-114`).
+
+---
+
+## 2. Read Budget — per Part Open {#read-budget}
+
+A part open involves resolving the part's ref, fetching the part-manifest, then streaming blob
+data. The following counts are **per query-thread part open** (not per query).
+
+### 2.1 Ref resolution (`resolveRef`) {#read-budget-resolve}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Root-shard read | `HEAD` + `GET` (cold) | 1 + 1 | `readShardDecoded` HEADs the shard key, GETs the body on a cache miss or token mismatch (`CasStore.cpp:465`, **code-derived**). |
+| Root-shard read (warm, single-flight) | 0 ops | 0 | If another thread resolved the same shard concurrently within the TTL window, the result is shared (**code-derived**, `CasStore.cpp:414` coalesced single-flight). |
+| Root-shard HEAD (warm, TTL cache) | 0 ops | 0 | If the shard was validated within `allow_stale` TTL, skip the HEAD entirely (**code-derived**, `CasStore.cpp:392`). Default: staleness-tolerant reads opt in; safety-critical reads (publish gate) stay force-fresh. |
+
+**Per distinct shard per shard-TTL window:** 1 `HEAD` + 0–1 `GET`. On a hot server with many
+concurrent reads to the same shard, single-flight coalesces to 1 `HEAD` total.
+
+**Decode-cache TTL (`shard_decode_cache_ttl_ms`, default 200):** a staleness-tolerant caller
+(`allow_stale = true`) may reuse a decode validated less than `shard_decode_cache_ttl_ms` ago
+**without a HEAD**; `0` disables the TTL (all callers force-fresh). Strict-freshness callers (the
+publish gate) always pass `allow_stale = false` and always HEAD, regardless of this value.
+**Absence is never TTL-cached:** the fast path applies only to PRESENT entries. A just-created ref
+must be observable by force-fresh callers, and staleness-tolerant callers re-validate on a miss — so
+a "not found" is never cached with a TTL. (Source
+`specs/2026-06-14-ca-reduce-s3-op-count-design.md`, Pillar B.)
+
+### 2.2 Part-manifest fetch (`readManifest`) {#read-budget-manifest}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Manifest HEAD | `HEAD` | 1 | Token check for the `(ManifestId, token)` decode cache (`CasStore.cpp:584`, **code-derived**). |
+| Manifest GET | `GET` | 0–1 | GET + parse if no cache hit or token changed. Typically 0 on re-open of the same part (**code-derived**). |
+
+**Per part per open:** 1 `HEAD` + 0–1 `GET`.
+
+### 2.3 Blob reads {#read-budget-blobs}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Blob GET (ranged) | `GET` (ranged) | per-column | One ranged GET per column-file read. Count depends on query selectivity. Not tracked per-part by the CA layer (delegated to the MergeTree reader). |
+| Blob dedup-cache skip | 0 ops | — | The `dedupCache` is write-path only; read path has no equivalent skip. |
+
+**Modeled/uncertain:** the per-column GET count is determined by MergeTree's read plan, not the
+CA layer. The CA layer passes blob keys to `ObjectStorage::readObject`; ranged reads reduce
+data transfer but each is one billed GET.
+
+### 2.4 Replication fetch (`DownloadPart`) {#read-budget-replication}
+
+**Measured** (`part_log` attribution, P0 soak, ~95 min): `DownloadPart` issues ~15 HEADs + ~15
+GETs per part (53,796 events). In a content-addressed shared pool the blobs already exist; a
+replica fetch could relink the manifest without re-downloading (P3 proposal, **TODO/DESIRABLE**,
+would eliminate ~1.6 M reads per 95 min window). **Currently the generic MergeTree download
+path is used.**
+
+---
+
+## 3. GC Budget — per Round {#gc-budget}
+
+The regular round is a **single pass**: a heartbeat ack floor, one LIST discovery sweep, one fold
+that runs the three-cursor merge (verify + graduate + condemn), pre-CAS deletes of previously-pending
+entries, and one `gc/state` CAS (`04 §gc-round`). Let `N` = number of live/mounted servers, `S` =
+active ref shards across all namespaces, `S_changed` = shards whose token advanced since last round,
+`C` = newly-condemned zero-in-degree candidates this round, and `G` = entries graduated to physical
+delete this round. The headline: the round is **O(delta) + O(servers)** requests plus **one** LIST
+sweep — no O(universe) GET or PUT phase.
+
+> **History (fence + recheck round, superseded 2026-07-02).** The old round ran four phases —
+> fold (R1), retire (R2), fence (R3), recheck (R4) — and R3/R4 were each ~O(universe): the fence
+> was one GET + one CAS-PUT on **every** present root shard (~2×O(universe) GET + O(universe)
+> CAS-PUT per round), and the recheck re-read every fenced shard body plus a per-candidate
+> `inDegreeInGeneration` whole-run re-read. At 100 000 tables × 8 root shards that was **~2.4 M
+> requests (~$4.6) per round**, repeated every round forever. The ack-floor round replaces both
+> phases with the causal floor + three-cursor merge; for the same slowly-changing pool it is
+> **~2 000–3 000 requests (~$0.001–0.01) and seconds** of wall time. The subsections below describe
+> the current round; the old per-phase tables are folded into this history note.
+
+### 3.1 Heartbeat ack floor {#gc-budget-floor}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Heartbeat enumeration | `LIST` (`gc/server-roots/`) + `GET` | 1 LIST + `N` GETs | LIST the server roots, GET each mount body to classify live/terminated/expired for fence-outs (`computeHeartbeatFloor`; the `observed_gc_round` ack read was removed in v3). `N` is single-digit. |
+| Expired-mount fence-out | `PUT` (`If-Match`) | rare (`≤` expired count) | A token-guarded `putOverwrite` setting `gc_fenced` on a lease-expired mount; only when a server actually died. |
+
+**Per-round floor cost:** 1 LIST + `N` GETs (+ rare fence-out PUT). This is the round's only clock
+and its only server-proportional cost.
+
+### 3.2 Discovery + fold + three-cursor merge {#gc-budget-fold}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Shard discovery | `LIST` (`cas/refs/`) | ⌈`S` / 1000⌉ | The round's single LIST sweep of the ref universe (**code-derived**, `discoverUniverse`). Returns shard tokens when the backend supports them. (Currently O(N²) over `roots/` on a real backend — see §backlog GC-DISCOVERY-LIST-QUADRATIC in `08-testing-and-soak.md`.) |
+| LIST-token skip (accelerator) | 0 GETs for skipped shards | `S - S_changed` | If the listed token matches the sealed folded token, the shard body GET is skipped. Clamped-coverage shards (`classification = 4`) are never skipped (`04 §three-cursor-merge`). |
+| Shard body read (changed) | `GET` | `S_changed` | Only shards whose token advanced. At steady state `S_changed ≪ S`. |
+| Prior retired-run read | `GET` | `gc_shards` | One GET per gc-shard for the third merge cursor (the current retired list from `retired_refs`). |
+| Newly-condemned candidate HEAD | `HEAD` | `C` | One HEAD per blob the merge newly condemns, to capture its exact token. Only **new** candidates — a previously-condemned entry stays in the retired list and is re-verified from the fold, not re-HEADed. |
+| In-degree run write | `PUT` | `gc_shards` | New snapshot run per gc-shard (deterministic). |
+| Retired-run write | `PUT` | `gc_shards` | New current retired list per gc-shard (always, even empty; observation-bearing). |
+| Fold-seal write | `PUT` | 1 | One deterministic write-once fold seal per round. |
+
+**Per-round fold cost (steady state):** ⌈`S`/1000⌉ LIST + (`S_changed` + `gc_shards`) GETs + `C`
+HEADs + (2·`gc_shards` + 1) PUTs. The condemn-time HEAD is bounded by `C` (new candidates), not by
+the cumulative deleted set — the P9 node-forgetting + retained retired list keep the old 404-HEAD
+storm (§4) from recurring.
+
+### 3.3 Deletes and the single CAS {#gc-budget-deletes}
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| `deleteExact` (previously-pending graduates) | `DELETE` | `G` | Exact-token deletes for entries published `delete_pending` by the *previous* round (two-phase graduation, `04 §two-phase-graduation`). **DELETE is free on AWS.** |
+| Manifest-body `deleteExact` | `DELETE` | per cleanup | Delete-after-adopted-decrements; free. |
+| Outcome-log write | `PUT` | ⌈outcomes/shard⌉ | Observation-bearing outcome log per gc-shard (`putIfAbsent`-adopt). |
+| Single `gc/state` CAS | `PUT` (`If-Match`) | 1 | The **only** CAS per round: publishes `round`, adopted generation/attempt, `retired_refs`, folded cursors, `snap_pruned_through`. |
+
+**Per-round delete/CAS cost:** `G` (+ cleanup) DELETEs (free) + ⌈outcomes/shard⌉ + 1 PUTs. Physical
+deletion lags condemnation by one pass (condemn → pending → delete), an intentional
+two-phase-graduation property, not extra requests.
+
+### 3.4 Snapshot-run reads and idle-round bytes {#gc-budget-bytes}
+
+**Status: DONE** (T2 streaming reads + T0 reference-parent runs, 2026-07-02;
+`specs/2026-07-02-cas-gc-snapshot-streaming-design.md`). Two properties of the per-gc-shard snapshot
+run set the read/write byte budget (`04 §snapshot-run-reads`):
+
+**Streaming reads — O(block) memory, fixed request profile.** Every run consumer opens a run with a
+fixed request profile rather than reading the whole object into RAM:
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| Object size | `HEAD` | 1 | `head(key)`. |
+| Footer (tail suffix) | `GET` (ranged) | 1 | Ranged `get` of `min(object_size, kRunHardCapBlockSize + 64 KiB)` — carries the `footer_len` trailer + CRC'd block index. |
+| Body stream | `GET` (stream) | 1 | `getStream` over the write-once body, positioned at the first block. |
+| Exact footer (large runs only) | `GET` (ranged) | 0–1 | +1 exact-footer ranged `get` when the footer exceeds the tail probe. |
+
+A linear scan is therefore **3 requests** (`head` + tail `get` + body `getStream`), or **4** for a
+large-footer run. A `seek` costs **+1 ranged `get` per touched block**; the pure-linear fold path
+never seeks. Resident memory is **O(block)** — the footer index plus one current block
+(≤ `kRunHardCapBlockSize` = 1 MiB) — regardless of run size, replacing the former
+3 × O(active edges) materialization (`get`-whole-then-`substr` + reader `full` copy + prior-edge
+`std::vector`).
+
+**Idle-round bytes — zero.** With reference-parent runs, an empty-delta gc-shard's new `fold_seal`
+carries the parent generation's `RunRef` verbatim and the fold neither reads nor writes that shard's
+run. A shard with an empty delta AND an empty retired list is pure ref-carry (zero run I/O); a fully
+idle round (no journal changes, no retired entries) touches **zero** run objects — no GET, no PUT.
+Combined with the ack-floor request profile, an idle round is one LIST sweep + `N` heartbeat GETs +
+one `gc/state` CAS and nothing else. This removes the former per-round 2 × snapshot-bytes idle churn
+(each pass previously re-read and rewrote a byte-identical successor run per shard).
+
+**Remaining byte axis (T1, DESIRABLE).** A HOT pool still rewrites the full snapshot run per changed
+gc-shard — O(active edges) **bytes** per round. The fix is delta-runs + periodic compaction (T1),
+the NEXT spec; it builds on this spec's streaming reader, `getStream` seam, ranged `get`, and
+seal-ref resolution unchanged. See `04-gc-protocol.md §snapshot-run-reads` and `ROADMAP.md`.
+
+### 3.5 Snap prune (per round, amortized) {#gc-budget-snap-prune}
+
+Old generation artifacts (`gc/gen//`) are reclaimed once they age past a retention window
+(`snap_pruned_through` cursor). Per round up to 64 generations are pruned wholesale:
+
+| Step | Operation | Count | Notes |
+|---|---|---|---|
+| LIST `gc/gen//` prefix | `LIST` | ≤ 64 × ⌈objects/1000⌉ | One LIST per generation per prune burst (**code-derived**, `CasGc.cpp:1238`). Bounded by `kMaxPrunePerRound = 64`. |
+| DELETE each listed artifact | `DELETE` | objects listed | Free on AWS. |
+| GC-state CAS (snap_pruned_through) | `PUT` (`If-Match`) | included in the round CAS | `snap_pruned_through` rides the single `gc/state` CAS (§3.3); the prune runs before it. |
+
+**Previous design (removed):** an extra per-round LIST of the current-generation fold prefix to
+clean up deposed-leader debris. This was eliminated in favor of the wholesale generation-retain
+prune, saving one LIST per round on the common (single-leader) path (**code-derived** comment,
+`CasGc.cpp:1242`).
+
+### 3.6 GC round summary {#gc-budget-summary}
+
+| Phase | PUTs | HEADs | GETs | LISTs | DELETEs |
+|---|---|---|---|---|---|
+| Heartbeat floor | rare fence-out | 0 | `N` | 1 | 0 |
+| Discover + fold + merge | 2·`gc_shards` + 1 | `C` | `S_changed` + `gc_shards` | ⌈`S`/1000⌉ | 0 |
+| Deletes + CAS | ⌈outcomes⌉ + 1 | 0 | 0 | 0 | `G` (+ cleanup, free) |
+| Snap prune | 0 | 0 | 0 | ≤ 64 × pages | `C_pruned` (free) |
+| **Total (steady state, `S`=64, `gc_shards`=8, `N`=2, `C`=0)** | **~20** | **~0** | **~12** | **~2** | **0** |
+| **Total (churned, `C`=64, `G`=64)** | **~20** | **~64** | **~76** | **~2** | **~64 (free)** |
+
+There is no livelock-amplified O(universe) phase to blow up any more: the fence and recheck are
+gone, `C` counts only **newly** condemned blobs (not the cumulative deleted set, which P9 node
+forgetting keeps out of the candidate set), and the round issues exactly one CAS. On a real backend
+the LIST sweep is still O(N²) over `roots/` today — the outstanding discovery-LIST fix (§backlog) —
+which is the round's remaining scalability item, not a per-phase O(universe) GET/PUT.
+
+---
+
+## 4. Reduction History {#reduction-history}
+
+Each row is one optimization, the problem it solved, and its measured or modeled impact.
+
+| ID | Optimization | Eliminated ops | Status | Source |
+|---|---|---|---|---|
+| **ETag fix (#1)** | `WriteBufferFromS3` captures the response ETag from `PutObject`/`CompleteMultipartUpload`; `nativeConditionalPut` uses it directly — no follow-up HEAD needed | ~73% of all HEADs (head-after-put was the single largest HEAD source, **measured**: `CasDbgMetaHit` 458 k ≈ `S3WriteRequestsCount` 340 k at 1:1 ratio) | **DONE** (commit `2a13fe5cc0f`) | `reports/2026-06-15-unattended-night-opcount-fixes.md` §#1; `CasObjectStorageBackend.cpp:127` |
+| **P1 dedup cache** | LRU byte-bounded set of known-present content hashes; on a cache hit the blob PUT is replaced by a cheap HEAD-first check | Eliminates body-PUT + follow-up HEAD on 64% of blob creates (dedup hit rate, **measured**, P0 instrumentation) | **DONE** (B168; `CasStore.cpp:57`) | `specs/2026-06-20-ca-dedup-cache-head-before-put-design.md` |
+| **P2 adaptive HEAD-before-PUT** | For blobs ≥ `dedup_head_first_min_bytes` (default 1 MiB), send a HEAD first even on a cache miss — converts a body-PUT+HEAD (write tier) into a single HEAD (read tier) on a dedup hit | Downgrades large-blob dedup cost from write-tier PUT to read-tier HEAD; protects against broken-pipe storms on large-body 412s (B187) | **DONE** (B168; `CasBuild.cpp:119`) | `specs/2026-06-20-ca-dedup-cache-head-before-put-design.md` |
+| **root_shards widen (soak config)** | Raised the soak fanout `root_shards` from 8 to 64 | Spreads per-key CAS contention + per-key 64-permit I/O cap (RustFS 503 congestion) across 64 shards; GC retire-contention failure rate dropped from ~78% to ~21% (**measured**, soak #6 t=343s) | **DONE** (commit `d0194412d0b`) | `reports/2026-06-15-unattended-night-opcount-fixes.md` §#4 |
+| **root_shards factory default fix** | Factory default for `root_shards` was silently 8 (mismatched `PoolConfig`'s already-32 default) — fixed to **32**, weighed against per-shard journal body size vs. GC discovery cost | Shipped default is now 32, not 64: with the flat-combining queue (below) removing most of the CAS-contention argument for large shard counts, 32 keeps a hot table's journal tail small without over-multiplying discovery `LIST` keys | **DONE** (commit `aa04ac9c3fb`) | `MetadataStorageFactory.cpp`, `CasStore.h:110-114` |
+| **Flat-combining shard-mutation queue** | Group concurrent `casPut` mutations to the same `(namespace, shard)` into one leader-driven read + one `casPut` per flush (`MutationScope`, `Store::mutateShard`) | Soak-validated ~2.3× CAS-write compression; intra-server conflicts dropped from ~257k/h to ~11/h (**measured**) | **DONE** (spec `2026-07-03-cas-shard-mutation-queue`) | `CasStore.h`/`CasStore.cpp` (`runShardQueueLeader`); `03-writer-protocol.md §shard-mutation-queue` |
+| **LIST-token skip** | Fold compares each shard's LIST-returned token against the sealed post-fence token; if equal, skips the shard body GET entirely | Reduces fold GETs to `S_changed` rather than `S` at every round; at steady state `S_changed ≪ S` | **DONE** (`CasGc.cpp:1430`) | `specs/2026-06-14-ca-reduce-s3-op-count-design.md` §2 |
+| **Snap prune LIST elimination** | Removed per-round LIST of current-generation fold prefix for deposed-leader debris; wholesale generation-retain prune handles it lazily | Saves 1 `LIST` per round on the common single-leader path | **DONE** (`CasGc.cpp:1242`) | Code comment in `CasGc.cpp` |
+| **P9 — GC 404-HEAD storm** | Prune confirmed-deleted/absent candidates from the fold in-degree snapshot (`GcSnap::forget`) so they are not re-HEADed on subsequent rounds | Removes the dominant HEAD source: **~90% of all read ops** on the GC leader = ~3.8 M / 23 min = ~367 M / day at `workers=2` (**measured**, P0 soak `CasBlobHeadMiss` + `CasTreeHeadMiss`); eliminates the livelock amplification; idle-round `HEAD`s went from ~46k to ~0 | **DONE** (TLA+-verified, soak-validated) | `reports/2026-06-17-ca-s3-opcount-optimization-proposals.md` §P9; `04-gc-protocol.md §node-pruning` |
+| **P3 — replication relink** | Replica `DownloadPart` adopts the existing manifest refs rather than re-downloading each blob | Removes ~15 HEADs + 15 GETs per replicated part (**measured**, ~53 k parts / 95 min at `workers=2`) | **DESIRABLE** (P3 proposal) | `reports/2026-06-17-ca-s3-opcount-optimization-proposals.md` §P3 |
+| **P4 batch publishes** | Commit multiple parts in one manifest CAS | Cuts `casPut` count + 35% CAS-conflict rate (**measured**) + per-shard write churn | **Largely subsumed** by the flat-combining shard-mutation queue (2026-07-03), which batches concurrent publishes into one `casPut` per shard; remaining delta is a deliberate multi-part single-commit API (B157/B149) | `reports/2026-06-17-ca-s3-opcount-optimization-proposals.md` §P4 |
+| **P6 dirty-only fence** | Fence only shards with pending retires rather than all `S` shards | Would have removed `S - S_dirty` fence PUTs per round | **SUPERSEDED** by the ack-floor round | `reports/2026-06-17-ca-s3-opcount-optimization-proposals.md` §P6 |
+| **Ack-floor GC round** | Replace the per-round all-shard fence + fold-through-fence recheck with a causal ack floor + one three-cursor merge; deletion gated by `condemn_round < min_ack` over live-server heartbeats | Removes both O(universe) phases: ~2×O(universe) GET + O(universe) CAS-PUT per round → O(delta)+O(servers) + 1 LIST sweep. ~2.4 M req/round → ~2–3 k req/round at 100k tables × 8 shards (**modeled**) | **DONE** (`cas-gc-ack-floor-fence`; soak validation TODO) | `specs/2026-07-02-cas-gc-ack-floor-fence-redesign.md` |
+| **Merged heartbeat** | Fold the per-server build-watermark PUT into the mount-lease beat; add one `gc/state` GET | −1 PUT / +1 GET per writer beat | **DONE** (`cas-gc-ack-floor-fence`) | same spec, Task 6 |
+
+---
+
+## 5. Measured vs Modeled {#measured-vs-modeled}
+
+| Section | Confidence | Method |
+|---|---|---|
+| ETag-fix HEAD reduction (~73%) | **Measured** | `CasDbgMetaHit` / `S3WriteRequestsCount` ratio, instrumented soak (2026-06-15, seed 20260616); `CasDbgMetaHit` was a throwaway diagnostic ProfileEvent, since removed (commit `87d97826711`) — the ratio is a historical measurement, not a live-queryable metric |
+| Blob dedup hit rate (64%) | **Measured** | `CasBlobPutDedup` / (`CasBlobPutDedup` + `CasBlobPut`), P0 instrumentation run (~23 min) |
+| GC HEAD storm magnitude | **Measured** | `CasBlobHeadMiss` + `CasTreeHeadMiss` ~3.81 M / 23 min (ch1 only); `S3HeadObject` 23.7 M / 3.2 h (run #5 aged pool) |
+| 412 manifest-CAS conflict rate (35%) | **Measured** | `CasRootCasConflict` 49 k / `CasRootCas` 93 k, P0 soak |
+| Per-part PUT count (novel path) | **Code-derived** | `CasBuild.cpp`: 1 PUT per blob + 1 tree + 1 precommit + 1 manifest + 1 commit = `f` + 4 |
+| LIST-token skip savings | **Code-derived** | `computeDiscoverDecisions` logic, `CasGc.cpp:1430`; not yet separately instrumented |
+| Replication HEAD+GET per part (~15+15) | **Measured** | `part_log` attribution, `DownloadPart` 53,796 events / ~95 min (P0 soak) |
+| Mutable-files S3 cost (0 ops) | **Code-derived** | `CasRootShardCodec.cpp:121` inline encoding |
+| P9 savings (~90% of read ops) | **Measured basis (historical)** | `CasBlobHeadMiss`+`CasManifestHeadMiss` (`CasTreeHeadMiss` was renamed) ≈ 90% of all `S3HeadObject`, P0 soak. NOTE: the specific `GcSnap::forget` node-pruning mechanism this row credited was later REMOVED — superseded by the source-edge-set GC model (no persisted node registry to forget from); see `04-gc-protocol.md` |
+| revalidateDeps per-promote HEAD count | **Removed** | `revalidateDeps` no longer exists (EDGE-BEFORE-OBSERVE, `2026-07-09-cas-writer-gc-simplification`): tokened deps are edge-protected; only non-tokened deps get a HEAD + `.meta` GET at promote |
+
+---
+
+## 6. Cost Summary (Corrected, `workers=2`, no-chaos, ~3.3 h steady) {#cost-summary}
+
+Extrapolated to per-day (×7.18) using the corrected P0 soak measurement:
+
+| Bucket | Requests/day | $/day | Waste? |
+|---|---|---|---|
+| Read (GET + HEAD) | ~517 M | ~$207 | ~$147 of that = GC 404-HEAD storm (82% miss, P9 target) |
+| Write (PUT + LIST) | ~73 M | ~$365 | ~$178 = 412-waste (dedup body-PUT + manifest-CAS retries) |
+| DELETE | many | $0 | — |
+| **Total (pre-P1/P2/P9)** | | **~$571/day** | **~57% is eliminable waste** |
+| **Estimated floor after P1/P2/P9** | | **~$245/day** | Modeled/uncertain; P1/P2/P9 have since all shipped (see §4) but this table has not been re-run against the fixed pipeline |
+
+Figures are for the soak workload at the time of measurement (`workers=2`, `root_shards=64` — the
+then-widened soak fanout, not today's shipped default of 32; see §4), no chaos, MergeTree with 18×
+merge amplification. A production workload's per-request ratios depend heavily on dedup hit rate,
+merge amplification factor, and GC round cadence.
+
+### 6.1 Post-fix live measurement, per operation kind (2026-07-19, `workers=6`, WITH chaos) {#cost-summary-per-kind}
+
+Re-run of §6 against the post-P1/P2/P9 pipeline, from the `cas-gc-rebuild` 5h soak (v11), ~2h45m
+in, `workers=6` insert/mutate workers + 4 SELECT workers, chaos stage active (kill/restart/pause
+faults firing). **Measured**, per operation *unit* rather than extrapolated per-day, using a
+different attribution method from §5/§6: grouping `ProfileEvents` in `system.query_log` (by
+`query_kind`), `system.part_log` (by `event_type`), and
+`system.content_addressed_garbage_collection_log` (`outcome='Success'` rounds only) — see
+`analyzing-cas-health` skill for the exact queries and the `blob_storage_log`
+`query_id` decoding technique used to attribute `MergeParts`/`MutatePart` PUTs (`part_log`'s own
+`ProfileEvents` never captures a nonzero PUT for those event types; the upload count comes from
+matching `blob_storage_log`'s synthetic `::` `query_id` against `part_log`
+by part name instead).
+
+| Unit | Count | PUT/unit | GET/unit | HEAD/unit | DELETE/unit | $/unit |
+|---|---:|---:|---:|---:|---:|---:|
+| `INSERT` | 44,753 | 13.3 | 15.8 | 30.7 | 0 | $0.000085 |
+| `MergeParts` | 16,530 | 21.0 | 23.4 | 40.6 | 0 | $0.000130 |
+| `MutatePart` | 4,964 | 10.6 | 14.0 | 33.3 | 0 | $0.000072 |
+| `DownloadPart` (replication fetch) | 44,157 | 0 | 4.0 | 5.0 | 0 | $0.0000036 |
+| GC round (`outcome='Success'`) | 416 | ~0 | 5,305 | 12,812 | 4,156 | $0.00725 |
+
+A GC round costs **~55–100× more per unit** than a single insert/merge/mutation — not from PUT (GC
+issues almost none), but from GET+HEAD volume in the retire/recheck verification pipeline (matches
+§3.2/§3.6's modeled GC-round shape). `DownloadPart` is the cheapest unit by far (~24× cheaper than
+an `INSERT`) — confirms §2.4's CAS-relink model: a replication fetch is a lightweight
+metadata-level operation reading an already-shared blob directly from S3, not a full byte transfer
+between replicas.
+
+Aggregate request cost for the measured window (request pricing only, no storage/data-transfer):
+
+| Source | PUT | GET | HEAD | DELETE | $ |
+|---|---:|---:|---:|---:|---:|
+| `INSERT` | 594,236 | 707,851 | 1,371,247 | 0 | $3.80 |
+| `MergeParts` | 346,309 | 387,304 | 671,832 | 0 | $2.16 |
+| `MutatePart` | 52,683 | 69,363 | 165,315 | 0 | $0.36 |
+| `DownloadPart` | 0 | 176,477 | 220,600 | 0 | $0.16 |
+| GC rounds (416) | 0 | 2,206,977 | 5,330,042 | 1,728,828 | $3.02 |
+| CAS-internal housekeeping (`.meta` blob sidecars, `cas/manifests`, `cas/refs`, `gc/gen` attempt/outcome records — PUT only, no query/part attribution by design) | 1,128,865 | — | — | — | $5.64 |
+| **Total (request cost only)** | | | | | **≈$15.13** |
+
+The CAS-internal housekeeping bucket is the single largest line item, larger than `INSERT` itself
+— dominated by the per-blob `.meta` sidecar object (one small PUT written alongside, not instead
+of, every real content blob's PUT). This is a distinct cost driver from anything in §1–§3's
+per-code-path models above and was not previously broken out on its own; see
+`utils/ca-soak/scenarios/BACKLOG.md`'s "S3 PUT budget" entry (2026-07-19) for the full attribution
+methodology and the query_id-decoding technique that resolved it.
+
+---
+
+## 7. References {#references}
+
+- `docs/superpowers/specs/2026-06-08-s3-ops-cost-model.md` — pricing tier reference.
+- `docs/superpowers/specs/2026-06-14-ca-reduce-s3-op-count-design.md` — Pillar A (incremental GC) + Pillar B (`resolveRef` decode cache) design.
+- `docs/superpowers/specs/2026-06-20-ca-dedup-cache-head-before-put-design.md` — P1/P2 dedup-cache + adaptive HEAD-before-PUT design.
+- `docs/superpowers/reports/2026-06-15-ca-soak-opcount-and-rustfs-findings.md` — ground-fix research; instrumented soak A1b definitive attribution.
+- `docs/superpowers/reports/2026-06-15-unattended-night-opcount-fixes.md` — #1 ETag fix + #4 root_shards widen + soak #6/#7 results.
+- `docs/superpowers/reports/2026-06-17-ca-s3-opcount-optimization-proposals.md` — P0–P9 proposals + corrected cost table.
+- `utils/ca-soak/scenarios/BACKLOG.md`, "RESOLVED — S3 PUT budget..." entry (2026-07-19) — the
+ `query_id`-decoding / `part_name`-matching methodology behind §6.1.
+- `~/.claude/skills/analyzing-cas-health/SKILL.md` — the executable checklist this section's
+ measurement technique was drawn from (step 6/8/10's exact queries).
+- `src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Pool/CasBuild.cpp` — write path; `putBlob`, `precommitAdd`, `promote`.
+- `src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Pool/CasStore.cpp` — `readShardDecoded`, `resolveRef`, `readManifest`, dedup-cache API.
+- `src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Gc/CasGc.cpp` — GC round: `runRegularRound` (heartbeat floor → fold + three-cursor merge → pre-CAS deletes → single CAS), `snapPruneOldGenerations`, `discoverUniverse`, `computeDiscoverDecisions`. `computeHeartbeatFloor` in `CasServerRoot.cpp`.
+- `src/Disks/DiskObjectStorage/MetadataStorages/ContentAddressed/Backend/CasObjectStorageBackend.cpp` — `nativeConditionalPut` (ETag capture), `deleteExact`.
+
+## Shard-mutation queue effect (2026-07-03) {#shard-queue-budget}
+
+Ref-shard `casPut` traffic is grouped per `(namespace, shard)` by the flat-combining queue
+(`03-writer-protocol.md §shard-mutation-queue`): concurrent part publishes/drops on one shard cost
+ONE read + ONE `casPut` per batch instead of one CAS loop per mutation, and intra-server CAS
+conflicts (measured 40-92% of attempts under load, each re-reading the full shard body) are
+structurally eliminated — the only remaining conflict source is the other replica's GC trim.
diff --git a/docs/superpowers/cas/08-testing-and-soak.md b/docs/superpowers/cas/08-testing-and-soak.md
new file mode 100644
index 000000000000..4653a2339abf
--- /dev/null
+++ b/docs/superpowers/cas/08-testing-and-soak.md
@@ -0,0 +1,649 @@
+---
+description: 'Testing and validation for the content-addressed (CAS) MergeTree: the adversarial scenario suite (S01–S35), the 24h soak harness, clickhouse-disks ca-fsck and ca-gc-dryrun introspection, the event and GC audit logs, and the standing findings and backlog pointers.'
+sidebar_label: 'Testing and soak'
+sidebar_position: 8
+slug: /superpowers/cas/testing-and-soak
+title: 'CAS — Testing, Soak, and Introspection'
+doc_type: 'guide'
+---
+
+# CAS — Testing, Soak, and Introspection {#cas-testing-and-soak}
+
+This document covers how the CAS MergeTree feature is empirically validated: the standalone
+adversarial scenario suite (S01–S35 in `utils/ca-soak/scenarios/`), the deterministic 24h soak
+harness (`utils/ca-soak/`), the independent pool-inspection tooling (`clickhouse-disks ca-fsck` and
+`ca-gc-dryrun`), and the two in-server audit log tables.
+
+Related documents: `04-gc-protocol.md` (GC protocol), `07-s3-budget.md` (S3 op budgets).
+Spec sources: `specs/2026-06-13-ca-soak-test-design.md`, `specs/2026-06-13-ca-fsck-readonly-design.md`
+(paths not found under `docs/superpowers/specs/` as of 2026-07-03; verify location before relying on them).
+
+## 1. Introspection layer {#introspection}
+
+### 1.1 Read-only disk mode {#read-only-mode}
+
+`ContentAddressedMetadataStorage` can be opened in a read-only mode (triggered by the disk's
+`readonly` config attribute, or internally by `clickhouse-disks`). In read-only mode:
+
+- The startup capability probe is a check-only read, never a mutating write.
+- No background GC scheduler or heartbeat thread is created.
+- Every mutating entry point (`writeFile`, `commit`, `moveFile`, `removeRecursive`, GC entry points)
+ throws fail-closed with a `READONLY` exception.
+- The full read API (`existsFile`, `listDirectory`, `readFile`, `resolveRef`, `readTree`, `locate`,
+ `listNamespaces`, `listRefs`) remains available.
+
+Read-only mode is also the basis for a **WORM deployment** (one writer server, multiple
+read-only mounters sharing the pool).
+
+### 1.2 `clickhouse-disks ca-fsck` {#ca-fsck}
+
+`clickhouse-disks ca-fsck --disk ` (the deprecated alias `fsck` still works, printing
+a stderr deprecation note) opens the target CA disk read-only and verifies
+pool reachability:
+
+1. **Reachable set**: walks every namespace → every ref → every tree entry, collecting the physical
+ object key of every `Blob` via `locate`. Uses the production read API, so a walk bug is also a
+ read bug.
+2. **All keys**: enumerates the full pool prefix via paginated `backend.list`.
+3. **Classifies** each content key:
+ - `reachable` — referenced by at least one live ref and present in the pool.
+ - `dangling` — referenced by a live ref but **absent** from the pool. This is an INV-NO-LOSS
+ violation. The ONLY hard class.
+ - present-but-unreferenced blobs are classified through the GC pipeline view (2026-07-02;
+ read for LABELING only — reachability never consults GC state). Under the ack-floor two-phase
+ pipeline a nonzero, churning unreferenced set is the NORMAL steady state of an active pool, so
+ the old single `unreachable` lump read as a leak when it was the pipeline working:
+ - `pending-gc` — the present incarnation is in the retired set (condemned at round N, or
+ `delete_pending`). Deletion is scheduled ~2-3 rounds out. EXPECTED.
+ - `awaiting-gc` — edges still in the GC snapshot (a drop/reclaim not folded yet), or GC has
+ not run on the pool at all. EXPECTED.
+ - `unaccounted` — outside the whole GC view. Normal only as a transient (created + dropped
+ between rounds); a PERSISTENT `unaccounted` object should be impossible under INV-2
+ (reachability-before-content, `03-writer-protocol.md`) and is the anomaly signal the soak
+ forensics trigger fires on.
+ - `unreachable` remains the class of pre-precommit manifest debris rows (labeled
+ `reclaimable-/in-flight-pre-precommit`); the summary `unreachable=` counter is the TOTAL of
+ all present-but-unreferenced objects, so residual-settling loops keep one monotone number.
+4. Reports `dedup_ratio` = `referenced_bytes / physical_bytes` (logical bytes referenced across all
+ parts, divided by distinct blob bytes on disk).
+
+**Exit code** is nonzero iff `dangling > 0`. The tool prints de-alarm `note:` lines when
+`pending_gc`/`awaiting_gc` are nonzero ("inside the normal GC deletion pipeline — expected") and a
+re-run hint when `unaccounted` is nonzero — beta testers should never have to interpret raw counters.
+
+`--detail` adds a per-object row (tab-separated `class, key, size[, reachable_from...]`) after the
+summary line. `--timeout ` aborts the scan with a clear error instead of hanging (default
+600; 0 = unbounded). There is no `--format json|tsv` option (not implemented as of 2026-07-03) —
+output is a plain `key=value` summary line plus optional tab-separated detail rows.
+
+### 1.3 `clickhouse-disks ca-gc-dryrun` {#gc-dryrun}
+
+`clickhouse-disks ca-gc-dryrun --disk ` opens the disk read-only and derives the set
+of objects the next GC round **would** delete, from the durable `gc/snap` (in-degree graph) and
+`gc/state` (round + `retired_refs`) — zero CAS writes and zero deletes.
+
+The key assertion (verified by the soak harness at every quiesced checkpoint):
+
+> **`{preview deletes} ⊆ {ca-fsck unreachable}`** — GC must never plan to delete an object that
+> `ca-fsck` can still reach from a live ref.
+
+Note: `ca-gc-dryrun` currently previews `zeroInDegree` only for target shard 0 when
+`gc_shards > 1`. This is a known gap (S31 regression guard; see BACKLOG section below).
+
+Both commands reject non-CA disks with a clear message.
+
+### 1.4 `gc/state` disaster recovery runbook {#gc-rebuild-runbook}
+
+**Symptom:** `system.content_addressed_garbage_collection_log` shows the round stuck at
+`outcome = 'Error'` with an `error` message containing `CORRUPTED_DATA` — the baseline guard
+(`04-gc-protocol.md §gc-rebuild`) has refused every regular round because it found a shard journal
+proving trimmed history with no healthy adopted baseline under `gc/state` (e.g. after an operator
+`mc rm` of `gc/state`, or backend corruption). GC makes NO further progress and deletes NOTHING while
+in this state — the guard is fail-closed by design.
+
+**Remedy — pick one:**
+
+1. **A live replica is up** (preferred): run
+ ```sql
+ SYSTEM CONTENT ADDRESSED GC REBUILD [FORCE] []
+ ```
+ on any server that mounts the affected disk. Omit `` to rebuild every content-addressed disk
+ on that node. The command throws with the refusal text if the rebuild itself refuses (e.g. another
+ leader holds the lease, or a committed ref names a missing manifest — real data loss that needs
+ `ca-fsck` forensics before reaching for `FORCE`). On success it logs the rebuilt `round`, `generation`,
+ `namespaces`, `shards`, `committed_refs`, `live_precommits`, `unowned_alive_manifests`, `edges`, and
+ `clamped_shards` counters.
+
+2. **No server is up** (e.g. investigating an offline pool): from a host with `clickhouse-disks`,
+ ```bash
+ clickhouse-disks --disk ca-gc-rebuild [--force]
+ ```
+ Requires the disk to be configured `true` in the `clickhouse-disks` config —
+ same rule as `ca-fsck`/`ca-gc-dryrun`: this tool must never claim a live server's mount. It prints the
+ report as `key=value` pairs and exits nonzero (with a `refusal=` line) if the rebuild refuses.
+
+**After the rebuild:** regular GC rounds resume; `ca-fsck` converges to `dangling=0` over the next few
+rounds as the ack-floor pipeline drains `pending-gc`/`awaiting-gc`. A rebuilt baseline is
+conservative — it may over-protect a trimmed-but-live build's manifest (design delta 2 in
+`04-gc-protocol.md §gc-rebuild`), so a small, bounded, `ca-fsck`-visible `unaccounted`/leaked set settling
+over a rebuild-to-rebuild window is expected, not a regression.
+
+## 2. Audit log tables {#audit-logs}
+
+### 2.1 `system.content_addressed_garbage_collection_log` {#gc-log}
+
+One `Start` row and one `Finish` row per GC round, emitted by `CasGcScheduler` via an injected
+`GcRoundLogger` callback (no `Interpreters` dependency in the disk layer). Key `Finish` columns:
+
+| column | description |
+|--------|-------------|
+| `event_type` | `'Start'` or `'Finish'` |
+| `disk_name` | the CA pool the round ran on |
+| `gc_id` | random `u128` hex identifying the scheduler instance (which mounter led) |
+| `trigger` | `'Scheduled'` (background tick) or `'Manual'` (`SYSTEM CONTENT ADDRESSED GC RUN`) |
+| `round` | monotone round counter |
+| `outcome` | `'Success'`, `'NotALeader'`, or `'Error'` |
+| `candidates_marked` | refs retired/marked this round |
+| `objects_deleted` | objects physically removed |
+| `objects_absent` | objects expected but already missing (concurrent delete or bug) |
+| `objects_replaced` | 412-spared objects (live incarnation token displaced the condemned one) |
+| `objects_spared` | objects not deleted for other reasons (e.g. protected by live build) |
+| `manifests_deleted` | owner-removed manifest bodies deleted this round (B11) |
+| `entries_condemned` | retired entries newly condemned this round (ack-floor stage 1) |
+| `entries_graduated` | entries newly floor-passed, republished `delete_pending` (stage 2; deleted NEXT round) |
+| `entries_redeleted` | pending exact-token blob deletes executed this round (stage 3) |
+| `fence_outs` | expired mounts fenced out by this round's heartbeat floor |
+| `min_ack` | the ack floor latched at round start (`UINT64_MAX` = no counted heartbeats) |
+| `anomalies` | fold clamps surfaced (and survived) this round |
+| `duration_ms` | wall time for the round |
+| `error` | exception message when `outcome = 'Error'`; empty otherwise |
+| `ProfileEvents` | `Map(String, UInt64)` delta of GC thread `Cas*` + `S3*` counters over the round |
+
+A `Start` row with no matching `Finish` marks a round that hung or crashed mid-flight.
+`NotALeader` rows on non-leader servers in a shared-pool cluster are expected.
+**Hard assertion**: no `'Error'` finish rows in positive scenarios.
+
+Useful diagnostic query — GC round timeline:
+```sql
+SELECT event_time, event_type, round, outcome, candidates_marked, objects_deleted,
+ objects_replaced, objects_spared, duration_ms, error
+FROM system.content_addressed_garbage_collection_log
+WHERE disk_name = 'ca'
+ORDER BY event_time;
+```
+
+### 2.2 `system.content_addressed_log` {#event-log}
+
+Per-event CAS audit log (B170). **On by default** since `cbe0ffb7608` — `programs/server/config.xml`
+ships a `` section (the CAS disk feature is experimental, so the audit log is the
+primary forensic instrument; it costs nothing when no CAS disk is configured). Remove the config section
+to disable; when disabled, the injected `CasEventSink *` is `nullptr` — a single branch, no row construction.
+Key schema columns:
+
+| column | description |
+|--------|-------------|
+| `event_type` | taxonomy value (see below) |
+| `disk_name` | the CA disk the event occurred on |
+| `namespace` | `roots/` (server/table) |
+| `ref_name` | part name or ref identifier |
+| `object_kind` | `LowCardinality(String)`: `none`, `blob`, `manifest`, `root`, `snap` |
+| `object_hash` | lowercase hex content hash |
+| `token` | incarnation token (S3 ETag or equivalent) |
+| `round` / `gen` | GC round and snap generation numbers |
+| `at_version` | manifest shard version driving the journal record |
+| `outcome` | `ok`, `adopt`, `resurrect`, `deleted`, `replaced`, `spared`, `absent`, etc. |
+| `reason` | human-readable *why* of the decision (mandatory, never decorative) |
+| `thread_id` / `query_id` | thread and query context of the event, when applicable |
+| `detail` | `Map(String,String)` structured facts for full reconstruction |
+
+**Event types the soak harness asserts must NOT appear in positive scenarios**:
+`read_missing`, `dangling_access`, `corrupt_dangle`, `corrupt_decode`,
+`snap_journal_incoherent`, `exception`.
+
+**Writer/mount insight events (2026-07-02)**: `retired_view_advance` — one row per view ADVANCE (never per
+beat): `round` = the installed round, `detail.from_round`, `detail.retired_entries` = the size of
+the retired list this writer just loaded; answers "when did this writer learn about round N".
+`mount_remount` — a self-remount attempt (`outcome` = `ok`/`failed`, `detail.writer_epoch`).
+`blob_copy_forward` — a condemned incarnation displaced by verified copy-forward at the promote
+gate (`detail.displaced_token`); join by `object_hash` against `blob_retire`/`blob_delete` to trace
+the full incarnation history.
+
+Useful for full object lifetime reconstruction:
+```sql
+SELECT event_time, event_type, object_hash, token, round, gen, outcome, reason, detail
+FROM system.content_addressed_log
+WHERE disk_name = 'ca' AND object_hash = ''
+ORDER BY event_time;
+```
+
+And for ref lifecycle:
+```sql
+SELECT event_time, event_type, ref_name, round, gen, reason, detail
+FROM system.content_addressed_log
+WHERE disk_name = 'ca' AND ref_name = ''
+ORDER BY event_time;
+```
+
+### 2.3 GC unit-test suites (ack-floor round) {#gc-unit-suites}
+
+The one-pass ack-floor round (`04 §gc-round`) is exercised by these gtest suites (`src/Disks/tests/`,
+`InMemoryBackend` with injected clocks/hooks):
+
+| Suite | File | What it covers |
+|---|---|---|
+| `CasGcAckFloor` | `gtest_cas_gc_ack_floor.cpp` | The protocol: condemn → `delete_pending` → exact-token delete pipeline; `NoOpRoundDoesNotMutateRefShards`; stale-ack-holds-the-floor; pre-ack publish spares; expired-mount fenced-out-and-excluded; recreated-blob delete is `TokenMismatch`-ok. Ported from `gtest_cas_gc_fence_recheck.cpp` (the fence test dropped, recheck/completion tests ported). |
+| `CasHeartbeatFloor` | `gtest_cas_mount.cpp` | `computeHeartbeatFloor` classification (live / terminated / expired) and the token-guarded fence-out (sleeper renewal permanently fails; a concurrent renewal wins ⇒ reclassified live). |
+| `CasStoreBeat` | `gtest_cas_store.cpp` | The merged beat: ack advances only after the view load; the drain blocks the ack while a mutation is in flight; a `gc/state` read failure leaves the ack unchanged. |
+| `CasThreeCursorMerge` | `gtest_cas_blob_indegree.cpp` | The three-cursor merge rules — spare / graduate→pending / condemn, including the `condemn_round = min_ack − 1` vs `= min_ack` boundary. |
+| `CasHeartbeat` | `gtest_cas_mount.cpp` / codecs | The merged keeper body (lease + `min_active` + `observed_gc_round`); `CAWM` watermark object gone. |
+| `CasGcReplay` | `gtest_cas_gc_*` | Crash-replay idempotence (renamed from the old resume suite): crash after artifacts but before the CAS ⇒ re-run under a fresh attempt succeeds; already-executed deletes land on `NotFound`. |
+
+**The reclaim-loop pattern (load-bearing for every reclaim assertion).** Because a blob is no longer
+deleted in the round that folds its removal, a test that asserts deletion must **run enough rounds
+AND advance the ack between them**: the pipeline is condemn at round K → `delete_pending` at the
+first pass whose floor `min_ack > K` → physical delete the pass after that (with all acks current:
+condemn K → pending K+1 → deleted K+2). Helpers `runRoundsUntilAbsent` / `blobAbsent` /
+`currentRetiredSet` drive this; each round calls `store->renewWatermarkOnce()` (runs the beat to keep
+the mount watermark fresh — graduation itself paces on GC rounds via `new_round`, not on the removed
+`observed_gc_round` ack), and fixpoint loops continue while the current retired list still holds an
+in-flight entry. A test failing because a blob is deleted *later* than
+the old protocol is expected drift; a test failing because a **referenced** blob is deleted, or an
+in-degree double-count appears, is a real bug.
+
+### 2.4 Live health-verification playbook {#health-checklist}
+
+When a CAS-backed server is running (a soak, staging, or production) and the question is "is CAS
+actually behaving correctly right now", this is the order of checks and what each one tells you —
+distilled from a full pass across every relevant `system.*` table during a 5h chaos soak on
+`cas-gc-rebuild`. Each step names the table, the query shape, and the gotcha that made an earlier
+pass over-claim or under-claim something.
+
+**1. Correctness invariants first, not performance.** Query `system.content_addressed_log`
+(`#event-log` above) for the event types the soak harness asserts must never appear:
+`read_missing`, `dangling_access`, `corrupt_dangle`, `corrupt_decode`, `snap_journal_incoherent`,
+`exception`. Also scan the free-text `reason` column broadly for `anomaly`/`clamp`/`error`/
+`corrupt`/`unexpected`/`fail` — not just inside the known-bad event types — since a genuinely new
+failure mode may not yet be in that list. Zero hits on both checks is the baseline "nothing is
+structurally wrong" signal; do this before looking at any performance number.
+
+**2. `system.errors` / `system.error_log` — read past the raw `value`.** Both tables carry
+`last_error_message` and `last_error_trace` columns; a bare error-code count with no further
+context tells you almost nothing. `system.text_log` is **not** authoritative on its own for how
+often an error fired — `LogSeriesLimiter` suppresses repeated identical log lines after the first
+few occurrences (the underlying `system.errors` counter still increments on every occurrence). A
+huge `system.errors` count alongside almost no matching `text_log` rows is the expected shape of a
+rate-limited, high-frequency error, not evidence the error is rare. Always cross-check
+`system.query_log`'s `exception_code != 0` count: if that is zero (or near-zero) despite a large
+`system.errors` total, the underlying failures are transient and being fully retried/absorbed
+before reaching the client.
+
+**3. Known-benign error codes worth recognizing on sight** (so they don't cost a fresh
+investigation every time): `S3_ERROR` / `"...PreconditionFailed..."` is CAS's own conditional-PUT
+collision-detection mechanism (`finalizeConditionalWriteInstrumented`,
+`CasObjectStorageBackend.cpp`) — every legitimate content-addressing/dedup collision throws (and
+thus counts) an `S3Exception` before being caught and reclassified as `PutOutcome::PreconditionFailed`,
+a normal outcome, not a real error (see `BACKLOG.md`'s "S3_ERROR inflates" entry for the
+observability-fix direction). `CANNOT_PARSE_INPUT_ASSERTION_FAILED` is ClickHouse's own
+`ValuesBlockInputFormat` fast-literal/slow-expression parser fallback (`ValuesBlockInputFormat.cpp`)
+firing whenever an `INSERT ... VALUES` row contains a function-call expression (e.g.
+`toDateTime64(...)`) instead of a bare literal — entirely unrelated to CAS.
+
+**4. `system.trace_log` — three `trace_type`s answer three different questions; don't conflate
+them.**
+- `CPU`: where the process spends actual processor cycles. Group by the exact `trace` array (not
+ just the leaf frame), condense to the top few frames per stack for readability, and remember the
+ single most-frequent *identical* stack may still be a small percentage of total CPU samples — it
+ is the modal repeated call path, not necessarily the dominant cost center.
+- `Real`: where threads spend wall-clock time, *including* blocking waits. Most of the top will be
+ healthy idle capacity (the query executor's async-task reactor loop, `BackgroundSchedulePool`
+ idle workers, HTTP keep-alive polls, ZooKeeper/S3 RPC round-trips) — the useful signal is a thread
+ blocked on a `pthread_cond_wait` **inside your own code's** internal queue/lock (not a generic I/O
+ wait), scaling with load.
+- `Memory`: samples allocation *events*, not resident memory. The leaf frame is always the
+ profiler's own capture routine (`StackTrace::StackTrace()`) — skip the first one or two frames
+ before reading the real allocation site. A high sample count here usually just means "this buffer
+ type gets constructed often," proportional to write/read volume, not a leak signal by itself.
+
+**5. `system.parts` — a single active-vs-outdated ratio is not a reliable signal; check the
+trend.** An extreme-looking ratio (this session saw 690:1) can be entirely normal pipeline depth
+within `old_parts_lifetime`'s grace window at the current creation rate. Before calling it a
+backlog: (a) re-check the same ratio a few minutes later — is the outdated count growing or
+shrinking; (b) check the oldest outdated part's age against the table's `old_parts_lifetime`
+setting; (c) cross-check `system.part_log`'s `RemovePart` vs `NewPart`+`MergeParts`+`MutatePart`
+event *rates* over the whole run, not a point-in-time count. In this session an earlier claim that
+a large outdated-parts ratio was evidence of a cleanup backlog turned out to be wrong once the trend
+was checked (the count was falling, and the oldest outdated part was barely past its grace window)
+— the claim was explicitly retracted in `BACKLOG.md` rather than left standing.
+
+**6. `system.part_log` — event-type breakdown plus its own `ProfileEvents` column.** Group by
+`event_type` (`NewPart`/`MergeParts`/`MutatePart`/`DownloadPart`/`RemovePart`/...) and sum
+`ProfileEvents['S3PutObject']` etc. per group to attribute S3/Cas cost to a part-lifecycle stage.
+Gotcha: `NewPart`'s `ProfileEvents` are the *same* underlying events as the originating `INSERT`
+query's `system.query_log` row (confirmed via `sumMap(ProfileEvents)` cross-checks on both tables)
+— summing them together double-counts. `MergeParts`/`MutatePart`/`DownloadPart` genuinely are
+additive on top of `query_log`, since the originating `Optimize`/`Alter` query_log rows show *zero*
+S3 `ProfileEvents` (their background execution isn't captured by query_log at all, only by
+part_log).
+
+**7. `system.query_log` — group `ProfileEvents` by `query_kind` for the synchronous/foreground
+view of cost,** and always check `countIf(exception_code != 0)` alongside any scary
+`system.errors` total (see step 2).
+
+**8. `system.blob_storage_log` — the write-path audit, with two gotchas.** (a) It does **not** log
+`Read` events by default — `enable_blob_storage_log_for_read_operations` defaults to `false`
+(requires `enable_blob_storage_log` too) — an all-zero `Read` row count is expected, not a gap to
+chase. (b) `data_size` on `Delete`-type rows is a `UInt64` sentinel
+(`18446744073709551615` = `UINT64_MAX`), not a real size — filter it out
+(`avgIf(data_size, data_size != 18446744073709551615)`) before computing size statistics, or the
+aggregate is nonsense. Aggregate by a derived path-prefix category (`blobs` vs
+`cas/manifests/` vs `cas/refs/` vs `gc/server-roots` vs `gc/other`) to get a
+per-object-kind cost/error/size/latency breakdown, and to spot cross-replica GC-leader asymmetry: a
+replica whose log shows `Delete`-only rows for the *other* replica's namespace, never `Upload`,
+confirms it currently holds GC leadership and is reclaiming pool-wide garbage.
+
+**9. `system.content_addressed_garbage_collection_log` — round duration by `outcome`, normalized
+by actual work done.** `outcome = 'Success'` vs `'NotALeader'` tells you which replica currently
+holds GC leadership — this can change mid-run (e.g. during a chaos-stage kill/restart), and that is
+expected, not a bug. For duration, normalize by
+`objects_deleted + candidates_marked + entries_graduated + entries_redeleted`: a big round taking a
+long time is fine if the per-item cost stays roughly constant across rounds; a round taking a long
+time while doing **zero** work is the real anomaly worth tracing. In this session, two such
+zero-work spikes both turned out to have a clean explanation once traced — an early cold-cache
+warmup at rounds 1–4 of the run, and a later spike landing almost exactly at the chaos-stage
+transition (correlating with a GC leadership handoff) — neither was dismissed without checking, but
+neither needed a new backlog entry either.
+
+**10. `system.events` — the ground-truth cumulative totals, and a second attribution axis.**
+Cross-reference the generic S3-level counters (`S3PutObject`/`S3GetObject`/`S3HeadObject`/
+`S3ListObjects`/`DiskS3DeleteObjects`) against CAS's own semantic per-object-kind counters
+(`Cas{Blob,Gc,Manifest,Meta,Root,Other}{Put,Get,Head,Delete,List}`, incremented at the S3-call site
+itself, independent of `ThreadGroup` attribution). In this session's soak, the Cas-level counters
+covered `PUT`/`GET`/`LIST` far better than the query_log/part_log/GC-log axis (77–96% vs 4–58%)
+because they don't depend on the calling thread being attributed back to a query or part-log event
+— while `HEAD` stayed persistently under-attributed on both axes (~56%), a genuine open question
+left for a follow-up rather than force-explained. See `BACKLOG.md`'s "S3 cost/capacity attribution"
+entry for the full worked comparison and `07-s3-budget.md` for the design-level S3 budget model.
+
+**11. Host/container-level context for elevated S3 error rates.** When `system.errors`/
+`system.events` show elevated S3 transport errors (`Broken pipe`, `Timeout`), check the *object
+storage backend's own* container/host CPU and memory (e.g. `docker stats`), not just the
+ClickHouse server's. A saturated local test-stand backend (a single `rustfs` container pinned above
+200% CPU was the case in this session) fully explains transient transport errors that are then
+silently absorbed by ClickHouse's own S3-client retry logic — confirmed via `system.query_log`
+showing zero real query failures. This is a test-stand characteristic, not a product bug, but it
+must be *confirmed* (backend CPU + zero query failures), not assumed.
+
+**General methodology notes.** Build a small "budget table" (cost broken down by source) whenever
+a single cumulative counter looks surprising — the counter tells you *that* something happened, not
+*where* it came from; cross-referencing two or three tables against the same-moment `system.events`
+snapshot tells you the shape of the cost. Never assert a finding from a single point-in-time
+snapshot ratio — check the trend over a few minutes before calling something a backlog item or a
+regression. When a later, more careful check contradicts an earlier claim, retract it explicitly in
+whatever document holds it, rather than let a wrong "corroborating evidence" note stand next to the
+real one. And distinguish "confirmed root cause" from "plausible, not fully proven" in the writeup
+itself — several findings in this session's `BACKLOG.md` entries carry that distinction
+deliberately.
+
+## 3. Scenario suite (S01–S35) {#scenario-suite}
+
+Located at `utils/ca-soak/scenarios/`. Each scenario is an independent, focused run against a
+fresh pool prefix. The common run contract, hard assertions, and recommended observations are in
+`utils/ca-soak/scenarios/README.md`.
+
+### 3.1 Common hard assertions {#common-assertions}
+
+Every positive scenario must satisfy:
+
+- **SQL correctness**: all replicas return the same aggregates as the scenario oracle.
+- **Storage correctness**: `clickhouse-disks ca-fsck --detail` reports `dangling = 0`.
+- **GC safety**: `ca-gc-dryrun` delete candidates are a subset of the `ca-fsck` unreachable set at
+ quiescence.
+- **Event audit**: `system.content_addressed_log` contains no `read_missing`, `dangling_access`,
+ `corrupt_dangle`, `corrupt_decode`, `snap_journal_incoherent`, or `exception` rows.
+- **GC rounds**: `system.content_addressed_garbage_collection_log` has no `Failed` finish rows.
+ `NotALeader` rows are expected on non-leader servers.
+- **No unbounded leftovers**: after forced GC, `unreachable = 0` (unless the scenario deliberately
+ abandons writes, in which case the residual must be classified and proved bounded).
+- **No excessive resource growth**: `MemoryResident`, scratch-dir bytes, and pool bytes return to
+ baseline or stay within the scenario budget.
+
+### 3.2 Scenario table {#scenario-table}
+
+| ID | Priority | What it stresses | Key risk targeted | D2 status |
+|----|----------|-----------------|-------------------|-----------|
+| S01 | P0 | Huge single blob upload | Memory materialization in `Build::putBlob` (now **FIXED** — streaming from staged temp file; confirmed <2x peak) | INCONCLUSIVE at dev scale; needs `--scale ci/full` |
+| S02 | P0 | Huge duplicate blob | Dedup skips body upload for existing large blobs | PASS |
+| S03 | P0 | Million-live-object idle GC | GC memory and LIST cost at scale; `CasBlobList = 0` for journal-driven rounds | INCONCLUSIVE (dev scale) |
+| S04 | P0 | Million-object orphan drain | GC reclaim throughput and memory at scale | FAIL (GC Error rows — pre-existing, now resolved by attempt-scoped generation) |
+| S05 | P0 | 10 000 sparse tables | GC does O(changed shards), not O(all tables), per round | FAIL (same GC Error rows) |
+| S06 | P0 | 10 000-column wide part | Manifest limits, memory, S3 op count for very wide parts | INCONCLUSIVE (timestamp parse bug in harness, now fixed) |
+| S07 | P0 | Manifest cap fail-closed | `LIMIT_EXCEEDED` before any ref is published; no orphans | INCONCLUSIVE (dev scale cannot trip the cap limit) |
+| S08 | P0 | Thousands of parts created quickly | CAS contention (`CasRootCasConflict`); root-shard size | INCONCLUSIVE |
+| S09 | P0 | Mutation carry-forward | Only changed columns are re-uploaded; identity updates produce zero large blob growth | INCONCLUSIVE |
+| S10 | P1 | Patch parts and lightweight deletes | No dangling refs during patch-part create/merge/remove | FAIL (harness scale/timing issue, not a product bug) |
+| S11 | P0 | Heavy `ALTER TABLE ... DELETE` | Mutation latency, queue depth, GC reclaim bounded after deletions | FAIL (GC Error rows — pre-existing) |
+| S12 | P1 | Ten replicas, shared pool, parallel inserts | Leader election, dedup across replicas, no data-size amplification | NOT RUN (`docker-compose-10replicas.yml` now exists (ch1..ch10); gap is `soak/cluster.py`'s `Cluster` class, hardcoded to 2 nodes) |
+| S13 | P0 | Process loss during write and GC | Abandoned precommits are safe; stale GC leaders cannot over-delete | FAIL (chaos kill/restart connection refused — harness issue) |
+| S14 | P0 | Restart with many refs | Startup time scales with table metadata, not blob count | FAIL (GC Error rows — pre-existing) |
+| S15 | P1 | GC target shard comparison (`gc_shards` 1/2/8) | Correctness is identical across shard counts; per-round memory decreases | not shown in D2 |
+| S16 | P1 | Hot content cycle with GC | Resurrection uses a fresh re-upload; no condemned-token adoption | FAIL (GC Error rows pre-existing; single forensic `dangling=1` was transient FP, not a safety bug) |
+| S17 | P1 | Detached, attach, and drop detached | Detached refs stay rooted; dropped detached content reclaimed by GC | PASS |
+| S18 | P1 | Freeze and unfreeze shadows | Shadow namespaces keep blobs alive; unfreeze releases shadow refs | INCONCLUSIVE (`SYSTEM UNFREEZE` disabled by default config) |
+| S19 | P1 | Clone and partition movement | Clone republishes refs, not blobs; gated paths fail closed | FAIL (missing `SYNC REPLICA` before agreement check — harness bug) |
+| S20 | P1 | Replicated fetch and relink | Followers reuse blobs; no byte amplification per replica | FAIL (counter not scoped per node — harness issue) |
+| S21 | P1 | Read-heavy many-ref workload | Read-path caching; column-subset queries fetch only required blobs | FAIL (`FINAL` on plain `ReplicatedMergeTree` → `ILLEGAL_FINAL` — harness bug) |
+| S22 | P1 | Object-store throttling and retry budget | Retryable errors visible; successful statements remain correct | NOT RUN (needs fault-injecting S3 proxy) |
+| S23 | P2 | Idle shared pool baseline | Background GC op count is minimal; memory flat; non-leaders quiet | FAIL (metric bug: `s3_ops` summed `*Microseconds` not op counts; real ops are tiny) |
+| S24 | P2 | Small dedup-cache capacity | Cache miss changes cost, not correctness; cache memory bounded | RUNNABLE (`docker-compose-small_dedup_cache.yml` wired as the `smalldedupcache` compose variant; `needs_infra` unset on the `S24` class) |
+| S25 | P2 | Non-`Atomic` database paths | Path parsing correct outside `store/` layout | FAIL (test setup bug — DB created on one node only) |
+| S26 | P2 | Table-level verbatim file churn | Verbatim files not accidentally content-addressed; regular GC does not need to scan them | FAIL (missing `SYNC REPLICA` — harness bug) |
+| S27 | P2 | Backend list pagination ambiguity | Ambiguous keys treated as changed and re-read; correctness preserved | NOT RUN (needs instrumented S3 proxy) |
+| S28 | P0 | Concurrent wide/large insert scratch pressure | Scratch approaches sum of all active staged part payloads under concurrent inserts | PASS |
+| S29 | P0 | Large non-direct-blob file memory spike | `CaInlineWriteBuffer` path; files outside `.bin`/mark/`primary.idx` buffer until `INLINE_CAP` | INCONCLUSIVE |
+| S30 | P0 | Repeated create/drop namespace churn | Monotone namespace registry; GC fanout must not grow without bound after D1 | PASS (D1 regression guard; D1 fixes monotone fanout) |
+| S31 | P1 | `ca-gc-dryrun` under `gc_shards > 1` | `previewDeletes` coverage across all target shards | FAIL (cluster boot failure — infra issue) |
+| S32 | P1 | TTL expiry reclaim | TTL-expired rows disappear; content reclaimed by GC | PASS |
+| S33 | P1 | Concurrent explicit GC leaders — reclaim-leak guard | Non-leader explicit rounds must not orphan owner-removal events permanently | PASS (attempt-scoped generations fix landed; S33 now a real regression guard) |
+| S34 | P0 | Create/drop churn (D1 namespace-reclaim win) | Per-round GC fanout does not grow across create/drop iterations after D1 | PASS |
+| S35 | P0 | Rapid same-name rotation (D1 corner case) | Shard incarnation handles same-namespace repeated create/drop safely | PASS |
+
+**D2 summary (2026-07-02, post-D1 sweep, seed 20260702)**: 8 PASS, 8 INCONCLUSIVE, 14 FAIL, 3 NOT RUN,
+1 RUNNABLE (S24, since wired to the `smalldedupcache` compose variant), S15 not shown in D2.
+Every FAIL is a pre-existing harness/infra/scale issue — zero D1 regressions.
+**2026-07-03 night re-triage**: the D2 FAILs were re-classified against the fixed stack — all of them
+resolved, superseded, or card bugs (the one real-looking S13 'fail' was the card comparing replicas
+before `SYSTEM SYNC REPLICA`; card fixed, re-run PASS 11/11). Current standing: 8 PASS, ZERO real
+fails; remaining inconclusives are honest scale gates (rerun at ci/full) and infra gates
+(S12/S22/S27). See `ROADMAP.md §area-testing` and `utils/ca-soak/scenarios/BACKLOG.md`.
+
+### 3.3 Running a scenario {#running-scenarios}
+
+```bash
+# List available scenarios
+cd utils/ca-soak && python3 -m scenarios.run --list
+
+# Run one scenario (default 15 min measurement window, dev scale)
+python3 -m scenarios.run --scenario S01 --seed 42 --duration 15m
+
+# Larger scale (CI/full)
+python3 -m scenarios.run --scenario S01 --seed 42 --scale ci/full
+```
+
+The run emits `report.md`, `report.json`, `metrics.sqlite`, raw system-table extracts,
+pool-size samples, and container resource samples in `runs//`.
+A scenario may be marked `inconclusive`, but never silently converted to `pass`.
+
+## 4. Soak harness {#soak-harness}
+
+Spec: `specs/2026-06-13-ca-soak-test-design.md`.
+Location: `utils/ca-soak/` (Python + docker-compose; nothing ships in the server binary).
+
+### 4.1 Topology {#topology}
+
+Two `ReplicatedMergeTree` replicas (`ch1`, `ch2`) share one CA pool on `rustfs1`, coordinated via
+`keeper1`. RustFS runs with `RUSTFS_SCANNER_ENABLED=false RUSTFS_HEAL_ENABLED=false` (stability
+fix from B93). `clickhouse-disks ca-fsck` runs read-only against the same pool from a node
+container at quiesced checkpoints.
+
+`docker-compose.yml` is the default RustFS-backed topology. A second compose,
+`docker-compose-awss3.yml`, targets a live AWS S3 bucket instead of RustFS for the release-gate
+real-S3 GC validation; it uses named Docker volumes for state and reads bucket credentials from
+the git-ignored `configs/aws.env`. The live-AWS variant was validated 2026-07-03.
+The live-GCS variant (`docker-compose-gcs.yml`, `storage_conf_gcs_*.xml` with
+`gcs_hmac`, HMAC pair in git-ignored `configs/gcs.env`) was validated
+the same day: probe + replication + two-phase reclaim + DROP-to-zero + ca-fsck all green. NOTE: the
+read-only ca-fsck disk lives in the standalone `configs/fsck_only_gcs.xml` passed only to
+`clickhouse-disks -C` — a `ca_ro` disk in the SERVER config breaks table load on restart
+(`UNKNOWN_DISK`; ROADMAP prod-gate row).
+
+### 4.2 Workload and oracle {#workload-oracle}
+
+Table `ca_stress` (forced Wide parts: `min_bytes_for_wide_part = 0`, `min_rows_for_wide_part = 0`)
+with columns `(op_id, writer, bucket, k, ts, version, v, payload, row_fp)`, TTL on `ts`.
+`payload = det_blob(seed, bucket, k % SHARED)` so identical content recurs across parts and
+replicas (real CA dedup).
+
+The **ledger** produces a fully seeded, reproducible stream of operations: `insert`, `update`,
+`delete`, `optimize`, `truncate`, `drop_partition`. The **oracle** (`model.py`) is an authoritative
+per-key `(bucket, k)` map; it applies each operation and knows the expected live row set at any
+quiesced checkpoint. Aggregates compared: `count()`, `sum(row_fp)`, `uniqExact((bucket,k))`,
+`sum(v)`, `min(op_id)`, `max(op_id)`. These are integer aggregates — exactly reproducible in Python
+and SQL with no hash-serialization matching.
+
+### 4.3 Quiescence protocol {#quiescence}
+
+Before any checkpoint assertion:
+1. Pause all workers.
+2. `SYSTEM SYNC REPLICA` on both nodes; wait `system.replication_queue` empty on both.
+3. Wait every `system.mutations` row `is_done`; no active `system.merges`.
+4. `OPTIMIZE TABLE ca_stress FINAL` + `ALTER TABLE ca_stress MATERIALIZE TTL`.
+5. Drive CA GC to **fixpoint** — poll until pool object set and `ca-fsck`'s `unreachable` field stop changing
+ across successive GC rounds (bounded retries, fail loudly on timeout).
+
+TTL boundary handling: checkpoint timing is arranged so no row sits within ±ε of its TTL boundary.
+If the ambiguous band is non-empty, the checkpoint fails (a scheduling bug, not tolerated).
+
+### 4.4 Checkpoint assertions {#checkpoint-assertions}
+
+At every quiesced checkpoint:
+- SQL oracle match on **both** replicas (`count`, `sum(row_fp)`, `uniqExact`, `sum(v)`, `min/max(op_id)`).
+- `clickhouse-disks ca-fsck --detail`: `dangling = 0` (INV-NO-LOSS, hard fail).
+- `clickhouse-disks ca-fsck`: `unreachable = 0` (GC drained to fixpoint).
+- `clickhouse-disks ca-gc-dryrun`: `{preview} ⊆ {ca-fsck unreachable}` (GC safety direction).
+- `system.content_addressed_log`: no error-class event types.
+- `system.content_addressed_garbage_collection_log`: no `'Error'` finish rows.
+
+### 4.5 Chaos {#chaos}
+
+`chaos.py` applies seeded faults: `{t_offset, target ∈ {ch1,ch2,both,rustfs}, action ∈ {kill-9,
+restart, pause/unpause}, duration}`. After every fault window → a recovery checkpoint (full
+quiescence + all assertions). Faults are bounded so a quiescent checkpoint is always reachable
+between them; a server that does not recover within the bound is a correctness failure.
+
+### 4.6 Known soak limitations {#soak-limitations}
+
+- **4h active-workload chaos soak** at `WORKERS=6` fills the disk in ~60–90 min. ROOT CAUSE
+ identified 2026-07-03: [rustfs#3231](https://github.com/rustfs/rustfs/issues/3231) (open) —
+ overwriting a >128 KiB object in an UN-VERSIONED bucket leaks the previous incarnation's data
+ directory. Every `casPut` of a `cas/refs/<...>/` body leaks one uuid dir (observed: 1600+
+ dirs on one busy shard key; `cas/refs` = 39 GB of a 45 GB pool vs 7.2 GB of real blob data), and
+ the metacache walk over those dirs is what produces the `list_merged err Io(timeout)` /
+ `walk_dir timeout` LIST storms (reproduce at just ~600k files). Re-enabling the RustFS scanner
+ was tested for one run and showed ZERO reclaim effect (the leak is not versions; the old
+ bring-up failure with the scanner was the unpinned `mc`, since pinned) — the scanner stays off.
+ Mitigations: `WORKERS=2`; durable fix = lower ref-shard `casPut` rate (journal batching, B157
+ family) and/or shard bodies under the 128 KiB inline threshold, or the upstream fix.
+- **Large-pool ca-fsck timeout**: `ca-fsck` at ~150 GB pool times out (>180 s) because
+ `ObjectStorageBackend::list` re-enumerates the whole `roots/` prefix per page (O(N²) in the
+ manifest backlog). Fix: paginate at source; separate shard objects into a dedicated prefix.
+ Tracked in the backlog below.
+- **TTL-band oracle ambiguity**: a row sitting within ±10 s of its TTL boundary cannot be predicted
+ exactly. A fault that delays TTL materialization can leave the band non-empty. Mitigation: disable
+ TTL in the soak table for long chaos runs, or widen the ambiguous-band wait.
+
+### 4.7 `lazy_load_tables` for CAS databases {#lazy-load-cas-databases}
+
+Host CAS tables in a database created with `lazy_load_tables = 1`:
+
+```sql
+CREATE DATABASE ca_soak ENGINE = Atomic SETTINGS lazy_load_tables = 1;
+```
+
+Why: without it, if the object store is briefly unreachable while a CAS table's async startup runs
+its ref-table recovery, the recovery seal `PUT` throws `NETWORK_ERROR`, and ClickHouse's
+`AsyncLoader` records the table's `load table` job as `FAILED` **terminally**. Every later touch
+(`SELECT`, `ATTACH`, even `DETACH`) then rethrows the cached error, and the only recovery is a full
+server restart — a permanent outage from a transient blip. With `lazy_load_tables = 1` the table
+attaches as a lightweight proxy and its real storage is built on first access, so a failed startup
+surfaces as a per-query error and is retried on the next access instead of being cached `FAILED`.
+
+Caveat: a lazily-loaded table does not start its background activity (replication queue, merges)
+until its first access. This pairs with the CAS-side bounded recovery retry
+(`cas_request_budget.recovery_retry_budget_ms`, default 120 s) which rides out short blips within a
+single startup; `lazy_load_tables` is the backstop for a blip that outlasts that budget. The soak
+harness creates its `ca_stress` table in a `ca_soak` lazy database for exactly this reason. See
+`docs/superpowers/specs/2026-07-20-cas-table-load-stuck-asyncloader-design.md` and the BACKLOG entry
+"a transient S3-backend NETWORK_ERROR during CAS table-startup recovery permanently strands the
+table".
+
+## 5. Standing findings and backlog {#backlog}
+
+The authoritative finding log is `utils/ca-soak/scenarios/BACKLOG.md` (newest at bottom).
+
+### 5.1 Confirmed fixed {#fixed}
+
+- **S01 `Build::putBlob` memory materialization** (suspected-bug, HIGH): peak memory grew linearly
+ with blob size (~6.5x for a 1 GiB blob) because `putBlob` materialized the staged `BlobSource`
+ into a `String` before `putIfAbsentStream`. **FIXED** (`uploadFromSource` signature changed to
+ stream from the staged temp file). Verified: 2 GiB INSERT peak 13 GiB → 4.33 GiB (~2x, matching
+ local-disk baseline). Residual ~2x is generic `max_block_size` buffering, not CA overhead.
+- **GC-CONCURRENT-LEADER-LEAK** (suspected-bug, HIGH): explicit `SYSTEM CONTENT ADDRESSED GARBAGE
+ COLLECTION` on >1 replica concurrently permanently orphaned blobs (reclaim leak). Root cause: the
+ fold-seal and `gc/state` CAS were not atomic, so a deposed leader could write a final-key seal
+ before its lease CAS failed, orphaning owner-removal events from ever being folded. **FIXED** by
+ attempt-scoped generations: every per-round artifact is keyed by `(gen, attempt = lease.seq)`;
+ a deposed leader's artifacts land under its own unadopted attempt and are invisible to every
+ decision path. TLA+ gate A green; S33 liveness verdict flipped to a real regression guard.
+- **CA-S3 stateless lane — system logs on CA + opt-in cache** (2026-07-10): the June **B86** workaround
+ that pinned every system log to the local `default` policy on the CA-S3 lane is REMOVED — the rewritten
+ CA write path flushes fast (`trace_log` 7087 rows in 77 ms; no 180 s timeouts), so system logs now live
+ on `content_addressed_s3` like user tables. An opt-in `content_addressed_s3_cache` disk (`cache>`
+ over the CA disk; integration test `test_cas_file_cache`) is validated warm ≈ local (0 S3 GETs) but is
+ NOT the lane default — it adds cold-populate cost to single-pass reads/writes, and the stateless suite is
+ mostly single-pass. Read-side companion: CA-S3 reads are cacheless (~3× local warm; see
+ `09-read-protocol.md`). Lane point-fixes: `04286` (directory-safe HEAD), `05008` (ack-floor
+ `entries_redeleted >= objects_deleted`), `05009` (default-ON log + `disk_name` filter), `01271`, `03829`
+ (`max_memory_usage` 150M→170M for the ~9 MiB CA write buffer). Remaining reds are conclusively non-CAS
+ (arch x86-vs-arm FP `01854_s2`/`02224_s2`/`03233_dynamic`; no-MySQL/IPv6 infra `02479`/`01880`/`02784`;
+ `04033_tpc_ds_*` on the `web` disk).
+
+### 5.2 Open backlog items {#open-backlog}
+
+- **GC-DISCOVERY-LIST-QUADRATIC-OVER-ROOTS** (HIGH, scalability): `listRootShardTokens` calls
+ `backend.list` with `max_keys = 0`, which re-enumerates the entire `roots/` prefix (including
+ all `_manifests`) on every page — O(N²/1000) S3 LIST round-trips. Fix: real paginated list at
+ the backend, or a dedicated shard prefix (IDEA-COMMON-SHARD-PREFIX-SINGLE-LIST: relocate all
+ shard objects into one flat prefix so GC discovery is a single LIST).
+- **S31 `ca-gc-dryrun` under `gc_shards > 1`**: `previewDeletes` previews `zeroInDegree` only
+ for target shard 0, so the dry-run oracle can be blind to candidates in other shards.
+- **S12 / 10-replica test**: `docker-compose-10replicas.yml` (`ch1`..`ch10`) exists and is wired
+ as the `tenreplicas` compose variant; the remaining gap is `soak/cluster.py`'s `Cluster` class,
+ which is hardcoded to 2 nodes and has no mechanism to address `ch3`..`ch10` (see `BACKLOG.md`
+ entry `NEEDS-INFRA-S12`).
+- **S22 / throttling**: requires a fault-injecting S3 proxy (not in current compose).
+- **S27 / list pagination ambiguity**: requires an instrumented object-store proxy.
+- **S23 idle RSS +82 MiB**: above the 64 MiB budget; confirm it does not grow unbounded in the 4h
+ soak.
+- **GC run-file streaming** (scalability): the O(buffer) reader debt is **DONE** (T2/T0, 2026-07-02) —
+ `RunFileReader` streams over true ranged reads in `CasObjectStorageBackend::get` + the `getStream`
+ seam; the whole-run `full` member is gone. The remaining byte-volume work (delta-runs + compaction,
+ **T1**) stays DESIRABLE — see [`BACKLOG.md`](BACKLOG.md) §2.
+- **S10, S19, S20, S21 harness bugs**: replica-agreement race (missing `SYNC REPLICA`), `FINAL` on
+ wrong engine type, per-node counter scoping — to be fixed in the harness (not product bugs).
+- **Ack-floor round soak validation** (TODO): the one-pass ack-floor round (`04 §gc-round`) is
+ implemented and unit/TLA+-covered but not yet soak-validated. Needed: hard-KILL a writer
+ mid-commit-burst and verify the next rounds spare-then-recondemn correctly (no dangle in `ca-fsck`);
+ a paused (SIGSTOP) writer holds the floor, then resumes, acks, and the floor advances; a scenario
+ asserting per-round request counts stay O(delta)+O(servers) — a regression guard against
+ reintroducing a universe sweep of GET/PUTs. Deletion latency is now condemn → pending (first
+ floor-pass) → delete (next pass), so soak fixpoint loops must advance acks between rounds.
diff --git a/docs/superpowers/cas/09-read-protocol.md b/docs/superpowers/cas/09-read-protocol.md
new file mode 100644
index 000000000000..a0c10967ea14
--- /dev/null
+++ b/docs/superpowers/cas/09-read-protocol.md
@@ -0,0 +1,488 @@
+---
+description: End-to-end read protocol for the CAS MergeTree feature — ref resolution, manifest fetch, ranged blob reads, column pruning, decode caches, read-your-writes, and GC safety.
+sidebar_label: Read Protocol
+sidebar_position: 9
+slug: /development/content-addressed-mergetree/read-protocol
+title: CAS MergeTree Read Protocol
+doc_type: reference
+---
+
+# CAS MergeTree Read Protocol {#cas-read-protocol}
+
+**Status:** DONE (core path operational; mutable-file and verbatim reads operational; decode caches
+operational; read-your-writes overlay operational; reader GC fence designed). Sources: this doc
+consolidates four deleted read-path docs (see §11) grounded against
+`Core/CasStore.cpp`, `ContentAddressedMetadataStorage.cpp`, `Core/CasObjectStorageBackend.cpp`,
+`IO/ReadBufferFromFileView.cpp`.
+
+---
+
+## 1. Overview and reading guide {#overview}
+
+A CAS MergeTree read never touches the classical local-disk metadata path. Every file access is
+served in one of four ways:
+
+| Access kind | How served |
+|---|---|
+| **Inline entry** (small eager files — `checksums.txt`, `columns.txt`, `count.txt`, `primary.idx`, `serialization.json`, `uuid.txt`, `txn_version.txt`, `metadata_version.txt`, `partition.dat`) | Memory — decoded from the tree catalog's inline-data section; no S3 op at all |
+| **Blob-backed file** (`.bin`, `.mrk`, `primary.idx` above the inline threshold) | Ranged S3 GET — one `GET` per column file per part open, bounded by `[payload_offset, payload_end)` |
+| **Verbatim file** (loose `roots//` objects, `@cas@/_files/` namespace files) | Plain `IObjectStorage::readObject` — no CAS indirection |
+
+The full read path for a blob-backed part file is:
+
+```
+resolveRef → readManifest → lookupPath → getBlobViewPlan → readBlobPayload (ranged GET) → ReadBufferFromFileView
+```
+
+For S3 budget tables see [`07-s3-budget.md §2`](07-s3-budget.md#read-budget); this doc covers the
+protocol mechanics, not the per-operation counts.
+
+---
+
+## 2. Ref resolution (`resolveRef`) {#resolve-ref}
+
+**Status: DONE** (`CasStore.cpp:771`, `Store::resolveRef`; called from `ContentAddressedMetadataStorage::resolveRouted` at `ContentAddressedMetadataStorage.cpp:529`)
+
+```
+resolveRef(ns, ref_name, allow_stale) → std::optional
+```
+
+`resolveRef` locates the manifest reference for a named part (`all_1_1_0`, etc.) inside its
+owning namespace (`store//@cas@/`). It is the first and only touch of the root shard
+on the read path.
+
+1. Compute `shardOf(ref_name)` — deterministic hash of the ref name mod `root_shards`.
+2. Call `readShardDecoded(ns, shard, allow_stale)` to obtain the decoded `RootShard` for that
+ shard (see §6 for the decode cache).
+3. Look up `ref_name` in `root->refs`. A miss returns `std::nullopt`; `FILE_DOESNT_EXIST` is not
+ thrown here — the callers that assert presence do so explicitly.
+4. On a hit, return `Resolved{manifest_id, mutable_files, published_at_ms}` — `mutable_files` is the
+ now-legacy per-ref sidecar (§9.1 below), pending removal; every part file, including the former
+ mutable set, is an ordinary manifest entry. Note: `manifest_size`
+ in the returned struct is always `0` — this is a known minor gap (B10 finding in `ROADMAP.md`).
+
+`allow_stale = true` (the normal read path) permits the shard decode cache TTL fast-path (see §6).
+`allow_stale = false` (the publish gate and force-fresh reads of write-time part files, §9.1) always
+issues a HEAD.
+
+---
+
+## 3. Part-manifest fetch (`readManifest`) {#read-manifest}
+
+**Status: DONE** (`CasStore.cpp:817`, `Store::readManifest`)
+
+```
+readManifest(ManifestId) → PartManifest
+```
+
+The manifest body lives at `cas/manifests////.proto`
+(the `manifestKey(id)` layout key). It is a protobuf object (`clickhouse.cas.format`, package
+renamed from `cas_root_shard.proto`; see `05-formats-and-backend.md §encoding-taxonomy`).
+
+The fetch sequence:
+
+1. `HEAD` the manifest key to obtain the current token. If the object is absent, throw
+ `FILE_DOESNT_EXIST` with the message "INV-NO-DANGLE" — a live ref naming a missing manifest
+ is a protocol violation and must never be silently substituted with an empty manifest.
+2. Check the `(ManifestId, Token)` decode cache (see §6). On a hit, return the cached
+ `PartManifest` immediately with no `GET`.
+3. On a cache miss, `GET` the manifest body. If the object vanishes between step 1 and 3
+ (concurrent GC or writer), throw `FILE_DOESNT_EXIST` (same INV-NO-DANGLE error — the
+ `std::nullopt` contract from `Backend::get` maps to `FILE_DOESNT_EXIST` at this level).
+4. Decode (protobuf deserialize) and validate two identity checks:
+ - `refMatchesBody`: the journal `ManifestRef` must equal the body's self-described `ref`.
+ A mismatch → `CORRUPTED_DATA` (the ref addresses the wrong object).
+ - `manifestNamespaceMatches`: the body's `root_namespace_id` must equal the owning namespace.
+ A mismatch → `CORRUPTED_DATA` (cross-namespace dangle would give the debris sweep wrong authority).
+5. Cache the decoded `PartManifest` by `(ManifestId, Token)` and return it.
+
+---
+
+## 4. Blob ranged reads (`CasObjectStorageBackend::get`) {#blob-ranged-reads}
+
+**Status: DONE** (`CasObjectStorageBackend.cpp:388`, `ObjectStorageBackend::get`; `ContentAddressedMetadataStorage.cpp:968–1004`)
+
+Once the manifest is decoded, `lookupPath(manifest, file_name)` finds the `ManifestEntry` for the
+requested file. `locate(entry)` returns a `BlobLocation{key, offset, length}` where:
+
+- `key` = the blob's content-addressed S3 key (`blobs//`).
+- `offset` = `blob_header_len` (256) for single-file blobs; for tree-packed blobs the entry's
+ own catalog offset within the payload.
+- `length` = the file's payload byte count.
+
+`getBlobViewPlan` builds a `BlobViewPlan`:
+
+- `object = StoredObject(physicalKey(key), path, offset + length)` — the `StoredObject.bytes_size`
+ covers the header + the file's payload slice so `IObjectStorage::readObject` opens the blob
+ starting at byte 0.
+- `payload_offset = offset`, `payload_end = offset + length`.
+
+`readBlobPayload(location, path, settings)` then:
+
+1. Issues `object_storage->readObject(StoredObject(physicalKey(key), path, offset + length), settings)`
+ — one S3 ranged GET whose effective byte range is `[0, offset + length)` from the blob key.
+2. Wraps the resulting `ReadBufferFromFileBase` in a `ReadBufferFromFileView(impl, path, offset, offset + length)`
+ that exposes only the `[payload_offset, payload_end)` window to its consumer (see §7).
+
+For `Backend::get` at the control-object level (root shards, manifests, GC state), ranged reads are
+done via `readObjectRanged`, which as of the 2026-07-02 snapshot-streaming rework is a TRUE ranged
+read (`seek` + `setReadUntilPosition` + a bounded `read`, clamped against a HEAD-supplied or fetched
+object size) — it no longer reads the whole object and substrings the `Range`. This path is shared
+with the snapshot-streaming GC reads, which can be GB-scale, so the whole-object-then-substring
+approach was replaced to keep the caller's memory budget at O(block).
+
+The `NoSuchKey`/404 contract: if the object is deleted between HEAD and GET inside `Backend::get`,
+the exception is caught and `std::nullopt` is returned — a legitimate concurrent-delete window
+surfaces cleanly as absence (never a raw S3 error code 499). Callers expecting presence propagate
+`FILE_DOESNT_EXIST`. This is documented in `05-formats-and-backend.md §get-nullopt`.
+
+### 4.1 Cacheless characteristic {#cacheless-reads}
+
+**Status: CHARACTERIZED (2026-07-10).** A blob-backed file read is billed and latency-bound on
+**every** access, not just the cold one: the CA disk holds no local byte cache, so a warm/repeat read
+of the same part re-fetches from the object store. The only read-path caches are the two *decode*
+caches (§6, shard/manifest metadata) — not column bytes. Profiled on the CA-S3 lane, a raw-CA warm
+scan is ~3× local (141 ms vs 47 ms; 186 S3 GETs; warm never improves); the `trace_log` hot path is
+`ReadBufferFromS3::nextImpl` → socket receive/poll, NOT the CA metadata layer (prefetch is on, marks
+are mark-cached, resolve is cheap, ~1.2 MB/GET).
+
+**Mitigation:** an opt-in filesystem `cache` disk over the CA disk (like plain-s3's `cached_s3`;
+`test_cas_file_cache`, and the `content_addressed_s3_cache` policy in the stateless-lane config).
+Validated: once warm it serves reads from local disk (0 S3 GETs, ≈ local latency). It helps
+**re-read-heavy** workloads; it does NOT help one-shot scans (the cold populate makes the first read
+~2× slower). This is distinct from the metadata-cache RFC in `cache.md`, which does not cache byte
+content. See `ROADMAP.md §area-read-protocol` and `08-testing-and-soak.md §5.1`.
+
+---
+
+## 5. Column pruning {#column-pruning}
+
+**Status: DONE** (implicit, by design — `lookupPath` is per-file)
+
+CAS column pruning is structural: `getStorageObjects` and `getBlobViewPlan` are called per-file by
+the MergeTree reader. The reader passes only the column files it needs to `prepareRead`; CA code
+has no list of "all columns" to filter — it simply looks up whatever file the reader requests and
+returns a ranged `StoredObject` for it. Columns not requested by the query are never fetched.
+
+For `Placement::Inline` files (small eager files in the tree catalog's inline-data section),
+`tryGetInManifestBytes` returns the bytes from memory and `prepareInManifestRead` serves them
+via `ReadBufferFromOwnMemoryFile` — zero S3 ops. The `getStorageObjects` path for these files
+returns a sized empty-key `StoredObject` placeholder to keep size-only consumers working; any
+attempt to read it through a non-CA path will fail loudly (the empty key is intentionally invalid).
+
+---
+
+## 6. Decode caches {#decode-caches}
+
+**Status: DONE**
+
+There are two independent decode caches, one per object kind on the read path.
+
+### 6.1 Shard decode cache {#shard-decode-cache}
+
+**Source:** `CasStore.cpp:633–765` (`readShardDecoded`, `coalescedReadShardDecoded`), `CasStore.h`
+
+Caches `key → ShardDecodeCacheEntry{token, shard, validated_at}`. A root shard is immutable
+within a token (ETag), so a token match means the decoded `RootShard` is still valid.
+
+Two fast-paths:
+
+1. **TTL fast-path** (`allow_stale = true`, `shard_decode_cache_ttl_ms > 0`): if an entry
+ exists and its `validated_at` is within `shard_decode_cache_ttl_ms` (default 200 ms), return
+ it without any S3 op. Absence is never TTL-cached — a freshly committed ref must be observable
+ by force-fresh callers.
+2. **Single-flight coalescing** (`coalescedReadShardDecoded`): if another thread is already
+ fetching the same shard, the current thread waits on a shared future and gets the same result.
+ This bounds the number of concurrent GETs for one shard to 1 regardless of query concurrency.
+
+After the HEAD, a token match against a cached entry updates `validated_at` (re-stamps for the TTL
+window) and returns without a GET. A miss or token change triggers a GET + re-decode + re-cache.
+
+B157 read-your-writes coherence: the write path increments `shard_write_seq[key]` under the cache
+mutex before evicting the old entry; the re-cache at GET time checks that the counter has not
+advanced during the GET and skips caching if a concurrent write landed in the window. This prevents
+a stale decode from being re-inserted after its invalidation erase has already run.
+
+### 6.2 `(ManifestId, Token)` decode cache {#manifest-decode-cache}
+
+**Source:** `CasStore.cpp:817–899` (`Store::readManifest`)
+
+Caches `ManifestCacheKey{manifest_id, token} → shared_ptr`. Part manifests
+are immutable content-addressed objects — the same `(manifest_id, token)` pair always denotes
+the same bytes. On a cache hit, the stored `PartManifest` is returned with no GET.
+
+The cache key combines the manifest id hash, the token value bytes, and the token type byte
+(so a re-incarnation of the same logical manifest id under a new physical token is a cache miss).
+
+Memory is bounded: if either cache grows past `MANIFEST_CACHE_MAX_ENTRIES` (or
+`SHARD_DECODE_CACHE_MAX_ENTRIES`), a wholesale clear is performed. Entries re-populate on demand;
+a cleared entry is never incorrect, only slower (one extra HEAD + GET).
+
+---
+
+## 7. `ReadBufferFromFileView` and `PackedFilesReader` (B115) {#readbufferfromfileview}
+
+**Status: DONE** (fixed in commit `440871098a9`; tests in `src/IO/tests/gtest_read_buffer_from_file_view.cpp`)
+
+### 7.1 Role {#fileview-role}
+
+`ReadBufferFromFileView` wraps an inner `ReadBufferFromFileBase` and exposes a sub-range
+`[left_bound, right_bound)` of it as if it were a standalone file. This is the mechanism that
+gives each blob-backed column file its own apparent `[0, size)` coordinate system: the inner
+buffer reads from the physical blob key starting at byte 0, and the view window starts at
+`payload_offset` (= `blob_header_len`, 256 bytes by default).
+
+`PackedFilesReader` is the only other in-tree user of `ReadBufferFromFileView`. It serves
+MergeTree column statistics (`statistics.packed`, `IMergeTreeDataPart::loadStatisticsPacked`)
+by reading each packed sub-file through a view over a shared packed-stats blob.
+
+### 7.2 B115 — position corruption {#b115-fix}
+
+**Root cause (now fixed):** `ReadBufferFromFileView` maintained `file_offset_of_buffer_end`
+(the absolute inner-file offset of `working_buffer.end()`) **incrementally**, assuming the inner
+buffer only mutates its working buffer in response to the view's own `next`/`seek`. That assumption
+is violated by `ReadBufferFromS3`, which **discards its working buffer on `setReadUntilPosition`**
+(rebases `offset`, calls `resetWorkingBuffer`, drops `impl`). After a `setReadUntilPosition` call,
+`file_offset_of_buffer_end` held the pre-discard value, so `getPosition()` returned a value
+teleported forward by the discarded byte count.
+
+A downstream seekable consumer (`CompressedReadBufferFromFile::seek`) trusts that position; its
+"already at required position" or "seek within working buffer" fast-paths made the wrong decision
+and re-served a stale decompressed block. The result was **wrong query results** (duplicated and
+missing granules) — not an exception, not a checksum failure.
+
+**Fix:** after **every** operation on the inner buffer (`next`, `seek`, `setReadUntilPosition`,
+`setReadUntilEnd`), `file_offset_of_buffer_end` is rebased from `impl->getPosition() + impl->available()`
+(computed inside the buffer-swap window via `executeWithOriginalBuffer`), not incremented
+from the stale value. This makes the invariant *view buffer-end == inner buffer-end* hold
+by construction, regardless of what the inner buffer does to its own state.
+
+**Impact:** This bug was latent in `PackedFilesReader` at the time of discovery because the only
+consumer (`loadStatisticsPacked`) used `CompressedReadBuffer` (sequential, non-seekable) and never
+called `setReadUntilPosition`. The CA read path used `CompressedReadBufferFromFile` (seekable,
+mark-range-narrowing) over a remote backend that discards on `setReadUntilPosition` — all three
+conditions required for B115 were present.
+
+**Test coverage:** `src/IO/tests/gtest_read_buffer_from_file_view.cpp` — parameterized over
+file-like (no-op `setReadUntilPosition`) vs remote-like (discard-on-`setReadUntilPosition`) inner
+buffers, with several chunk sizes and a randomized sequence checked against a golden model. 14 of 36
+cases failed on the unfixed code; all 36 pass with the fix.
+
+---
+
+## 8. In-flight read-your-writes (B59, projection read-back) {#read-your-writes}
+
+**Status: DONE** (`ContentAddressedTransaction.cpp`, `DataPartStorageOnDiskFull.cpp`)
+
+### 8.1 The problem (B59) {#b59-problem}
+
+The CA write model is "build everything → commit once → then readable." However, the MergeTree
+projection spill-and-merge flow does read-your-own-uncommitted-writes mid-build: it writes temp
+projection blocks (`/_.tmp_proj`) via the parent part's open
+`ContentAddressedTransaction`, then a sub-`MergeTask` (with `NO_TRANSACTION_PTR`) reads those
+temp blocks back to merge them into the final `.proj` file.
+
+On CA those temp blobs are uploaded and recorded in the transaction's `recorded` map but no ref
+or manifest is committed yet. The read-back via `ContentAddressedMetadataStorage::getStorageObjects`
+→ `resolveRef` (committed-only) found no ref → `FILE_DOESNT_EXIST`.
+
+**Same class, merge path:** the same latent bug existed for a merge that produced multi-block
+projections. The B58 single-block fast-path worked only because one temp block was rekeyed directly
+into the parent manifest without a read-back.
+
+### 8.2 The overlay {#b59-overlay}
+
+`ContentAddressedTransaction` gained three virtual overrides on `IMetadataTransaction`:
+
+- `tryGetInFlightStorageObjects(path)` — if `path` maps to a blob recorded in the transaction's
+ `recorded` map, return a `StoredObjects` singleton for the already-uploaded blob key. Returns
+ `std::nullopt` on a miss or on a mutable (inline) file (those are served by `tryReadFileInFlight`).
+- `tryReadFileInFlight(path, settings, read_hint)` — for blob entries, delegates to
+ `object_storage->readObject`; for `recorded_mutable` (inline staged bytes), returns a
+ `ReadBufferFromOwnString`.
+- `tryGetInFlightFileSize(path)` — reads the size from the `recorded` or `recorded_mutable` entry.
+
+`DiskObjectStorageTransaction` forwards all three to its held `metadata_transaction`.
+
+`DataPartStorageOnDiskFull`'s read-ish methods (`getStorageObjects`, `readFile`,
+`readFileIfExists`, `existsFile`, `getFileSize`, `prepareRead`) gained a guarded prelude:
+
+```cpp
+if (transaction)
+ if (auto inflight = transaction->tryGetInFlightStorageObjects(full_path))
+ return *inflight;
+// else: unchanged — volume->getDisk()->…(path)
+```
+
+The gate is `transaction != nullptr`. A **committed** part being read by a normal `SELECT` has no
+open transaction, so for all ordinary reads this is a single null-check with no behavior change.
+
+### 8.3 Directory granularity (projection carry-forward) {#b59-directory}
+
+A mutation that carries a projection forward hardlinks the projection's inner files into the
+open transaction's `recorded` map via `createHardLink` → `recordBlob`. During finalize,
+`IMergeTreeDataPart::loadProjections` probes each projection with `existsDirectory`. That call
+was added an analogous overlay:
+
+- `ContentAddressedTransaction::hasInFlightDirectory(path)` — returns `true` iff any staged entry
+ in `recorded` or `recorded_mutable` for the path's part has a key that starts with the
+ `