All findings · Lucene finding · LFV-942 · lucene94-fnm-javadoc-grammar-stale

The default codec's .fnm file-format javadoc gives a wrong field grammar: it leaves out the IndexOptions byte and later elements, and its tables miss codes Lucene writes

The javadoc that defines the default codec's field infos file (.fnm) lists each field's bytes without the IndexOptions byte written since 5.0.0 and five elements added later, and its tables miss codes Lucene writes. Lucene reads its own files fine; anyone decoding .fnm from this javadoc misreads the first field.

Summary

Priority
Low, rank 1278The default .fnm grammar has been wrong since 5.0 and misleads a re-implementer on the first field of every index.
Impact
minor
Since
5.0.0 (LUCENE-6013, 2014) for the sequence; 4.2.0 for the doc-values tableintroduced 2014-10-21 · over 12 years
Affected
5.0.0 to 10.6.0 and main
Status
documented
Found by
non-formal: parsing default-codec index files with the javadoc's grammar

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

The javadoc that documents the default .fnm format lists a field record's bytes in an order the writer has not used since 5.0.0.

Background: the .fnm file and its javadoc grammar

  1. Every Lucene segment has one .fnm file. For each field it records the name and number and what is indexed for it: postings options, doc values, points, vectors.
  2. Each codec format class documents its file as a grammar in its class javadoc: an ordered list of named elements, each with a type (Byte, VInt, String, ...). The default codec's file-format overview links these pages "for details".
Diagram of the bug. Bytes 45-60 of _0.fnm for one text field f: 01 66, 00, 00, 03, 00, 00, eight ff bytes, 02. Left, the javadoc's reading: FieldName f, FieldNumber 0 and FieldBits 0 match; DocValuesBits 3, DocValuesGen -65536 and an attribute count of 49151 are wrong. Right, the order write() uses: FieldName, FieldNumber, FieldBits, IndexOptions 3, DocValuesType 0, DocValuesSkipIndexType 0, DocValuesGen -1, attribute count 2. Expected: doc values NONE, DocValuesGen -1, 2 attributes, no points, no vectors. Actual: DocValuesBits 3 (SortedDocValues), DocValuesGen -65536, attribute count 49151, then java.io.EOFException. Also stale: the sequence lacks IndexOptions, DocValuesSkipIndexType, PointIndexDimensionCount and the three vector elements; the tables lack FieldBits 0x8 and 0x10, doc-values codes 4 and 5, IndexOptions 5 and VectorEncoding 2.
Bytes 45-60 of a one-field .fnm: the javadoc's reading (left) against the order write() uses (right). The javadoc reads the IndexOptions byte 3 as DocValuesBits, and every later value is off. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You index one document with one text field, f = "a", with the default codec, and decode its .fnm file in the order the Lucene94FieldInfosFormat javadoc gives: FieldName, FieldNumber, FieldBits, DocValuesBits, DocValuesGen, ...
  2. You expect FieldName "f", FieldNumber 0 and FieldBits 0, then DocValuesBits 0, because the field has no doc values.
  3. The first three match, but the next byte is 3, which the javadoc's table reads as "SortedDocValues". From there every value is off: DocValuesGen reads -65536 instead of -1, the attribute count 49151 instead of 2, and the parse runs off the end of the file.
  4. Why: since 5.0.0 the writer puts an IndexOptions byte right after FieldBits (3 = DOCS_AND_FREQS_AND_POSITIONS). The javadoc lists its type but never placed it in the sequence. Later releases added a skip-index byte, PointIndexDimensionCount and three vector elements, also never placed.
  5. The tables are stale too: doc-values codes 4 and 5, FieldBits 0x8 and 0x10, IndexOptions 5 and VectorEncoding 2 are written but not listed. Lucene's reader matches its writer, so only the text is wrong.

Who is affected

People who read or re-implement the .fnm format from Lucene's documentation: ports to other languages, index inspectors, recovery tools. For them it fails on the first field of every default-codec .fnm that has at least one field. Lucene users who only call its APIs are not affected; nothing wrong happens at runtime.

Mechanism, terms and numbers674 words

The class javadoc of Lucene94FieldInfosFormat, the field infos format of the default codec (Lucene104Codec.java:72), gives the .fnm file as Header, FieldsCount, <FieldName, FieldNumber, FieldBits, DocValuesBits, DocValuesGen, Attributes, DimensionCount, DimensionNumBytes>^FieldsCount, Footer (Lucene94FieldInfosFormat.java:48-50 on main 645d4fe14a20). The writer (:431-456) writes, per field, FieldName, FieldNumber, FieldBits, IndexOptions, a doc-values type byte, a doc-values skip-index byte, DocValuesGen, Attributes, PointDimensionCount, PointIndexDimensionCount and PointNumBytes only when the dimension count is non-zero, then VectorDimension, VectorEncoding and VectorSimilarityFunction; the reader (:147-213) reads the same order.

IndexOptions is typed as a Byte (:58) but never placed; the skip-index byte and PointIndexDimensionCount appear nowhere; the vector elements are described (:104-118) but not placed. The tables miss written values: DocValuesBits is described as a norms nibble plus a doc-values nibble with codes 0..3 (:88-97), but the byte holds one code, and SORTED_SET is 4 and SORTED_NUMERIC 5; FieldBits lists 0x1, 0x2 and 0x4 but 0x8 (soft-deletes field) and 0x10 (parent field) are set; IndexOptions 5 (since 10.5.0) and VectorEncoding 2 (FLOAT16, main only) are missing.

A parser that follows the javadoc reads a text field's IndexOptions byte (3) as "SortedDocValues" and is misaligned from the first field on. The sequence has been wrong since 5.0.0: LUCENE-6013 moved IndexOptions into its own byte and updated the type list, not the sequence. Later changes edited parts of the javadoc (6.0.0 added the point elements to the sequence, 9.0.0 and 9.4.0 described the vector elements) but none placed IndexOptions or brought the sequence and the tables up to date. Documentation only; the fix is javadoc text.

The documented record (main 645d4fe14a20, Lucene94FieldInfosFormat.java:48-50)

FieldInfos (.fnm) --> Header,FieldsCount, <FieldName,FieldNumber,
FieldBits,DocValuesBits,DocValuesGen,Attributes,DimensionCount,DimensionNumBytes>
FieldsCount,Footer

What write() writes per field (:431-456; read() :147-213 mirrors it)

#elementtypein the javadoc sequence?
1FieldNameStringyes
2FieldNumberVIntyes (typed VInt, linked to writeInt)
3FieldBits (0x1, 0x2, 0x4, 0x8 soft-deletes, 0x10 parent)Byteyes; 0x8 and 0x10 not in the table
4IndexOptions (0..5)Byteno (typed at :58, never placed); 5 not in the table
5doc-values type (one code 0..5)Byteas "DocValuesBits": two nibbles, codes 0..3
6DocValuesSkipIndexType (0..1)Byteno (since 10.0.0)
7DocValuesGenInt64yes
8AttributesMap<String,String>yes
9PointDimensionCountVIntas "DimensionCount"
10PointIndexDimensionCount, only if #9 != 0VIntno (since 7.6.0)
11PointNumBytes, only if #9 != 0VIntas "DimensionNumBytes", unconditional
12VectorDimensionVIntno (described at :104)
13VectorEncoding (0..2)Byteno (described at :105-109 with 0..1)
14VectorSimilarityFunction (0..3)Byteno (described at :110-118)

The one-field example, byte by byte

For one TextField f = "a", the field record is 01 66 | 00 | 00 | 03 | 00 | 00 | ff ff ff ff ff ff ff ff | 02 ...(2 attributes)... | 00 | 00 | 01 | 00 (dumped by the fix's validation check, which parses it with the corrected grammar exactly to the footer):

  • Lucene reads: FieldName "f", FieldNumber 0, FieldBits 0, IndexOptions 3 (DOCS_AND_FREQS_AND_POSITIONS), doc values 0, skip index 0, DocValuesGen -1, 2 attributes (the per-field postings format and suffix), PointDimensionCount 0, VectorDimension 0, VectorEncoding 1 (FLOAT32), similarity 0 (EUCLIDEAN).
  • The javadoc's order takes 03 as DocValuesBits (low nibble 3 = SortedDocValues), the next 8 bytes, 00 00 ff ff ff ff ff ff, as DocValuesGen (-65536, not -1), and ff ff 02 as the attribute count (49151, not 2); reading 49151 map entries then throws java.io.EOFException at the end of the 153-byte file (the diagram's probe, assertions on and off). An independent verifier's literal-sequence test also measured -65536 for point and vector fields.

Also stale on the same page

The first line says "Lucene 9.0 Field Infos format." (:44); the field descriptions call the point elements PointDimensionCount and PointNumBytes (:102) while the sequence says DimensionCount and DimensionNumBytes; and the write() comment "pack the DV type and hasNorms in one byte" (:444) has been wrong since LUCENE-6006 (5.0.0).

Exposure and likelihood201 words
  • Who is hit. Anyone who decodes .fnm from the class javadoc instead of from read(): ports of Lucene to other languages, index inspectors, recovery or forensic tools, and developers learning the format.
  • Default vs opt-in. The default codec: Lucene104Codec installs this format (Lucene104Codec.java:72), and the file-format overview links this class for the .fnm file (lucene104/package-info.java:147, :243). Every field record carries the IndexOptions byte, so the mismatch shows on the first field of every default-codec .fnm with at least one field, whatever the field (a segment with no fields has no field record to misread).
  • Scale. A parser built from the sequence misreads the first field and every later one; one built from the tables rejects or misreads valid codes (any SORTED_SET or SORTED_NUMERIC field, any index with a soft-deletes or parent field). The hunt's grammar check found the documented grammar diverging on 125 of 125 .fnm files from 64 generated default-codec indexes, while the writer's layout parsed all of them to the footer.
  • What it does not do. No wrong results, no index change, no runtime path: Lucene's writer and reader agree, and TestLucene94FieldInfosFormat passes. No earlier report was found. Impact class minor, priority low.

Minimal repro

One JUnit test indexes one document with one TextField using the default codec and non-compound files, then reads the .fnm in the documented order. FieldsCount, FieldName, FieldNumber and FieldBits match; the next byte, documented as DocValuesBits, is 3. Deterministic, single thread, about 1.4 s; the same with assertions on and off.

TestLucene94FieldInfosFormatJavadoc.java (JUnit, one test)56 lines
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.codecs.lucene94;

import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.SegmentInfos;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

public class TestLucene94FieldInfosFormatJavadoc extends LuceneTestCase {

  public void testDocumentedFieldSequence() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      IndexWriterConfig iwc =
          new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()).setUseCompoundFile(false);
      try (IndexWriter w = new IndexWriter(dir, iwc)) {
        Document doc = new Document();
        doc.add(new TextField("f", "a", Field.Store.NO));
        w.addDocument(doc);
      }
      byte[] id = SegmentInfos.readLatestCommit(dir).info(0).info.getId();
      try (IndexInput in = dir.openInput("_0.fnm", IOContext.READONCE)) {
        CodecUtil.checkIndexHeader(in, "Lucene94FieldInfos", 0, Integer.MAX_VALUE, id, "");
        assertEquals("FieldsCount", 1, in.readVInt());
        assertEquals("FieldName", "f", in.readString());
        assertEquals("FieldNumber", 0, in.readVInt());
        assertEquals("FieldBits", 0, in.readByte());
        // expected 0 (no doc values); actual 3: the IndexOptions byte the documented sequence omits
        assertEquals("DocValuesBits, DocValues nibble", 0, in.readByte() & 0x0F);
      }
    }
  }
}
Command
cp TestLucene94FieldInfosFormatJavadoc.java $LUCENE/lucene/core/src/test/org/apache/lucene/codecs/lucene94/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.codecs.lucene94.TestLucene94FieldInfosFormatJavadoc -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
Expected
After FieldsCount 1, FieldName "f", FieldNumber 0 and FieldBits 0, DocValuesBits reads 0: the field has no doc values.
Observed
It reads 3 (expected:<0> but was:<3>), the IndexOptions byte the documented sequence leaves out; the same with assertions off.

All files of this repro: repro/LFV-942/

More files, environment, full output239 words
  • The test reads the header with CodecUtil.checkIndexHeader and then the documented elements one by one. The failing byte is written at Lucene94FieldInfosFormat.java:442 (indexOptionsByte), right before the real doc-values byte at :445.
  • Results: fails 1 of 1 on main with assertions on and off (seed DEADBEEF; seed 5EED1234 gives the same message; the test has no random input), standalone and through gradle. The hunt's larger tests, which also check the value tables, fail on main, the 10.6.0 release commit and 10.5.2. On 9.12.3 only its sequence test is evidence (it fails): its value-table test assumes the 10.0 skip-index byte and ends in an EOFException there.
  • Even simpler for a reader: put the sequence line (:48-50) next to write() (:431-456).
  • As a regression test: this test would not guard the javadoc. It transcribes today's documented order, and a doc fix leaves the bytes unchanged, so a regression form would read the corrected order (as the fix's validation check does).

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); the hunt's tests also fail on the 10.6.0 release commit and 10.5.2, and its sequence test on 9.12.3
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestLucene94FieldInfosFormatJavadoc.java $LUCENE/lucene/core/src/test/org/apache/lucene/codecs/lucene94/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.codecs.lucene94.TestLucene94FieldInfosFormatJavadoc -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)

Observed output

TestLucene94FieldInfosFormatJavadoc > testDocumentedFieldSequence FAILED
    java.lang.AssertionError: DocValuesBits, DocValues nibble expected:<0> but was:<3>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:AF4ECEFFEFFD713C]: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.apache.lucene.codecs.lucene94.TestLucene94FieldInfosFormatJavadoc.testDocumentedFieldSequence(TestLucene94FieldInfosFormatJavadoc.java:52)

Why it looks unintended

  • Written as an exact grammar, every element typed and linked to its writer method; the format overview points readers to it.
  • IndexOptions is typed as a Byte but never placed; the 2014 change that split it out updated only the type list.
  • The tables are wrong, not abbreviated: a norms nibble gone since 5.0.0, only 4 of 6 doc-values codes.
The full argument206 words
  • Contract text. Lucene94FieldInfosFormat.java:48-62: production notation with every element typed and linked to a DataOutput method. lucene104/package-info.java:50: "This document defines the index file formats used in this version of Lucene"; :188: "Details on each of these are provided in their linked pages."
  • History. In LUCENE-6013 (#7075, 2014) the author wrote "I'll update the javadocs for the format changes" and then "New patch, fixing docs"; the type list and the tables were updated, the sequence line was not, and none of the saved issue comments discusses the sequence, so this reads as an omission, not a decision. Later changes that added bytes or values changed the writer and reader together; some edited the javadoc (LUCENE-6861 put the point elements into the sequence, LUCENE-9322 and LUCENE-10577 described the vector elements), but none placed IndexOptions or the newer bytes, and none of their saved PR discussions mentions the stale sequence.
  • Tests. No test compares a format javadoc with the bytes. TestLucene94FieldInfosFormat tests codec behaviour (the BaseFieldInfoFormatTestCase round trips, and testVectorSimilarityFuncs checks SIMILARITY_FUNCTIONS directly) but never compares the javadoc grammar with the written bytes.
  • Precedent. Upstream has fixed file-format documentation errors of this kind before: LUCENE-713 (#1788), LUCENE-3072 (#4145), LUCENE-7532 (#8583, "Add back lost codec file format documentation").
The devil's-advocate check150 words

Strongest case for "intended" (the refute reviewer, the intended-lens cross-model reviewer and the devil's-advocate verifier; rated moderate): the format classes are @lucene.experimental (:121); LUCENE-2946 (#4020, 2012) moved the file-format docs from "bit-for-bit" to "high-level", and package-info.java:54 still says "This document attempts to provide a high-level definition"; the supported way to read .fnm is FieldInfosFormat.read; and the runtime round trip is tested.

Why it fails: in the same LUCENE-2946 thread rmuir wrote (2012-04-25) "we can go into detail, but we can't do bit-for-bit with even StandardCodec... For the simple metadata files, and even stored fields and postings its fine (for now)", and .fnm is such a simple metadata file. The page is presented in exact-looking production notation; its tables are wrong rather than abbreviated; and @lucene.experimental allows the format to change, not the current format to be described wrongly. Every verifier and the adjudication rejected the "intended" reading.

Suggested fix

Recommended A. Rewrite the grammar, type list and tables to match write()

Rewrite the class javadoc to match write(): place IndexOptions, the skip-index byte, the conditional point elements and the three vector elements; note how the older format versions differ; make the doc-values byte one code 0..5; add the missing codes. Javadoc only. Validated: it parses a 12-field segment to the footer; the suite and static checks pass.

All options and trade-offs3 options ยท 1,408 words

Three ways to resolve it; A was validated. Targets: main and branch_11x, plus branch_10_6 if wanted (branch_10x was decommissioned, #16911). The file is byte-identical on all of them and on the 10.6.0 tag, so the diff applies unchanged; for branch_10_6 drop the FLOAT16 line, since 10.6.0's VectorEncoding has no FLOAT16; the format-version paragraph applies there unchanged (10.6.0 also writes version 2 and reads 0 and 1).

Optional extras for the same change: the write() comment "pack the DV type and hasNorms in one byte" (:444, a code comment, not in the diff), and, on main, the frozen backward-codecs Lucene90FieldInfosFormat, which carries the same stale text (branch_10_6 also has Lucene60FieldInfosFormat).

A. Rewrite the grammar, type list and tables to match write() (validated by the checks) (recommended)

The new sequence is <FieldName,FieldNumber,FieldBits,IndexOptions,DocValuesType,DocValuesSkipIndexType,DocValuesGen,Attributes,PointDimensionCount,PointIndexDimensionCount?,PointNumBytes?,VectorDimension,VectorEncoding,VectorSimilarityFunction>^FieldsCount. The type list names every byte element and links the VInt ones to writeVInt; the tables gain FieldBits 0x8 and 0x10 (linked to IndexWriterConfig#setSoftDeletesField and #setParentField), IndexOptions 5, a single-code doc-values table 0..5, a skip-index table (0 NONE, 1 RANGE) and VectorEncoding 2; IndexOptions 1 is named DOCS, the current enum constant, not DOCS_ONLY; a new paragraph says this is format version 2 (written since 10.0) and that read() also accepts version 1 (9.10-9.12, no DocValuesSkipIndexType byte) and version 0 (9.4-9.9, also never FieldBits 0x10); the first line becomes "Lucene 9.4 Field Infos format.". One file, +32/-14 lines.

diff --git a/lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java b/lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java
index acd4bf55152..b0c37f52dd7 100644
--- a/lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java
+++ b/lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java
@@ -41,13 +41,18 @@ import org.apache.lucene.store.IndexInput;
 import org.apache.lucene.store.IndexOutput;
 
 /**
- * Lucene 9.0 Field Infos format.
+ * Lucene 9.4 Field Infos format.
  *
  * <p>Field names are stored in the field info file, with suffix <code>.fnm</code>.
  *
  * <p>FieldInfos (.fnm) --&gt; Header,FieldsCount, &lt;FieldName,FieldNumber,
- * FieldBits,DocValuesBits,DocValuesGen,Attributes,DimensionCount,DimensionNumBytes&gt;
- * <sup>FieldsCount</sup>,Footer
+ * FieldBits,IndexOptions,DocValuesType,DocValuesSkipIndexType,DocValuesGen,Attributes,
+ * PointDimensionCount,PointIndexDimensionCount?,PointNumBytes?,
+ * VectorDimension,VectorEncoding,VectorSimilarityFunction&gt; <sup>FieldsCount</sup>,Footer
+ *
+ * <p>This is format version 2, written since Lucene 10.0. The reader also accepts version 1 (Lucene
+ * 9.10 to 9.12), which has no DocValuesSkipIndexType byte, and version 0 (Lucene 9.4 to 9.9), which
+ * in addition never sets FieldBits 0x10.
  *
  * <p>Data types:
  *
@@ -55,8 +60,10 @@ import org.apache.lucene.store.IndexOutput;
  *   <li>Header --&gt; {@link CodecUtil#checkIndexHeader IndexHeader}
  *   <li>FieldsCount --&gt; {@link DataOutput#writeVInt VInt}
  *   <li>FieldName --&gt; {@link DataOutput#writeString String}
- *   <li>FieldBits, IndexOptions, DocValuesBits --&gt; {@link DataOutput#writeByte Byte}
- *   <li>FieldNumber, DimensionCount, DimensionNumBytes --&gt; {@link DataOutput#writeInt VInt}
+ *   <li>FieldBits, IndexOptions, DocValuesType, DocValuesSkipIndexType, VectorEncoding,
+ *       VectorSimilarityFunction --&gt; {@link DataOutput#writeByte Byte}
+ *   <li>FieldNumber, PointDimensionCount, PointIndexDimensionCount, PointNumBytes, VectorDimension
+ *       --&gt; {@link DataOutput#writeVInt VInt}
  *   <li>Attributes --&gt; {@link DataOutput#writeMapOfStrings Map&lt;String,String&gt;}
  *   <li>DocValuesGen --&gt; {@link DataOutput#writeLong(long) Int64}
  *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}
@@ -76,36 +83,47 @@ import org.apache.lucene.store.IndexOutput;
  *         <li>If the second lowest order-bit is set (0x2), norms are omitted for the indexed field.
  *         <li>If the third lowest-order bit is set (0x4), payloads are stored for the indexed
  *             field.
+ *         <li>If the fourth lowest-order bit is set (0x8), the field is the soft-deletes field (see
+ *             {@link org.apache.lucene.index.IndexWriterConfig#setSoftDeletesField}).
+ *         <li>If the fifth lowest-order bit is set (0x10), the field is the parent field (see
+ *             {@link org.apache.lucene.index.IndexWriterConfig#setParentField}).
  *       </ul>
  *   <li>IndexOptions: a byte containing index options.
  *       <ul>
  *         <li>0: not indexed
- *         <li>1: indexed as DOCS_ONLY
+ *         <li>1: indexed as DOCS
  *         <li>2: indexed as DOCS_AND_FREQS
  *         <li>3: indexed as DOCS_AND_FREQS_AND_POSITIONS
  *         <li>4: indexed as DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS
+ *         <li>5: indexed as DOCS_AND_CUSTOM_FREQS
  *       </ul>
- *   <li>DocValuesBits: a byte containing per-document value types. The type recorded as two
- *       four-bit integers, with the high-order bits representing <code>norms</code> options, and
- *       the low-order bits representing {@code DocValues} options. Each four-bit integer can be
- *       decoded as such:
+ *   <li>DocValuesType: a byte containing the field's doc values type:
  *       <ul>
  *         <li>0: no DocValues for this field.
  *         <li>1: NumericDocValues. ({@link DocValuesType#NUMERIC})
- *         <li>2: BinaryDocValues. ({@code DocValuesType#BINARY})
- *         <li>3: SortedDocValues. ({@code DocValuesType#SORTED})
+ *         <li>2: BinaryDocValues. ({@link DocValuesType#BINARY})
+ *         <li>3: SortedDocValues. ({@link DocValuesType#SORTED})
+ *         <li>4: SortedSetDocValues. ({@link DocValuesType#SORTED_SET})
+ *         <li>5: SortedNumericDocValues. ({@link DocValuesType#SORTED_NUMERIC})
+ *       </ul>
+ *   <li>DocValuesSkipIndexType: a byte containing the type of the doc values skip index:
+ *       <ul>
+ *         <li>0: no skip index. ({@link DocValuesSkipIndexType#NONE})
+ *         <li>1: a skip index over value ranges. ({@link DocValuesSkipIndexType#RANGE})
  *       </ul>
  *   <li>DocValuesGen is the generation count of the field's DocValues. If this is -1, there are no
  *       DocValues updates to that field. Anything above zero means there are updates stored by
  *       {@link DocValuesFormat}.
  *   <li>Attributes: a key-value map of codec-private attributes.
- *   <li>PointDimensionCount, PointNumBytes: these are non-zero only if the field is indexed as
- *       points, e.g. using {@link org.apache.lucene.document.LongPoint}
+ *   <li>PointDimensionCount: it is non-zero only if the field is indexed as points, e.g. using
+ *       {@link org.apache.lucene.document.LongPoint}. PointIndexDimensionCount and PointNumBytes
+ *       follow only if it is non-zero.
  *   <li>VectorDimension: it is non-zero if the field is indexed as vectors.
  *   <li>VectorEncoding: a byte containing the encoding of vector values:
  *       <ul>
  *         <li>0: BYTE. Samples are stored as signed bytes
  *         <li>1: FLOAT32. Samples are stored in IEEE 32-bit floating point format.
+ *         <li>2: FLOAT16. Samples are stored in IEEE 16-bit floating point format.
  *       </ul>
  *   <li>VectorSimilarityFunction: a byte containing distance function used for similarity
  *       calculation.

Trade-offs

  • The ? marks for the two conditional point elements follow the notation the default postings format javadoc already uses (Lucene104PostingsFormat.java:104); the description says they follow only when PointDimensionCount is non-zero.
  • Renaming DocValuesBits to DocValuesType is the one cosmetic choice; keeping the old name gives a smaller diff, but the text must still say the byte is a single code.
  • DOCVALUES_SKIPPER = 0x20 (:479, a leftover of #13449 that #13784 replaced before any release) is never written, so the text leaves it out.
  • The grammar is the current writer layout (format version 2, FORMAT_CURRENT at :467-471, written at :424). read() also accepts versions 0 and 1 (:138-145): it reads the skip-index byte only from version 2 (:189-194) and rejects the 0x10 bit below version 1 (:165-176); FORMAT_CURRENT is 0 at the 9.4.0-9.9.2 tags and 1 at 9.10.0-9.12.3. The fix states this in one paragraph; the validation parses current-format files only.
  • If the open WIP PR #13525 (multi-vector) lands, it adds one more element to the same grammar.

What the checks validated

Validated on 2026-11-01 in scratch worktrees of 645d4fe14a20 with the diff applied (removed afterwards), seed DEADBEEF, first for the first version of the diff and again for the final one shown here (format-version paragraph and the DOCS name added after the fact checks):

  • Fixed-text check (a validation helper, not the repro): a parser written from the new text, with the corrected value tables transcribed from it, reads the repro's one-field .fnm and a 12-field segment (11 fields plus the parent field setParentField adds) with every element and every new value (FieldBits 0x1, 0x2, 0x8, 0x10; IndexOptions 1, 3 and 5; doc-values 1, 4 and 5; skip index 1; a LongPoint with PointIndexDimensionCount 1 and PointNumBytes 8; FLOAT32 and FLOAT16 vectors) exactly to the footer, every value equal to Lucene's reader: 2 of 2 pass with assertions on and off. It covers the current writer layout (format version 2) only; the sentence on versions 0 and 1 rests on reading read() and FORMAT_CURRENT at the release tags.
  • ./gradlew :lucene:core:test --tests TestLucene94FieldInfosFormat --tests TestLucene94FieldInfosFormatFixedJavadoc: 14 tests (the suite's 12, the check's 2), BUILD SUCCESSFUL with assertions on and off, for both versions of the diff.
  • :lucene:core:check -x test (ecjLint, forbiddenApis, google-java-format, licenses) and :lucene:core:renderJavadoc: BUILD SUCCESSFUL for both versions; the rendered page of the final one shows the new sequence, the format-version paragraph and 1: indexed as DOCS. google-java-format rewrapped two list items of the first draft and the new paragraph of the final one; the diff shown is the formatted one.
  • The minimal repro still fails on the patched tree (expected:<0> but was:<3>, first version), as it must: a javadoc fix leaves the bytes unchanged.
  • The full Lucene test suite was not run (javadoc-only change).

B. Fix only the sequence line (not run)

Insert IndexOptions and the skip-index byte and add PointIndexDimensionCount and the vector elements to the sequence; leave the tables.

Trade-offs

  • Fixes the alignment, which breaks a parser first.
  • Leaves the norms-nibble description and the missing codes, so a parser built from the tables still rejects valid files (any SORTED_SET field, any soft-deletes index).

What the checks validated

Not run.

C. Replace the grammar with a pointer to write()/read() (not run)

Keep only the prose and say that the .fnm layout is defined by write() and read().

Trade-offs

  • Nothing left to drift; fits LUCENE-2946's "high-level" direction.
  • Removes the only written description of the default .fnm format, which the file-format overview promises (package-info.java:188).

What the checks validated

Not run.

Downstream note

Downstream relevance only for tools that parse .fnm themselves from this javadoc; Lucene, and products that read indexes through Lucene, are unaffected.

How it was found

Non-formal at first: a probe parsed a default-codec .fnm with the javadoc's grammar and compared it with Lucene's reader. A semi-formal differential check then parsed 125 .fnm files from 64 generated indexes with the documented grammar and with the writer's: all 125 diverge, while the writer's grammar parses every file to the footer.

The method in full154 words
  • First probe (2026-10-28): parsed one default-codec .fnm by the class javadoc and compared every value with Lucene's reader: out of sync at the first field, doc-values codes 4 and 5, FieldBits 0x8 (main, assertions on and off).
  • Hunt unit: every file grammar written as data; the documented grammar transcribed only from the javadoc, the layout grammar from the writer; one interpreter over the real bytes of 64 generated default-codec indexes (1114 file checks). For .fnm, 125 of 125 files diverge; the layout grammar parses all of them exactly to the footer (positive control), and 14 of 14 single-element layout mutants across the check's 7 formats are caught (vacuity check), both .fnm ones (drop DocValuesSkipIndex, drop VectorSimilarityFunction) included. An alignment of the two grammars gave the element-level diff: the skip-index byte, PointIndexDimensionCount, the vector elements, FieldBits 0x10, IndexOptions 5 and VectorEncoding 2.
  • This is a differential grammar check, not model checking or a proof.

Prior reports, history and checks

Prior reports

No prior report found in GitHub issues, PRs and commits, the JIRA archive, the mailing lists, downstream projects or the web (searched 2026-10-28, rechecked 2026-11-01). Nearest: LUCENE-6013 (#7075), the change that left the sequence stale. Not fixed upstream.

Searches3 searches
  • Related, not this bug: #7075 / LUCENE-6013 (made the sequence stale; no one noted it); #13449, #13784, #12829, #15584, #16383 (added bytes or values; none mentions the class javadoc); #11818 (a different, fixed defect in the same class: the codec header name in code); #4020 / LUCENE-2946 (the 2012 doc policy); #1788, #4145, #8583 (older file-format doc fixes in other files); #16939 (closed: a broken link to the file-formats page in the 10.6.0 docs, not this text).
  • Open work on the class: #16863 changes read() and imports in this class, not the javadoc; open WIP #13525 adds one javadoc bullet for a new multi-vector byte and leaves the stale grammar. A series of javadoc-vs-code fixes (issues #16821-#16828, #16920-#16922, #16927, #16928, #16933-#16936 and PR #16949) has no file-format grammar item.
  • Not fixed upstream: upstream main f815e679bf6c (2026-11-01), branch_10_6 and branch_11x hold the same file; its latest commit is still #15584 (2026-04-24), which did not touch the javadoc.
What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits; the JIRA archive; downstream repositories2026-10-2861 scripted prior-report queries (over GitHub issues, PRs and commits, downstream repositories, JIRA, the lists and the web); 45 manual apache/lucene searches (class names, DocValuesBits, FieldBits, docValuesByte, indexOptionsByte, SOFT_DELETES_FIELD, PARENT_FIELD_FIELD, DocValuesSkipIndexType, DOCS_AND_CUSTOM_FREQS, FLOAT16, "file format" with javadoc/wrong/outdated/stale, LUCENE-6013); 24 downstream and GitHub-wide searches; 12 JIRA queries (4 of them scripted); the changed files of all 413 open PRs; a pickaxe over the javadoc lines in the full historyno report; in apache/lucene issues, PRs and commits, DocValuesBits, IndexOptions byte fnm and docValuesByte SORTED_SET have 0 hits (the few downstream and JIRA results for such terms were judged unrelated); two open PRs touch the file, neither fixes the text
Mailing lists and web2026-10-2838 queries over dev@, java-user@, issues@ and commits@ (20 of them scripted, counted in the 61 above); 16 web searches (8 of them scripted)no thread about the .fnm javadoc; only mirrors of old 4.x format pages
apache/lucene (recheck)2026-11-01upstream main HEAD and the file's commits; the file blob on main, branch_10_6 and branch_11x; 7 searches over items updated since 2026-10-28 (Lucene94FieldInfosFormat, FieldInfosFormat javadoc, fnm javadoc, DocValuesBits, fnm format, field infos format javadoc, file format javadoc) with a control search; the changed files of every open PRno report (0 hits; the control search returns 20 unrelated items); only #16863, #13525 and #16480 touch a FieldInfosFormat file or the codec overview, none fixing this

History

Sequence wrong since 5.0.0 (LUCENE-6013, 2014); doc-values table incomplete since 4.2.0; later format changes (points, soft deletes, vectors, parent field, skip index, new codes) edited the javadoc at most in part and never placed IndexOptions. 4.10.4 parses as documented; every release from 5.0.0 on, and main, does not.

Versions tested4 rows
  • Primary part: LUCENE-6013 (#7075, SVN era), mikemccand, 8f9f8a3252c (trunk) / 9ac215e4f99 (branch_5x), author date 2014-10-21, released in 5.0.0: IndexOptions moved out of FieldBits into its own byte, added to the type list and tables, not to the sequence.
  • Layers (each changed writer and reader together; where it edited the javadoc, as noted, it did not bring the sequence and tables up to date; a single hash is a main commit, and a release on another branch got the change through a backport): SORTED_SET = 4 b44aa618348 (LUCENE-4765, 4.2.0); SORTED_NUMERIC = 5 398af4596bd (LUCENE-5748, 4.9.0); norms nibble dropped 78e445ce579 (LUCENE-6006, 5.0.0); conditional DimensionNumBytes d3869edb96d (LUCENE-6861, 6.0.0; it added DimensionCount and DimensionNumBytes to the sequence, both unconditional); FieldBits 0x8 e7a0a12926c (LUCENE-8335, 7.4.0); PointIndexDimensionCount 1118299c338 (LUCENE-8496, 7.6.0); vector elements c02f07f2d5d (LUCENE-9322, 9.0.0; described, not placed); the Lucene94FieldInfosFormat class, copied from the stale Lucene90 text with a VectorEncoding byte described but not placed, a693fe819b0 (#1054, 9.4.0); FieldBits 0x10 df6bd25ce44 (#12829, 9.10.0); skip-index byte b59a357e586 (#13784, 10.0.0); IndexOptions 5 5158a52a082 (#15584, 10.5.0); VectorEncoding 2 bfe5c847d3c (#16383, main only).
  • From 5.0.0 to 9.3.0 the stale sequence sat in the then-default Lucene50, Lucene60 and Lucene90FieldInfosFormat; 9.4.0 is the first release of the current class. On main only the frozen backward-codecs Lucene90FieldInfosFormat remains (Lucene60FieldInfosFormat was removed by af85bbcf1c0, #15712, and is still on branch_10_6; Lucene50FieldInfosFormat by 4b3e8d7ce8f, LUCENE-9627).
  • Caveats: 8.9.0-8.11.3 are not tagged in the history clone (javadoc read at 8.8.x, behaviour run on 8.11.4); the later layers' PR numbers and dates come from git log reads, not a bisect.
Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 (and a9779ef3c193) affectedthe minimal repro fails with assertions on and off; the hunt's tests fail on the sequence, codes 4 and 5, FieldBits 0x8 and 0x10, IndexOptions 5 and VectorEncoding 2
10.6.0 (release commit 4ae946e5a9b), 10.5.2 affectedthe finder's tests fail 2 of 2; IndexOptions 5 is written on 10.5.2
10.0.0, 9.12.3, 9.4.0, 9.0.0, 8.11.4, 7.6.0, 6.0.0, 5.0.0 affectedthe documented sequence fails on the first field (history probes)
4.10.4, 4.2.0 partlythe documented sequence parses to the footer; only the doc-values table misses code 4 (and 5 from 4.9.0)

Independent checks

Confirmed by an independent reproducer, reviews by a reviewer from another model family (OpenAI GPT-5.6), an intended-behaviour devil's advocate and an adjudication (high confidence); an audit confirmed it as a low-priority doc defect. This doc's gradle run validated the fix.

Checks and verdicts441 words

Every check either re-ran a test on main or read the same javadoc and writer lines; none found a reading under which the documented sequence is meant to differ from the bytes.

Each independent check and its verdict
CheckVerdictWhat it found
cross-model review, refute lens (OpenAI GPT-5.6, executing) real, high confidenceIts own StringField test reads the IndexOptions byte (1 = DOCS) where DocValuesBits is documented, on four seeds with assertions on and off; its empty-document control passes; intended argument moderate.
independent reproducer realOwn tests fail 4 of 5 on main (on and off), 3 of 4 on 10.5.2 and 9.12.3; even a generous reading with IndexOptions inserted diverges on every field (DocValuesGen -256 on main, from the skip-index byte); its writer-layout control parses to the footer.
cross-model review, intended lens (OpenAI GPT-5.6, reading only) real, high confidenceIt could not run any code, so it read only; a separate run re-ran the finder's test with assertions off: 2 of 2 fail. Intended argument moderate.
intended-behaviour devil's advocate real (documentation defect)Built and rejected the LUCENE-2946 / @lucene.experimental case; showed IndexOptions 5 is written on 10.5.2 too and FLOAT16 writes 2 on main.
adjudication confirmed, high confidence, documentation defect, minorRe-ran the independent reproduction's tests with assertions on and off (5 run, 4 failed, control passes) and read the javadoc, writer and reader lines.
credibility audit confirmed, minorRe-ran the finder's test on a new seed: 2 of 2 fail. Its "committer view" is the AI auditor's own simulated judgement, not a statement by any Lucene committer: a javadoc-only PR would likely merge without much discussion.
fact checks of this doc (2026-11-01; Claude, and OpenAI GPT-5.6 as an independent reviewer from another model family) bug holds; text errors fixedBoth re-ran the repro (fails 1 of 1, assertions on and off) and re-read the code lines, history and sources; the bug, impact and classification hold. They found text errors (field count of the validation segment, which backward-codecs copies exist on main, a seed claim, the mutant scope, the 9.12.3 evidence, the format versions the grammar covers, the DOCS_ONLY name, the zero-field case, the later javadoc edits, the test description and the scope of a 0-hit search). All are fixed here; the fix diff was revised and re-validated.
this doc's validation (2026-11-01) fix A validatedIn scratch worktrees with fix A (first and final version): the fixed-text check parses the repro's file and a 12-field segment to the footer (on and off); TestLucene94FieldInfosFormat (12 tests), :lucene:core:check -x test and renderJavadoc pass; the repro fails through gradle on stock, on and off.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,828 words

Finding record

LFV-942, snapshot of 2026-11-06

Registry title
The file-format javadoc of the default field infos format (Lucene94FieldInfosFormat) gives a wrong .fnm grammar: the per-field sequence leaves out IndexOptions, the doc-values skip-index byte, PointIndexDimensionCount and the three vector elements, and the value tables miss written values (doc-values codes 4 and 5, FieldBits 0x8 and 0x10, IndexOptions 5 since 10.5.0, VectorEncoding 2 on main) (documentation defect; the writer and reader agree)
Area
codecs / field infos (Lucene94FieldInfosFormat .fnm file-format javadoc vs its writer and reader)
Priority
Low, rank 1278: The default .fnm grammar has been wrong since 5.0 and misleads a re-implementer on the first field of every index.
Impact class
minor
Impact note
Documentation only: Lucene's writer and reader agree, so indexing, merging, CheckIndex and search are unaffected in every configuration; the people misled are those who read, inspect or re-implement the .fnm format from this class javadoc (Lucene104Codec installs this format and the default codec's package docs link it for the details), and a parser that follows the documented sequence misreads the first field of every .fnm the default codec writes, then every later field (executed); the sequence has been wrong in every release since 5.0.0 (2015), the doc-values value table since 4.2.0, and no scale or special setting is needed.
Summary
You decode a field infos (.fnm) file of the default codec by following the Lucene94FieldInfosFormat javadoc, which gives each field as FieldName, FieldNumber, FieldBits, DocValuesBits, DocValuesGen, Attributes, DimensionCount, DimensionNumBytes, documents DocValuesBits as a norms nibble plus a doc-values nibble with codes 0..3, and lists only FieldBits 0x1, 0x2 and 0x4. The writer instead puts an IndexOptions byte right after FieldBits, writes a single doc-values code (SORTED_SET = 4, SORTED_NUMERIC = 5) followed by a skip-index byte, writes PointIndexDimensionCount and PointNumBytes only when the dimension count is non-zero, appends VectorDimension, VectorEncoding and VectorSimilarityFunction, and sets FieldBits 0x8 (soft-deletes field) and 0x10 (parent field), so a parser built from the text reads the IndexOptions byte of a text field (3) as 'SORTED doc values' and loses alignment on the first field. Lucene's own reader matches the writer, so nothing breaks at runtime; only the documentation is wrong, and its sequence has been since 5.0.0.
Introduced
layered [follow-up]. Primary part (the per-field sequence): 8f9f8a3252c73428e67bc5d390e58d1370e060ba (trunk) / 9ac215e4f99a8813b23ad700a05b154b37f728ff (branch_5x, the commit that shipped in 5.0.0), LUCENE-6013 'remove IndexableFieldType.indexed and FieldInfo.indexed' (now apache/lucene#7075; SVN era, no GitHub PR), by mikemccand: it moved IndexOptions out of FieldBits into its own byte written after FieldBits and added it to the type list and the tables but not to the sequence [read: git show in the full-history clone; exec: the documented sequence parses on 4.10.4 and fails on 5.0.0, where the same sequence with IndexOptions inserted parses]. Every other layer changed the writer and reader together and left the javadoc as it was: DocValuesBits table 0..3 without SORTED_SET = 4 since b44aa618348 (LUCENE-4765, 4.2.0) and SORTED_NUMERIC = 5 since 398af4596bd (LUCENE-5748, 4.9.0); norms nibble dropped by 78e445ce579 (LUCENE-6006, 5.0.0); DimensionNumBytes written only for point fields since d3869edb96d (LUCENE-6861, 6.0.0); FieldBits 0x8 e7a0a12926c (LUCENE-8335, 7.4.0); PointIndexDimensionCount 1118299c338 (LUCENE-8496, 7.6.0); vector elements c02f07f2d5d (LUCENE-9322, 9.0.0); the Lucene94 class itself a693fe819b0 (apache/lucene#1054, LUCENE-10577, 9.4.0), which copied the stale Lucene90 text and added a VectorEncoding byte, described but not placed; FieldBits 0x10 df6bd25ce44 (#12829, 9.10.0); skip-index byte b59a357e586 (#13784, 10.0.0); IndexOptions 5 5158a52a082 / branch_10x 3a55cc9ac5e (#15584, 10.5.0); VectorEncoding 2 bfe5c847d3c (#16383, main only) [read: git log in the full-history clone, subjects and author dates checked at the registration check; exec: a before/after release pair, or a compile error where the API did not exist yet, for each layer] (2014-10-21 (author date of 8f9f8a3252c and 9ac215e4f99: the per-field sequence); 2013-02-20 (author date of b44aa618348: the first value missing from the DocValuesBits table); later layers 2014-06-12 to 2026-07-27 (see introduced_commit))
First affected version
5.0.0 [exec] for the per-field sequence (the last unaffected release is 4.10.4 [exec], whose documented Lucene46FieldInfosFormat sequence parses the file exactly to the footer); 4.2.0 [exec] for the incomplete DocValuesBits table; 9.4.0 [exec] is the first release of Lucene94FieldInfosFormat (5.0.0-9.3.0 carry the same text in the then-default Lucene50, Lucene60 and Lucene90FieldInfosFormat)
Affected versions
every release from 5.0.0 through 10.6.0 for the sequence (4.2.0-4.10.4 for the DocValuesBits table only; 10.6.0 run as its release revision, the branch_10_6 head 4ae946e5a9b) and main a9779ef3c193; not fixed upstream [exec: per-release public-API history probes fail the documented sequence on 5.0.0, 5.5.5, 6.0.0, 7.5.0, 7.6.0, 8.11.4, 9.0.0, 9.3.0, 9.4.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da) and pass on 4.2.0 and 4.10.4, with the writer-layout control parsing in every run; value-table probes fail from each value's first release (codes 4 and 5 on 4.2.0 and 4.9.0, FieldBits 0x8 on 7.4.0, PointIndexDimensionCount on 7.6.0, FieldBits 0x10 on 9.10.0, IndexOptions 5 on 10.5.0, VectorEncoding 2 on main) and do not compile on the release before, where the API did not exist; the finder's JUnit fails 2 of 2 on 10.0.0, 10.5.2, the branch_10_6 head and main (-ea and -da), and its sequence test fails on 9.4.0 and 9.12.3 (on 9.x its value-table test's own layout assumes the 10.0 skip-index byte, so that EOF is not evidence); the independent reproduction's JUnit fails 4 of 5 on main (-ea and -da) and 3 of 4 on 10.5.2 and 9.12.3, with its writer-layout control passing; read: the javadoc sequence line and the 0..3 table in the release sources of every tagged release 4.2.0-10.5.2]. Per part: IndexOptions missing from the sequence and the stale norms nibble 5.0.0 on; doc-values code 4 4.2.0 on, code 5 4.9.0 on; conditional DimensionNumBytes 6.0.0 on; FieldBits 0x8 7.4.0 on; PointIndexDimensionCount 7.6.0 on; vector elements 9.0.0 on (VectorEncoding 9.4.0 on); FieldBits 0x10 9.10.0 on; skip-index byte 10.0.0 on; IndexOptions 5 10.5.0 on (also branch_10x and branch_10_6); VectorEncoding 2 main only. Caveats: 8.9.0-8.11.3 are not tagged in the history clone (javadoc read at 8.8.x, behaviour executed on 8.11.4); upstream main 645d4fe14a20 (2026-10-28) holds a byte-identical file; all runs used JDK 25 on Linux aarch64
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
Upstream and prior reports
none found (searched 2026-10-28: 61 scripted prior-report queries over GitHub issues, PRs and commits, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and downstream repositories, plus 45 manual apache/lucene GitHub searches, 24 downstream and global GitHub searches, 8 JIRA, 18 list and 8 web queries; 'DocValuesBits', 'IndexOptions byte fnm' and 'docValuesByte SORTED_SET' have 0 hits; all 413 open apache/lucene PRs enumerated by changed files and the 22 PRs closed since 2026-10-25 checked; pickaxe over the javadoc lines in the full-history clone). Nearest items, not the same bug: #7075 / LUCENE-6013 (the change that left the sequence stale; its author wrote 'I'll update the javadocs for the format changes' and nobody discussed the sequence), #13449, #13784, #12829, #15584 and #16383 (the changes that added bytes or values; none mentions the class javadoc), #11818 (a different defect in the same class, the codec header name in code, fixed 2022), #4020 / LUCENE-2946 (2012 policy for high-level file-format docs), #1788 / LUCENE-713, #4145 / LUCENE-3072 and #8583 / LUCENE-7532 (precedents: file-format doc errors fixed upstream). Open work, not prior art: #16863 changes read() in this class without touching the javadoc; open draft #13525 adds a javadoc line for a new multi-vector byte and leaves the stale sequence and tables; an ongoing series of javadoc-vs-code issues (16 issues, #16821-#16828, #16920-#16922, #16927, #16928, #16933-#16935) contains no file-format grammar item. Recheck (2026-10-28): none of the 30 apache/lucene issues and PRs created since 2026-10-26 is about the .fnm javadoc; 'Lucene94FieldInfosFormat' returns only #16863, #16211, #16506, #13449, #11818 and #11755, and 'FieldInfosFormat javadoc', 'fnm javadoc', 'DocValuesBits', 'fnm grammar' and 'field infos format documentation' return nothing; none of the open PRs created since 2026-10-27 changes a FieldInfosFormat file or lucene104/package-info.java; by author date the file's latest upstream commits are 5158a52a082 (2026-04-24, #15584), b59a357e586 (2024-09-17, #13784) and 3da48d0403e (2024-09-11, #13759); upstream main head 645d4fe14a20 (2026-10-28) still has the stale text. Not fixed upstream
Lucene commit tested
main 645d4fe14a20: the minimal repro with assertions on and off, standalone (seeds DEADBEEF and 5EED1234) and through gradle in a scratch worktree (on and off); fix A in that worktree: the fixed-text check (on and off), TestLucene94FieldInfosFormat, :lucene:core:check -x test and :lucene:core:renderJavadoc. The hunt's and the verifiers' tests on main a9779ef3c193, 10.5.2, 9.12.3 and the 10.6.0 release commit 4ae946e5a9b; history probes on releases from 4.2.0 to 10.5.2 and main. Lucene94FieldInfosFormat.java is byte-identical (blob acd4bf55152) on 645d4fe14a20, a9779ef3c193, upstream main f815e679bf6c (2026-11-01), branch_10_6, branch_11x and the 10.6.0 tag (read).

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java:44 ('Lucene 9.0 Field Infos format.'), :48-50 (documented sequence), :58 (FieldBits, IndexOptions, DocValuesBits --> Byte; IndexOptions never placed), :59 (FieldNumber, DimensionCount, DimensionNumBytes labelled VInt, linked to writeInt), :72-79 (FieldBits 0x1, 0x2, 0x4 only), :80-87 (IndexOptions 0..4), :88-97 (DocValuesBits as two four-bit integers, norms high, codes 0..3), :102-118 (point and vector elements described, not placed; VectorEncoding 0..1), :121 (@lucene.experimental), :147-213 (reader mirrors the writer), :258-276 (docValuesByte: SORTED_SET 4, SORTED_NUMERIC 5), :370-388 (indexOptionsByte: DOCS_AND_CUSTOM_FREQS 5), :427-457 (writer, incl. the comment 'pack the DV type and hasNorms in one byte'), :477-478 (SOFT_DELETES_FIELD 0x8, PARENT_FIELD_FIELD 0x10); identical at Lucene main ea57360912ed (compared 2026-10-28)
  • lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104Codec.java:72 (the default codec installs new Lucene94FieldInfosFormat()) and lucene104/package-info.java:50 ('defines the index file formats used in this version'), :54 ('a high-level definition'), :147 and :243 (links Lucene94FieldInfosFormat), :188 ('Details on each of these are provided in their linked pages')
  • lucene/backward-codecs/src/java/org/apache/lucene/backward_codecs/lucene90/Lucene90FieldInfosFormat.java:48, :57 (the same stale text in the frozen read-only copy; the Lucene50 and Lucene60 copies no longer exist on main)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java (lines 44, 48-50, 55-62, 72-118 javadoc; 147-213 read(); 258-276 docValuesByte; 370-388 indexOptionsByte; 427-457 write(); 474-479 flag constants)
  • lucene/core/src/java/org/apache/lucene/index/VectorEncoding.java (lines 29, 32, 38: BYTE, FLOAT32, FLOAT16) and index/IndexOptions.java (line 56)
  • lucene/core/src/java/org/apache/lucene/store/DataInput.java (lines 102-108, 146-148: little-endian readInt/readLong)
Who is affected
  • lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104Codec.java (line 72) and codecs/lucene104/package-info.java (lines 50, 147, 188, 243)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/codecs/lucene94/Lucene94FieldInfosFormat.java (lines 48-62, 121) and lucene/core/src/java/org/apache/lucene/codecs/lucene104/package-info.java (lines 50, 54, 188)
  • lucene/core/src/test/org/apache/lucene/codecs/lucene94/TestLucene94FieldInfosFormat.java (lines 25-39: testVectorSimilarityFuncs)
Suggested fix
  • lucene/core/src/java/org/apache/lucene/index/IndexOptions.java (line 36: DOCS)
  • a Lucene clone tags releases/lucene/9.4.0, 9.9.2, 9.10.0, 9.12.3, 10.0.0 (Lucene94FieldInfosFormat FORMAT_CURRENT 0, 0, 1, 1, 2)
  • lucene/core/src/java/org/apache/lucene/codecs/lucene104/Lucene104PostingsFormat.java (line 104, the ? notation)
  • a Lucene clone tag releases/lucene/10.6.0 (VectorEncoding.java: no FLOAT16; Lucene94FieldInfosFormat.java blob acd4bf55152)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made from the code at 645d4fe14a20 and an executed probe on the minimal repro's index (the same bytes parsed in the javadoc's order and in write()'s order, assertions on and off), in parallel with this doc.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.