One sentence of the documentation front page names a Lucene version that does not exist. The link next to it and the Migration Guide itself are correct.
./gradlew :lucene:documentation:createDocumentationIndexturnslucene/documentation/src/markdown/index.template.mdintoindex.html, the first page of the documentation (docs/index.htmlin the binary release).- Groovy's
SimpleTemplateEnginefills in the template: each${...}is a Groovy expression over values the build supplies, hereproject.versionandproject.majorVersion. - In Groovy,
-on a number subtracts, but-on a String removes the first occurrence of the right-hand text:12 - 1is 11,"12" - 1is "2".
- You open the documentation front page,
docs/index.htmlof the Lucene 10.6.0 binary release, and read the "Migration Guide" entry. - You expect "What changed in Lucene 10; how to migrate code from Lucene 9.x.", as 10.5.2 says: the page template writes
${project.majorVersion-1}, the major version minus 1. - Instead it says "... how to migrate code from Lucene 0.x." A build of main says "Lucene 12; ... from Lucene 2.x", branch_11x "Lucene 11; ... from Lucene 1.x". The build prints no warning.
- Why: since a 2025 build refactor (backported to 10.x in 2026), the major version reaches the template as the text "10", not the number 10. The template engine is Groovy, where
"10" - 1removes the first "1" from the text: "10" becomes "0", "12" becomes "2". With the number, 10 - 1 = 9.
Anyone reading the 10.6.0 documentation front page (in the binary release now; on the website if that page is published from the same build), and every documentation build of main, branch_11x and branch_10_6. Default build, no opt-in, no warning. Only one descriptive sentence is wrong: the link works, and nothing in code, indexes or APIs changes.
Mechanism, terms and numbers459 words
The front page of the 10.6.0 documentation (docs/index.html in the binary release, line 38) reads "What changed in Lucene 10; how to migrate code from Lucene 0.x." It should say 9.x, as 10.5.2 did. The template line is What changed in Lucene ${project.majorVersion}; how to migrate code from Lucene ${project.majorVersion-1}.x. (index.template.md:51-52), evaluated by Groovy's SimpleTemplateEngine. Since #14862 (main, 2025-07-01), and on the 10.x line since the build-infra backport #16592 (2026-09-05), the build's majorVersion is a String (LuceneBuildGlobalsExtension.java:51), and DocumentationConfigPlugin.java:233 binds it unchanged. For a String, Groovy's - removes the first occurrence of the operand's text: "10" - 1 is "0", "12" - 1 is "2". Through 10.5.2 the template called a closure that returned an int, ${project.majorVersion()-1}. Main builds print "Lucene 12; ... from Lucene 2.x.", branch_11x "Lucene 11; ... from Lucene 1.x.". Only this sentence is wrong: the link opens the correct Migration Guide and nothing in code, indexes or APIs changes. Binding Integer.parseInt(buildGlobals.majorVersion) restores "9.x" (main: "11.x"); the rebuilt page changes in that one line only.
- The 10.6.0 binary release's
docs/index.html, line 38:<li><a href="MIGRATE.html">Migration Guide</a>: What changed in Lucene 10; how to migrate code from Lucene 0.x.</li>. 10.5.2's page has "... from Lucene 9.x." on the same line. The 10.6.0 website copy of the page is not published yet (HTTP 404 on 2026-11-02, while10_6_0/MIGRATE.htmlis live). - A build of main
e1faeba08bdeprints "What changed in Lucene 12; how to migrate code from Lucene 2.x."; branch_11x "Lucene 11; ... 1.x."; branch_10_6, the 10.6.1 line, "Lucene 10; ... 0.x.".
RegisterBuildGlobalsPlugin.java:149-152returns the major version as the regex group of the version string, aString;:128stores it:globals.majorVersion = majorVersion;.LuceneBuildGlobalsExtension.java:50-51:/** Major Lucene version ({@code x} in {@code x.y.z}). */ public String majorVersion;. A Gradle init script on the real build printedmajorVersion=12 type=java.lang.String.DocumentationConfigPlugin.java:227-233binds it unchanged:Map.entry("majorVersion", buildGlobals.majorVersion)(line 234 at the 10.6.0 tag).DocumentationConfigPlugin.java:331-336evaluates the template withnew SimpleTemplateEngine();index.template.md:51-52:What changed in Lucene ${project.majorVersion}; how to migrate code from Lucene ${project.majorVersion-1}.x.- Groovy's
String.minus(Object)removes the first"1": String "10" gives 0, "11" gives 1, "12" gives 2; Integer 10 gives 9, Integer 12 gives 11 (executed with Groovy 4.0.33, the version the main build's Gradle 9.8.1 ships, by checkers independently; a cross-model review got the same result for "11" and Integer 11 with Groovy 6.0.0 jars).
Through 10.5.2 the value came from an int closure (build.gradle:71: // "majorVersion" is an integer with just the major version. Compute it.; :77: return m[0][1] as int), and the template called ${project.majorVersion()-1}. The build has no other reader of buildGlobals.majorVersion at e1faeba08bde.
Exposure and likelihood189 words
- Who meets it: anyone reading the documentation front page of the 10.6.0 binary release, or the website page if it is published from that build; a later 10.6.x or 11.x release built from the current branches before a fix would carry it too. The page is the documentation's entry page.
- What they get: a sentence naming a version that does not exist ("0.x", "1.x", "2.x"). The link next to it opens the correct guide, so nobody is sent to the wrong one.
- What does not happen: no effect on code, indexes, APIs, queries or results.
- Scale: one line of one generated page, but deterministic on every documentation build of the affected branches, and it would repeat for later majors while the value stays a String.
- How much it matters: little. The link works and readers will likely see "0.x" as a glitch. Still, it is a build-code regression (a type change in a refactor that expected no functional change), not a typo; it is in a released artifact; the fix is one line; and this page is already being fixed for 10.6.1 (#16939, #16941).
One JUnit test, one method, no index: it reads a built index.html (directory in -Ddocs.dir), takes N from "What changed in Lucene N" and asserts that the sentence names N-1. It fails on the 10.6.0 release page and on a main build, and passes on 10.5.2 and on the fixed build.
package org.apache.lucene;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestMigrationGuidePreviousMajor extends LuceneTestCase {
// Expected "from Lucene N-1.x"; actual: main "Lucene 12; ... from Lucene 2.x", 10.6.0 "Lucene 10; ... from Lucene 0.x".
public void testPreviousMajor() throws Exception {
String index = Files.readString(Path.of(System.getProperty("docs.dir"), "index.html"));
Matcher m = Pattern.compile("Lucene (\\d+); how to migrate code from\\s+Lucene (\\d+)\\.x").matcher(index);
assertTrue(m.find());
assertEquals(Integer.parseInt(m.group(1)) - 1, Integer.parseInt(m.group(2)));
}
}
cd $LUCENE && cp $REPRO/TestMigrationGuidePreviousMajor.java lucene/core/src/test/org/apache/lucene/ && ./gradlew :lucene:documentation:createDocumentationIndex && ./gradlew :lucene:core:test --tests org.apache.lucene.TestMigrationGuidePreviousMajor -Ptests.seed=DEADBEEF -Ptests.asserts=true "-Ptests.jvmargs=-Ddocs.dir=$PWD/lucene/documentation/build/site"
- Expected
- The sentence names N-1: "Lucene 12; how to migrate code from Lucene 11.x" on main (10.6.0: "Lucene 10; ... from Lucene 9.x").
- Observed
AssertionError: expected:<11> but was:<2>on a main build ("from Lucene 2.x");expected:<9> but was:<0>on the 10.6.0 release page ("from Lucene 0.x").
More files, environment, full output369 words
The test reads only page text, so the Lucene jars, the seed and -ea/-da cannot change the result; it uses no Lucene search or index API, only LuceneTestCase and its assertions, and the package only lets it run in :lucene:core:test. -Ptests.jvmargs passes the property to the forked test JVM; it replaces gradle's default extra test JVM flags, which does not matter here. The file has no Apache license header, so the build's checkLicenses would reject it in the tree.
The simplest demonstration needs no test: grep -n 'migrate code' lucene-10.6.0/docs/index.html prints line 38, "... how to migrate code from Lucene 0.x."; on main, run ./gradlew :lucene:documentation:createDocumentationIndex and grep lucene/documentation/build/site/index.html.
No regression test is suggested. The test needs a built page, so it does not fit gradlew test as it stands, and the fix is one line; it is evidence only. If committers want a guard, it is this same test.
| lucene | main `e1faeba08bde` (12.0.0-SNAPSHOT, Gradle wrapper 9.8.1) and the 10.6.0 release page: fail; 10.5.2 and 9.12.3 release pages and main with the fix: pass |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 (the only platform run) |
cd $LUCENE # main, branch_11x or branch_10_6
./gradlew :lucene:documentation:createDocumentationIndex
cp $REPRO/TestMigrationGuidePreviousMajor.java lucene/core/src/test/org/apache/lucene/
./gradlew :lucene:core:test --tests org.apache.lucene.TestMigrationGuidePreviousMajor -Ptests.seed=DEADBEEF -Ptests.asserts=true "-Ptests.jvmargs=-Ddocs.dir=$PWD/lucene/documentation/build/site"
# same failure with -Ptests.asserts=false; for the released page: -Ddocs.dir=<unpacked lucene-10.6.0>/docs
# gradle on a worktree of main e1faeba08bde after createDocumentationIndex, seed DEADBEEF, -Ptests.asserts=true (identical with false)
> Task :lucene:core:test FAILED
TestMigrationGuidePreviousMajor > testPreviousMajor FAILED
java.lang.AssertionError: expected:<11> but was:<2>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:3D9592C008903CD1]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:647)
at org.junit.Assert.assertEquals(Assert.java:633)
at org.apache.lucene.TestMigrationGuidePreviousMajor.testPreviousMajor(TestMigrationGuidePreviousMajor.java:16)
:lucene:core:test (FAILURE): 1 test, 1 failure
BUILD FAILED in 51s
# the standalone JUnit runner, seed DEADBEEF
# 10.6.0 release docs/index.html -ea FAIL expected:<9> but was:<0> -da FAIL (same)
# main e1faeba08bde build -ea FAIL expected:<11> but was:<2> -da FAIL (same)
# 10.5.2 release docs/index.html -ea PASS -da PASS
# main + fix (bind the int) -ea PASS -da PASS
# the page line, main build before and after the fix
38c38
< <li><a href="MIGRATE.html">Migration Guide</a>: What changed in Lucene 12; how to migrate code from Lucene 2.x.</li>
---
> <li><a href="MIGRATE.html">Migration Guide</a>: What changed in Lucene 12; how to migrate code from Lucene 11.x.</li>- The template's arithmetic,
${project.majorVersion-1}, exists to name the previous major; every release from 9.0.0 to 10.5.2 printed it right (10.5.2: "from Lucene 9.x"). - Through 10.5.2 the build documented the value as "an integer with just the major version" (
return m[0][1] as int). - The String came with #14862, a build refactor whose PR description says "no functional changes are expected".
The full argument300 words
- The sentence and its arithmetic came with LUCENE-9321 (
06df50e759a, "Port markdown task to Gradle (#1477)", Uwe Schindler, 2020-05-17, first released in 9.0.0), which added the template line${project.majorVersion()-1}together with the int closure (// "majorVersion" is an integer with just the major version.,return m[0][1] as int). Before that, the Ant build'sindex.xslhard-coded the sentence for each major (since LUCENE-4008,a20aa3e0c99, 2012); LUCENE-9577 (2020-10-09) only moved the template. - The build's own contract: 10.5.2
build.gradle:71:// "majorVersion" is an integer with just the major version. Compute it.;:77:return m[0][1] as int. - History of the type (four steps; commit-level builds): through 10.5.2
majorVersionwas a closure returning an int, called as${project.majorVersion()-1}. #14764 (42123980c25, 2025-06-14) evaluated the closure eagerly, so it became an int property, and switched the template to the property form; harmless: a build of6071569926f(#14879, the parent of #14862, which already has #14764's template and eager int) prints "Lucene 11; ... from Lucene 10.x.". #14862 (a7fc3d0ae83, 2025-07-01, "Gradle plugins to java, batch 1" by dweiss; PR description: "no functional changes are expected") introducedpublic String majorVersion; its build prints "... from Lucene 1.x.". #15350 (a0bd3c5f3c6) carried the String into the Java documentation task, and #16592 (f6fce7e42c5, 2026-09-05, a broad backport of main's build-infra changes) brought both to the 10.x line (parent "9.x", commit "0.x"). The searches found no discussion of the type or the sentence in these PRs. - What the link covers: 10.6.0's
MIGRATE.md:102is "## Migration from Lucene 9.x to Lucene 10.0". - No test or release check reads the sentence: no test in the main tree reads it, and the release smoke test only checks that the MIGRATE and CHANGES files exist.
The devil's-advocate check145 words
Strongest case for leaving it (the intended-behaviour devil's advocate and the cross-model lenses): String is the declared type of the build global (javadoc "Major Lucene version ({@code x} in {@code x.y.z})"), Groovy's String.minus does exactly what it is documented to do, and the sentence is cosmetic: the link works.
Why it fails: the consumer is Lucene's own template, whose arithmetic only makes sense on a number and was written when the build documented the value as an integer; the String came from a refactor that expected no functional change; and "from Lucene 0.x" has no intended reading. Being cosmetic does not make it intended; it keeps the priority Low, while the false version still ships in a released artifact and comes from a build-code regression. An independent reviewer from another model family (OpenAI GPT-5.6) rated the "intended" case weak in both of its lenses.
Recommended Bind the major version as an int (validated)
Bind the major version as a number in DocumentationConfigPlugin.java:233: Map.entry("majorVersion", Integer.parseInt(buildGlobals.majorVersion)). Validated on main: the rebuilt page changes only "2.x" to "11.x", the test passes with assertions on and off, and the build-infra checks (format, ecj, forbidden APIs, licenses) pass.
@@ -230,7 +230,7 @@ public class DocumentationConfigPlugin extends LuceneGradlePlugin {
"project",
Map.ofEntries(
Map.entry("version", project.getVersion().toString()),
- Map.entry("majorVersion", buildGlobals.majorVersion)));
+ Map.entry("majorVersion", Integer.parseInt(buildGlobals.majorVersion))));
task.getBinding()
.put(All options and trade-offs3 options ยท 380 words
One line in the documentation plugin (line 234 on branch_10_6). LFV-1087 is a second defect of the same front page (it lists benchmark-jmh javadocs that no build renders), and #16941 fixed a third one, the File Formats link, for 10.6.1. On main the fixed text "from Lucene 11.x" points at a MIGRATE.md section that does not exist yet (main's top section is "Migration from Lucene 10.x to Lucene 11.0"), which is normal before a major release.
DocumentationConfigPlugin.java:233 on main and branch_11x, :234 on branch_10_6 and the 10.6.0 tag.
@@ -230,7 +230,7 @@ public class DocumentationConfigPlugin extends LuceneGradlePlugin {
"project",
Map.ofEntries(
Map.entry("version", project.getVersion().toString()),
- Map.entry("majorVersion", buildGlobals.majorVersion)));
+ Map.entry("majorVersion", Integer.parseInt(buildGlobals.majorVersion))));
task.getBinding()
.put(Trade-offs
Smallest change; the build global keeps its String type. Integer.parseInt cannot fail for any real version: the \d+ regex group guarantees digits, and only a major above 2147483647 would overflow. Not applied to branch_10_6 here (same line text, one line lower).
What the checks validated
In a scratch worktree of main e1faeba08bde, OpenJDK 25.0.3, Gradle wrapper 9.8.1: createDocumentationIndex rebuilds the page, which differs from the stock build in line 38 only ("2.x" -> "11.x") and is byte-identical to the finder's fixed page; the minimal test through gradle passes with -Ptests.asserts=true and false (it fails on the stock page); ./gradlew :build-tools:build-infra-shadow:check, which re-applies Lucene's checks to the build-infra sources, passes (checkGoogleJavaFormat, ecjLint, forbiddenApisMain, checkLicenses, validatePlugins). Neither that project nor the documentation module has tests, so these are the closest checks. The worktree was removed afterwards.
index.template.md:52 becomes Lucene ${(project.majorVersion as int) - 1}.x.
Trade-offs
Fixes only this expression; any later template arithmetic on majorVersion hits the same trap, and the template is harder to read.
What the checks validated
Same scratch worktree, fix A reverted: the rebuilt page changes line 38 to "11.x" and the minimal test passes (-ea).
Declare public int majorVersion in LuceneBuildGlobalsExtension.java:51 and parse it once in RegisterBuildGlobalsPlugin.
Trade-offs
Restores the 10.5.2 contract ("an integer") for every consumer, but changes a public field of the build's extension and touches two files; at e1faeba08bde the documentation binding is its only reader.
Non-formal. A finder compared the 10.5.2 Groovy documentation build item by item with its Java port and diffed the shipped 10.5.2 and 10.6.0 front pages: "from Lucene 9.x" against "from Lucene 0.x". A Gradle init script on the real build then printed the template's value: majorVersion=12, type java.lang.String.
The method in full95 words
- Lead: the hunt unit started from another defect on the same page (the API Javadocs list links benchmark-jmh, now LFV-1087) and swept the rest of the Groovy-to-Java port of the documentation build (
gradle/documentation/*.gradleat 10.5.2 againstDocumentationConfigPlugin.java, 15 items). - Differential: the shipped 10.5.2 and 10.6.0 front pages differ in the Migration Guide sentence ("9.x" against "0.x").
- Build model: a Gradle init script printed the template binding on main, branch_11x and branch_10_6 (String every time) and on main with the fix (Integer).
- No model checker, SMT solver or prover was involved.
No public report found (2026-11-02: 49 scripted queries over GitHub, the mailing lists and the JIRA archive, 36 manual GitHub searches, all 410 open PRs' files, the 35 mails of the 10.6.0 release threads; rechecked the same day). A screenshot in #16939, a different link on the same page, happens to show the "0.x" line; nobody mentions it.
Searches2 searches
Related, not this bug: #16939 / #16941 (the File Formats link on the same page, fixed for 10.6.1 on 2026-10-29; the #16939 screenshot shows the "0.x" sentence, and neither its text, its 7 comments nor #16941 mention it); Uwe Schindler's 10.6.0 RC1 vote mail, which reviewed the same page and reported only the link; #14862, #15350 and #16592 (the introducing and carrying changes, no discussion of the type or the sentence); #15613 (open, touches the plugin, codec regex only).
Recheck on 2026-11-02: no apache/lucene issue or PR created on 2026-11-02 matches "migrate code from", "majorVersion" or "Migration Guide" (a fact-check query from 2026-11-01 found none either); the binding line is unchanged on main, branch_11x and branch_10_6, and the last commit touching the plugin on all three is #16941.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive), apache/lucene-site, Solr, Elasticsearch, OpenSearch; dev@, java-user@, issues@, commits@; the web | 2026-11-02 | scripted prior-report search (20 issue/PR, 3 commit, 2 downstream, 20 list, 4 JIRA queries; 542 hits, 0 high); 36 manual GitHub searches (symptom strings, code terms, last 48 h); file lists of all 410 open PRs; commits on the four fix-site files on main, branch_11x, branch_10_6, branch_10x; all 35 mails of the 10.6.0 release threads; 5 web searches | nothing reporting it; nearest #16939, #16941, the RC1 vote mail, #14862, #15350, #16592, #15613 |
| apache/lucene upstream (GitHub API) and the project website | 2026-11-02 | last commits on the plugin, the template and the globals class on main, branch_11x, branch_10_6; the binding line on each head; issue/PR searches since 2026-11-01; HTTP status of 10_6_0/index.html and 10_6_0/MIGRATE.html | still present on all three branches; no issue or PR; the 10.6.0 site front page is not published yet (404) |
Introduced on main by #14862 (a7fc3d0ae83, 2025-07-01), which made majorVersion a String; reached the 10.x line with the backport #16592 (f6fce7e42c5, 2026-09-05); first released in 10.6.0. Every release from 9.0.0 to 10.5.2 is correct, and commit-level builds flip from correct to wrong at exactly these two commits.
Versions tested7 rows
The template line and the int closure behind it date from LUCENE-9321 (06df50e759a, 2020-05-17, first released in 9.0.0); the Ant build hard-coded the sentence before (index.xsl, since LUCENE-4008, 2012). #14764 (42123980c25, 2025-06-14) made the int eager and switched the template to ${project.majorVersion-1} while the value was still an int: harmless (a build of 6071569926f, #14879, the parent of #14862, which already has #14764's template, prints "Lucene 11; ... 10.x."). #14862 made it a String on main; #15350 (a0bd3c5f3c6, 2025-10-22) carried it into the Java documentation task; #16592 brought both to branch_10x. git tag --contains f6fce7e42c5 lists only releases/lucene/10.6.0. The main pair was built on the same Gradle 8.14.6, so the commit is the only difference; #16592 itself moves 10.x from Gradle 8.14 to 9.7, so that pair used two Gradle versions, and both logs print the bound value and type.
| Version or commit | Verdict | Result |
|---|---|---|
10.6.0 (4ae946e5a9b, released 2026-10-29) | affected | binary release docs/index.html line 38: "Lucene 10; ... from Lucene 0.x."; the test fails with -ea and -da |
main e1faeba08bde | affected | "Lucene 12; ... from Lucene 2.x."; the test fails through gradle and the standalone JUnit runner, assertions on and off |
branch_11x da3ea39fa59, branch_10_6 91eaab4fb29 | affected | "Lucene 11; ... 1.x." and "Lucene 10; ... 0.x."; the test fails |
main a7fc3d0ae83 (#14862) vs parent 6071569926f | flips here (vs parent) | String "11": "... 1.x."; parent, Integer 11: "... 10.x." |
branch_10x f6fce7e42c5 (#16592) vs parent 260a5c772c4 | flips here (vs parent) | String "10": "... 0.x."; parent, Integer 10: "... 9.x." |
| 9.0.0 to 10.5.2 | not affected | int closure at every tag; the 9.12.3 and 10.5.2 release pages pass ("8.x", "9.x") |
| main + fix (bind the int) | not affected | "Lucene 12; ... from Lucene 11.x."; the test passes, build-infra checks pass |
Confirmed (high) by an adjudication after an independent reproducer, cross-model reviews (OpenAI GPT-5.6) and a devil's advocate all called it real; a credibility audit agreed, at Low. Re-run for this doc: a Gradle build of main, the test through gradle (fails), and both fixes (pass).
Checks and verdicts362 words
Every verifier ran a failing check on a stock page and a passing one on 10.5.2 or a fixed page. Both cross-model runs used the hunt's older checkout ea57360912ed (11.0.0-SNAPSHOT, same binding, field and template lines) and could not run Gradle, and the refute lens ran its Groovy check on the Groovy 6.0.0 jars named in gradle/libs.versions.toml, not on the Gradle-bundled Groovy 4.0.33 the documentation build uses; the e1faeba08bde executions come from the finder, the independent reproduction, the intended-behaviour check, the adjudication, the registration check and this doc. The 10.5.2 and 9.12.3 controls were read from the sha512-verified release archives (finder, the version-history runs) and from the website copies (by the independent reproducer); both give the same sentence.
| Check | Verdict | What it found |
|---|---|---|
| Finder (sibling sweep, page differential, build model) | reproduced (documentation) | The test fails on the 10.6.0 release page and on main, branch_11x and branch_10_6 builds, passes on 10.5.2 and the fixed build; the init script prints String (Integer with the fix). |
| Independent reproducer | real | Own test, own Gradle build of main and own sha512-verified 10.6.0 page: fail -ea/-da; 10.5.2, 9.12.3 and the Integer build pass; own Groovy check. |
| Cross-model adversary, intended lens (OpenAI GPT-5.6, executed) | real (high) | Own tests on the template and the supplied pages; the "String is the declared type" defence rated weak. |
| Cross-model adversary, refute lens (OpenAI GPT-5.6, executed) | real (high) | Ran the live template with "11" and got 1 instead of 10 (-ea, -da, a second seed; Groovy 6.0.0 jars); the Integer control passes. |
| Intended-behaviour devil's advocate | real | The strongest "intended" case fails; own Groovy run and own sha512-verified 10.6.0 page agree. |
| Adjudication | confirmed (high) | 4 of 4 runs fail as expected (main and 10.6.0 pages, -ea/-da); own Groovy check. |
| Registration check and credibility audit | confirmed (Low) | Fresh seeds 5EED0503 and A0D17004: the 10.6.0 and main pages fail, the fixed page passes; not fixed upstream. |
| This doc's runs (Gradle build, gradle test, fix validation) | reproduced; fix validated | The stock main build fails the test with assertions on and off; fix A passes it and the build-infra checks; fix B passes it. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 468 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,413 words
- Registry title
- The 10.6.0 documentation front page says the Migration Guide covers migrating 'from Lucene 0.x' (main builds: 'Lucene 12; ... from Lucene 2.x'), because the build binds majorVersion as a String and the template's ${project.majorVersion-1} then deletes the first '1' character instead of subtracting (documentation build defect)
- Area
- build-tools / documentation build (DocumentationConfigPlugin createDocumentationIndex template binding; lucene/documentation index.template.md)
- Priority
- Low, rank 1290: Documentation only and cosmetic: the generated 10.6.0 documentation front page and every later documentation build name a nonexistent previous version in the descriptive Migration Guide sentence. The link and the guide itself are correct, so it misleads no API use.
- Impact class
- minor
- Impact note
- Documentation only, with no code, index or API effect: every reader of the documentation front page (docs/index.html) of the 10.6.0 binary release, and of every documentation build of main, branch_11x and branch_10_6, reads a Migration Guide description that names a nonexistent previous version ('0.x' for 10, '1.x' for 11, '2.x' for 12); the standard build prints no warning, the MIGRATE.html link and the guide itself are correct, and the 10.6.0 site page, still unpublished on 2026-11-02, will carry the same text once published.
- Summary
- You open the documentation front page of Lucene 10.6.0 (docs/index.html) and read the Migration Guide entry. You expect 'What changed in Lucene 10; how to migrate code from Lucene 9.x.', as 10.5.2 says, because the template computes the previous major as ${project.majorVersion-1}; instead it says 'how to migrate code from Lucene 0.x.' (a main build says 'Lucene 12; ... from Lucene 2.x.'). Since the build's version globals became Java (#14862 on main, backported to 10.x in #16592), the template gets majorVersion as the String '10', and Groovy's String minus 1 deletes the first '1' character instead of subtracting; binding the value as an int restores 'from Lucene 9.x' (main: '11.x').
- Introduced
- a7fc3d0ae8314ebf1236eea3076096b836d19ca8 (apache/lucene#14862 'Gradle plugins to java, batch 1' by dweiss, main; PR text 'no functional changes are expected'): it replaced the build's int majorVersion ('"majorVersion" is an integer with just the major version', 'm[0][1] as int') with LuceneBuildGlobalsExtension's 'public String majorVersion' [read: git show; exec: commit-level Gradle builds, its parent 6071569926f renders 'Lucene 11; ... from Lucene 10.x.' (Integer 11) and a7fc3d0ae83 renders '... from Lucene 1.x.' (String "11"), on the same Gradle 8.14.6]. a0bd3c5f3c6 (#15350, 2025-10-22) carried the same String into the Java documentation task (DocumentationConfigPlugin: Map.entry("majorVersion", buildGlobals.majorVersion)). On the 10.x line it arrived with f6fce7e42c5 (#16592 'Build infra modernization (backport)' by dweiss, branch_10x) [exec: parent 260a5c772c4 renders '9.x', f6fce7e42c5 renders '0.x']. The template line and its int closure date from LUCENE-9321 (06df50e759a, 2020-05-17, first in 9.0.0; LUCENE-9577 only moved the template) and were correct until then; #14764 (42123980c25, 2025-06-14) changed the template to ${project.majorVersion-1} while the value was still an int, which was harmless [exec at 6071569926f]. A regression of the build's value type, not a day-one bug (2025-07-01 on main (a7fc3d0ae83, author date = commit date 2025-07-01T11:34:44+02:00); 2026-09-05 on branch_10x (f6fce7e42c5, author date = commit date 2026-09-05T11:48:44+02:00))
- First affected version
- 10.6.0 [exec] (released 2026-10-29; the only release tag that contains f6fce7e42c5); last unaffected 10.5.2 [exec]
- Affected versions
- 10.6.0 (released 2026-10-29, tag releases/lucene/10.6.0 -> 4ae946e5a9b) [exec: the sha512-verified binary distribution's docs/index.html line 38 reads 'What changed in Lucene 10; how to migrate code from Lucene 0.x.'; the test fails with -ea and -da (the finder, the independent reproduction against 10.6.0, the adjudication and the registration check); the intended-behaviour check checked the page text independently]; unreleased main since a7fc3d0ae83 [exec: a Gradle createDocumentationIndex build of e1faeba08bd reads 'Lucene 12; ... from Lucene 2.x.' (finder and the independent reproduction); upstream main 254ce48660d read: binding, field and template unchanged], branch_11x [exec: da3ea39fa59 build, 'Lucene 11; ... from Lucene 1.x.'], branch_10_6, the 10.6.1 line [exec: 91eaab4fb29 build, 'Lucene 10; ... from Lucene 0.x.'] and branch_10x since f6fce7e42c5 [exec; the branch is decommissioned by #16911]. Not affected: every release 9.0.0 to 10.5.2 [read: ${project.majorVersion()-1} with the int closure at every tag; exec: the 9.12.3 and 10.5.2 release pages pass]. The 10.6.0 site page (lucene.apache.org/core/10_6_0/index.html) was not yet published on 2026-11-02 (HTTP 404, while 10_6_0/MIGRATE.html is live), so today the text ships in the binary distribution's docs/ and reaches the site when the page is published
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal; also reached by exhaustive check, code reading
- Upstream and prior reports
- none found (searched 2026-11-02; the finder's searches and those of the independent reproduction and the intended-behaviour check agree; rechecked the same day): a scripted prior-report search (20 GitHub issue and PR queries, 3 commit queries, 2 downstream-repository queries, 20 mailing-list queries over dev@, java-user@, issues@ and commits@, 4 JIRA-archive queries; 542 hits, 0 high, 12 medium, all term-overlap noise), 36 manual GitHub searches on apache/lucene, apache/lucene-site, apache/solr, Elasticsearch and OpenSearch (symptom strings, code terms and the last 48 h), the file lists of all 410 open apache/lucene PRs, the upstream commits touching the four fix-site files on main, branch_11x, branch_10_6 and branch_10x (author and committer dates compared), all 35 mails of the 10.6.0 release threads on dev@, and 5 web searches. Nearest items, not reports of this bug: #16939 (a different defect on the same page, the File Formats link from the codec regex, fixed by #16941 on main, branch_11x and branch_10_6 on 2026-10-29), whose issue-body screenshot happens to show the '0.x' sentence with no comment on it; Uwe Schindler's 10.6.0 RC1 vote mail on dev@, which reviewed the same front page and reported only the link; #14862, #15350 and #16592 (the introducing and carrying changes, with no discussion of the type or the sentence). Not fixed upstream: the last commit touching DocumentationConfigPlugin.java on main, branch_11x and branch_10_6 is #16941 (codec regex only, authored 2026-10-29); index.template.md and LuceneBuildGlobalsExtension.java are unchanged since the porting commits; the only open PR touching DocumentationConfigPlugin.java (#15613) changes only the codec regex. Recheck (2026-11-02): no apache/lucene issue or PR created since late on 2026-11-01, and the two items updated in the hour before the recheck (#16877 release wizard, #16700 QueryRescorer) are unrelated
- Lucene commit tested
- Main
e1faeba08bde(12.0.0-SNAPSHOT, Gradle wrapper 9.8.1):createDocumentationIndexbuilt in a scratch worktree for this doc; the minimal test through gradle fails with assertions on and off (expected:<11> but was:<2>); with the one-line fix the page changes only in line 38 ("2.x" -> "11.x"), the test passes both ways and:build-tools:build-infra-shadow:checkpasses; a template-side fix also passes. The 10.6.0 release page (sha512-verified binary archive): fails with-eaand-da(expected:<9> but was:<0>). The 10.5.2 and 9.12.3 release pages pass (read from the sha512-verified release archives by the hunt and its follow-up, and from the website copies by an independent reproducer). Builds of branch_11xda3ea39fa59and branch_10_691eaab4fb29, and commit-level builds of #14862 and #16592 and their parents, by the hunt and its follow-up. OpenJDK 25.0.3, Linux aarch64.
- Finding record
Lucene main e1faeba08bde build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/documentation/DocumentationConfigPlugin.java:227-233 (binding 'project' = Map.ofEntries(version, Map.entry("majorVersion", buildGlobals.majorVersion)))Lucene main e1faeba08bde build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/globals/LuceneBuildGlobalsExtension.java:50-51 ('Major Lucene version'; public String majorVersion)Lucene main e1faeba08bde build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/globals/RegisterBuildGlobalsPlugin.java:128 (globals.majorVersion = majorVersion), :149-152 (getMajorVersion returns the regex group, a String)lucene/documentation/src/markdown/index.template.md:51-52 (at e1faeba08bde) ('What changed in Lucene ${project.majorVersion}; how to migrate code from Lucene ${project.majorVersion-1}.x.')
- The finding in 30 seconds
a Lucene main checkout (e1faeba08bde) build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/globals/RegisterBuildGlobalsPlugin.java:40, 56, 128, 149-152a Lucene main checkout (e1faeba08bde) build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/globals/LuceneBuildGlobalsExtension.java:50-51a Lucene main checkout (e1faeba08bde) build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/documentation/DocumentationConfigPlugin.java:30, 227-233, 331-336a Lucene main checkout (e1faeba08bde) lucene/documentation/src/markdown/index.template.md:51-52a Lucene clone releases/lucene/10.6.0 (4ae946e5a9b) DocumentationConfigPlugin.java:234, index.template.md:51-52; releases/lucene/10.5.2 (145d89cd30c) build.gradle:71-77, index.template.md:51-52
- Minimal repro
a Lucene main checkout gradle/wrapper/gradle-wrapper.properties:4 (Gradle 9.8.1)
- Why it looks unintended
a Lucene clone releases/lucene/10.5.2 build.gradle:71-77; releases/lucene/10.6.0 lucene/MIGRATE.md:102
- Suggested fix
a Lucene main checkout build-tools/build-infra-shadow/build.gradle (redirects its sources to build-infra so the checks apply); lucene/MIGRATE.md:20
- History
a Lucene clone and the full-history clone: git show 06df50e759a, a20aa3e0c99, 6071569926f, 42123980c25, a7fc3d0ae83, a0bd3c5f3c6, f6fce7e42c5; git tag --contains f6fce7e42c5
- Diagrams
a Lucene main checkout (e1faeba08bde) lucene/documentation/src/markdown/index.template.md:51-52; build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/documentation/DocumentationConfigPlugin.java:233
- TestMigrationGuidePreviousMajor.java (one test class):
repro/LFV-1086/TestMigrationGuidePreviousMajor.java(18 lines)
Made from the template line, the binding, the shipped 10.5.2 and 10.6.0 front pages and the Groovy check.
assets/LFV-1086/bug.png(468 KB)
