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-website.git


The following commit(s) were added to refs/heads/master by this push:
     new 08980bdbf7c [doc-tools](skill) Add doris-release-docs skill (#4160)
08980bdbf7c is described below

commit 08980bdbf7cf1c1238970467f24f6b86c2b93b1e
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Sep 21 18:31:57 2026 +0800

    [doc-tools](skill) Add doris-release-docs skill (#4160)
    
    ## What this adds
    
    `doc-tools/skills/doris-release-docs/` — a skill for auditing and
    updating the user documentation of a Doris release from the commit diff
    between two refs (for example `4.1.3..4.1.4-rc04`). It is distilled from
    the 4.1.4 round (#4123), including every mistake made there and the step
    that now prevents it.
    
    ## Workflow
    
    1. **Ask first** which doc branch gets the Chinese version (dev / 4.x /
    ...) and where to sync afterwards.
    2. **Extract the hard user-facing surface** deterministically: FE/BE/MS
    configs, session variables, grammar, builtin functions, system tables,
    SHOW/ADMIN commands, HTTP endpoints, metrics, property analyzers,
    shipped `conf/` files, dependency versions, build modules. This is the
    baseline that subagent reports can add to but never override.
    3. **Classify every production commit** with parallel subagents (split
    by touched paths — the `[fix](test)` commit that added a session
    variable is the reason subject tags are ignored).
    4. **Decide per change**: FIX / ADD / NEW / NOTE / SKIP / ASK — feature
    commits with no obvious home, large new capability areas and
    "open-source build has no implementation" cases are collected into one
    question for the user.
    5. **Write the Chinese docs** for the chosen branch, run the static
    validator, open a PR.
    6. **After approval, sync** to the other branches and English — with a
    master-vs-tag difference table first, because the dev docs describe
    master (four variables, three defaults, the Paimon version and a module
    rename differed in the 4.1.4 round).
    
    ## Iron rules
    
    - Anything a user can perceive needs a doc.
    - Every new feature, behavior change, default change or removal carries
    a "since version X.Y.Z" note, verified against the release tags, on
    every release line it shipped on.
    - Every feature commit needs a documented home, or a question to the
    user — never a silent "no location found".
    - Facts come only from the code at the release tag: defaults,
    mutability, gating conditions, error strings, column names, privilege
    checks, counting semantics.
    - Unreleased capabilities are recorded, not documented.
    - Sidebars are shared across locales, so a new page waits for its
    English twin; removed features keep their page as a removal notice with
    a migration mapping; no `yarn build`, static checks only.
    
    ## Scripts
    
    | Script | Purpose |
    | --- | --- |
    | `surface-diff.sh` | per-area diffs between two refs plus an identifier
    list |
    | `split-commits.sh` | production vs test-only commits, cut into batches
    |
    | `check-version-claims.py` | which tags contain an identifier; `--path`
    for FE/BE name clashes |
    | `compare-refs.py` | default value, mutability and presence of
    configs/variables on two refs (handles both `@VariableMgr.VarAttr` and
    master's `@VarAttrDef.VarAttr`) |
    | `validate-docs.py` | front matter, relative links, bare JSX-like tags
    in `.mdx` (import-aware), CJK in English trees, forbidden identifiers,
    table column consistency |
    
    All five were run against the real 4.1.3..4.1.4-rc04 range and against
    the merged history of #4123; the validator reports 3 genuine
    pre-existing issues over 488 files and nothing else.
    
    ## References
    
    - the two subagent prompts (classification, cross-tree port)
    - writing conventions: version-note wording per situation, FE/BE config
    entry formats, the session-variable page format, removal pages, MDX
    constraints, and a "claim → how to verify it" table
    - a report template
    - `pitfalls.md`: the 21 concrete mistakes from the 4.1.4 round
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    https://claude.ai/code/session_01GRr7pw6ymrf99x3DUr2zxg
    
    ---------
    
    Co-authored-by: morningman <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 doc-tools/skills/doris-release-docs/SKILL.md       | 204 +++++++++++++++++++++
 .../references/classify-agent-prompt.md            |  85 +++++++++
 .../references/doc-conventions.md                  | 136 ++++++++++++++
 .../doris-release-docs/references/pitfalls.md      |  78 ++++++++
 .../references/port-agent-prompt.md                |  91 +++++++++
 .../references/report-template.md                  |  38 ++++
 .../scripts/check-version-claims.py                |  74 ++++++++
 .../doris-release-docs/scripts/compare-refs.py     | 108 +++++++++++
 .../doris-release-docs/scripts/split-commits.sh    |  29 +++
 .../doris-release-docs/scripts/surface-diff.sh     |  65 +++++++
 .../doris-release-docs/scripts/validate-docs.py    | 139 ++++++++++++++
 11 files changed, 1047 insertions(+)

diff --git a/doc-tools/skills/doris-release-docs/SKILL.md 
b/doc-tools/skills/doris-release-docs/SKILL.md
new file mode 100644
index 00000000000..b9f6c52a287
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/SKILL.md
@@ -0,0 +1,204 @@
+---
+name: doris-release-docs
+description: Audit and update the user documentation for an Apache Doris 
release from the commit diff between two refs (for example 4.1.3..4.1.4-rc04). 
Extract the hard user-facing surface (FE/BE/MS configs, session variables, 
grammar, functions, system tables, HTTP endpoints, metrics, shipped conf files, 
dependency versions), classify every commit with parallel subagents, then add, 
fix or create docs under three iron rules — anything a user can perceive needs 
a doc, every new feature or [...]
+---
+
+# Release docs from the commit diff
+
+Goal: given `FROM_REF..TO_REF` (for example `4.1.3..4.1.4-rc04`), make sure 
that **every user-perceivable
+change has documentation, every new feature or behavior change is 
version-annotated, and every feature
+commit has a documented home**, then report the result to the user in three 
buckets: done, needs your
+decision, deliberately skipped.
+
+Paths used below (confirm at the start):
+
+| Variable | Meaning |
+| --- | --- |
+| `DORIS` | a local `apache/doris` clone, read-only, with the release tags and 
an `upstream` remote for `master` |
+| `SITE` | this repository (doris-website) |
+| `ZH` | the Chinese doc tree the user chose: `4.x` -> 
`$SITE/i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x`, `dev` -> 
`.../current` (**ask first, never assume**) |
+| `OUT` | a scratch directory for diffs, batches and subagent reports |
+
+Scripts live in `doc-tools/skills/doris-release-docs/scripts/`, references in 
`references/`.
+
+```bash
+S=doc-tools/skills/doris-release-docs/scripts
+$S/surface-diff.sh          $DORIS 4.1.3 4.1.4-rc04 $OUT/surface     # hard 
user-facing surface
+$S/split-commits.sh         $DORIS 4.1.3 4.1.4-rc04 $OUT/commits 30   # 
production vs test-only, batches
+$S/check-version-claims.py  $DORIS --tags 4.0.7 4.0.8 4.1.3 4.1.4-rc04 -- 
<id>...   # before writing any version note
+$S/compare-refs.py          $DORIS 4.1.4-rc04 upstream/master --from-file 
$OUT/surface/identifiers.txt  # before syncing dev
+$S/validate-docs.py         $SITE --base upstream/master --forbidden 
$OUT/forbidden.txt   # before every commit
+```
+
+---
+
+## 0. Iron rules
+
+1. **A commit that adds a capability must have a documented home.** A subject 
tagged `feat` / `feature`,
+   or a diff that introduces a new user-facing surface (new syntax, new 
endpoint, a new group of configs,
+   a new data source, a new runtime mechanism such as cross-compute-group peer 
cache reads) gets
+   documentation: a new section on an existing page, or a new page. If there 
is no obvious home, or you
+   are not sure where it belongs, **ask the user** — never silently park it in 
a "no location found" list.
+2. **New features, behavior changes, default changes and removals are always 
version-annotated**
+   ("Supported since version 4.1.4"). Every version number must be checked 
against the tags with
+   `check-version-claims.py`; a change that shipped on several release lines 
(4.0.x and 4.1.x) is
+   annotated for each line.
+3. **Anything a user can perceive needs a doc**: configs, session variables, 
SQL syntax, functions, system
+   table / SHOW columns, HTTP endpoints, metrics and profile counters, error 
messages, privilege
+   requirements, type mappings, support matrices, dependency versions, and the 
shipped `conf/` files,
+   start scripts and jar directory names. Pure refactors, logging and 
optimizations with no interface
+   change are not documented.
+4. **Facts come only from the code at `TO_REF`** — not from commit messages, 
PR titles, subagent
+   reports or the existing docs. Default values, mutability, gating 
conditions, error strings, column
+   names, privilege checks and counting semantics are verified with `git show` 
/ `git grep` before they
+   are written (see `references/doc-conventions.md` §7).
+5. **Unreleased capabilities are not documented, but they are recorded.** 
Signals: the PR says the
+   open-source build only ships a framework / no-op, the user names the 
feature, or it is visibly
+   half-finished. For a large new capability area, **ask the user before 
writing** — writing a whole
+   chapter and then pulling it is wasted work.
+6. **The dev trees describe `master`, not `TO_REF`.** Before syncing to dev, 
run `compare-refs.py` and
+   build a difference table; master can lack backported variables, use 
different defaults, dependency
+   versions, module names and error strings. Record the master SHA the table 
was built from.
+7. **Sidebars are shared between locales.** While only the Chinese tree is 
written, a new page is not
+   added to the sidebar (the English site would fail to build); link to it 
from an existing page and add
+   the sidebar entry once every tree has the file.
+8. **Never delete a page.** A removed feature keeps its page, rewritten as a 
removal notice plus a
+   migration mapping plus the historical usage.
+9. **No `yarn build`.** The only verification is `validate-docs.py` (front 
matter, relative links, bare
+   JSX-like tags in `.mdx`, Chinese text in English trees, forbidden 
identifiers, table column counts).
+10. **First question, before touching anything: which doc branch gets the 
Chinese version** (dev / 4.x /
+    3.x ...). The cadence is fixed: **write that one Chinese branch -> open a 
PR -> the user reviews and
+    approves -> then sync to the other branches and to English.** The sync 
scope is confirmed with the
+    user at review time (for example whether a 4.x change also goes to dev or 
3.x). Every PR description
+    states the current sync status.
+11. **Subagents only read the Doris repo** and only write their own report 
file. The main agent
+    aggregates, verifies facts and writes the docs (or dispatches per-file 
batches, see §6).
+
+---
+
+## 1. Preflight
+
+```bash
+cd $DORIS && git fetch upstream master --tags
+git tag -l '4.1.*'                       # both FROM and TO must exist
+git log --oneline FROM..TO | wc -l
+```
+
+**Ask the user two things before extracting anything** (one question, all at 
once):
+
+1. Which doc branch gets the Chinese version: `4.x` 
(`versioned_docs/version-4.x` +
+   `i18n/.../version-4.x`), `dev` (`docs/` + `i18n/.../current`) or another 
active version
+   (`versions.json` lists them). This sets `$ZH` and decides which release 
line version notes refer to.
+2. Which branches and locales to sync to after approval (default: the same 
version in English plus dev in
+   both locales; the user may add or remove).
+
+Restate the commit range, the target version and the active version list in 
the question so a wrong
+assumption is easy to spot. Put every topic the user says "not yet" to into 
`$OUT/forbidden.txt` (one
+identifier or regex per line) for `validate-docs.py`.
+
+## 2. Extract the hard surface (deterministic, no judgment involved)
+
+```bash
+$S/surface-diff.sh $DORIS FROM TO $OUT/surface
+less $OUT/surface/summary.txt
+```
+
+Read the `fe-config` / `session-var` / `be-config` / `ms-config` / `grammar` / 
`functions` /
+`schema-tables` / `conf-files` / `deps` sections and list, directly from the 
`+`/`-` lines: additions,
+removals, default changes, mutability changes, grammar additions and removals, 
function / TVF additions
+and removals, system-table columns, shipped conf lines, dependency versions. 
This is the **baseline
+list** — subagent reports can add to it but never override it.
+
+`$OUT/surface/identifiers.txt` holds the config and variable names found in 
the diffs; feed it to
+`check-version-claims.py` and `compare-refs.py` with `--from-file`.
+
+## 3. Classify every commit (parallel subagents)
+
+```bash
+$S/split-commits.sh $DORIS FROM TO $OUT/commits 30     # splits by touched 
PATHS, never by [subject tag]
+```
+
+Start one `general-purpose` subagent per `batch-NN`, all at once, with the 
prompt in
+`references/classify-agent-prompt.md`. Each writes `$OUT/commits/report-NN.md` 
and replies with a short
+summary. When all are in:
+
+```bash
+for f in $OUT/commits/report-*.md; do awk '/^## NONE/{exit} /^## /{print} 
/Impact|Likely doc area|Flags/{print}' $f; done
+```
+
+gives a HIGH / MEDIUM / LOW index. **Re-check every default in the reports 
against `summary.txt`** —
+an intermediate commit can set a value that a later commit reverts.
+
+## 4. Decide and ask
+
+Merge the index and the baseline list into one table and decide per row: `FIX` 
an existing page /
+`ADD` to an existing page / `NEW` page / `NOTE` a version note only / `SKIP` 
internal change / `ASK` the
+user.
+
+Always `ASK` for: feature commits with no obvious home; large new capability 
areas; PRs that say the
+open-source build has no implementation; anything a subagent flagged. 
**Collect every ASK into one
+question** instead of interrupting repeatedly. Whatever the user declines goes 
into `forbidden.txt`.
+
+Finding a home: `grep -rln <identifier> $ZH`; failing that, the page for the 
same topic (configs in
+`admin-manual/config/*-config.md`, session variables in 
`sql-manual/basic-element/session-variables.md`
+plus the owning feature page, lakehouse in `lakehouse/catalogs/*`, load in 
`data-operate/import/*`,
+upgrade impact in `admin-manual/cluster-management/upgrade.md`). Before 
creating a page run
+`find $SITE/docs $SITE/i18n -name '<slug>*'` — dev may already have a better 
version; reuse it and fix
+the version note.
+
+## 5. Write the Chinese docs (only the branch chosen in §1, `$ZH`)
+
+Follow `references/doc-conventions.md` (version-note wording, FE/BE config 
entry formats, the session
+variable page, removal pages, MDX constraints, the fact-checking table). The 
most common mistakes:
+
+- Version numbers: run `check-version-claims.py` first, annotate every release 
line, and audit the
+  existing notes on the page while you are there.
+- Claims: read the diff body, not the title; find the gating condition; read 
the annotation for
+  mutability; read the call arguments for counting semantics; copy error 
strings literally; copy
+  property names from the code constants.
+- Tables: append new rows at the end; never put a blockquote between table 
rows.
+- When a rule changes, replace the examples on the page that no longer satisfy 
it.
+- Upgrade guide: add an "Upgrading to X.Y.Z" section with three tables — 
removed interfaces and
+  syntax / default and behavior changes / deployment and config-file changes — 
each row
+  `Change | Affected scope | What to do`.
+- Session variable page: overview table (added / default or semantics changed 
/ removed), one section
+  per variable, a removed-variables table.
+
+Then:
+
+```bash
+$S/validate-docs.py $SITE --base upstream/master --forbidden $OUT/forbidden.txt
+```
+
+Write the review report from `references/report-template.md` into 
`plan-doc/doris-<ver>-doc-review.md`
+(**not committed to the PR**), create a branch, commit, `gh pr create`. The PR 
description must contain:
+the range and method, the list of statements in the existing docs that were 
wrong and are now fixed, what
+was deliberately left out, new pages awaiting a sidebar entry, and changes 
with no home.
+
+## 6. After approval: sync to the other branches and to English
+
+Enter this section only when the user says the Chinese version is approved; 
confirm the sync scope from
+§1. The targets are usually the same version in English plus dev in both 
locales (source 4.x), or dev in
+English plus 4.x in both locales (source dev).
+
+1. `cd $DORIS && git fetch upstream master`; note the SHA.
+2. `compare-refs.py $DORIS TO upstream/master --from-file 
$OUT/surface/identifiers.txt`, plus feature
+   presence checks (`git grep -l <distinctive string> upstream/master | grep 
-v regression-test` —
+   **no narrow pathspecs**, master has been refactored), assembled into a 
difference table.
+3. Fill `references/port-agent-prompt.md` into `$OUT/sync-context.md` 
(difference table, forbidden
+   topics, master SHA) and start 6–8 file-batch subagents; each ports its 
files into all target trees;
+   large files (`upgrade.md`, `iceberg-catalog.mdx`, `fe-config.md` + 
`be-config.md`) get a batch of
+   their own.
+4. The main agent does: the other-language / other-branch versions of new 
pages (dev versions edited per
+   the difference table), sidebar entries, and pages that exist in one branch 
but not another although
+   the feature ships in this version.
+5. A subagent reporting "the SOURCE is wrong" means fixing every tree at once; 
"dev already has a better
+   version" means adopting it.
+6. Run `validate-docs.py` over everything; re-run `compare-refs.py` right 
before pushing (master moves);
+   update the difference table and the corrections list in the PR description.
+
+## 7. Wrap up
+
+- Append the round's new lessons to `references/pitfalls.md`.
+- The report stays in `plan-doc/`, out of the PR.
+- Note the PR number, branch, master SHA used for the comparison and the 
topics the user declined.
diff --git 
a/doc-tools/skills/doris-release-docs/references/classify-agent-prompt.md 
b/doc-tools/skills/doris-release-docs/references/classify-agent-prompt.md
new file mode 100644
index 00000000000..0e9189dacc1
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/references/classify-agent-prompt.md
@@ -0,0 +1,85 @@
+# Subagent prompt: classify the doc impact of each commit
+
+How to use: pass the block below as the prompt of a `general-purpose` agent, 
filling the `{{...}}`
+placeholders. One agent per `batch-NN` file (about 30 commits); start all 
batches at once. The agent
+only reads the Doris repo, writes `{{OUT_DIR}}/report-{{NN}}.md`, and replies 
with a summary of at
+most 25 lines.
+
+The main agent MUST do two things with every report and may not take it at 
face value:
+1. Re-check every default value / behavior against the final code at 
`{{TO_REF}}`. An intermediate
+   commit can set a value that a later commit reverts 
(`enable_expr_zonemap_filter` went `true` then
+   back to `false` inside one release).
+2. "HIGH" does not mean "write it" and "LOW" does not mean "skip it"; iron 
rules 1–3 decide.
+
+---
+
+You are analyzing Apache Doris commits for the {{VERSION}} release, to 
determine what user-facing
+documentation needs to be added / corrected on the Doris docs website.
+
+Repo: `{{DORIS_REPO}}` — **READ ONLY**. Only run read-only git commands (`git 
show`, `git log`,
+`git diff`, `git grep`). NEVER checkout, commit, stash, reset, fetch, or edit 
any file in that repo.
+
+Your batch of commits: `{{OUT_DIR}}/batch-{{NN}}` (format: `<short-hash> 
<subject>` per line).
+
+For EACH commit in your batch:
+1. `git show --stat <hash>` to see files touched.
+2. If it only touches tests (`regression-test/`, `be/test/`, `*/src/test/`), 
CI (`.github/`), docker,
+   or build scripts -> mark NONE and move on quickly.
+3. Otherwise read the relevant parts of the diff (`git show <hash> -- 
<paths>`) and the full commit
+   message (`git log -1 --format=%B <hash>`). The subject tag is NOT a 
reliable signal: a
+   `[fix](test)` commit has added a session variable before. Judge by the diff.
+4. Decide whether the change is USER-VISIBLE, i.e. a Doris user reading the 
docs would need to know
+   about it. User-visible things include:
+   - New / changed / removed SQL syntax or statements (parser .g4, Command 
classes)
+   - New / removed / renamed SQL functions, or changed function semantics / 
return type /
+     NULL handling / accepted argument types
+   - New / changed / removed FE config (`Config.java`), BE config 
(`be/src/common/config.*`,
+     `be/src/cloud/config.*`), MS config, or session variables 
(`SessionVariable.java`) —
+     INCLUDING default-value changes and mutability changes 
(`@ConfField(mutable = ...)`)
+   - New / changed table properties, catalog properties, load / job properties 
(`PROPERTIES(...)` keys)
+   - Observable behavior changes: different query results, error messages 
users hit, privilege
+     requirements, type-support matrices, supported data source / file format 
capability
+   - New / removed system tables, information_schema columns, SHOW statement 
output columns,
+     HTTP API endpoints, metrics, profile counters
+   - Limits / restrictions added or lifted (e.g. "now supports X on Y table 
type")
+   - Deprecations / removals
+   - Shipped config files (`conf/fe.conf`, `conf/be.conf`), start scripts, 
module / jar renames,
+     dependency version bumps that docs quote (Paimon, Iceberg, Hive shade, 
Arrow ...)
+   - Bug fixes where the OLD documented behavior is now wrong, or where the 
doc should state a
+     version-specific difference
+5. Pure internal refactors, logging, memory / perf optimizations with no 
observable interface change,
+   and flaky-test fixes -> NONE.
+
+For every non-NONE commit, ALSO record verbatim from the diff (not 
paraphrased):
+   - exact identifier names and their default values as they stand at the END 
of the commit
+   - exact error-message strings (copy the string literal; note if it is 
concatenated across lines)
+   - the exact gating condition of any "on upgrade" / "when enabled" behavior 
(e.g. a
+     `variable_version < 400` check means it only fires for 3.x -> 4.x 
upgrades)
+   - whether the PR checklist says "Does this need documentation" and whether 
it links a doc PR
+   - anything the PR text says is NOT included in the open-source build (e.g. 
"OSS no-op")
+
+Output: write a markdown report to `{{OUT_DIR}}/report-{{NN}}.md` with one 
section per non-NONE
+commit:
+
+```
+## <hash> <subject>
+- **PR**: #<number(s)>
+- **Impact**: HIGH | MEDIUM | LOW
+- **What changed (user-visible)**: <precise description with exact names / 
defaults / strings>
+- **Doc action**: ADD | FIX | NEW-PAGE | NOTE — <what the doc should say>
+- **Likely doc area**: <e.g. lakehouse/catalogs/iceberg-catalog, 
admin-manual/config/fe-config, ...>
+- **Flags**: <"feature commit — needs a doc home", "PR says no docs needed", 
"OSS no-op", "unsure">
+```
+
+End the report with:
+```
+## NONE (no doc impact)
+<hash> <subject>
+...one line each
+```
+
+Be precise and factual — quote exact identifier names, default values and 
error strings from the
+diff. Do not speculate about docs you have not read; your job is only to 
characterize the code change.
+
+Finally, reply with a SHORT summary (<= 25 lines): counts of HIGH / MEDIUM / 
LOW / NONE, then a
+bullet list of only the HIGH and MEDIUM items (hash + one-line what changed), 
then any Flags.
diff --git a/doc-tools/skills/doris-release-docs/references/doc-conventions.md 
b/doc-tools/skills/doris-release-docs/references/doc-conventions.md
new file mode 100644
index 00000000000..02d617dfddd
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/references/doc-conventions.md
@@ -0,0 +1,136 @@
+# doris-website writing conventions for release docs
+
+The existing wording in the repository takes precedence over this file. This 
file pins down the
+things that came up repeatedly in the 4.1.4 round and are easy to get wrong.
+
+## 1. Version notes (how iron rule 2 is written)
+
+| Situation | Chinese | English |
+| --- | --- | --- |
+| New feature / config / variable / column | `自 4.1.4 版本起支持。` or, in a table, 
`4.1.4 新增` | `Supported since version 4.1.4.` / `Added in 4.1.4` |
+| Default value changed | `默认值:8(4.1.4 之前为 4)` plus one sentence on why | 
`Default: 8 (4 before 4.1.4)` |
+| Behavior changed / old doc described behavior that was a bug | `:::caution 
版本行为变更(4.1.4)` block stating before -> after and the affected scope | 
`:::caution Behavior change (4.1.4)` |
+| Shipped on several release lines | `Doris 4.0 系列自 4.0.8 版本起、4.1 系列自 4.1.4 
版本起` | `in version 4.0.8 in the Doris 4.0 series and in version 4.1.4 in the 
4.1 series` |
+| Removal | keep the page; top `:::caution` with the removal, the error users 
now see, the replacement and a migration mapping table; add "(before 4.1.4)" to 
the historical headings | same |
+| Experimental | `4.1.4 新增(实验性)` | `Added in 4.1.4 (experimental)` |
+
+Run `scripts/check-version-claims.py` before writing a version number; check 
every active release
+line. The X in "since X" is **the first official release on each line that 
contains the change**, not
+the date the PR merged to master.
+
+Audit the notes already on the page too. In the 4.1.4 round 
`require_partition_filter` was
+documented as "4.1 series since 4.1.2" but was not in 4.1.2 or 4.1.3; 
multimodal EMBED said 4.1.5
+but shipped in 4.1.4; six "since 4.0.8" notes only reached the 4.1 line in 
4.1.4.
+
+## 2. Config entry formats
+
+**fe-config.md (English)**
+```
+#### `config_name`
+
+Default: false
+
+Is it possible to dynamically configure: true
+
+Is it a configuration item unique to the Master FE node: true
+
+Added in 4.1.4. <one-sentence purpose>
+
+<value semantics, example, caveats>
+```
+"dynamically configure" comes from `@ConfField(mutable = true)`, "Master only" 
from `masterOnly = true`.
+A bare `@ConfField` is NOT dynamically configurable. Chinese: `默认值:` / 
`是否可以动态配置:` /
+`是否为 Master FE 节点独有的配置项:`.
+
+**be-config.md (English)**
+```
+#### `config_name`
+
+* Type: int32
+* Description: Added in 4.1.4. <purpose>
+* Default value: 10
+```
+A `DEFINE_m*` prefix (`mInt32`, `mBool`) means the value can be changed at 
runtime; say so in the
+description. Chinese: `* 类型:` / `* 描述:` / `* 默认值:`.
+
+Both files are **curated, not exhaustive** (BE documented 205 of 749 configs 
at 4.1.4). Document what
+users tune; pure internal knobs (pool sizes, shard counts) may be left out but 
must be listed in the
+report so the user can decide.
+
+Put a new entry in its section (`### Metadata and cluster management` / `### 
Service` /
+`### Query engine` / `### Load and export` / `### Storage` / `### External 
table` /
+`### Compute-storage decoupled mode` ...) next to related entries; do not 
append everything to the end.
+
+## 3. Session variables
+
+The 4.x docs had no complete session-variable reference; the 4.1.4 round 
created
+`sql-manual/basic-element/session-variables.md` with an "under construction" 
notice at the top and
+only the variables that release touched. Later releases append to it in this 
format:
+
+```
+### `variable_name`
+
+| Item | Value |
+| --- | --- |
+| Type | Boolean |
+| Default | `true` |
+| Version | Added in 4.1.4 |
+
+<description>
+```
+plus a "What changed in X.Y.Z" overview table (added / default or semantics 
changed / removed) and a
+"Removed variables" table. A variable also goes into its owning feature page 
(runtime filter variables
+into runtime-filter.md, and so on), with links both ways.
+
+## 4. Removed features, endpoints, syntax
+
+- **Never delete the page** (sidebars are shared across locales; a missing id 
breaks the build). Turn
+  it into: removal notice + migration mapping + historical usage.
+- Pages that used it in examples or tutorials switch to the replacement and 
note "before 4.1.4 you
+  could also use X".
+- Add a row to the upgrade guide.
+
+## 5. New pages
+
+- `versioned_sidebars/version-4.x-sidebars.json` and `sidebars.ts` are shared 
by both locales:
+  **no sidebar entry while the English page does not exist**, or the English 
build fails. During the
+  Chinese-first phase create only the Chinese page, link to it from an 
existing page, and list it in
+  the report as "awaiting sidebar"; add the entry when syncing English.
+- When the id goes into the sidebar, all four trees (zh 4.x / en 4.x / zh dev 
/ en dev) must have the
+  file.
+- One statement per page is the convention, but a small derived statement 
(`ADMIN COMPACT TABLET`
+  next to `COMPACT TABLE`) can share the parent page; say so in the report.
+
+## 6. Docusaurus / MDX constraints
+
+- Front matter is JSON (a few key-features / community pages use YAML); 
`"language"` is `zh-CN` in
+  Chinese files and `en` in English files.
+- `markdown.format: 'detect'`: `.md` is CommonMark, `.mdx` is MDX. **In 
`.mdx`, any `<xxx>` outside
+  a code fence or inline code is parsed as JSX** — placeholders go in 
backticks or fenced blocks.
+- Relative links work with or without the extension; anchors are heading slugs 
(Chinese headings keep
+  the Chinese: `#支持的运算与-cast-规则`).
+- Never put a blockquote or blank line between table rows — it splits the 
table (the MaxCompute
+  property table broke this way); append rows at the end or keep the version 
order.
+- Admonitions: `:::caution Title`, `:::info Title`, `:::tip Title`, with a 
blank line before and after.
+- No `yarn build`; `scripts/validate-docs.py` is the static check.
+
+## 7. Where facts come from (iron rule 4)
+
+Before writing any claim, verify it in the Doris repo at the **final tag**:
+
+| Claim | How to verify |
+| --- | --- |
+| Default value | `git show <tag>:<file>`, read the field initializer; never 
trust the commit message |
+| Dynamically configurable | `@ConfField(mutable = ...)`; BE: the `DEFINE_m*` 
prefix |
+| "What happens on upgrade" | find the gating condition (e.g. 
`variable_version < 400`) before concluding |
+| Error message | copy the string literal; join fragments concatenated across 
lines |
+| Column names / count | the definition (`SchemaTable.java`, 
`InsertJob.SCHEMA`, ...) |
+| Privileges | the `checkGlobalPriv` / `checkTblPriv` call sites |
+| Counting semantics | the call arguments (`getAllClusterBackends(false)` is 
ALL backends, not the live ones) |
+| Dependency version | `<xxx.version>` in `fe/pom.xml` |
+| Property name | copy from the code constant, never retype 
(`mc.enable.namespace.schema` was once written with underscores) |
+
+## 8. Examples must match the rule
+
+When a rule changes (for example the floating-point output format), recompute 
or replace the examples
+already on the page; an example that contradicts the rule it sits under is 
worse than no example.
diff --git a/doc-tools/skills/doris-release-docs/references/pitfalls.md 
b/doc-tools/skills/doris-release-docs/references/pitfalls.md
new file mode 100644
index 00000000000..21479e77f42
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/references/pitfalls.md
@@ -0,0 +1,78 @@
+# Lessons from the 4.1.4 round (each maps to a step in SKILL.md)
+
+Recorded as "what went wrong -> how it was caught -> how it is prevented now". 
Append after every
+release.
+
+## A. Classification
+
+1. **Filtering by the `[tag]` in the subject misses changes.** `[fix](test) 
stabilize the flaky
+   shuffle_left_join` added the session variable 
`bucket_shuffle_downgrade_ratio`;
+   `[fix](regression) Adjust large TTL cache regression case` added the 
upper-bound check on
+   `file_cache_ttl_seconds`. -> `split-commits.sh` splits by touched paths 
only.
+2. **A subagent's default value can be an intermediate state.** 
`enable_expr_zonemap_filter` was
+   `true` in `009bcf44b3d` and back to `false` in `5e6f47d7cdc`. -> the main 
agent re-checks against
+   the final diff from `surface-diff.sh` and `compare-refs.py`.
+3. **"Does this need documentation: No" in the PR does not mean no docs** 
(`ADMIN COMPACT TABLET`
+   and peer read both had it unchecked), but "the open-source build only ships 
a no-op" (TLS) is a
+   real reason not to write. -> record both signals and let the user decide.
+4. **The same identifier can exist once in FE and once in BE.**
+   `enable_group_commit_streamload_be_forward` existed as an FE config since 
4.0.7; the BE config
+   arrived in 4.0.8 / 4.1.4. -> `check-version-claims.py --path` restricts the 
search to one file.
+
+## B. Writing (factual errors that were made)
+
+5. **Treating the commit title as the conclusion.** `5ea5dd73e13` 
"replication_num /
+   replication_allocation" was documented as "now mutually exclusive"; the 
code removes the other
+   legacy property when one is modified. -> read the diff body before writing 
a behavior claim.
+6. **Missing the gating condition.** The `enable_nereids_distribute_planner` 
refresh sits inside
+   `if (variableVersion < 400)`, i.e. only 3.x -> 4.x upgrades; it was 
documented as "every
+   upgrade". -> conventions §7, "what happens on upgrade".
+7. **Mutability inverted twice.** 
`enable_forward_group_commit_stream_load_to_follower` is
+   `mutable = true`; `default_get_version_from_ms_timeout_second` is a bare 
`@ConfField`. ->
+   `compare-refs.py` now prints mutability.
+8. **Embellishing a count.** `BackendNum` was described as "live, healthy 
backends"; the code calls
+   `getAllClusterBackends(false)` = all of them. -> read the argument, don't 
guess.
+9. **Rule changed, example kept.** After the shortest-round-trip float format 
landed, the old
+   `cast('12345678' as float) -> 1.234568e+07` example stayed under the new 
rule and contradicted
+   it. -> conventions §8.
+10. **A mid-table insert with a blockquote split the table** (MaxCompute 
properties). ->
+    `validate-docs.py` checks column counts; new rows go at the end.
+11. **A retyped property name** (`mc.enable_namespace_schema` for 
`mc.enable.namespace.schema`).
+    -> copy from the code constant.
+12. **Inherited "since 4.0.8" notes without checking the 4.1 line.** Six of 
them were 4.1.4 on that
+    line; `require_partition_filter` "since 4.1.2" was 4.1.4; multimodal EMBED 
"4.1.5" was 4.1.4. ->
+    every version number goes through `check-version-claims.py`.
+13. **Documented unreleased capabilities.** Whole chapters on Paimon writes 
and Variant V2 were
+    written and then withdrawn. -> ask before writing a large new capability 
area (SKILL.md §4).
+14. **Wrote new function pages although dev already had better ones.** The dev 
`parse_to_variant`
+    page was more complete. -> `find` all four trees before creating a page; 
reuse and fix the
+    version note.
+
+## C. Sync
+
+15. **dev is not a copy of 4.x.** master lacked four variables 
(`enable_external_scan_task_reuse`,
+    `file_split_size_on_fe/be`, `external_meta_cache_max_weight`), three 
defaults differed
+    (`enable_expr_zonemap_filter`, `max_scanners_concurrency`, 
`meta_service_rpc_rate_limit_enabled`),
+    Paimon was 1.3.1 vs 1.4.2, `paimon-scanner` was not renamed, Iceberg 
`ALTER TABLE SET` and the
+    database-property matrix did not exist, several error strings differed, 
`SHOW TABLETS` without
+    `ORDER BY` behaved differently. -> run `compare-refs.py` first and paste 
the table into the port
+    prompt.
+16. **master moves.** `external_meta_cache_max_weight` was absent from master 
on sync day and
+    landed hours later (#67726). -> record the master SHA in the report / PR; 
re-run
+    `compare-refs.py` before pushing.
+17. **Narrow pathspecs miss on master.** Connectors moved to 
`fe/fe-connector/`; searching
+    `fe/fe-core/src/main/java/` for `osstables` returned nothing. -> search 
the whole tree, then drop
+    test hits.
+18. **dev had already structured the same topic differently.** Schema change 
is split into
+    `schema-change-mysql.md` / `schema-change-postgresql.md` on dev. -> "find 
the equivalent place
+    and adapt", never paste. Conversely, a page that dev has and 4.x lacks
+    (`schema-change-mysql.md`) is ported down when the feature ships in this 
version.
+19. **Sidebars are shared.** Adding an entry in the Chinese-first phase breaks 
the English build. ->
+    link from an existing page first; add the entry when English exists.
+
+## D. Process
+
+20. **Chinese first -> user review -> English + other branches** is the 
requested cadence; every PR
+    description states the sync status.
+21. The report file (`plan-doc/doris-<ver>-doc-review.md`) stays out of the 
PR; its summary goes into
+    the PR description.
diff --git 
a/doc-tools/skills/doris-release-docs/references/port-agent-prompt.md 
b/doc-tools/skills/doris-release-docs/references/port-agent-prompt.md
new file mode 100644
index 00000000000..b1e0ae9337c
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/references/port-agent-prompt.md
@@ -0,0 +1,91 @@
+# Subagent prompt: sync the approved Chinese branch to the other branches and 
to English
+
+How to use: fill the placeholders and save the block below as 
`{{OUT_DIR}}/sync-context.md`. Then
+start subagents in file batches (5–10 files each; large files such as 
`upgrade.md`,
+`iceberg-catalog.mdx`, `fe-config.md` + `be-config.md` get a batch of their 
own). Each batch prompt
+only needs "Read sync-context.md in full first, then handle these files: ..." 
plus batch-specific hints.
+
+The source and target trees come from the answer to SKILL.md §1 — do not 
assume the source is 4.x:
+- source = Chinese 4.x -> targets are usually English 4.x, Chinese dev, 
English dev
+- source = Chinese dev -> targets are usually English dev, Chinese 4.x, 
English 4.x (then it is the
+  SOURCE that describes master, and rule 4 must verify against the 4.x tag 
instead)
+The table below is written for source = 4.x; swap the rows for a dev source 
and replace "master" in
+rule 4 with the release tag.
+
+**Before starting any subagent the main agent runs** `scripts/compare-refs.py` 
and pastes the result
+into "Verified differences". The subagents' own checks supplement it; they do 
not replace it.
+
+---
+
+# Doris {{VERSION}} doc sync — shared context
+
+## Goal
+
+The Chinese {{SOURCE_LABEL}} docs have been updated for the Doris {{VERSION}} 
release and approved by
+the user. Port each change to the other doc trees.
+
+Repo: `{{WEBSITE_REPO}}` (branch `{{BRANCH}}`).
+
+| Tree | Path prefix | Language |
+| --- | --- | --- |
+| **SOURCE** {{SOURCE_LABEL}} (reviewed and approved) | `{{SOURCE_PREFIX}}` | 
Chinese |
+| target {{TARGET1_LABEL}} | `{{TARGET1_PREFIX}}` | English |
+| target {{TARGET2_LABEL}} | `{{TARGET2_PREFIX}}` | Chinese |
+| target {{TARGET3_LABEL}} | `{{TARGET3_PREFIX}}` | English |
+
+(Path prefixes: 4.x English `versioned_docs/version-4.x/`, 4.x Chinese
+`i18n/zh-CN/docusaurus-plugin-content-docs/version-4.x/`, dev English `docs/`, 
dev Chinese
+`i18n/zh-CN/docusaurus-plugin-content-docs/current/`.)
+
+For each file in your batch, get the change to port with:
+
+```
+git diff {{BASE_REF}} -- {{SOURCE_PREFIX}}<FILE>
+```
+
+Then apply the equivalent change to the target paths.
+
+## Rules
+
+1. **Do not blind-patch.** The target files have diverged: different wording, 
section order, extra
+   or missing sections. Read the target, find the equivalent place, adapt. If 
the section does not
+   exist in a target, add it where it fits that file's structure — or skip and 
report if it
+   genuinely does not apply. The other trees may already document the topic in 
a better or different
+   way (dev did for `parse_to_variant` and the schema-change pages): reuse, 
don't duplicate.
+2. **English must read like the surrounding English doc.** Match existing 
terminology (e.g.
+   "compute-storage decoupled mode"), heading style, admonition style 
(`:::caution Behavior change
+   (X.Y.Z)`), table format. Do not translate literally. Keep front matter 
valid; `"language": "en"`
+   in English files, `"language": "zh-CN"` in Chinese files.
+3. **Version annotations stay.** Dev docs carry the same "Since version X" 
notes as the versioned
+   docs, in the phrasing the target file already uses for similar notes.
+4. **The dev trees document `master`, not {{VERSION}}.** Before porting a 
factual claim (default
+   value, "since version", whether a feature exists, an error string, a 
dependency version) into
+   `docs/` or `i18n/zh-CN/.../current/`, check it on master in 
`{{DORIS_REPO}}` — READ ONLY:
+   `git grep <pattern> {{MASTER_REF}}` / `git show {{MASTER_REF}}:<path>`. 
Never checkout / commit /
+   edit / fetch there. Master has been refactored (e.g. 
`fe/fe-connector/...`), so do NOT narrow
+   `git grep` with pathspecs — search the whole tree and drop 
`regression-test` hits. Verified
+   differences are listed below; verify anything else yourself.
+5. **Never introduce these topics into any tree** — deliberately undocumented 
for now:
+   {{EXCLUDED_TOPICS}}
+   If a target already contains such content, leave it; just don't add more.
+6. Do not reformat or restructure anything the change does not touch.
+7. Do not touch sidebar files.
+8. If you find an error in the SOURCE file — wrong default, wrong claim, a 
table broken by a
+   mid-table insert, a typo in an identifier — fix the source too, and flag it 
prominently in your
+   report. Verify against `{{TO_REF}}` first.
+
+## Verified master-vs-{{VERSION}} differences
+
+(paste the `compare-refs.py {{TO_REF}} {{MASTER_REF}}` table here, plus 
feature-presence findings)
+
+| Item | {{VERSION}} | master (dev) | What to do in dev docs |
+| --- | --- | --- | --- |
+| ... | ... | ... | omit / state master value / reword |
+
+Master SHA used for this table: `{{MASTER_SHA}}`. Master moves; if you fetch, 
say so.
+
+## Reporting
+
+When done, reply with a short report: for each file, one line per target tree 
saying `ported`,
+`already present`, `adapted (how)` or `skipped (why)`. List every claim you 
verified on master and
+every SOURCE error you fixed. Flag anything you were unsure about.
diff --git a/doc-tools/skills/doris-release-docs/references/report-template.md 
b/doc-tools/skills/doris-release-docs/references/report-template.md
new file mode 100644
index 00000000000..9d13499d4e8
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/references/report-template.md
@@ -0,0 +1,38 @@
+# Doris {{VERSION}} user-doc audit report ({{SOURCE_LABEL}})
+
+Range: `apache/doris` `{{FROM_REF}}..{{TO_REF}}`, **N** commits (M touching 
production code, K
+test/CI only). Scope of this round: {{Chinese 4.x only | Chinese 4.x + English 
4.x + dev}}. Master SHA
+used for the dev comparison: `{{MASTER_SHA}}`.
+
+Method: `surface-diff.sh` for the hard user-facing surface -> 
`split-commits.sh` into batches -> N
+subagents classifying every commit -> `check-version-claims.py` / 
`compare-refs.py` for facts -> doc
+edits -> `validate-docs.py`.
+
+---
+
+## 1. Documentation changes made
+
+### 1.1 Syntax and interfaces added or removed
+| Change | commit | Doc change |
+| --- | --- | --- |
+
+### 1.2 Lakehouse
+### 1.3 Load / streaming jobs
+### 1.4 Configs and session variables
+### 1.5 Other
+### 1.6 Deliberately not written (reason: unreleased / no open-source 
implementation / user decision)
+
+---
+
+## 2. Needs your decision
+
+### A. Chinese pages written, awaiting sidebar entry + English
+### B. Changes with no obvious home (suggested location / whether to create a 
page)
+### C. Product decisions (publish or not)
+### D. Errors found in existing docs unrelated to this release
+
+---
+
+## 3. Deliberately skipped
+- Internal refactors / logging / optimizations (about N commits)
+- Correctness fixes better suited to the release notes than to user docs
diff --git 
a/doc-tools/skills/doris-release-docs/scripts/check-version-claims.py 
b/doc-tools/skills/doris-release-docs/scripts/check-version-claims.py
new file mode 100755
index 00000000000..a1a104a3dc3
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/scripts/check-version-claims.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""Which release tags contain a given identifier? Use this BEFORE writing any
+"自 X.Y.Z 版本起支持" / "Since version X.Y.Z" sentence, and to audit claims
+already in the docs.
+
+usage:
+  check-version-claims.py <doris-repo> --tags 4.0.7 4.0.8 4.1.2 4.1.3 
4.1.4-rc04 -- <identifier>...
+  check-version-claims.py <doris-repo> --tags ... --from-file identifiers.txt
+  check-version-claims.py <doris-repo> --tags ... --path 
be/src/common/config.h -- <identifier>...
+
+--path restricts the search to one pathspec. Use it when FE and BE share a
+name: `enable_group_commit_streamload_be_forward` existed as an FE config
+since 4.0.7 but the BE config of the same name only arrived in 4.0.8 / 4.1.4,
+so an unrestricted search says "since 4.0.7" and is wrong for the BE entry.
+Error strings are often concatenated across lines in Java/C++ — grep a
+distinctive fragment, not the whole sentence.
+
+An identifier can be a config / variable name, an error string fragment, a
+grammar token, a class name — anything git grep can find. Test directories
+are excluded so a regression-test mention does not count as "supported".
+
+Reading the table: the first tag in each release line that contains the
+identifier is the version to annotate. Doris keeps several lines alive
+(4.0.x and 4.1.x), so a feature backported to both needs BOTH first
+versions, e.g. "Doris 4.0 系列自 4.0.8 起、4.1 系列自 4.1.4 起". In the
+4.1.4 round `require_partition_filter` was documented as "4.1 系列自 4.1.2
+起" — it was not in 4.1.2 or 4.1.3, only 4.1.4.
+"""
+import subprocess, sys
+
+def present(repo, tag, ident, path=None):
+    specs = [path] if path else [':!regression-test', ':!*/test/*', 
':!be/test', ':!docker']
+    r = subprocess.run(['git', '-C', repo, 'grep', '-l', '-F', '--', ident, 
tag, '--'] + specs,
+                       capture_output=True, text=True)
+    return r.returncode == 0 and r.stdout.strip() != ''
+
+def main():
+    args = sys.argv[1:]
+    if not args or '--tags' not in args:
+        print(__doc__); sys.exit(1)
+    repo = args[0]
+    ti = args.index('--tags')
+    tags = []
+    path = None
+    i = ti + 1
+    while i < len(args) and args[i] not in ('--', '--from-file', '--path'):
+        tags.append(args[i]); i += 1
+    if i < len(args) and args[i] == '--path':
+        path = args[i + 1]; i += 2
+    idents = []
+    if i < len(args) and args[i] == '--from-file':
+        idents = [l.split()[-1] for l in open(args[i + 1]) if l.strip() and 
not l.startswith('#')]
+    elif i < len(args) and args[i] == '--':
+        idents = args[i + 1:]
+    if not tags or not idents:
+        print(__doc__); sys.exit(1)
+    for t in tags:
+        if subprocess.run(['git', '-C', repo, 'rev-parse', '-q', '--verify', t 
+ '^{commit}'],
+                          capture_output=True).returncode != 0:
+            print(f'unknown ref: {t}', file=sys.stderr); sys.exit(1)
+    w = max(len(x) for x in idents) + 2
+    print(' ' * w + '  '.join(f'{t:>12s}' for t in tags))
+    for ident in dict.fromkeys(idents):
+        cells = []
+        first = None
+        for t in tags:
+            ok = present(repo, t, ident, path)
+            cells.append(f'{"YES" if ok else "-":>12s}')
+            if ok and first is None:
+                first = t
+        print(f'{ident:{w}s}' + '  '.join(cells) + (f'   first: {first}' if 
first else '   (nowhere)'))
+
+if __name__ == '__main__':
+    main()
diff --git a/doc-tools/skills/doris-release-docs/scripts/compare-refs.py 
b/doc-tools/skills/doris-release-docs/scripts/compare-refs.py
new file mode 100755
index 00000000000..dbca054f3a3
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/scripts/compare-refs.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+"""Compare the default value / presence of Doris configs and session variables
+between two refs (typically the release tag and upstream master).
+
+usage:
+  compare-refs.py <doris-repo> <ref-a> <ref-b> <identifier>...
+  compare-refs.py <doris-repo> <ref-a> <ref-b> --from-file identifiers.txt
+
+Identifiers are config / session-variable NAMES as the user sees them
+(e.g. enable_expr_zonemap_filter, autobucket_min_buckets,
+group_commit_max_wal_num_per_table). Each one is looked up in
+  FE Config.java (public static <type> <name> = <default>)
+  SessionVariable.java (@VarAttr(name = "<name>" or CONST) -> field default)
+  be/src/common/config.cpp and be/src/cloud/config.cpp (DEFINE_*(name, 
"default"))
+  cloud/src/common/config.h (CONF_*(name, "default"))
+and reported per ref. Lines marked <== DIFF need a dev-doc decision:
+absent on master -> omit from dev docs; different default -> state master's.
+
+Why this exists: in the 4.1.4 round four variables backported to branch-4.1
+did not exist on master at all, and three defaults differed. The dev docs
+describe master, so porting the 4.x text verbatim would have been wrong.
+"""
+import re, subprocess, sys, os
+
+def show(repo, ref, path):
+    r = subprocess.run(['git', '-C', repo, 'show', f'{ref}:{path}'], 
capture_output=True, text=True)
+    return r.stdout if r.returncode == 0 else ''
+
+FILES = {
+    'fe':   ['fe/fe-common/src/main/java/org/apache/doris/common/Config.java'],
+    'sv':   
['fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java',
+             
'fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java'],
+    'be':   ['be/src/common/config.cpp', 'be/src/cloud/config.cpp'],
+    'ms':   ['cloud/src/common/config.h'],
+}
+
+def fe_default(txt, name):
+    m = re.search(r'\n\s*public static (?:volatile\s+)?[\w<>\[\]]+\s+' + 
re.escape(name) + r'\s*=\s*([^;]+);', txt)
+    if not m:
+        return None
+    # the annotation is the nearest preceding @ConfField; stop at the previous 
field declaration
+    before = txt[:m.start()]
+    ann_start = before.rfind('@ConfField')
+    prev_field = before.rfind('public static')
+    if ann_start < 0 or ann_start < prev_field:
+        return f'{m.group(1).strip()} (no @ConfField?)'
+    ann = before[ann_start:]
+    mut = 'mutable=true' if re.search(r'mutable\s*=\s*true', ann) else 
'mutable=false'
+    return f'{m.group(1).strip()} ({mut})'
+
+def sv_default(txt, name):
+    # name = "<name>" literal, or name = CONST where CONST = "<name>"
+    const = None
+    mc = re.search(r'String\s+([A-Z0-9_]+)\s*=\s*"' + re.escape(name) + r'"', 
txt)
+    if mc:
+        const = mc.group(1)
+    # branch-4.x uses @VariableMgr.VarAttr, master uses @VarAttrDef.VarAttr; 
accept any qualifier
+    pat = r'@(?:\w+\.)?VarAttr\(\s*name\s*=\s*(?:"' + re.escape(name) + r'"' + 
(r'|' + re.escape(const) if const else '') + r')\b'
+    m = re.search(pat, txt)
+    if not m:
+        return None
+    tail = txt[m.end():m.end() + 3000]
+    f = 
re.search(r'\n\s*(?:public|private|protected)\s+(?:static\s+)?[\w<>\[\],\s]+?\s+(\w+)\s*=\s*([^;]+);',
 tail)
+    if not f:
+        return '?(field not found)'
+    val = f.group(2).strip()
+    flags = []
+    if re.search(r'varType\s*=\s*VariableAnnotation\.EXPERIMENTAL', 
txt[m.start():m.end()+600]): flags.append('EXPERIMENTAL')
+    if re.search(r'needForward\s*=\s*true', txt[m.start():m.end()+600]): 
flags.append('needForward')
+    return val + (' [' + ','.join(flags) + ']' if flags else '')
+
+def be_default(txt, name):
+    m = re.search(r'DEFINE_([A-Za-z0-9]+)\(\s*' + re.escape(name) + 
r'\s*,\s*("(?:[^"\\]|\\.)*")', txt)
+    return f'{m.group(2)} ({m.group(1)})' if m else None
+
+def ms_default(txt, name):
+    m = re.search(r'CONF_([A-Za-z0-9]+)\(\s*' + re.escape(name) + 
r'\s*,\s*("(?:[^"\\]|\\.)*")', txt)
+    return f'{m.group(2)} ({m.group(1)})' if m else None
+
+LOOKUP = {'fe': fe_default, 'sv': sv_default, 'be': be_default, 'ms': 
ms_default}
+
+def main():
+    if len(sys.argv) < 5:
+        print(__doc__); sys.exit(1)
+    repo, a, b = sys.argv[1:4]
+    ids = sys.argv[4:]
+    if ids and ids[0] == '--from-file':
+        ids = [l.split()[-1] for l in open(ids[1]) if l.strip() and not 
l.startswith('#')]
+    src = {ref: {k: '\n'.join(show(repo, ref, p) for p in ps) for k, ps in 
FILES.items()} for ref in (a, b)}
+    print(f'{"identifier":52s} {"kind":4s} {a[:22]:24s} {b[:22]:24s}')
+    diffs = 0
+    for name in dict.fromkeys(ids):
+        found = False
+        for kind, fn in LOOKUP.items():
+            va, vb = fn(src[a][kind], name), fn(src[b][kind], name)
+            if va is None and vb is None:
+                continue
+            found = True
+            sa, sb = (va or 'ABSENT'), (vb or 'ABSENT')
+            flag = '   <== DIFF' if sa != sb else ''
+            if flag: diffs += 1
+            print(f'{name:52s} {kind:4s} {sa[:22]:24s} {sb[:22]:24s}{flag}')
+        if not found:
+            print(f'{name:52s} ??   not found in any known config/variable 
file on either ref')
+    print(f'\n{diffs} difference(s)')
+
+if __name__ == '__main__':
+    main()
diff --git a/doc-tools/skills/doris-release-docs/scripts/split-commits.sh 
b/doc-tools/skills/doris-release-docs/scripts/split-commits.sh
new file mode 100755
index 00000000000..00b1e125214
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/scripts/split-commits.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# List the commits in a range, split them into "touches production code" vs
+# "tests / CI / docker / build scripts only", and cut the production list into
+# batches for parallel subagent classification.
+#
+# usage: split-commits.sh <doris-repo> <from-ref> <to-ref> <out-dir> 
[batch-size=30]
+#
+# Do NOT filter by the [tag] in the subject line: in the 4.1.4 round a
+# "[fix](test)" commit added a session variable and a "[fix](regression)"
+# commit added a table-property validation. Only the touched paths are a
+# reliable signal.
+set -euo pipefail
+REPO=$1; FROM=$2; TO=$3; OUT=$4; BATCH=${5:-30}
+mkdir -p "$OUT"; cd "$REPO"
+git log --pretty=format:'%h %s' "$FROM".."$TO" > "$OUT/commits-all.txt"
+echo >> "$OUT/commits-all.txt"
+: > "$OUT/commits-prod.txt"; : > "$OUT/commits-testonly.txt"
+NONPROD='^(regression-test|docker|\.github|thirdparty|be/test|fe/fe-core/src/test|fe/fe-common/src/test|cloud/test|samples|tools|extension|be/benchmark)/'
+while read -r h rest; do
+  [ -z "$h" ] && continue
+  n=$( { git show --pretty=format: --name-only "$h" | grep -v '^$' | grep -vE 
"$NONPROD" || true; } | wc -l | tr -d ' ')
+  if [ "$n" -gt 0 ]; then echo "$h $rest" >> "$OUT/commits-prod.txt"; else 
echo "$h $rest" >> "$OUT/commits-testonly.txt"; fi
+done < "$OUT/commits-all.txt"
+( cd "$OUT" && rm -f batch-* && split -l "$BATCH" -d commits-prod.txt batch- )
+printf 'all=%s  prod=%s  test-only=%s  batches=%s (size %s)\n' \
+  "$(grep -c . "$OUT/commits-all.txt")" "$(grep -c . "$OUT/commits-prod.txt")" 
\
+  "$(grep -c . "$OUT/commits-testonly.txt")" "$(ls "$OUT"/batch-* | wc -l | tr 
-d ' ')" "$BATCH"
+echo "subject-tag histogram of production commits:"
+sed -E 's/^[0-9a-f]+ //; s/^(branch-[0-9.]+: ?|\[branch-[0-9.]+\] ?)//' 
"$OUT/commits-prod.txt" | grep -oE '^\[[A-Za-z_ -]+\]' | tr 'A-Z' 'a-z' | sort 
| uniq -c | sort -rn | head -12
diff --git a/doc-tools/skills/doris-release-docs/scripts/surface-diff.sh 
b/doc-tools/skills/doris-release-docs/scripts/surface-diff.sh
new file mode 100755
index 00000000000..939945ee75e
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/scripts/surface-diff.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# Extract the user-facing "surface" diff between two Doris refs.
+#
+# usage: surface-diff.sh <doris-repo> <from-ref> <to-ref> <out-dir>
+#
+# Writes one .diff per surface area plus summary.txt (only +/- lines, no 
context)
+# so the reader can see every added/removed/changed config, session variable,
+# grammar rule, builtin function, system-table column, HTTP endpoint, metric,
+# shipped conf file line and dependency version WITHOUT trusting commit 
messages.
+set -euo pipefail
+REPO=$1; FROM=$2; TO=$3; OUT=$4
+mkdir -p "$OUT"
+cd "$REPO"
+git rev-parse --verify -q "$FROM^{commit}" >/dev/null || { echo "unknown ref: 
$FROM" >&2; exit 1; }
+git rev-parse --verify -q "$TO^{commit}"   >/dev/null || { echo "unknown ref: 
$TO" >&2; exit 1; }
+
+area() { # name, then pathspecs
+  local name=$1; shift
+  git diff "$FROM".."$TO" -- "$@" > "$OUT/$name.diff" 2>/dev/null || true
+  local n; n=$(grep -cE '^[+-]' "$OUT/$name.diff" 2>/dev/null || echo 0)
+  printf '%-16s %6s changed lines  (%s)\n' "$name" "$n" "$*" >> 
"$OUT/summary.txt"
+}
+: > "$OUT/summary.txt"
+echo "# surface diff $FROM..$TO" >> "$OUT/summary.txt"
+
+area fe-config      
'fe/fe-common/src/main/java/org/apache/doris/common/Config.java'
+area session-var    
'fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java' \
+                    
'fe/fe-core/src/main/java/org/apache/doris/qe/GlobalVariable.java'
+area be-config      'be/src/common/config.cpp' 'be/src/common/config.h' 
'be/src/cloud/config.cpp' 'be/src/cloud/config.h'
+area ms-config      'cloud/src/common/config.h'
+area grammar        'fe/fe-core/src/main/antlr4/**'
+area functions      
'fe/fe-core/src/main/java/org/apache/doris/catalog/Builtin*.java' \
+                    
'fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/**'
+area schema-tables  
'fe/fe-core/src/main/java/org/apache/doris/catalog/SchemaTable.java' \
+                    
'fe/fe-core/src/main/java/org/apache/doris/catalog/InternalSchema.java' \
+                    
'fe/fe-core/src/main/java/org/apache/doris/catalog/InternalSchemaInitializer.java'
+area show-commands  
'fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/Show*.java'
 \
+                    
'fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/Admin*.java'
+area http-be        'be/src/service/http_service.cpp' 
'be/src/service/http/action/**' 'be/src/http/action/**'
+area http-fe        'fe/fe-core/src/main/java/org/apache/doris/httpv2/**'
+area metrics        
'fe/fe-core/src/main/java/org/apache/doris/metric/MetricRepo.java' 
'be/src/util/doris_metrics.*' 'be/src/util/metrics.*'
+area properties     
'fe/fe-core/src/main/java/org/apache/doris/common/util/PropertyAnalyzer.java' \
+                    
'fe/fe-core/src/main/java/org/apache/doris/datasource/property/**' \
+                    
'fe/fe-common/src/main/java/org/apache/doris/job/cdc/DataSourceConfigKeys.java'
+area conf-files     'conf/fe.conf' 'conf/be.conf' 
'conf/apache_hdfs_broker.conf' 'bin/*.sh'
+area deps           'fe/pom.xml' 'thirdparty/vars.sh' 'be/CMakeLists.txt'
+area build-modules  'build.sh' 'fe/be-java-extensions/pom.xml'
+
+# Quick-read extracts (only +/- lines, drop file headers)
+for f in fe-config session-var be-config ms-config grammar functions 
schema-tables conf-files deps; do
+  { echo "===== $f ====="; grep -E '^[+-]' "$OUT/$f.diff" | grep -vE 
'^[+-]{3}' ; echo; } >> "$OUT/summary.txt" 2>/dev/null || true
+done
+
+# Config/variable identifiers that appear in the diffs — handy for 
check-version-claims.py
+{
+  grep -hoE '^\+\s*public static [a-zA-Z_<>\[\]]+ [a-z0-9_]+ *=' 
"$OUT/fe-config.diff" | sed -E 's/.* ([a-z0-9_]+) *=/\1/' | sed 's/^/fe-config 
/'
+  grep -hoE '^\+.*name = "[a-z0-9_]+"' "$OUT/session-var.diff" | sed -E 
's/.*name = "([a-z0-9_]+)".*/\1/' | sed 's/^/session-var /'
+  grep -hoE '^\+.*VarAttr\(name = [A-Z0-9_]+' "$OUT/session-var.diff" | sed -E 
's/.*name = ([A-Z0-9_]+).*/\1/' | sed 's/^/session-var-const /'
+  grep -hoE '^\+DE(FINE|CLARE)_[A-Za-z0-9]+\([a-z0-9_]+' "$OUT/be-config.diff" 
| sed -E 's/.*\(//' | sed 's/^/be-config /'
+  grep -hoE '^-\s*public static [a-zA-Z_<>\[\]]+ [a-z0-9_]+ *=' 
"$OUT/fe-config.diff" | sed -E 's/.* ([a-z0-9_]+) *=/\1/' | sed 's/^/REMOVED 
fe-config /'
+  grep -hoE '^-DE(FINE|CLARE)_[A-Za-z0-9]+\([a-z0-9_]+' "$OUT/be-config.diff" 
| sed -E 's/.*\(//' | sed 's/^/REMOVED be-config /'
+} | sort -u > "$OUT/identifiers.txt" || true
+
+echo "wrote $OUT/summary.txt ($(wc -l < "$OUT/summary.txt") lines) and $(ls 
"$OUT"/*.diff | wc -l | tr -d ' ') diff files"
+echo "identifiers: $OUT/identifiers.txt ($(wc -l < "$OUT/identifiers.txt") 
entries)"
diff --git a/doc-tools/skills/doris-release-docs/scripts/validate-docs.py 
b/doc-tools/skills/doris-release-docs/scripts/validate-docs.py
new file mode 100755
index 00000000000..2d8cc8483f9
--- /dev/null
+++ b/doc-tools/skills/doris-release-docs/scripts/validate-docs.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+"""Static checks for changed doris-website docs. No yarn build — this is the
+only verification the doc trees get, so run it before every commit.
+
+usage:
+  validate-docs.py <website-repo> [--base <git-ref>] [--forbidden <file>]
+
+Checks every .md/.mdx that differs from --base (default: working tree vs
+index+HEAD via `git status`) under docs/, versioned_docs/ and i18n/:
+  1. JSON front matter parses; "language" matches the tree (en / zh-CN)
+  2. every relative markdown link resolves to an existing .md/.mdx
+  3. .mdx files: no bare <tag> outside code fences / inline code that is not
+     a known component (MDX would treat it as JSX and fail the build)
+  4. no CJK characters in lines ADDED to English trees
+  5. added lines contain none of the forbidden identifiers (--forbidden:
+     one pattern per line, e.g. unreleased-feature config names)
+  6. markdown tables in added lines keep a consistent column count within
+     each table block (a row with a blockquote between rows splits a table)
+Exit code 1 if anything fails.
+"""
+import json, os, re, subprocess, sys
+
+KNOWN_TAGS = 
{'Tabs','TabItem','details','summary','br','img','b','i','p','table','tr','td','th','thead','tbody',
+              
'a','code','div','span','strong','em','ul','li','ol','hr','sup','sub','h1','h2','h3','h4','h5','h6',
+              
'center','font','pre','video','source','iframe','DorisVideo','Video','Tab','Admonition','kbd','u',
+              
'Head','Link','style','script','details','figure','figcaption','picture','svg','path','g','rect','circle','line','text'}
+
+def run(cmd, cwd):
+    return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True).stdout
+
+def changed_files(root, base):
+    if base:
+        out = run(['git', 'diff', '--name-only', base], root)
+        files = out.split()
+    else:
+        out = run(['git', 'status', '--porcelain'], root)
+        files = [l[3:].strip() for l in out.splitlines()]
+    return sorted({f for f in files if f.endswith(('.md', '.mdx'))
+                   and f.startswith(('docs/', 'versioned_docs/', 'i18n/'))})
+
+def added_lines(root, base, rel):
+    args = ['git', 'diff', '--unified=0'] + ([base] if base else []) + ['--', 
rel]
+    out = run(args, root)
+    return [l[1:] for l in out.splitlines() if l.startswith('+') and not 
l.startswith('+++')]
+
+def main():
+    args = sys.argv[1:]
+    if not args:
+        print(__doc__); sys.exit(1)
+    root = os.path.abspath(args[0]); base = None; forb = []
+    if '--base' in args: base = args[args.index('--base') + 1]
+    if '--forbidden' in args:
+        forb = [l.strip() for l in open(args[args.index('--forbidden') + 1]) 
if l.strip() and not l.startswith('#')]
+    files = changed_files(root, base)
+    problems = []
+    for rel in files:
+        p = os.path.join(root, rel)
+        if not os.path.isfile(p):
+            continue
+        t = open(p, encoding='utf-8').read()
+        en_tree = rel.startswith(('docs/', 'versioned_docs/'))
+        # 1 front matter
+        if not t.startswith('---\n'):
+            problems.append((rel, 'no front matter'))
+        else:
+            raw = t[4:t.index('\n---\n', 3) + 1].strip()
+            fm = None
+            if raw.startswith('{'):
+                try:
+                    fm = json.loads(raw)
+                except Exception as e:
+                    problems.append((rel, f'front matter is not valid JSON: 
{e}'))
+            else:  # YAML front matter (key-features, community docs): only 
sanity-check the shape
+                if not all(re.match(r'^(\s*-|\s*\w[\w.-]*\s*:|\s*$)', l) for l 
in raw.split('\n')):
+                    problems.append((rel, 'front matter is neither JSON nor 
simple YAML'))
+                m = re.search(r'^language:\s*"?([\w-]+)', raw, re.M)
+                fm = {'language': m.group(1)} if m else {}
+            if fm is not None:
+                want = 'en' if en_tree else 'zh-CN'
+                lang = fm.get('language')
+                if lang is not None and lang != want and not (want == 'en' and 
lang == 'en-US'):
+                    problems.append((rel, f'language={lang}, expected {want}'))
+        # 2 relative links
+        d = os.path.dirname(p)
+        for m in re.finditer(r'\]\((\.\.?/[^)#\s]+)(#[^)\s]*)?\)', t):
+            f = os.path.normpath(os.path.join(d, m.group(1)))
+            if not (os.path.exists(f) or os.path.exists(f + '.md') or 
os.path.exists(f + '.mdx')):
+                problems.append((rel, f'broken link {m.group(1)}'))
+        # 3 mdx raw tags
+        if rel.endswith('.mdx'):
+            imported = set(re.findall(r'^import\s+(\w+)', t, re.M))
+            for grp in re.findall(r'^import\s*\{([^}]*)\}', t, re.M):
+                imported.update(x.strip().split(' as ')[-1] for x in 
grp.split(',') if x.strip())
+            infence = False
+            for i, l in enumerate(t.split('\n'), 1):
+                if l.strip().startswith('```'):
+                    infence = not infence; continue
+                if infence: continue
+                s = re.sub(r'`[^`]*`', '', l)
+                for mm in re.finditer(r'<([A-Za-z][A-Za-z0-9_-]*)', s):
+                    if mm.group(1) not in KNOWN_TAGS and mm.group(1) not in 
imported:
+                        problems.append((rel, f'line {i}: bare <{mm.group(1)}> 
outside code (MDX/JSX risk)'))
+        added = added_lines(root, base, rel)
+        # 4 CJK in English trees
+        if en_tree:
+            for l in added:
+                if re.search(r'[\u4e00-\u9fff]', l):
+                    problems.append((rel, f'Chinese text in English tree: 
{l.strip()[:80]}'))
+        # 5 forbidden identifiers
+        for pat in forb:
+            for l in added:
+                if re.search(pat, l):
+                    problems.append((rel, f'forbidden "{pat}": 
{l.strip()[:80]}'))
+        # 6 table column consistency (per contiguous table block in the full 
file)
+        block = []
+        infence = False
+        for i, l in enumerate(t.split('\n') + [''], 1):
+            if l.strip().startswith('```'):
+                infence = not infence
+                l = ''
+            if not infence and l.lstrip().startswith('|'):
+                cells = re.sub(r'`[^`]*`', '', l).replace('\\|', '')
+                block.append((i, cells.strip().count('|')))
+            else:
+                if len(block) >= 2:
+                    counts = {c for _, c in block}
+                    if len(counts) > 1:
+                        problems.append((rel, f'table at lines 
{block[0][0]}-{block[-1][0]} has inconsistent column counts {sorted(counts)}'))
+                block = []
+    print(f'checked {len(files)} changed doc files')
+    if problems:
+        for rel, msg in problems:
+            print(f'  {rel}: {msg}')
+        print(f'{len(problems)} problem(s)')
+        sys.exit(1)
+    print('no problems')
+
+if __name__ == '__main__':
+    main()


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

Reply via email to