Skip to content

Commit 08b6cd5

Browse files
dfa1claude
andcommitted
test(integration): DocsConsistencyTest — the docs get a compiler
Machine-checks the living docs (CLAUDE.md, README, docs/*.md) against the codebase: project FQNs must resolve (nested classes via $ fallback), META-INF/services mentions must name real files, relative links must not dangle, and backticked Class.method(...) claims must name real methods — external receivers live on a conscious allowlist extended in the same diff that cites a new API. Historical records (adr/, released CHANGELOG sections) are exempt by policy. First run caught 11 fossils the same-day MANUAL audit missed, all fixed here: pre-refactor FQNs in tutorial.md imports, a phantom io.github.dfa1.vortex.extension package, Registry.* claims from two renames ago and an ArraySegments API that no longer exists in compatibility.md, plus Registry/Encoding diagram names and a "DType blob (Protobuf)" claim in explanation.md. The vortex- reviewer agent gains a standing stale-docs dimension for the prose claims a regex cannot check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 06d7aad commit 08b6cd5

7 files changed

Lines changed: 326 additions & 35 deletions

File tree

.claude/agents/vortex-reviewer.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ Group findings by severity: **Blocker** / **Should-fix** / **Nit**. Each finding
5757
relevant `-pl <mod> -Dtest=...`) and quote real output. End with a verdict: APPROVE or CHANGES
5858
NEEDED. Be specific and skeptical — your job is to catch what the coder missed, especially wrong-answer
5959
and memory-safety bugs.
60+
61+
## Standing dimension: stale docs
62+
63+
For every identifier the diff renames, moves, or deletes (classes, methods, packages, service
64+
files), grep the LIVING docs — `CLAUDE.md`, `README.md`, `docs/*.md` — for remaining mentions.
65+
A stale mention is a finding (severity: convention). `adr/` and released CHANGELOG sections are
66+
historical and exempt. `DocsConsistencyTest` (integration) machine-checks FQNs/method claims/
67+
links, but prose claims about behavior or policy drift too — check those by reading, not grep.

TODO.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,13 @@ Per-encoding gotchas:
6767
## Build
6868

6969
- [ ] use JPMS, watch out for "dfa1" in package name
70-
- [ ] **Docs need a compiler** — living prose (CLAUDE.md, `docs/*.md`) drifts from code with no
71-
gate (2026-07-04 found: phantom `register(ExtensionDecoder)`, stale "not yet implemented" on
72-
MASKED/PATCHED, dead service-file path, stale FQNs after package moves). Tiered plan:
73-
1. Markdown link checker in CI (relative links; the `docs/adr``adr/` move rewrote ~40 by hand).
74-
2. `DocsConsistencyTest` (integration module): extract backticked FQNs / `Class#member` refs /
75-
`META-INF/services` paths from living docs, assert existence via reflection. Historical files
76-
(`adr/`, released CHANGELOG sections) exempt by policy.
77-
3. Golden-test or generate enumerable tables (encodings/extensions/layout ids in
70+
- [ ] **Docs compiler — remaining tiers**`DocsConsistencyTest` (integration) now gates FQNs,
71+
method claims, service-file paths, and relative links in the living docs (first run caught 11
72+
fossils the same-day manual audit missed). Remaining:
73+
1. Golden-test or generate enumerable tables (encodings/extensions/layout ids in
7874
`docs/reference.md`, `docs/compatibility.md`) from `EncodingId.WellKnown`/`LayoutId.WellKnown`/
7975
service manifests — a declared capability the code lacks is a bug, in both directions.
80-
4. Compile ` ```java ` blocks from living docs (defer until 1–3 pay rent).
81-
Plus: standing stale-docs dimension in the vortex-reviewer agent (grep living docs for
82-
identifiers a diff renames/moves/deletes).
76+
2. Compile ` ```java ` blocks from living docs (tutorial imports were stale for months).
8377

8478
## Tooling
8579

docs/compatibility.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ resolves only the standalone decoders in `reader`; no encoder class is loaded.
3232
| Item | Introduced | Java status |
3333
|------|------------|-------------|
3434
| `DType::Union` (`fbs.DType.Type.Union = 12`) | Rust 0.71.0 | ❌ Decode throws `VortexException("unsupported DType typeType=12")`. No `DType.Union` variant in Java's sealed type. |
35-
| `vortex.onpair` experimental string encoding | Rust 0.74.0 | ❌ Not registered. Files using it fail to decode unless `Registry.allowUnknown()` is enabled. |
35+
| `vortex.onpair` experimental string encoding | Rust 0.74.0 | ❌ Not registered. Files using it fail to decode unless `ReadRegistry.builder().allowUnknown()` is enabled. |
3636
| `vortex.variant` arbitrary nested objects | Rust (`vortex.parquet.variant`) | ⚠️ Java encodes/decodes variant columns of **typed scalar** values (constant / chunked-of-constants core, optional shredded child); Java↔Rust round-trip verified. Arbitrary nested JSON objects and real path-based shredding need the `vortex.parquet.variant` physical encoding — deferred ([ADR 0014](../adr/0014-variant-encoding-strategy.md)). |
3737
| Arrow extension array import affecting Variant shape | Rust 0.74.0 (#8125) | Untested. Re-run integration fixtures against v0.74.0 once published. |
3838
| Duplicate struct field names | Rust writer rejects ("StructLayout must have unique field names"); Rust reader tolerates foreign files (first-match access) | ⚠️ Deliberate divergence on read: Java rejects such files with `VortexException("duplicate field name in file schema")` instead of tolerating them — the name-keyed `Chunk` API cannot represent both columns, and silent column loss is worse than a loud failure on a file the reference writer refuses to produce. Java's writer mirrors the Rust writer's rejection. |
@@ -87,7 +87,7 @@ decoder falls into one of three shapes:
8787
- **Lazy** — output is a `LazyXxxArray` / `ChunkedXxxArray` record that holds the encoded child
8888
plus the transform parameters. Per-row `getXxx(i)` applies the transform on demand. No
8989
output buffer is allocated unless a caller explicitly materializes via
90-
`ArraySegments.of(arr, arena)`.
90+
`Array.materialize(arena)`.
9191
- **Materialized** — output is a buffer allocated from `ctx.arena()` populated during `decode()`.
9292
Required for decompression-style encodings (Bitpacked, Pco, Zstd, etc.) where reading element
9393
`i` would require decoding a window.
@@ -156,21 +156,21 @@ metadata. The Rust catalog lives in
156156
[`vortex-array/src/extension/`](https://github.com/vortex-data/vortex/tree/develop/vortex-array/src/extension);
157157
each subdir below names a canonical extension id and its on-disk shape.
158158

159-
Extensions live in `io.github.dfa1.vortex.extension`. Each spec extension is a
160-
singleton implementing the `Extension` interface, with typed encode/decode
161-
methods on the concrete impl. Resolve a column to its impl via
162-
`Registry.lookup(ExtensionId)`, or grab the singleton directly:
159+
Read-side extensions live in `io.github.dfa1.vortex.reader.extension` (write-side encoders
160+
implement `ExtensionEncoder` in the writer module). Each spec extension is a singleton
161+
implementing the `ExtensionDecoder` interface, with typed decode methods on the concrete
162+
impl — grab the singleton directly:
163163

164164
```java
165165
DType.Extension dtype = (DType.Extension) schema.field("birthdays");
166-
List<LocalDate> values = DateExtension.INSTANCE.decodeAll(chunk.column("birthdays"));
166+
List<LocalDate> values = DateExtensionDecoder.INSTANCE.decodeAll(chunk.column("birthdays"));
167167
```
168168

169169
End-to-end round-trip — write a `List<LocalDate>`, read it back:
170170

171171
```java
172172
var schema = DType.structBuilder()
173-
.field("birthdays", DateExtension.INSTANCE.dtype(false))
173+
.field("birthdays", DateExtensionDecoder.INSTANCE.dtype(false))
174174
.build();
175175
writer.writeChunk(c -> c.put("birthdays", dates)); // Collection auto-routed
176176

@@ -182,9 +182,8 @@ try (var iter = reader.scan(ScanOptions.all());
182182

183183
`Chunk.as(name, Class)` hides the per-extension decode dispatch for the four
184184
spec extensions (`LocalDate``vortex.date`, `LocalTime``vortex.time`,
185-
`Instant``vortex.timestamp`, `UUID``vortex.uuid`). Third-party
186-
extensions still go through `Registry.lookup(ExtensionId)` and the impl's own
187-
typed methods.
185+
`Instant``vortex.timestamp`, `UUID``vortex.uuid`); the dispatch is closed over the
186+
spec set. Third-party extensions call their own impl's typed methods directly.
188187

189188
`ExtensionId` is the enum of known spec ids (`VORTEX_DATE`, `VORTEX_TIME`,
190189
`VORTEX_TIMESTAMP`, `VORTEX_UUID`). Unknown wire ids on `DType.Extension`

docs/explanation.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ Re-measured 2026-06-08 against commit `051a794`.
6868

6969
Two Parquet variants are measured to isolate format cost from API overhead:
7070

71-
- **batch**: `ColumnReader.nextBatch()` + loop over `getDoubles()`/`getInts()` arrays — apples-to-apples with Vortex's
71+
- **batch**: Hardwood's `ColumnReader.nextBatch()` + loop over `getDoubles()`/`getInts()` arrays — apples-to-apples with Vortex's
7272
batch fold
73-
- **row-by-row**: `RowReader.next()` + `getDouble("col")` per row — measures the full row-cursor overhead on top of
73+
- **row-by-row**: Hardwood's `RowReader.next()` + `getDouble("col")` per row — measures the full row-cursor overhead on top of
7474
format decode
7575

7676
| Benchmark | ops/s | vs Parquet batch |
@@ -367,8 +367,8 @@ vortexReader.scan(opts) → ScanIterator
367367
ScanIterator.next() → Chunk (per row-group, AutoCloseable; owns its own Arena)
368368
└─ decodeLayout(layout, dtype, chunk.arena)
369369
├─ Flat → slice MemorySegment from mmap region
370-
│ └─ Registry.decodeSegment(seg, …)
371-
│ └─ Encoding.decode(DecodeContext) → Array (zero-copy)
370+
│ └─ SerializedArrayDecoder.decode(seg, …)
371+
│ └─ ReadRegistry → EncodingDecoder.decode(DecodeContext) → Array (zero-copy)
372372
├─ Chunked → collect Flat children, decode each, concatenate buffers
373373
├─ Zoned → skip zone-map metadata, recurse into child layout
374374
└─ Dict → decode values layout + codes layout separately, then expand
@@ -394,21 +394,21 @@ writer.writeChunk(c -> c.put(column, data[]).put(...))
394394
└─ record Layout node (encoding ID + rowCount + segment index)
395395
396396
writer.close()
397-
└─ write DType blob (Protobuf)
397+
└─ write DType blob (FlatBuffer)
398398
└─ write Footer blob (FlatBuffer) → SegmentSpec[] + ArraySpec[]
399399
└─ write Layout blob (FlatBuffer) → Struct → Zoned(Stats) → Chunked → [Flat …]
400400
└─ write Postscript (FlatBuffer) → blob offsets + lengths
401401
└─ write 8-byte trailer → version · postscriptLen · magic (VTXF)
402402
```
403403

404-
### How `Registry` resolves encodings
404+
### How `ReadRegistry` resolves encodings
405405

406-
`Registry.loadAll()` uses `ServiceLoader` to discover all `Encoding`
407-
implementations on the classpath. Each encoding declares its ID via `encodingId()`.
408-
At decode time the registry maps the ID string from the Layout node to the right
409-
`Encoding` instance and calls `decode(DecodeContext)`.
406+
`ReadRegistry.loadAll()` uses `ServiceLoader` to discover all `EncodingDecoder`
407+
implementations on the classpath. Each decoder declares its identity via `encodingId()`.
408+
At decode time the registry maps the typed id from the array node to the right
409+
`EncodingDecoder` instance and calls `decode(DecodeContext)`.
410410

411-
Custom encodings can be added at build time: `Registry.builder().register(myEncoding).build()`.
411+
Custom decoders can be added at build time: `ReadRegistry.builder().registerServiceLoaded().register(myDecoder).build()`.
412412
Files with unrecognized IDs throw `VortexException` unless the builder enabled `allowUnknown()`.
413413

414414
## Testing strategy

docs/testing.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,16 @@ run-once part and leave the hot, specialized part alone, with a comment saying w
181181

182182
The throughline: let the tools point at the data, then decide with the context they do not
183183
have.
184+
185+
## Docs consistency gate
186+
187+
`DocsConsistencyTest` (integration module, plain surefire) machine-checks the living docs
188+
(`CLAUDE.md`, `README.md`, `docs/*.md`) against the codebase: project FQNs must resolve,
189+
`META-INF/services` mentions must name real files, relative links must not dangle, and
190+
backticked `Class.method(...)` claims must name real methods (external receivers live on a
191+
conscious allowlist in the test). Historical records (`adr/`, released CHANGELOG sections)
192+
are exempt. Run it alone with:
193+
194+
```bash
195+
./mvnw test -pl integration -am -Dtest=DocsConsistencyTest
196+
```

docs/tutorial.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ The file is complete and readable as soon as `VortexWriter` is closed.
100100
## 4. Read it back
101101

102102
```java
103-
import io.github.dfa1.vortex.io.VortexReader;
104-
import io.github.dfa1.vortex.scan.ScanOptions;
103+
import io.github.dfa1.vortex.reader.VortexReader;
104+
import io.github.dfa1.vortex.reader.ScanOptions;
105105
import io.github.dfa1.vortex.reader.array.DoubleArray;
106106
import io.github.dfa1.vortex.reader.array.LongArray;
107107

0 commit comments

Comments
 (0)