This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 7933a82bddd [opt](build) Enforce BE compile-time hygiene at configure 
time (#66901)
7933a82bddd is described below

commit 7933a82bddd09d1e4667665706ecf833a1153ac8
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Wed Aug 19 10:07:58 2026 +0800

    [opt](build) Enforce BE compile-time hygiene at configure time (#66901)
    
    ### What problem does this PR solve?
    
    Related PR: #66510 (the BE build-time series, #66615..#66826, that this
    PR locks in)
    
    Problem Summary:
    
    The build-time series cut BE cold builds by ~49% and took
    `olap_common.h` / `uid_util.h` off the everything-rebuilds line. Every
    one of those wins can regress **silently**: a stray include, a lost
    `extern template`, a header added to the PCH — the code still compiles,
    only every build afterwards is slower, and nothing reports it. `#66052`
    already showed the failure mode: the layering rules in
    `check-header-deps.py` only worked because the author happened to
    maintain them by hand; nothing in CI runs them.
    
    This PR turns each known regression vector into a **fail-loud,
    seconds-level gate that runs at configure time**, so a violation is a
    build error on every path that builds the BE (CI, `build.sh`, the UT
    build) with zero extra CI wiring — the same mounting pattern the FE side
    already uses for its architecture gates (`exec-maven-plugin` running
    `check-fe-core-metadata-funnel.sh` at `validate`).
    
    **Checks** (pure text scans, ~1.3s combined, zero build dependency, zero
    false positives on current master):
    
    | Gate | Regression it stops |
    |---|---|
    | Header layering rules (30, +6 new) | a hub header (e.g. `exec_env.h`,
    `function.h`, `pch.h`) re-growing an include edge into a subsystem that
    was deliberately cut |
    | Third-party ban table (new) |
    `fmt`/`boost`/`concurrentqueue.h`/`<ranges>` returning to headers whose
    bodies were moved out of line to contain template-instantiation cost
    (`<ranges>` in a src header also breaks the `-fno-access-control` UT
    build on libc++) |
    | Two-axis budgets (new) | the edges nobody names: light hubs must stay
    light (include-closure budget, zero slack), heavy payloads must not
    spread (transitive-reach baseline +10%); `--closure` / `--reach`
    subcommands rank the offending edges for the fix |
    | pch whitelist (new) | any quoted include added to `pch/pch.h` — the
    single most leveraged regression in the repo (touch one such header and
    the whole backend plus the PCH rebuild) |
    | `check-extern-template-pairing.py` (new) | the one **silent**
    extern-template failure: a `.cpp` keeps the explicit instantiation but
    the header loses the `extern` declaration, so every TU quietly
    re-instantiates; also the loud direction (declaration with no
    definition), reported at once instead of at link |
    | `check-unity-skip-coverage.py` (new) | a test `#include`-ing a src
    `.cpp` that is not opted out of unity batching — reported at configure
    with the exact skip entry to add, instead of as duplicate symbols at the
    end of the BE UT link |
    
    `build-support/check-build-hygiene.sh` runs all three scripts;
    `be/CMakeLists.txt` executes it at configure time. Escape hatches, by
    design:
    
    - `-DENABLE_BUILD_HYGIENE=OFF` (default ON; a missing `python3` is a
    loud FATAL naming this switch, never a silent skip);
    - every budget/whitelist/rule table lives in the scripts themselves, so
    the legal way past a gate is a **one-line reviewed diff in the same PR**
    — the gate is never a dead end. Every failure message carries the
    mechanism (why this edge is expensive) and the concrete fix path.
    
    **Found by the new gates on day one, fixed here:** `core/types.h` has
    carried five `extern template struct fmt::formatter<Decimal*>;`
    declarations with **no matching definition anywhere** since #53483 —
    every member is defined in-class, so the linker never noticed; deleted.
    Also made the FE funnel self-test's `sed -i` invocation portable to
    BSD/macOS.
    
    Blind spot, by design: instantiations expanded from macros (e.g.
    `DECLARE_OPERATOR` in `operator.cpp`) are invisible to the pairing scan
    on both sides — macro bodies are skipped; a comment at the site says so.
    
    `be/README.md` (new) documents the gates, what to do when each fires,
    and the `*_fwd.h` convention as the sanctioned way through a layering
    barrier.
    
    The same rules are encoded for AI-assisted development and review:
    `be/AGENTS.md` (new — include discipline while writing BE code + review
    checkpoints), `build-support/AGENTS.md` (new — maintenance discipline
    for the gate scripts themselves), an include-src-cpp checkpoint in
    `be/test/AGENTS.md`, and a Header Hygiene entry in the root `AGENTS.md`,
    so coding agents and the review pipeline apply them automatically.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [x] Manual test (add detailed scripts or steps below)
    - `build-support/check-build-hygiene.sh` on master: all green, ~1.3s,
    zero false positives.
    - Self-tests `bash build-support/tests/run.sh` (7/7 PASS): red/green
    injection per gate family — each seeded violation turns red with the
    mechanism + fix in the message, restoring the file turns it green again.
    - Real configure on macOS arm64: green path passes the gate section;
    with an injected violation, configure dies at the gate in under a second
    with the full message; `-DENABLE_BUILD_HYGIENE=OFF` bypasses; a missing
    `python3` is a loud exit-2 FATAL naming the switch.
    - This PR's own Linux CI exercises the configure mount on the second
    platform.
    
    - Behavior changed:
    - [x] Yes. BE configure now fails fast (with mechanism + fix in the
    message) on header-hygiene violations; emergency bypass via
    `-DENABLE_BUILD_HYGIENE=OFF`. No runtime behavior change — the only
    source-code changes are comments and the deletion of five never-defined
    `extern template` declarations.
    
    - Does this need documentation?
    - [x] No. Developer-facing docs are included in-repo (`be/README.md`).
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    https://claude.ai/code/session_01EyZL631YTeuNXa9WLjvk7s
    
    ---------
    
    Co-authored-by: Claude Fable 5 <[email protected]>
---
 AGENTS.md                                          |   2 +
 be/AGENTS.md                                       |  70 +++
 be/CMakeLists.txt                                  |  26 ++
 be/README.md                                       |  85 ++++
 be/src/core/column/column_decimal.cpp              |   2 +
 be/src/core/column/column_string.cpp               |   2 +
 be/src/core/column/column_vector.cpp               |   4 +
 be/src/core/types.h                                |   6 -
 be/src/exec/operator/operator.cpp                  |   5 +
 be/test/AGENTS.md                                  |   4 +
 build-support/AGENTS.md                            |  49 ++
 build-support/check-build-hygiene.sh               | 118 +++++
 build-support/check-extern-template-pairing.py     | 271 ++++++++++++
 build-support/check-header-deps.py                 | 491 ++++++++++++++++++++-
 build-support/check-unity-skip-coverage.py         | 226 ++++++++++
 build-support/tests/test-build-hygiene-entry.sh    | 101 +++++
 .../tests/test-build-hygiene-extern-pairing.sh     |  91 ++++
 .../tests/test-build-hygiene-header-deps.sh        | 136 ++++++
 .../tests/test-build-hygiene-unity-skip.sh         |  92 ++++
 .../tests/test-fe-core-metadata-funnel.sh          |   4 +-
 20 files changed, 1761 insertions(+), 24 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 34617013274..5d551bb1c3e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -30,6 +30,8 @@ All code must pass style checks before committing. Use the 
corresponding skill f
 
 **BE (C++) Static Analysis**: After building BE (which generates 
`compile_commands.json`), run `build-support/run-clang-tidy.sh` to check 
modified C++ files against the `.clang-tidy` config. The script parses `git 
diff` to filter warnings to changed lines where possible, reducing noise from 
pre-existing code (diagnostics from included headers may still appear). For 
Cloud C++ files, pass `--build-dir` pointing to the Cloud compilation database 
(e.g., `cloud/build_ASAN`). Try to fix all re [...]
 
+**BE (C++) Header Hygiene**: BE configure enforces compile-time hygiene gates 
(header layering rules, include-closure/reach budgets, pch whitelist, 
extern-template pairing, unity-skip coverage) via 
`build-support/check-build-hygiene.sh` (~1s, pure text). Run it directly after 
touching BE headers, includes, template instantiation lists, or unity skip 
lists — do not wait for CI. Violations are build errors whose messages carry 
the mechanism and fix; deliberate budget/whitelist changes are  [...]
+
 **FE (Java) Style**: Checkstyle is integrated into the Maven build 
(`maven-checkstyle-plugin`). Running `build.sh --fe` automatically validates 
style via `mvn validate`. If checkstyle fails, fix the reported issues 
according to `fe/check/checkstyle/checkstyle.xml`. See the `fe-code-style` 
skill for details.
 
 ## Code Review
diff --git a/be/AGENTS.md b/be/AGENTS.md
new file mode 100644
index 00000000000..497d8a10eda
--- /dev/null
+++ b/be/AGENTS.md
@@ -0,0 +1,70 @@
+# AGENTS.md — Doris BE
+
+Scope: `be/`. This file is operational — what to do while developing or
+reviewing BE C++ code so the compile-time invariants hold. The gate mechanics
+and the `*_fwd.h` convention are documented in `be/README.md` (read it when a
+gate fires). Repository-wide rules (commit format, build/test commands,
+clang-format/clang-tidy) live in the root `AGENTS.md`; nothing here overrides
+them.
+
+## Machine-Checked Obligations (configure-time gates)
+
+BE configure runs `build-support/check-build-hygiene.sh` (option
+`ENABLE_BUILD_HYGIENE`, default ON; scripts and self-tests live in
+`build-support/` and `build-support/tests/`). It is pure text and takes about
+a second — **run it directly after any change touching BE headers, includes,
+template instantiations, unity skip lists, or test files that include src
+sources**; do not wait for configure or CI to tell you.
+
+Every failure message carries the mechanism and the fix path. The legal way
+past a gate is never to bypass it, but one of:
+
+- fix the edge as the message says (forward-declare + include in the `.cpp`,
+  or route declarations through a `*_fwd.h`);
+- when the change is deliberate, edit the corresponding table
+  (rule exception set / `ANGLE_BANS` / `FORWARD_CLOSURE_BUDGETS` /
+  `REVERSE_REACH_BASELINES` / `PCH_QUOTED_WHITELIST` / `ALLOW`) **in the same
+  commit** and justify it in the commit message — the table diff is the
+  review signal.
+
+## Rules while writing BE code
+
+- **Adding an `#include` to a widely-included (hub) header is a design
+  decision, not a convenience.** Everything a hub includes is reparsed by
+  every TU behind it (~1000 TUs for `exec_env.h`, `thread_context.h`,
+  `runtime_state.h`, `function.h`, `dependency.h`, `column.h`, ...). Prefer
+  forward declarations; put the real include in the `.cpp`. If many files
+  need the declarations, use a `*_fwd.h` (declarations and lightweight
+  aliases only — never bodies or non-fwd project includes).
+- **Never add a quoted include to `pch/pch.h`.** Every header on the PCH
+  rebuilds the whole backend (plus the PCH itself) when touched; it is the
+  single most leveraged regression surface in the repo.
+- **Keep `extern template` families paired.** An explicit instantiation in a
+  `.cpp` needs the matching `extern template` in the header, spelled with the
+  same template arguments — a missing extern compiles and links fine and just
+  silently re-instantiates in every TU. Instantiations expanded from macros
+  (e.g. `DECLARE_OPERATOR` in `operator.cpp`) are invisible to the pairing
+  gate on both sides: keep those in sync by hand.
+- **A test that `#include`s a be/src `.cpp`** needs that file opted out of
+  unity batching via `doris_skip_unity_inclusion` in the owning
+  `be/src/.../CMakeLists.txt`; otherwise the BE UT link fails with duplicate
+  symbols an hour later. The gate error names the exact entry to add.
+- **Do not re-add banned third-party includes** (`fmt`/`boost`/
+  `concurrentqueue.h`/`<ranges>`) to the headers listed in `ANGLE_BANS`:
+  their bodies were deliberately moved out of line, and `<ranges>` in a src
+  header additionally breaks the `-fno-access-control` UT build on libc++.
+
+## Review checkpoints (AI review and self-review)
+
+- [ ] New includes in hub headers: could a forward declaration or `*_fwd.h`
+      carry this instead? Does the PR pay a closure/reach budget bump — and
+      if so, does the commit message justify it?
+- [ ] Any edit to a gate table (`RULES` exceptions, budgets, whitelist,
+      `ALLOW`) must be deliberate, minimal, and explained in the same
+      commit; an unexplained table edit is a red flag, not a fix.
+- [ ] Any change to `pch/pch.h` is near-always wrong; demand the reasoning.
+- [ ] New explicit instantiation lists or `extern template` blocks: both
+      sides present, same spelling? New test `#include` of a src `.cpp`:
+      skip entry present?
+- [ ] `git grep` for a deleted/renamed header in skip lists and gate tables:
+      stale entries fail configure loudly — fix them in the same PR.
diff --git a/be/CMakeLists.txt b/be/CMakeLists.txt
index b2f1b8129ce..093a787cd9f 100644
--- a/be/CMakeLists.txt
+++ b/be/CMakeLists.txt
@@ -159,6 +159,32 @@ endif()
 set(THIRDPARTY_DIR "$ENV{DORIS_THIRDPARTY}/installed")
 message(STATUS "THIRDPARTY_DIR is ${THIRDPARTY_DIR}")
 
+# Build-hygiene gates: seconds-level, zero-build-dependency text checks that
+# keep the backend's compile-time invariants from regressing (header layering
+# rules and budgets, extern-template pairing, unity-skip coverage for
+# test-included sources). Running them at configure time makes a violation a
+# build error on every path that builds the BE -- CI, build.sh, the UT build --
+# with no separate wiring, and it fails in the first seconds instead of
+# somewhere inside a long compile. Every failure message carries the mechanism
+# and the fix path; see build-support/check-build-hygiene.sh.
+option(ENABLE_BUILD_HYGIENE "Run the build-hygiene gates at configure time" ON)
+message(STATUS "ENABLE_BUILD_HYGIENE is ${ENABLE_BUILD_HYGIENE}")
+if (ENABLE_BUILD_HYGIENE)
+    execute_process(
+        COMMAND bash "${BASE_DIR}/../build-support/check-build-hygiene.sh"
+        ERROR_VARIABLE BUILD_HYGIENE_ERRORS
+        RESULT_VARIABLE BUILD_HYGIENE_RESULT
+    )
+    if (NOT BUILD_HYGIENE_RESULT EQUAL 0)
+        message(FATAL_ERROR
+            "build-hygiene gates failed (exit ${BUILD_HYGIENE_RESULT}):\n"
+            "${BUILD_HYGIENE_ERRORS}"
+            "Fix the violation (each message ends with its fix path), or "
+            "configure with -DENABLE_BUILD_HYGIENE=OFF to bypass in an "
+            "emergency.")
+    endif()
+endif()
+
 option(MAKE_TEST "ON for make unit test or OFF for not" OFF)
 message(STATUS "make test: ${MAKE_TEST}")
 option(BUILD_BENCHMARK "ON for make google benchmark or OFF for not" OFF)
diff --git a/be/README.md b/be/README.md
new file mode 100644
index 00000000000..758e6407a9b
--- /dev/null
+++ b/be/README.md
@@ -0,0 +1,85 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Doris BE development notes
+
+## Compile-time hygiene gates
+
+BE configure runs `build-support/check-build-hygiene.sh` (option
+`ENABLE_BUILD_HYGIENE`, default `ON`): a set of seconds-level text checks that
+keep the backend's compile-time invariants from regressing. They exist because
+include-graph regressions are silent -- the code still compiles, only every
+build afterwards is slower. The gates make them loud, in the first seconds of
+configure, with the mechanism and the fix in the message.
+
+What each gate guards, and what to do when it fires:
+
+* **Header layering rules** (`check-header-deps.py`). A hub header
+  (`exec_env.h`, `thread_context.h`, ...) must not reach a named subsystem,
+  because everything a hub includes is reparsed by the ~1000 TUs behind it.
+  Fix: forward-declare the type in the header and include the real header in
+  the `.cpp`; if only declarations are needed by many files, route them
+  through a `*_fwd.h` (see below). A genuinely leaf-like header can be added
+  to the rule's exception set -- in the same PR, with the reasoning in the
+  commit message.
+* **Third-party bans**. A few headers deliberately moved their `fmt`/`boost`/
+  `<ranges>`-using bodies out of line; the ban keeps the template machinery
+  from coming back. Fix: put the code that needs the library into the matching
+  `.cpp`.
+* **Closure / reach budgets**. The safety net for edges no rule names: light
+  hubs must stay light (`--closure <header>` lists what grew), heavy payloads
+  must not spread (`--reach <header>` ranks the spreading edges). Fix: cut the
+  new edge, or -- when the growth is intended -- bump the number in the budget
+  table in the same PR and say why in the commit message. The diff of the
+  budget table is the review signal; the gate is never a dead end.
+* **PCH whitelist**. `pch/pch.h`'s quoted includes are pinned exactly: every
+  header on the PCH rebuilds the whole backend when touched. Adding one is
+  almost never right; if it is, change the whitelist in the same PR.
+* **extern template pairing** (`check-extern-template-pairing.py`). Every
+  `extern template` declaration in a header must have its explicit
+  instantiation definition in a `.cpp`, and -- within a family that uses
+  externs -- every definition must have its declaration. The missing-extern
+  direction is the one silent case: it compiles and links, every TU just
+  quietly re-instantiates the specialization. Fix: keep both sides in sync,
+  spelling the template arguments the same way.
+* **Unity-skip coverage** (`check-unity-skip-coverage.py`). A test that
+  `#include`s a be/src `.cpp` needs that file opted out of unity batching
+  (`doris_skip_unity_inclusion`), otherwise the BE UT link fails with
+  duplicate symbols an hour later. The error names the exact entry to add.
+
+The gates are python3 sources and resolve their own interpreter (`python3`,
+then `python`, then `/usr/libexec/platform-python`, the RHEL8/AlmaLinux8 system
+Python that the build-env image ships in place of `/usr/bin/python3`). They
+deliberately ignore `PYTHON`, which `env.sh` exports as the interpreter the
+*build* uses for code generation and which is python2 by default. Set
+`BUILD_HYGIENE_PYTHON` to pin a specific interpreter.
+
+Escape hatch for emergencies: configure with `-DENABLE_BUILD_HYGIENE=OFF`.
+Self-tests live in `build-support/tests/` (`run.sh`).
+
+## The `*_fwd.h` convention
+
+`*_fwd.h` headers are the sanctioned way through a layering barrier: they
+carry forward declarations (and lightweight aliases) only, so they cost
+nothing to include and are exempt from the layering rules by suffix. When a
+hub header needs a subsystem's type names but not its definitions, put the
+declarations in `<subsystem>_fwd.h` (precedent:
+`exec/common/hash_table/phmap_fwd_decl.h`) and include the real headers only
+in `.cpp` files. Do not put function bodies, class bodies, or includes of
+non-fwd project headers into a `*_fwd.h`.
diff --git a/be/src/core/column/column_decimal.cpp 
b/be/src/core/column/column_decimal.cpp
index fed2fe04c3a..314bccd5d54 100644
--- a/be/src/core/column/column_decimal.cpp
+++ b/be/src/core/column/column_decimal.cpp
@@ -591,6 +591,8 @@ typename ColumnDecimal<T>::CppNativeType 
ColumnDecimal<T>::get_fractional_part(s
     }
 }
 
+// A new instantiation here needs the matching 'extern template' declaration
+// in column_decimal.h (enforced by check-extern-template-pairing.py).
 template class ColumnDecimal<TYPE_DECIMAL32>;
 template class ColumnDecimal<TYPE_DECIMAL64>;
 template class ColumnDecimal<TYPE_DECIMALV2>;
diff --git a/be/src/core/column/column_string.cpp 
b/be/src/core/column/column_string.cpp
index b8b7a6c451e..1ea2f24c1e7 100644
--- a/be/src/core/column/column_string.cpp
+++ b/be/src/core/column/column_string.cpp
@@ -774,6 +774,8 @@ bool ColumnStr<T>::is_valid_utf8() const {
     return true;
 }
 
+// A new instantiation here needs the matching 'extern template' declaration
+// in column_string.h (enforced by check-extern-template-pairing.py).
 template class ColumnStr<uint32_t>;
 template class ColumnStr<uint64_t>;
 } // namespace doris
diff --git a/be/src/core/column/column_vector.cpp 
b/be/src/core/column/column_vector.cpp
index 31e4862187f..00de7a17ba7 100644
--- a/be/src/core/column/column_vector.cpp
+++ b/be/src/core/column/column_vector.cpp
@@ -545,6 +545,10 @@ void ColumnVector<T>::replace_float_special_values() {
 }
 
 /// Explicit template instantiations - to avoid code bloat in headers.
+/// A new instantiation here needs the matching 'extern template' declaration
+/// in column_vector.h: without it, every TU that uses the specialization
+/// silently instantiates its own copy again (enforced by
+/// build-support/check-extern-template-pairing.py at configure time).
 template class ColumnVector<TYPE_BOOLEAN>;
 template class ColumnVector<TYPE_TINYINT>;
 template class ColumnVector<TYPE_SMALLINT>;
diff --git a/be/src/core/types.h b/be/src/core/types.h
index 0ea2a81c3bf..e732c31d3e8 100644
--- a/be/src/core/types.h
+++ b/be/src/core/types.h
@@ -626,9 +626,3 @@ struct fmt::formatter<doris::Decimal<T>> {
         return fmt::format_to(ctx.out(), "{}", to_string(value.value));
     }
 };
-
-extern template struct fmt::formatter<doris::Decimal32>;
-extern template struct fmt::formatter<doris::Decimal64>;
-extern template struct fmt::formatter<doris::Decimal128V2>;
-extern template struct fmt::formatter<doris::Decimal128V3>;
-extern template struct fmt::formatter<doris::Decimal256>;
diff --git a/be/src/exec/operator/operator.cpp 
b/be/src/exec/operator/operator.cpp
index cfc9d664511..2eeb27d2970 100644
--- a/be/src/exec/operator/operator.cpp
+++ b/be/src/exec/operator/operator.cpp
@@ -819,6 +819,11 @@ Status AsyncWriterSink<Writer, 
Parent>::close(RuntimeState* state, Status exec_s
     return Base::close(state, exec_status);
 }
 
+// An instantiation added outside the macros below needs the matching
+// 'extern template' declaration in the operator's header (enforced by
+// build-support/check-extern-template-pairing.py). Instantiations expanded
+// through DECLARE_OPERATOR are invisible to that check -- macro bodies are
+// skipped -- so their extern pairing has to be kept in sync by hand.
 #define DECLARE_OPERATOR(LOCAL_STATE) template class 
DataSinkOperatorX<LOCAL_STATE>;
 DECLARE_OPERATOR(HashJoinBuildSinkLocalState)
 DECLARE_OPERATOR(ResultSinkLocalState)
diff --git a/be/test/AGENTS.md b/be/test/AGENTS.md
index 3b0e9fed7a6..ce6d84d22f1 100644
--- a/be/test/AGENTS.md
+++ b/be/test/AGENTS.md
@@ -3,3 +3,7 @@
 ## Access Control
 
 BE-UT has actually been configured to ignore access control, so it can access 
all private interfaces.
+
+## Including src .cpp files
+
+- [ ] A test that `#include`s a be/src `.cpp` (to reach file-static helpers) 
must have that file opted out of unity batching via 
`doris_skip_unity_inclusion` in the owning `be/src/.../CMakeLists.txt` — 
otherwise the batch object's copy of the definitions collides with the test's 
inlined copy and the BE UT link fails with duplicate symbols. Enforced at 
configure time by `build-support/check-unity-skip-coverage.py`; its error 
message names the exact entry to add. Prefer not including `.cpp [...]
diff --git a/build-support/AGENTS.md b/build-support/AGENTS.md
new file mode 100644
index 00000000000..8f140275ee5
--- /dev/null
+++ b/build-support/AGENTS.md
@@ -0,0 +1,49 @@
+# AGENTS.md — build-support
+
+Operational rules for the architecture/hygiene gates in this directory (the
+`check-*.sh` / `check-*.py` scripts wired into FE `mvn validate` and BE
+configure). These scripts fail other people's builds — treat them as
+production code with a stricter bar than the code they guard.
+
+## Gate maintenance discipline
+
+- **Zero false positives, non-negotiable.** A per-commit gate must be
+  deterministic, pure-text, and second-level. Anything heuristic
+  (nm audits, closure sweeps, wall-clock trends) belongs in offline reports,
+  never in the configure/validate path. One false positive burns more trust
+  than ten missed regressions.
+- **Fail loud, never silently skip.** A missing tool, a table entry pointing
+  at a renamed file, a sentinel nobody references any more — each is an
+  error with a message, not a silent pass (precedents:
+  `doris_skip_unity_inclusion` in `be/CMakeLists.txt`, the missing-python3
+  branch of `check-build-hygiene.sh`, the missing-header errors in
+  `check-header-deps.py`).
+- **Every failure message carries three parts**: the violation, the
+  *mechanism* (why this edge/entry is expensive — model:
+  `check-header-deps.py`'s reason/chain/fix form), and a concrete fix path
+  the reader can act on without opening the script.
+- **Escape hatches are tables in the script, not flags.** Budgets,
+  whitelists, exception sets and `ALLOW` lists live next to the rules so a
+  deliberate change is a one-line reviewed diff in the same commit. Do not
+  add bypass environment variables or config files.
+- **Rebaselining budgets**: forward closure budgets carry zero slack (bump =
+  explicit, justified edit); reverse reach baselines carry +10% and are
+  re-measured on the audit cadence with `check-header-deps.py --budget`.
+
+## Changing a gate script
+
+- Any behavior change to a `check-*` script requires updating its self-test
+  in `build-support/tests/` (red/green injection form: every seeded
+  violation turns red with the fix path in the message, restoring turns it
+  green) and running `bash build-support/tests/run.sh` — all of it, since
+  the entry scripts aggregate.
+- The BE hygiene self-tests briefly mutate working-tree files (backed up and
+  restored by EXIT traps): do not run them concurrently with a
+  build/configure of the same tree, and do not "fix" them by pointing at
+  fixtures — the gate tables name real headers on purpose.
+- Keep scripts portable: bash 3.2 (macOS), BSD *and* GNU userland — in
+  particular `sed -i` needs the `-i.bak` + `rm` form. Python: stdlib only,
+  no third-party imports.
+- Performance envelope: the combined configure gate is ~1s today; keep any
+  addition within a low single-digit second budget, with zero build
+  dependency (no compiler, no compile_commands.json).
diff --git a/build-support/check-build-hygiene.sh 
b/build-support/check-build-hygiene.sh
new file mode 100755
index 00000000000..5f0a51619a8
--- /dev/null
+++ b/build-support/check-build-hygiene.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Unified entry for the BE build-hygiene gates. Runs every seconds-level,
+# zero-build-dependency check that protects the backend's compile-time
+# invariants:
+#
+#   check-header-deps.py             header layering rules, third-party bans,
+#                                    the two-axis closure/reach budgets and the
+#                                    pch include lock
+#   check-extern-template-pairing.py extern template declarations and explicit
+#                                    instantiation definitions stay paired in
+#                                    both directions
+#   check-unity-skip-coverage.py     every src .cpp a test #includes is opted
+#                                    out of unity batching
+#
+# All checks are pure text scans and run in about a second combined. Every
+# failure message carries the mechanism (why the edge/entry is expensive) and
+# the fix path. All checks run even when an early one fails, so one pass shows
+# everything there is to fix.
+#
+# Mounted at configure time by be/CMakeLists.txt (ENABLE_BUILD_HYGIENE, default
+# ON), which is what makes a violation a build error rather than advice. To run
+# by hand:
+#
+#   build-support/check-build-hygiene.sh
+#
+# Exit code: 0 -- all checks passed; 1 -- at least one violation (details on
+# stderr); 2 -- environment problem (no Python 3 interpreter).
+#
+# Self-tests: build-support/tests/test-build-hygiene-*.sh (run.sh runs them).
+
+set -u
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# The gates are python3 sources, so they need a Python 3 interpreter -- which 
is
+# not the interpreter the build itself uses. env.sh exports PYTHON as
+# DORIS_BUILD_PYTHON_VERSION, defaulting to `python`; on the official build-env
+# image (FROM almalinux:8) that is python2.7, under which every gate dies with 
a
+# SyntaxError on its first f-string. So resolve an interpreter here and never
+# read PYTHON.
+#
+# The probe order covers the two shapes that exist in practice: `python3` on
+# developer boxes and most distros, and /usr/libexec/platform-python -- the
+# RHEL8/AlmaLinux8 system interpreter (3.6), which the build-env image ships
+# while carrying no /usr/bin/python3 at all.
+#
+# BUILD_HYGIENE_PYTHON names an interpreter explicitly (a venv, a non-standard
+# prefix). It selects which python runs the gates; it cannot switch them off --
+# that is -DENABLE_BUILD_HYGIENE=OFF.
+is_python3() {
+    "$1" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 6) else 1)' \
+        >/dev/null 2>&1
+}
+
+PYTHON=""
+if [ -n "${BUILD_HYGIENE_PYTHON:-}" ]; then
+    # An explicit choice is honoured strictly: if it is unusable, say so rather
+    # than quietly running the gates under some other interpreter.
+    if is_python3 "${BUILD_HYGIENE_PYTHON}"; then
+        PYTHON="${BUILD_HYGIENE_PYTHON}"
+    else
+        echo "check-build-hygiene: 
BUILD_HYGIENE_PYTHON=${BUILD_HYGIENE_PYTHON} is not found, or is not Python >= 
3.6." >&2
+        echo "Point it at a Python 3 interpreter, or re-run with 
-DENABLE_BUILD_HYGIENE=OFF to skip the gates." >&2
+        exit 2
+    fi
+else
+    for candidate in python3 python /usr/libexec/platform-python; do
+        if is_python3 "${candidate}"; then
+            PYTHON="${candidate}"
+            break
+        fi
+    done
+    if [ -z "${PYTHON}" ]; then
+        echo "check-build-hygiene: no Python >= 3.6 found (tried python3, 
python, /usr/libexec/platform-python); the build-hygiene gates need one." >&2
+        echo "Install python3 or set BUILD_HYGIENE_PYTHON, or re-run with 
-DENABLE_BUILD_HYGIENE=OFF to skip the gates." >&2
+        exit 2
+    fi
+fi
+
+FAILURES=0
+for check in \
+    check-header-deps.py \
+    check-extern-template-pairing.py \
+    check-unity-skip-coverage.py; do
+    if ! "${PYTHON}" "${SCRIPT_DIR}/${check}"; then
+        FAILURES=$((FAILURES + 1))
+    fi
+done
+
+if [ "${FAILURES}" -ne 0 ]; then
+    echo "" >&2
+    echo "build hygiene: ${FAILURES} check(s) failed (details above)." >&2
+    echo "Each message ends with its fix path; the budget/whitelist tables 
live in the" >&2
+    echo "check scripts themselves, so a deliberate change is a one-line, 
reviewed diff." >&2
+    exit 1
+fi
+# Name the resolved interpreter: it is the one thing about this run that varies
+# per environment, and the configure log is where you look when a gate behaves
+# differently in CI than it did locally.
+echo "build hygiene: all checks passed (python: ${PYTHON} $("${PYTHON}" -c 
'import sys; print("%d.%d.%d" % sys.version_info[:3])'))"
diff --git a/build-support/check-extern-template-pairing.py 
b/build-support/check-extern-template-pairing.py
new file mode 100755
index 00000000000..602c99d2cad
--- /dev/null
+++ b/build-support/check-extern-template-pairing.py
@@ -0,0 +1,271 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Pairing guard for `extern template` families.
+
+The BE uses extern-template families (ColumnVector, ColumnDecimal, the operator
+and serde stacks, ...) to instantiate hot template specializations once instead
+of in every translation unit. Of all the ways such a pairing can rot, exactly
+one is silent: a .cpp carries the explicit instantiation definition but the
+header lost (or never gained) the matching `extern template` declaration. The
+code still compiles and links -- every TU just quietly goes back to implicitly
+instantiating that specialization, and the build slows down with no diagnostic
+anywhere. This script turns both pairing directions into an error:
+
+  forward   every `extern template` declaration in a be/src header must have a
+            matching explicit instantiation definition in some be/src .cpp
+            (missing ones eventually break the link, but only when a TU 
odr-uses
+            a non-inline member -- this reports them at once, with the file the
+            definition should go in).
+  reverse   within a COVERED family -- a template name that already has at 
least
+            one extern declaration -- every explicit instantiation definition
+            must have a matching extern declaration (the silent case). Families
+            with no externs at all are intentionally out of scope: 
instantiation
+            without extern is a legitimate TU-local idiom there.
+
+Matching is textual, on a normalized signature: whitespace folded, comments
+stripped, namespace qualifiers dropped, known type aliases canonicalized, and
+multi-line statements joined. Zero build dependency; runs in about half a
+second.
+
+Usage:
+    build-support/check-extern-template-pairing.py            # enforce
+    build-support/check-extern-template-pairing.py --list     # dump the 
pairing tables
+"""
+
+import argparse
+import collections
+import os
+import re
+import sys
+
+SRC_ROOT = "be/src"
+
+# Spellings that legitimately differ between a declaration site and its
+# definition site are rewritten to one canonical form ON BOTH SIDES before
+# matching. Because the rewrite is uniform, a pair that already spells its
+# arguments consistently can never be broken by an entry here; entries are only
+# ever needed when the header and the .cpp spell the same type differently.
+# Keep this table small -- the better fix for a new mismatch is to spell both
+# sides identically.
+TYPE_ALIASES = {
+    # core/types.h vectorized aliases vs the underlying fixed-width types
+    # (column_string.h declares ColumnStr<UInt32>, column_string.cpp
+    # instantiates ColumnStr<uint32_t>).
+    "UInt32": "uint32_t",
+    "UInt64": "uint64_t",
+    # wide:: aliases vs the underlying wide::integer specializations
+    # (wide_integer_to_string.h declares to_string(const Int128&), the .cpp
+    # instantiates to_string(const integer<128, signed>&)).
+    "Int128": "integer<128,signed>",
+    "UInt128": "integer<128,unsigned>",
+    "Int256": "integer<256,signed>",
+    "UInt256": "integer<256,unsigned>",
+}
+
+# Escape hatch, deliberately empty. An entry is
+#     ("be/src/path/to/file.ext", "<normalized signature>")
+# as printed by a failure (or by --list), and silences that one declaration or
+# definition. Adding one must be a reviewed decision with a comment saying why
+# the pairing is intentionally broken.
+ALLOW = set()
+
+EXTERN_START = re.compile(r"^\s*extern\s+template\b")
+# An explicit instantiation begins `template` followed by anything but `<`
+# (`template <...>` opens a template definition, not an instantiation).
+DEF_START = re.compile(r"^\s*template\s+[A-Za-z_:]")
+LINE_COMMENT = re.compile(r"//.*$")
+BLOCK_COMMENT = re.compile(r"/\*.*?\*/")
+
+
+def statements(path, start):
+    """Yields (lineno, statement) for namespace-scope statements matching
+    `start`, joining continuation lines up to the terminating semicolon and
+    skipping preprocessor directives and macro bodies (an instantiation-shaped
+    line inside a #define is not an instantiation)."""
+    with open(path, encoding="utf-8", errors="ignore") as handle:
+        lines = handle.read().splitlines()
+    in_macro = False
+    i = 0
+    while i < len(lines):
+        raw = lines[i]
+        if in_macro or raw.lstrip().startswith("#"):
+            in_macro = raw.rstrip().endswith("\\")
+            i += 1
+            continue
+        if not start.match(raw):
+            i += 1
+            continue
+        stmt, j = raw, i
+        while ";" not in stmt and j + 1 < len(lines) and j - i < 20:
+            j += 1
+            stmt += " " + lines[j]
+        if ";" in stmt and "\\" not in stmt:
+            yield i + 1, stmt.split(";")[0].strip()
+        i = j + 1
+
+
+def normalize(sig):
+    """Collapses a declaration/definition to the comparable skeleton."""
+    s = BLOCK_COMMENT.sub(" ", sig)
+    s = LINE_COMMENT.sub(" ", s)
+    s = re.sub(r"\s+", " ", s).strip()
+    s = re.sub(r"^extern\s+", "", s)
+    s = re.sub(r"^template\s+", "", s)
+    s = re.sub(r"^(class|struct)\s+", "", s)
+    s = re.sub(r"\b[A-Za-z_]\w*::", "", s)
+    for alias, canonical in TYPE_ALIASES.items():
+        s = re.sub(rf"\b{alias}\b", canonical, s)
+    return s.replace(" ", "")
+
+
+def family(norm):
+    """The template name a normalized signature instantiates: the identifier
+    before the argument list for a type, the function name for a function."""
+    paren = norm.find("(")
+    if paren != -1:
+        m = re.search(r"([A-Za-z_]\w*)(?:<[^()]*>)?\($", norm[: paren + 1])
+        if m:
+            return m.group(1)
+    m = re.match(r"([A-Za-z_]\w*)<", norm)
+    return m.group(1) if m else norm
+
+
+def walk(suffixes):
+    for directory, _, names in os.walk(SRC_ROOT):
+        for name in names:
+            if name.endswith(suffixes):
+                yield os.path.join(directory, name)
+
+
+def collect():
+    externs, defs = [], []
+    for path in walk((".h", ".hpp")):
+        with open(path, encoding="utf-8", errors="ignore") as handle:
+            if "extern template" not in handle.read():
+                continue
+        for lineno, stmt in statements(path, EXTERN_START):
+            externs.append((path, lineno, stmt, normalize(stmt)))
+    for path in walk((".cpp", ".cc")):
+        for lineno, stmt in statements(path, DEF_START):
+            if re.match(r"^\s*extern\b", stmt):
+                continue
+            defs.append((path, lineno, stmt, normalize(stmt)))
+    return externs, defs
+
+
+def enforce(externs, defs):
+    def_index = collections.defaultdict(list)
+    for path, lineno, stmt, norm in defs:
+        def_index[norm].append((path, lineno))
+    extern_index = collections.defaultdict(list)
+    covered = collections.defaultdict(set)  # family -> headers declaring it
+    for path, lineno, stmt, norm in externs:
+        extern_index[norm].append((path, lineno))
+        covered[family(norm)].add(path)
+
+    failures = 0
+    for path, lineno, stmt, norm in externs:
+        if norm in def_index or (path, norm) in ALLOW:
+            continue
+        failures += 1
+        print(
+            "error: extern template declaration pairs with no explicit "
+            "instantiation definition",
+            file=sys.stderr,
+        )
+        print(f"  decl:   {path}:{lineno}: {stmt};", file=sys.stderr)
+        print(f"  norm:   {norm}", file=sys.stderr)
+        print(
+            "  reason: 'extern template' promises the specialization is "
+            "instantiated in some .cpp; without one, any TU that odr-uses a "
+            "non-inline member fails to link -- and if every member is inline, 
"
+            "the declaration is dead weight",
+            file=sys.stderr,
+        )
+        print(
+            "  fix:    add the matching 'template class/struct/... ;' to the "
+            ".cpp that owns this family, or delete the declaration; an "
+            "intentionally unpaired declaration goes in ALLOW with a comment",
+            file=sys.stderr,
+        )
+    for path, lineno, stmt, norm in defs:
+        fam = family(norm)
+        if fam not in covered or norm in extern_index or (path, norm) in ALLOW:
+            continue
+        failures += 1
+        headers = ", ".join(sorted(covered[fam]))
+        print(
+            "error: explicit instantiation lacks the matching 'extern "
+            "template' declaration",
+            file=sys.stderr,
+        )
+        print(f"  def:    {path}:{lineno}: {stmt};", file=sys.stderr)
+        print(f"  norm:   {norm}", file=sys.stderr)
+        print(f"  family: {fam} is extern-covered in {headers}", 
file=sys.stderr)
+        print(
+            "  reason: every extern-declared specialization of this family is "
+            "instantiated once, but this one re-instantiates in every "
+            "including TU -- the one pairing mistake that is silent: it "
+            "compiles, links, and only slows the build down",
+            file=sys.stderr,
+        )
+        print(
+            "  fix:    add 'extern template ...;' next to the family's other "
+            "externs in the header above, spelling the template arguments the "
+            "same way as the definition, or list the definition in ALLOW with "
+            "a comment",
+            file=sys.stderr,
+        )
+    return failures
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "--list",
+        action="store_true",
+        help="dump every declaration and definition with its normalized form",
+    )
+    args = parser.parse_args()
+
+    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    os.chdir(root)
+    externs, defs = collect()
+
+    if args.list:
+        for path, lineno, stmt, norm in externs:
+            print(f"decl {path}:{lineno}\n     {norm}")
+        for path, lineno, stmt, norm in defs:
+            print(f"def  {path}:{lineno}\n     {norm}")
+        return 0
+
+    failures = enforce(externs, defs)
+    if failures:
+        print(f"\n{failures} extern/instantiation pairing violation(s)", 
file=sys.stderr)
+        return 1
+    families = {family(n) for _, _, _, n in externs}
+    print(
+        f"extern/instantiation pairing: {len(externs)} declaration(s), "
+        f"{len(families)} covered family(s), all paired"
+    )
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/build-support/check-header-deps.py 
b/build-support/check-header-deps.py
index 2e22721f390..5c5783991e6 100755
--- a/build-support/check-header-deps.py
+++ b/build-support/check-header-deps.py
@@ -24,12 +24,30 @@ every translation unit, so one stray include can multiply 
rebuild cost by an ord
 of magnitude and does so silently: the code still compiles, only the build 
slows
 down. This script turns that into a build error instead.
 
-Each rule names a hub header and a directory prefix it must not reach. Keeping 
a
-hub out of a subsystem is what lets that subsystem's headers be edited cheaply.
+Four kinds of guard, all on the same text include graph (be/src + be/test, all
+ifdef branches -- not comparable to ninja-measured radii, each baseline 
compares
+only with itself):
+
+  RULES                    a hub header must not reach a subsystem. Keeping a
+                           hub out of a subsystem is what lets that subsystem's
+                           headers be edited cheaply.
+  ANGLE_BANS               a header must not include a named third-party header
+                           whose template machinery was deliberately moved out
+                           of line.
+  FORWARD/REVERSE budgets  the two-axis safety net for edges no rule names:
+                           light hubs must stay light (closure budget, zero
+                           slack), heavy payloads must not spread (reach
+                           baseline +10%).
+  PCH whitelist            pch/pch.h's quoted project includes are pinned
+                           exactly; the PCH is the single most leveraged
+                           regression surface in the repo.
 
 Usage:
-    build-support/check-header-deps.py            # enforce the rules
-    build-support/check-header-deps.py --report   # rank headers by rebuild 
cost
+    build-support/check-header-deps.py                  # enforce everything
+    build-support/check-header-deps.py --report         # rank headers by 
rebuild cost
+    build-support/check-header-deps.py --budget         # budgets only 
(rebaselining)
+    build-support/check-header-deps.py --closure HEADER # list a hub's 
closure, with chains
+    build-support/check-header-deps.py --reach HEADER   # rank an includer's 
edge weights
 """
 
 import argparse
@@ -288,19 +306,213 @@ RULES = [
         "rec_cte_shared_state.cpp; the brpc client stack must not ride a "
         "SharedState header",
     ),
+    (
+        "exprs/function/function.h",
+        "storage/",
+        {
+            # Plain result struct (<cstdint> only); the sanctioned carrier left
+            # behind when the zonemap machinery edge was cut.
+            "storage/index/zone_map/zonemap_filter_result.h",
+            # Plain id struct, methods defined in rowset_id.cpp; rides in via
+            # core/column/column.h.
+            "storage/rowset_id.h",
+        },
+        "function.h is the base header of every scalar function; cutting its "
+        "three storage edges (expr_zonemap_filter, inverted_index_iterator, "
+        "inverted_index_parser) removed 122,885 preprocessed lines from the "
+        "exprs TUs, and one edge flowing back re-couples the whole expression "
+        "layer to the storage stack",
+    ),
+    (
+        "core/field.h",
+        "util/json/path_in_data.h",
+        set(),
+        "a single using-alias here used to drag path_in_data.h and with it "
+        "gen_cpp/segment_v2.pb.h (11.6k lines) into every TU that sees a "
+        "Field; the alias users include path_in_data.h themselves now",
+    ),
+    (
+        "core/data_type/primitive_type.h",
+        "util/json/path_in_data.h",
+        set(),
+        "primitive_type.h is included by ~44 project headers transitively; "
+        "the path_in_data edge would put gen_cpp/segment_v2.pb.h behind the "
+        "most basic type-enum header in the backend",
+    ),
+    (
+        "core/value/variant/variant_field.h",
+        "util/json/path_in_data.h",
+        set(),
+        "the field.h cut moved the variant alias down here, so this header "
+        "is where the path_in_data edge would most naturally regrow; variant "
+        "consumers that need PathInData include it directly",
+    ),
+    (
+        "core/types.h",
+        "storage/olap_common.h",
+        set(),
+        "the int128/uint128 typedefs moved to core/extended_types.h precisely "
+        "so core/types.h stops paying for the storage domain; the binary_cast "
+        "include chain used to put all of olap_common behind two lines of "
+        "typedef",
+    ),
+    (
+        "pch/pch.h",
+        "storage/olap_common.h",
+        set(),
+        "every header on the PCH is on the everything-rebuilds line: touch it "
+        "and the whole backend plus the precompiled header itself rebuild; "
+        "olap_common.h alone used to carry 22 project headers onto that line",
+    ),
+]
+
+# Third-party bans, (header, banned include, why). The include graph above only
+# follows quoted project includes and <gen_cpp/...>, so forbidden *third-party*
+# edges get their own single-file check: the named header must not include the
+# banned spelling (a trailing '/' bans the whole directory, otherwise the match
+# is exact; angle and quoted forms are both caught). These are headers whose
+# formatting/queueing bodies were deliberately moved out of line -- the ban 
keeps
+# the heavy third-party template machinery from re-entering every includer.
+ANGLE_BANS = [
+    (
+        "core/uint24.h",
+        "fmt/",
+        "to_string/to_buffer bodies live in uint24.cpp precisely so the "
+        "FMT_COMPILE formatter templates (53.5 CPU s over ~1150 TUs for the "
+        "date format alone) are instantiated once instead of in every 
includer",
+    ),
+    (
+        "core/value/large_int_value.h",
+        "fmt/",
+        "the fmt formatting bodies moved to large_int_value.cpp under the same 
"
+        "contract as core/uint24.h: the formatter templates must be "
+        "instantiated once, not in every includer",
+    ),
+    (
+        "exec/pipeline/dependency.h",
+        "concurrentqueue.h",
+        "was a dead 152 KB third-party include here; the moodycamel users "
+        "(local_exchanger.h, scanner_context.h, async_result_writer.h) "
+        "include it themselves",
+    ),
+    (
+        "util/pretty_printer.h",
+        "boost/",
+        "one boost::algorithm::join dragged ~60k preprocessed lines into every 
"
+        "TU that sees runtime_profile.h; the join is a plain loop in "
+        "pretty_printer.cpp now",
+    ),
+    # <ranges> is both a heavy header (object_pool.h alone reaches most of the
+    # backend) and a build breaker: libc++'s <ranges> rejects the
+    # -fno-access-control flag that doris_be_test builds with, so a <ranges>
+    # include in a src header can take the whole UT build down (#66615).
+    (
+        "common/object_pool.h",
+        "ranges",
+        "widely-included header; <ranges> is heavy for every includer and "
+        "breaks the -fno-access-control UT build on libc++",
+    ),
+    (
+        "exprs/lambda_function/lambda_execution_context.h",
+        "ranges",
+        "rides into every lambda-capable expression TU; <ranges> is heavy and "
+        "breaks the -fno-access-control UT build on libc++",
+    ),
+    (
+        "format/table/iceberg_reader_mixin.h",
+        "ranges",
+        "rides the table-reader stack; <ranges> is heavy and breaks the "
+        "-fno-access-control UT build on libc++",
+    ),
+    (
+        "storage/index/inverted/query_v2/collect/top_k_collector.h",
+        "ranges",
+        "rides the inverted-index query stack; <ranges> is heavy and breaks "
+        "the -fno-access-control UT build on libc++",
+    ),
+    (
+        "storage/index/inverted/query_v2/composite_reader.h",
+        "ranges",
+        "rides the inverted-index query stack; <ranges> is heavy and breaks "
+        "the -fno-access-control UT build on libc++",
+    ),
+    (
+        "storage/index/inverted/query_v2/wand/block_wand.h",
+        "ranges",
+        "rides the inverted-index query stack; <ranges> is heavy and breaks "
+        "the -fno-access-control UT build on libc++",
+    ),
 ]
 
-# Not expressible as RULES entries (the scanner only follows quoted project
-# includes and <gen_cpp/...>): core/uint24.h and core/value/large_int_value.h
-# must not regain <fmt/compile.h> / <fmt/format.h>. Their to_string/to_buffer
-# bodies live in the matching .cpp files precisely so the FMT_COMPILE formatter
-# templates (53.5 CPU s over ~1150 TUs for the uint24 date format alone) are
-# instantiated once instead of in every includer.
+# Budgets: the edge rules and bans above pin the edges someone has already
+# thought about; the two budget axes below catch the ones nobody thought about.
+# Both read the same text include graph (be/src + be/test, all ifdef branches);
+# neither is comparable to ninja-measured rebuild radii -- each compares only
+# against its own baseline.
 #
-# Likewise <concurrentqueue.h> must not return to exec/pipeline/dependency.h:
-# it was a dead 152 KB third-party include there; the moodycamel users
-# (local_exchanger.h, scanner_context.h, async_result_writer.h) include it
-# themselves.
+# Forward axis, "this header must stay light": the number of project headers
+# transitively reachable from the hub (the hub itself excluded). Slack is ZERO
+# by design -- growing a hub's closure must be a visible, deliberate act, so 
the
+# legal way over the budget is to bump the number here in the same PR and say
+# why in the commit message.
+# Baselines: master 9a48f8120c0, 2026-08-18.
+FORWARD_CLOSURE_BUDGETS = {
+    "common/logging.h": 0,  # a leaf on purpose: logging must not drag project 
headers
+    "util/uid_util.h": 1,
+    "common/status.h": 6,
+    "runtime/exec_env.h": 10,
+    "core/pod_array.h": 18,
+    "util/pretty_printer.h": 31,
+    "storage/olap_common.h": 32,
+    "core/types.h": 35,
+    "runtime/runtime_state.h": 43,
+    "core/data_type/primitive_type.h": 44,
+    "runtime/thread_context.h": 55,
+    "core/field.h": 60,
+    "core/column/column.h": 66,
+    "exprs/function/function.h": 106,
+    "exec/pipeline/dependency.h": 357,
+    "pch/pch.h": 8,
+}
+
+# Reverse axis, "this heavy payload must not spread": how many TUs (src + test)
+# transitively include the header. New TUs legitimately reference these, so the
+# limit is baseline +10% (rounded up); rebaseline when the audit cadence
+# re-measures, or in the same PR with justification when a real growth burst is
+# intended.
+# Baselines: master 9a48f8120c0, 2026-08-18.
+REVERSE_REACH_BASELINES = {
+    "gen_cpp/segment_v2.pb.h": 1234,
+    "gen_cpp/PaloInternalService_types.h": 1133,
+    "util/threadpool.h": 988,
+    # parsed_page.h holds RleDecoder<bool> by value and constructs it in the
+    # header, so the whole storage read stack sees the RLE machinery; a live
+    # dependency, budgeted as-is (trimming it is a refactor, not a gate).
+    "util/rle_encoding.h": 823,
+    "gen_cpp/internal_service.pb.h": 741,
+    "gen_cpp/FrontendService_types.h": 576,
+    "storage/options.h": 464,
+    "gen_cpp/cloud.pb.h": 342,
+    "gen_cpp/BackendService_types.h": 222,
+    "gen_cpp/data.pb.h": 220,
+    "io/fs/s3_file_system.h": 109,  # the AWS SDK surface
+    "util/brpc_closure.h": 61,
+    "runtime/workload_group/workload_group.h": 42,  # thrift type universe 
carrier
+}
+REVERSE_SLACK = 0.10
+
+# The PCH is the single most leveraged file in the repo: every header on it is
+# on the everything-rebuilds line (touch one and the whole backend plus the
+# precompiled header itself rebuild). Its quoted project includes are therefore
+# pinned exactly; the transitive closure is capped by the pch/pch.h entry in
+# FORWARD_CLOSURE_BUDGETS. Generated <gen_cpp/...> includes are the PCH's whole
+# point and stay out of this lock.
+PCH_HEADER = "pch/pch.h"
+PCH_QUOTED_WHITELIST = {
+    "common/config.h",
+    "common/status.h",
+    "common/version_internal.h",
+}
 
 # Forward-declaration headers are the sanctioned way through a barrier: they 
carry
 # declarations only, so they cost nothing to include.
@@ -398,6 +610,213 @@ def enforce(includes):
     return failures
 
 
+def forward_closure(hub, includes):
+    """Project headers transitively reachable from `hub`, hub excluded."""
+    chains = reachable(hub, includes)
+    return sorted(
+        h for h in chains if h != hub and resolve(h, includes) is not None
+    )
+
+
+ANY_INCLUDE = re.compile(r'^\s*#\s*include\s+[<"]([^>"]+)[>"]')
+
+
+def enforce_angle_bans():
+    failures = 0
+    for header, banned, why in ANGLE_BANS:
+        path = os.path.join(INCLUDE_ROOT, header)
+        if not os.path.exists(path):
+            print(f"error: angle ban names a missing header: {header}", 
file=sys.stderr)
+            failures += 1
+            continue
+        with open(path, encoding="utf-8", errors="ignore") as handle:
+            spellings = [m.group(1) for m in map(ANY_INCLUDE.match, handle) if 
m]
+        hits = [
+            s
+            for s in spellings
+            if s == banned or (banned.endswith("/") and s.startswith(banned))
+        ]
+        for hit in hits:
+            failures += 1
+            print(f"error: {header} must not include <{hit}>", file=sys.stderr)
+            print(f"  reason: {why}", file=sys.stderr)
+            print(
+                "  fix:    move the code that needs it into the matching .cpp "
+                "(that is where the previous cut put it), or take this ban out 
"
+                "of ANGLE_BANS in the same PR and justify it in the commit "
+                "message",
+                file=sys.stderr,
+            )
+    return failures
+
+
+def enforce_budgets(includes):
+    failures = 0
+    for hub, budget in FORWARD_CLOSURE_BUDGETS.items():
+        if resolve(hub, includes) is None:
+            print(
+                f"error: forward budget names a missing header: {hub} "
+                "(renamed or moved? update FORWARD_CLOSURE_BUDGETS)",
+                file=sys.stderr,
+            )
+            failures += 1
+            continue
+        actual = len(forward_closure(hub, includes))
+        if actual > budget:
+            failures += 1
+            print(
+                f"error: {hub} include closure grew to {actual} project "
+                f"headers (budget {budget})",
+                file=sys.stderr,
+            )
+            print(
+                "  reason: everything this hub includes is reparsed by every "
+                "TU that includes the hub, so closure growth is a rebuild-cost 
"
+                "multiplier nobody sees in a diff; the budget makes it 
visible",
+                file=sys.stderr,
+            )
+            print(
+                f"  list:   build-support/check-header-deps.py --closure 
{hub}",
+                file=sys.stderr,
+            )
+            print(
+                "  fix:    cut the new edge (forward-declare the type, or "
+                "route it through a *_fwd.h), or bump the budget in "
+                "FORWARD_CLOSURE_BUDGETS in the same PR and justify it in the "
+                "commit message",
+                file=sys.stderr,
+            )
+    counts = translation_units_affected(includes)
+    for header, baseline in REVERSE_REACH_BASELINES.items():
+        limit = -(-baseline * (100 + int(REVERSE_SLACK * 100)) // 100)
+        actual = counts.get(header, 0)
+        is_project = resolve(header, includes) is not None
+        if not is_project and not header.startswith("gen_cpp/"):
+            print(
+                f"error: reverse budget names a missing header: {header} "
+                "(renamed or moved? update REVERSE_REACH_BASELINES)",
+                file=sys.stderr,
+            )
+            failures += 1
+            continue
+        if actual == 0:
+            # A watched header nobody includes any more is either renamed
+            # (the budget is watching nothing) or genuinely dead -- both mean
+            # the table must be updated, loudly.
+            print(
+                f"error: reverse budget sentinel is no longer referenced: "
+                f"{header} (renamed, or truly unused? update "
+                "REVERSE_REACH_BASELINES)",
+                file=sys.stderr,
+            )
+            failures += 1
+            continue
+        if actual > limit:
+            failures += 1
+            print(
+                f"error: {header} now reaches {actual} TUs "
+                f"(baseline {baseline} +{int(REVERSE_SLACK * 100)}% = 
{limit})",
+                file=sys.stderr,
+            )
+            print(
+                "  reason: this is a heavy payload (generated code / SDK "
+                "surface / template machinery); some header on a popular path "
+                "gained an include of it, taxing every TU downstream",
+                file=sys.stderr,
+            )
+            print(
+                f"  list:   build-support/check-header-deps.py --reach 
{header}",
+                file=sys.stderr,
+            )
+            print(
+                "  fix:    cut the spreading edge (forward-declare, or move "
+                "the include into the .cpp), or rebaseline in "
+                "REVERSE_REACH_BASELINES in the same PR and justify it in the "
+                "commit message",
+                file=sys.stderr,
+            )
+    return failures
+
+
+def enforce_pch(includes):
+    path = resolve(PCH_HEADER, includes)
+    if path is None:
+        print(f"error: {PCH_HEADER} not found", file=sys.stderr)
+        return 1
+    with open(path, encoding="utf-8", errors="ignore") as handle:
+        quoted = {m.group(1) for m in map(INCLUDE.match, handle) if m}
+    extra = sorted(quoted - PCH_QUOTED_WHITELIST)
+    missing = sorted(PCH_QUOTED_WHITELIST - quoted)
+    if not extra and not missing:
+        return 0
+    print("error: pch/pch.h quoted includes diverged from the whitelist", 
file=sys.stderr)
+    for header in extra:
+        print(f"  added:   \"{header}\"", file=sys.stderr)
+    for header in missing:
+        print(f"  removed: \"{header}\"", file=sys.stderr)
+    print(
+        "  reason: every header on the PCH is on the everything-rebuilds "
+        "line -- touch it and the whole backend plus the precompiled header "
+        "itself rebuild; adding one is the single most leveraged regression "
+        "in the repo",
+        file=sys.stderr,
+    )
+    print(
+        "  fix:    include the header in the TUs that need it instead, or "
+        "change PCH_QUOTED_WHITELIST in the same PR and justify it in the "
+        "commit message",
+        file=sys.stderr,
+    )
+    return 1
+
+
+def closure_listing(hub, includes):
+    if resolve(hub, includes) is None:
+        print(f"error: no such project header: {hub}", file=sys.stderr)
+        return 1
+    chains = reachable(hub, includes)
+    members = forward_closure(hub, includes)
+    print(f"{hub}: {len(members)} project header(s) in the include closure")
+    for member in members:
+        print("  " + " -> ".join(chains[member]))
+    return 0
+
+
+def reach_listing(header, includes):
+    """Direct includers of `header` ranked by how many TUs each edge 
carries."""
+    users = collections.defaultdict(set)
+    for path, headers in includes.items():
+        for h in headers:
+            users[h].add(path)
+    if not users.get(header):
+        print(f"error: nothing includes {header}", file=sys.stderr)
+        return 1
+
+    def tus(banned_edge=None):
+        seen, frontier = set(), [header]
+        while frontier:
+            current = frontier.pop()
+            for user in users.get(current, ()):
+                if banned_edge and (user, current) == banned_edge:
+                    continue
+                if user in seen:
+                    continue
+                seen.add(user)
+                if user.startswith(INCLUDE_ROOT + "/"):
+                    frontier.append(user[len(INCLUDE_ROOT) + 1:])
+        return sum(1 for f in seen if f.endswith((".cpp", ".cc")))
+
+    total = tus()
+    print(f"{header}: reaches {total} TU(s); direct includers by edge weight")
+    ranked = sorted(
+        ((total - tus((user, header)), user) for user in users[header]),
+        reverse=True,
+    )
+    for marginal, user in ranked:
+        print(f"  {marginal:>5} via this edge alone  {user}")
+    return 0
+
+
 def report(includes):
     counts = translation_units_affected(includes)
     ranked = sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:30]
@@ -413,6 +832,21 @@ def main():
         action="store_true",
         help="rank headers by how many translation units they force a rebuild 
of",
     )
+    parser.add_argument(
+        "--budget",
+        action="store_true",
+        help="run only the closure/reach budget checks (used when 
rebaselining)",
+    )
+    parser.add_argument(
+        "--closure",
+        metavar="HEADER",
+        help="list the project headers in HEADER's include closure, with 
chains",
+    )
+    parser.add_argument(
+        "--reach",
+        metavar="HEADER",
+        help="list HEADER's direct includers ranked by how many TUs each edge 
carries",
+    )
     args = parser.parse_args()
 
     root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -422,12 +856,35 @@ def main():
     if args.report:
         report(includes)
         return 0
+    if args.closure:
+        return closure_listing(args.closure, includes)
+    if args.reach:
+        return reach_listing(args.reach, includes)
 
-    failures = enforce(includes)
+    if args.budget:
+        failures = enforce_budgets(includes)
+    else:
+        failures = (
+            enforce(includes)
+            + enforce_angle_bans()
+            + enforce_budgets(includes)
+            + enforce_pch(includes)
+        )
     if failures:
-        print(f"\n{failures} header layering violation(s)", file=sys.stderr)
+        print(f"\n{failures} header hygiene violation(s)", file=sys.stderr)
         return 1
-    print(f"header layering: {len(RULES)} rule(s) satisfied")
+    if args.budget:
+        print(
+            f"header budgets: {len(FORWARD_CLOSURE_BUDGETS)} forward + "
+            f"{len(REVERSE_REACH_BASELINES)} reverse within budget"
+        )
+    else:
+        print(
+            f"header hygiene: {len(RULES)} layering rule(s), "
+            f"{len(ANGLE_BANS)} third-party ban(s), "
+            f"{len(FORWARD_CLOSURE_BUDGETS)}+{len(REVERSE_REACH_BASELINES)} "
+            "budget(s), pch whitelist: all satisfied"
+        )
     return 0
 
 
diff --git a/build-support/check-unity-skip-coverage.py 
b/build-support/check-unity-skip-coverage.py
new file mode 100755
index 00000000000..c72c18bd63e
--- /dev/null
+++ b/build-support/check-unity-skip-coverage.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Unity-skip coverage for test-included source files.
+
+A few tests `#include` a be/src .cpp directly (to reach file-static helpers or
+to instantiate a template with test types). That only links because the source
+file is opted out of unity batching: if it stays in a batch, the batch object 
is
+pulled into the doris_be_test link for a *sibling's* symbol and its copy of the
+file's definitions collides with the test's inlined copy -- duplicate strong
+symbols, reported at the end of the BE UT build, an hour after the mistake.
+This script reports the mistake at configure time instead, with the exact skip
+entry to add.
+
+It is the mirror of the fail-loud check inside doris_skip_unity_inclusion()
+(be/CMakeLists.txt): that one catches skip entries pointing at files that no
+longer exist; this one catches files that need an entry and do not have one.
+
+A source file counts as covered when its owning CMakeLists (nearest ancestor
+with a CMakeLists.txt, else any CMakeLists naming it) does one of:
+
+  literal entry     the file's path appears in a list fed to
+                    doris_skip_unity_inclusion(), via set()/list(APPEND)/direct
+                    arguments;
+  whole-target      the skip list is initialized from the target's own source
+  opt-out           list (`set(<VAR> ${SRC_FILES})`), minus any
+                    `list(FILTER <VAR> EXCLUDE REGEX ...)` the file matches;
+  no unity          the owning CMakeLists never sets UNITY_BUILD, so there are
+                    no batches to collide with (archive link rules protect
+                    plain object files).
+
+Any other CMake idiom is unknown to this script on purpose: it fails loudly so
+the idiom is either replaced with a literal entry or taught here, instead of
+being silently guessed wrong.
+
+Usage:
+    build-support/check-unity-skip-coverage.py
+"""
+
+import os
+import re
+import sys
+
+TEST_ROOT = "be/test"
+SRC_ROOT = "be/src"
+
+TEST_INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+\.(?:cpp|cc))"')
+SKIP_CALL = re.compile(r"doris_skip_unity_inclusion\s*\(([^)]*)\)", re.S)
+SET_BLOCK = re.compile(r"\bset\s*\(\s*(\w+)([^)]*)\)", re.S)
+APPEND_BLOCK = re.compile(r"\blist\s*\(\s*APPEND\s+(\w+)([^)]*)\)", re.S)
+FILTER_BLOCK = re.compile(
+    r'\blist\s*\(\s*FILTER\s+(\w+)\s+EXCLUDE\s+REGEX\s+"([^"]*)"\s*\)'
+)
+VAR_REF = re.compile(r"\$\{(\w+)\}")
+
+
+def strip_comments(text):
+    return "\n".join(line.split("#", 1)[0] for line in text.splitlines())
+
+
+def cpp_tokens(blob):
+    """Path-shaped tokens in a CMake argument blob, ${...} prefixes 
stripped."""
+    tokens = []
+    for token in blob.split():
+        if not token.endswith((".cpp", ".cc")):
+            continue
+        tokens.append(re.sub(r"\$\{\w+\}", "", token).lstrip("/"))
+    return tokens
+
+
+class CMakeFile:
+    def __init__(self, path):
+        self.path = path
+        self.directory = os.path.dirname(path)
+        with open(path, encoding="utf-8", errors="ignore") as handle:
+            text = strip_comments(handle.read())
+        self.sets_unity = "UNITY_BUILD" in text
+
+        sets = {}
+        for name, blob in SET_BLOCK.findall(text):
+            sets.setdefault(name, []).append(blob)
+        for name, blob in APPEND_BLOCK.findall(text):
+            sets.setdefault(name, []).append(blob)
+        filters = {}
+        for name, regex in FILTER_BLOCK.findall(text):
+            filters.setdefault(name, []).append(regex)
+
+        # Literal skip entries and whether the skip list is seeded from the
+        # target's whole source list.
+        self.entries = []
+        self.whole_target = False
+        self.whole_target_excludes = []
+        for blob in SKIP_CALL.findall(text):
+            self.entries.extend(cpp_tokens(blob))
+            for var in VAR_REF.findall(blob):
+                for var_blob in sets.get(var, ()):
+                    self.entries.extend(cpp_tokens(var_blob))
+                    if re.search(r"\$\{\w*SRC_FILES\}", var_blob):
+                        self.whole_target = True
+                        self.whole_target_excludes.extend(filters.get(var, ()))
+
+    def resolved_entries(self):
+        """Skip entries as repo-relative paths (handles ../ hops)."""
+        for entry in self.entries:
+            yield os.path.normpath(os.path.join(self.directory, entry))
+
+    def covers(self, repo_path):
+        """Does this CMakeLists skip-list `repo_path` (repo-relative .cpp)?"""
+        for resolved in self.resolved_entries():
+            if resolved == repo_path or resolved.endswith("/" + repo_path):
+                return True
+        if self.whole_target and repo_path.startswith(self.directory + "/"):
+            # cmake applies FILTER regexes to absolute paths; approximate with
+            # the repo-relative one, which shares every path component that the
+            # tree's regexes (".*/format_v2/.*") anchor on.
+            return not any(
+                re.search(regex, repo_path)
+                for regex in self.whole_target_excludes
+            )
+        return False
+
+
+def owning_cmakelists(repo_path, cmake_files):
+    directory = os.path.dirname(repo_path)
+    while directory.startswith(SRC_ROOT):
+        candidate = os.path.join(directory, "CMakeLists.txt")
+        if candidate in cmake_files:
+            return cmake_files[candidate]
+        directory = os.path.dirname(directory)
+    return None
+
+
+def test_included_sources():
+    """(test file, line, be/src-relative include) for every src .cpp a test
+    includes."""
+    found = []
+    for directory, _, names in os.walk(TEST_ROOT):
+        for name in names:
+            if not name.endswith((".cpp", ".cc", ".h", ".hpp")):
+                continue
+            path = os.path.join(directory, name)
+            with open(path, encoding="utf-8", errors="ignore") as handle:
+                for lineno, line in enumerate(handle, 1):
+                    match = TEST_INCLUDE.match(line)
+                    if not match:
+                        continue
+                    include = match.group(1)
+                    if os.path.exists(os.path.join(SRC_ROOT, include)):
+                        found.append((path, lineno, include))
+    return found
+
+
+def main():
+    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    os.chdir(root)
+
+    cmake_files = {}
+    for directory, _, names in os.walk(SRC_ROOT):
+        if "CMakeLists.txt" in names:
+            path = os.path.join(directory, "CMakeLists.txt")
+            cmake_files[path] = CMakeFile(path)
+
+    failures = 0
+    checked = 0
+    for test_path, lineno, include in test_included_sources():
+        checked += 1
+        repo_path = os.path.join(SRC_ROOT, include)
+        owner = owning_cmakelists(repo_path, cmake_files)
+        if owner is not None and not owner.sets_unity:
+            continue  # no unity batches in this target, nothing to collide 
with
+        if owner is not None and owner.covers(repo_path):
+            continue
+        if any(f.covers(repo_path) for f in cmake_files.values()):
+            continue  # cross-directory entry (e.g. appended into a sibling's 
list)
+        failures += 1
+        print(
+            f"error: {test_path}:{lineno} includes {repo_path}, which is not "
+            "opted out of unity batching",
+            file=sys.stderr,
+        )
+        print(
+            "  reason: the unity batch object containing this file gets pulled 
"
+            "into the doris_be_test link for a sibling's symbol, and its "
+            "definitions collide with the test's inlined copy -- duplicate "
+            "strong symbols at the end of the BE UT build",
+            file=sys.stderr,
+        )
+        if owner is not None:
+            entry = os.path.relpath(repo_path, owner.directory)
+            print(
+                f"  fix:    add ${{CMAKE_CURRENT_SOURCE_DIR}}/{entry} to the "
+                f"doris_skip_unity_inclusion list in {owner.path}",
+                file=sys.stderr,
+            )
+        else:
+            print(
+                "  fix:    add the file to the doris_skip_unity_inclusion "
+                "list of the CMakeLists whose source glob compiles it; for "
+                "the cross-directory entry form, see the adbc_reader.cpp "
+                "entry in be/src/format/CMakeLists.txt",
+                file=sys.stderr,
+            )
+    if failures:
+        print(f"\n{failures} unity-skip coverage violation(s)", 
file=sys.stderr)
+        return 1
+    print(f"unity-skip coverage: {checked} test-included source file(s) all 
covered")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/build-support/tests/test-build-hygiene-entry.sh 
b/build-support/tests/test-build-hygiene-entry.sh
new file mode 100755
index 00000000000..6bfc157a635
--- /dev/null
+++ b/build-support/tests/test-build-hygiene-entry.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Self-test for the check-build-hygiene.sh unified entry:
+#   - clean tree exits 0 with the summary line
+#   - with violations in TWO different checks, BOTH are reported (the entry
+#     keeps running after a failing check) and the exit code is 1
+#   - a missing python3 is a loud error (exit 2) naming the OFF switch, never
+#     a silent skip
+#   - env.sh's PYTHON (python2 on the build-env image) is not consulted
+#
+# Injects into working-tree files, restores via EXIT trap. Do not run
+# concurrently with a build/configure of the same tree.
+#
+# Usage:  bash build-support/tests/test-build-hygiene-entry.sh
+
+set -u
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+ENTRY="${ROOT}/build-support/check-build-hygiene.sh"
+
+UINT24="be/src/core/uint24.h"
+COLVEC="be/src/core/column/column_vector.h"
+BACKUP="$(mktemp -d)"
+cp "${ROOT}/${UINT24}" "${BACKUP}/uint24.h"
+cp "${ROOT}/${COLVEC}" "${BACKUP}/column_vector.h"
+restore() {
+    cp "${BACKUP}/uint24.h" "${ROOT}/${UINT24}"
+    cp "${BACKUP}/column_vector.h" "${ROOT}/${COLVEC}"
+    rm -rf "${BACKUP}"
+}
+trap restore EXIT
+
+FAILED=0
+fail() { echo "FAIL: $1"; FAILED=1; }
+
+# ---- clean tree: exit 0, summary line present ----
+OUT="$(bash "${ENTRY}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "clean tree expected exit 0, got ${EC}:"$'\n'"${OUT}"
+echo "${OUT}" | grep -q "build hygiene: all checks passed" \
+    || fail "clean run lacks the all-passed summary"
+
+# ---- two violations in two different checks: both reported, exit 1 ----
+printf '#include <fmt/format.h>\n' >> "${ROOT}/${UINT24}"
+printf 'extern template class ColumnVector<TYPE_FAKE_ENTRY_TEST>;\n' >> 
"${ROOT}/${COLVEC}"
+OUT="$(bash "${ENTRY}" 2>&1)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "violations expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "core/uint24.h must not include <fmt/format.h>" \
+    || fail "header-deps violation missing from aggregated output"
+echo "${OUT}" | grep -q "TYPE_FAKE_ENTRY_TEST" \
+    || fail "pairing violation missing from aggregated output (entry must run 
every check)"
+echo "${OUT}" | grep -q "2 check(s) failed" \
+    || fail "aggregated failure count missing or wrong"
+restore
+trap - EXIT
+BACKUP="$(mktemp -d)"  # restore() removed it; re-arm for the paths below
+cp "${ROOT}/${UINT24}" "${BACKUP}/uint24.h"
+cp "${ROOT}/${COLVEC}" "${BACKUP}/column_vector.h"
+trap restore EXIT
+
+# ---- unusable BUILD_HYGIENE_PYTHON: loud exit 2 with the OFF-switch hint ----
+OUT="$(BUILD_HYGIENE_PYTHON=/nonexistent/python3 bash "${ENTRY}" 2>&1)"; EC=$?
+[ "${EC}" -eq 2 ] || fail "missing python expected exit 2, got ${EC}"
+echo "${OUT}" | grep -q "not found" || fail "missing-python error not reported"
+echo "${OUT}" | grep -q "ENABLE_BUILD_HYGIENE=OFF" \
+    || fail "missing-python error lacks the OFF-switch hint"
+
+# ---- env.sh's PYTHON is not consulted ----
+# env.sh exports PYTHON=${DORIS_BUILD_PYTHON_VERSION:-python}, which is python2
+# on the build-env image; honouring it made every gate die with a SyntaxError 
on
+# its first f-string. The gates must resolve their own Python 3 regardless.
+OUT="$(PYTHON=/nonexistent/python2 bash "${ENTRY}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] \
+    || fail "the gates must resolve their own python3, not env.sh's PYTHON 
(exit ${EC}):"$'\n'"${OUT}"
+
+# ---- green again ----
+OUT="$(bash "${ENTRY}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "restored tree expected exit 0, got 
${EC}:"$'\n'"${OUT}"
+
+if [ "${FAILED}" -eq 0 ]; then
+    echo "PASS: entry aggregates all checks, resolves its own python3, fails 
loud without one, and is green when clean."
+    exit 0
+fi
+exit 1
diff --git a/build-support/tests/test-build-hygiene-extern-pairing.sh 
b/build-support/tests/test-build-hygiene-extern-pairing.sh
new file mode 100755
index 00000000000..c9ff755e8e7
--- /dev/null
+++ b/build-support/tests/test-build-hygiene-extern-pairing.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Self-test for check-extern-template-pairing.py: both pairing directions must
+# fire on injection and the clean tree must stay green (which also proves the
+# normalizer -- namespace dropping, alias table, comment stripping -- because
+# the real tree pairs ColumnStr<UInt32> with ColumnStr<uint32_t> and the wide
+# to_string declarations with their integer<...> definitions).
+#
+# Injects into working-tree files, restores via EXIT trap. Do not run
+# concurrently with a build/configure of the same tree.
+#
+# Usage:  bash build-support/tests/test-build-hygiene-extern-pairing.sh
+
+set -u
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+GATE="${ROOT}/build-support/check-extern-template-pairing.py"
+# Not ${PYTHON}: env.sh exports that as the build's interpreter (python2 on
+# the build-env image). Same selector the gates themselves use.
+PYTHON="${BUILD_HYGIENE_PYTHON:-python3}"
+
+HEADER="be/src/core/column/column_vector.h"
+BACKUP="$(mktemp -d)"
+cp "${ROOT}/${HEADER}" "${BACKUP}/column_vector.h"
+restore() { cp "${BACKUP}/column_vector.h" "${ROOT}/${HEADER}"; rm -rf 
"${BACKUP}"; }
+trap restore EXIT
+
+FAILED=0
+fail() { echo "FAIL: $1"; FAILED=1; }
+
+# ---- baseline: clean tree green (normalizer handles the real alias pairs) 
----
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "clean tree expected green, got ${EC}:"$'\n'"${OUT}"
+echo "${OUT}" | grep -q "all paired" || fail "clean run lacks the summary line"
+
+# ---- RED (the silent case): drop one extern, its definition loses its pair 
----
+# Commenting the declaration out is exactly the "header forgot the extern"
+# regression the reverse direction exists for.
+sed -i.bak \
+    's|^extern template class ColumnVector<TYPE_BOOLEAN>;|// injected-out: 
extern template class ColumnVector<TYPE_BOOLEAN>;|' \
+    "${ROOT}/${HEADER}" && rm -f "${ROOT}/${HEADER}.bak"
+grep -q "injected-out" "${ROOT}/${HEADER}" || fail "reverse injection did not 
apply"
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "reverse injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "explicit instantiation lacks the matching 'extern 
template'" \
+    || fail "reverse violation not reported"
+echo "${OUT}" | grep -q "ColumnVector<TYPE_BOOLEAN>" \
+    || fail "reverse violation does not name the specialization"
+echo "${OUT}" | grep -q "extern-covered in be/src/core/column/column_vector.h" 
\
+    || fail "reverse violation does not point at the family's header"
+echo "${OUT}" | grep -q "fix:" || fail "reverse violation lacks a fix path"
+cp "${BACKUP}/column_vector.h" "${ROOT}/${HEADER}"
+
+# ---- RED: an extern declaration with no definition anywhere ----
+printf 'extern template class ColumnVector<TYPE_DATETIMEV2_FAKE_INJECTED>;\n' \
+    >> "${ROOT}/${HEADER}"
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "forward injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "pairs with no explicit instantiation definition" \
+    || fail "forward violation not reported"
+echo "${OUT}" | grep -q "TYPE_DATETIMEV2_FAKE_INJECTED" \
+    || fail "forward violation does not name the declaration"
+cp "${BACKUP}/column_vector.h" "${ROOT}/${HEADER}"
+
+# ---- GREEN again ----
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "restored tree expected green, got 
${EC}:"$'\n'"${OUT}"
+
+if [ "${FAILED}" -eq 0 ]; then
+    echo "PASS: both pairing directions fire on injection and restore to 
green."
+    exit 0
+fi
+exit 1
diff --git a/build-support/tests/test-build-hygiene-header-deps.sh 
b/build-support/tests/test-build-hygiene-header-deps.sh
new file mode 100755
index 00000000000..7b9c3cfeed7
--- /dev/null
+++ b/build-support/tests/test-build-hygiene-header-deps.sh
@@ -0,0 +1,136 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Self-test for the check-header-deps.py gate families: layering rules,
+# third-party bans, forward/reverse budgets, pch whitelist.
+#
+# The gate runs against the real tree (its rule and budget tables name real
+# headers), so RED cases are proven by injecting one violation per family into
+# a working-tree file and GREEN by restoring it. Injected files are backed up
+# first and restored by an EXIT trap. Do not run concurrently with a
+# build/configure of the same tree.
+#
+# Usage:  bash build-support/tests/test-build-hygiene-header-deps.sh
+
+set -u
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+GATE="${ROOT}/build-support/check-header-deps.py"
+# Not ${PYTHON}: env.sh exports that as the build's interpreter (python2 on
+# the build-env image). Same selector the gates themselves use.
+PYTHON="${BUILD_HYGIENE_PYTHON:-python3}"
+
+BACKUP="$(mktemp -d)"
+TARGETS=(
+    "be/src/core/types.h"
+    "be/src/core/uint24.h"
+    "be/src/common/logging.h"
+    "be/src/util/pretty_printer.h"
+    "be/src/pch/pch.h"
+)
+for t in "${TARGETS[@]}"; do
+    mkdir -p "${BACKUP}/$(dirname "${t}")"
+    cp "${ROOT}/${t}" "${BACKUP}/${t}"
+done
+restore() {
+    for t in "${TARGETS[@]}"; do
+        cp "${BACKUP}/${t}" "${ROOT}/${t}"
+    done
+    rm -rf "${BACKUP}"
+}
+trap restore EXIT
+
+FAILED=0
+fail() { echo "FAIL: $1"; FAILED=1; }
+
+run_gate() { "${PYTHON}" "${GATE}" 2>&1; }
+
+# ---- baseline: the clean tree is green ----
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 0 ] || { fail "clean tree expected green, got 
${EC}:"$'\n'"${OUT}"; }
+
+# ---- RED: layering rule (types.h must not reach olap_common.h) ----
+printf '#include "storage/olap_common.h"\n' >> "${ROOT}/be/src/core/types.h"
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "rule injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "core/types.h must not reach storage/olap_common.h" \
+    || fail "rule violation not reported for types.h -> olap_common.h"
+echo "${OUT}" | grep -q "chain:" || fail "rule failure lacks the include chain"
+echo "${OUT}" | grep -q "fix:" || fail "rule failure lacks a fix path"
+cp "${BACKUP}/be/src/core/types.h" "${ROOT}/be/src/core/types.h"
+
+# ---- RED: third-party ban (uint24.h must not include fmt) ----
+printf '#include <fmt/format.h>\n' >> "${ROOT}/be/src/core/uint24.h"
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "angle-ban injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "core/uint24.h must not include <fmt/format.h>" \
+    || fail "angle-ban violation not reported"
+cp "${BACKUP}/be/src/core/uint24.h" "${ROOT}/be/src/core/uint24.h"
+
+# ---- RED: forward closure budget (logging.h is budgeted at 0) ----
+printf '#include "util/uid_util.h"\n' >> "${ROOT}/be/src/common/logging.h"
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "forward-budget injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "common/logging.h include closure grew" \
+    || fail "forward budget breach not reported"
+echo "${OUT}" | grep -q -- "--closure common/logging.h" \
+    || fail "forward budget failure lacks the --closure fix pointer"
+cp "${BACKUP}/be/src/common/logging.h" "${ROOT}/be/src/common/logging.h"
+
+# ---- RED: reverse reach budget (workload_group.h must not spread) ----
+printf '#include "runtime/workload_group/workload_group.h"\n' \
+    >> "${ROOT}/be/src/util/pretty_printer.h"
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "reverse-budget injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "runtime/workload_group/workload_group.h now reaches" \
+    || fail "reverse budget breach not reported"
+echo "${OUT}" | grep -q -- "--reach runtime/workload_group/workload_group.h" \
+    || fail "reverse budget failure lacks the --reach fix pointer"
+cp "${BACKUP}/be/src/util/pretty_printer.h" 
"${ROOT}/be/src/util/pretty_printer.h"
+
+# ---- RED: pch whitelist (quoted include added to pch.h) ----
+printf '#include "common/logging.h"\n' >> "${ROOT}/be/src/pch/pch.h"
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "pch injection expected exit 1, got ${EC}"
+echo "${OUT}" | grep -q "pch/pch.h quoted includes diverged from the 
whitelist" \
+    || fail "pch whitelist divergence not reported"
+echo "${OUT}" | grep -q 'added:   "common/logging.h"' \
+    || fail "pch failure does not name the added include"
+cp "${BACKUP}/be/src/pch/pch.h" "${ROOT}/be/src/pch/pch.h"
+
+# ---- diagnostics: --closure and --reach stay usable ----
+OUT="$("${PYTHON}" "${GATE}" --closure common/status.h 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "--closure expected exit 0, got ${EC}"
+echo "${OUT}" | grep -q "common/status.h: .* project header(s)" \
+    || fail "--closure output missing summary line"
+OUT="$("${PYTHON}" "${GATE}" --reach io/fs/s3_file_system.h 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "--reach expected exit 0, got ${EC}"
+echo "${OUT}" | grep -q "via this edge alone" \
+    || fail "--reach output missing edge ranking"
+
+# ---- GREEN again after all restores ----
+OUT="$(run_gate)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "restored tree expected green, got 
${EC}:"$'\n'"${OUT}"
+
+if [ "${FAILED}" -eq 0 ]; then
+    echo "PASS: rules/bans/budgets/pch-lock all fire on injection, restore to 
green, diagnostics work."
+    exit 0
+fi
+exit 1
diff --git a/build-support/tests/test-build-hygiene-unity-skip.sh 
b/build-support/tests/test-build-hygiene-unity-skip.sh
new file mode 100755
index 00000000000..d49fdf92c1a
--- /dev/null
+++ b/build-support/tests/test-build-hygiene-unity-skip.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Self-test for check-unity-skip-coverage.py. Seeds throwaway files (a src
+# .cpp that is in no skip list, plus probe tests under be/test) to prove:
+#   RED    a test including an unlisted, unity-batched src .cpp is flagged,
+#          with the exact skip entry to add
+#   SILENT the literal-entry idiom (cross-directory APPEND included)
+#   SILENT the whole-target opt-out idiom (set(<VAR> ${SRC_FILES}))
+#   SILENT a commented-out include
+# and that the clean tree is green before and after.
+#
+# Creates files only (never edits tracked ones); removes them via EXIT trap.
+#
+# Usage:  bash build-support/tests/test-build-hygiene-unity-skip.sh
+
+set -u
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+GATE="${ROOT}/build-support/check-unity-skip-coverage.py"
+# Not ${PYTHON}: env.sh exports that as the build's interpreter (python2 on
+# the build-env image). Same selector the gates themselves use.
+PYTHON="${BUILD_HYGIENE_PYTHON:-python3}"
+
+PROBE_SRC="${ROOT}/be/src/util/tmp_hygiene_probe.cpp"
+PROBE_DIR="${ROOT}/be/test/tmp_hygiene_probe"
+cleanup() { rm -f "${PROBE_SRC}"; rm -rf "${PROBE_DIR}"; }
+trap cleanup EXIT
+
+FAILED=0
+fail() { echo "FAIL: $1"; FAILED=1; }
+
+# ---- baseline: clean tree green ----
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "clean tree expected green, got ${EC}:"$'\n'"${OUT}"
+
+# ---- fixtures ----
+echo "namespace doris { int tmp_hygiene_probe_symbol = 0; }" > "${PROBE_SRC}"
+mkdir -p "${PROBE_DIR}"
+# RED: util sets UNITY_BUILD and lists skips literally; the probe src is new,
+# so it is in no list.
+printf '#include "util/tmp_hygiene_probe.cpp"\n' > 
"${PROBE_DIR}/red_probe_test.cpp"
+# SILENT: cross-directory literal entry (appended into format's skip list).
+printf '#include "format_v2/table/adbc_reader.cpp"\n' > 
"${PROBE_DIR}/literal_probe_test.cpp"
+# SILENT: whole-target opt-out (format skip list is seeded from SRC_FILES).
+printf '#include "format/orc/orc_file_reader.cpp"\n' > 
"${PROBE_DIR}/wholetarget_probe_test.cpp"
+# SILENT: commented include is not an include.
+printf '//#include "format_v2/parquet/reader/native/decoder.cpp"\n' > 
"${PROBE_DIR}/comment_probe_test.cpp"
+
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 1 ] || fail "expected exit 1 with the red probe present, got 
${EC}"
+N="$(echo "${OUT}" | grep -c '^error:')"
+[ "${N}" -eq 1 ] || fail "expected exactly 1 violation (silent probes must 
stay silent), got ${N}:"$'\n'"${OUT}"
+echo "${OUT}" | grep -q "red_probe_test.cpp:1 includes 
be/src/util/tmp_hygiene_probe.cpp" \
+    || fail "red probe not reported"
+echo "${OUT}" | grep -q 'add ${CMAKE_CURRENT_SOURCE_DIR}/tmp_hygiene_probe.cpp 
to the doris_skip_unity_inclusion list in be/src/util/CMakeLists.txt' \
+    || fail "fix path does not name the exact entry and CMakeLists"
+echo "${OUT}" | grep -q "duplicate strong symbols" \
+    || fail "failure lacks the mechanism explanation"
+
+# ---- GREEN again once the red probe is gone ----
+rm -f "${PROBE_DIR}/red_probe_test.cpp"
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "expected green with only silent probes, got 
${EC}:"$'\n'"${OUT}"
+
+cleanup
+trap - EXIT
+OUT="$("${PYTHON}" "${GATE}" 2>&1)"; EC=$?
+[ "${EC}" -eq 0 ] || fail "restored tree expected green, got 
${EC}:"$'\n'"${OUT}"
+
+if [ "${FAILED}" -eq 0 ]; then
+    echo "PASS: unlisted include flagged with exact fix; 
literal/whole-target/comment cases stay silent."
+    exit 0
+fi
+exit 1
diff --git a/build-support/tests/test-fe-core-metadata-funnel.sh 
b/build-support/tests/test-fe-core-metadata-funnel.sh
index 4736f9ab003..c3a1a3b449f 100755
--- a/build-support/tests/test-fe-core-metadata-funnel.sh
+++ b/build-support/tests/test-fe-core-metadata-funnel.sh
@@ -142,8 +142,10 @@ bash "${GATE}" "${FX}" >/dev/null 2>&1 && CLEAN_EC=0 || 
CLEAN_EC=$?
 [ "${CLEAN_EC}" -eq 0 ] || fail "expected exit 0 on clean tree (only 
funnel/marked/no-arg/comment), got ${CLEAN_EC}"
 
 # ---- run 3: the marker is load-bearing -> strip it and both write calls are 
flagged ----
-sed -i 's/getMetadata-funnel-exempt/marker-removed-here/' \
+# (-i.bak + rm: the in-place flag's argument form differs between GNU and BSD 
sed)
+sed -i.bak 's/getMetadata-funnel-exempt/marker-removed-here/' \
     "${SRC}/write/WriterTrailing.java" "${SRC}/write/WriterAbove.java"
+rm -f "${SRC}/write/WriterTrailing.java.bak" 
"${SRC}/write/WriterAbove.java.bak"
 OUT3="$(bash "${GATE}" "${FX}" 2>&1)"; EC3=$?
 REP3="$(printf '%s\n' "${OUT3}" | grep -E "^${FX}.*:[0-9]+:" || true)"
 N3="$(printf '%s\n' "${REP3}" | grep -c 'getMetadata' || true)"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to