Skip to content

fix(docs): recover symbols the manifest extraction was silently dropping - #427

Merged
coderdan merged 2 commits into
mainfrom
docs/manifest-extraction-fixes
Jul 28, 2026
Merged

fix(docs): recover symbols the manifest extraction was silently dropping#427
coderdan merged 2 commits into
mainfrom
docs/manifest-extraction-fixes

Conversation

@coderdan

@coderdan coderdan commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Four independent bugs in the Doxygen → manifest pipeline, each of which lost or misdescribed part of the API surface without failing the docs build. Found while pinning the docs site to EQL 3.0.3.

1. @example deletes the symbol it documents

Doxygen's \example takes a filename — it documents an example source file. Used as an inline snippet marker, it pulls the block out of the member's documentation.

There are exactly two @example blocks in the 3.0.3 tree, and they are exactly the two symbols missing from the manifest:

Symbol @example Before After
eql_v3.grouped_value (src/v3/aggregates.sql) yes no memberdef emitted at all present
eql_v3.lints() (src/v3/lint/lints.sql) yes empty description → dropped by the no-documentation guard present
eql_v3_internal.grouped_value_sfunc (same file) no present present

The failure mode inverts the usual one: it penalises the best-documented symbols. grouped_value has the richest doc block of any aggregate in the codebase — brief, params, return, a note on parallel-aggregation determinism, a worked example — and that is why it vanished. Both blocks now use @code{.sql} / @endcode.

2. CREATE AGGREGATE bodies break the C++ parse

The trailing (sfunc = …, stype = …) is a second parenthesised group, which C++ has no form for, so Doxygen misreads the declaration — differently depending on the argument type:

CREATE AGGREGATE eql_v3.grouped_value(jsonb) (...)
  -- extracted as a function NAMED `jsonb`; eql_v3.grouped_value is absorbed
  -- into the return type and disappears
CREATE AGGREGATE eql_v3.min(public.eql_v3_bigint_ord) (...)
  -- named `min`, but the argument list truncates mid-body to
  -- `(public.eql_v3_bigint_ord)(sfunc`

So grouped_value was doubly lost — the @example fix alone does not recover it. The input filter now strips aggregate bodies (exactly as it already does for dollar-quoted function bodies — they carry no documentation either), and the extractor reads an aggregate's argument types from its declaration and its return type from @return instead of trusting the C++ parse.

This also fixes the 60 min/max aggregates, which were present but garbled — and it was reaching the published page:

before after
signature min(eql_v3_bigint_ord) min(public.eql_v3_bigint_ord)
params {name: "public.", type: "eql_v3_bigint_ord"} {name: "input", type: "public.eql_v3_bigint_ord"}
returns.type "CREATE AGGREGATE eql_v3" "public.eql_v3_bigint_ord"

3. The match capability is computed from the pre-3.0.1 operator

_capabilities_from_ops still tested ("@>", "<@"). Fuzzy match was renamed to @@ in 3.0.1, so no domain could be assigned match at all — across all 52 domains the vocabulary was {storage, equality, order, json} with zero match.

For public.eql_v3_text_match the consequence is worse than a missing label: its only operator is @@, so it matched no branch and fell through to the or ["storage"] default. The manifest described the fuzzy-match domain as storage-only, contradicting the supportedOperators: ["@@"] and termFunctions: ["eql_v3.match_term"] beside it. text_search silently lost match too.

Containment on encrypted JSON legitimately keeps @> / <@, so it is now reported as its own containment capability rather than conflated with fuzzy match.

4. public.eql_v3_json is in no list at all

dump_catalog filtered the json family's bare storage domain out of stevec as scalar, on the assumption it would surface via types — but types iterates scalar_families(), which excludes the mixed json family wholesale. The SQL materializer creates the domain (families_with_scalar_domains(), generate.rs:1024); the dump listed it nowhere. The stale comment gave the history away, still naming the pre-3.0.1 json / jsonb_entry.

It is now carried in the json-family inventory behind a scalar flag, so consumers do not describe it with the SteVec query surface (it stores and decrypts, nothing more).

I deliberately did not add it to types: that list is the scalar-matrix surface, and the test:matrix:inventory coverage task requires a scalars::<token>::matrix_* suite for every (type, domain) it lists. The json family has no such matrix (its coverage lives in the separate v3_json_storage_tests binary), so adding it there would have broken that check.

Verification

Ran the real pipeline (dump-catalogdoxygenxml-to-json) against the 3.0.3 tree:

  • 1988 → 1989 functions — gains grouped_value and lints, drops the phantom jsonb function the old aggregate mis-parse invented.
  • 52 → 53 domains — gains public.eql_v3_json with capabilities: ["storage"].
  • match restored to text_match, text_search, text_search_ore; text_match no longer reports storage-only.
  • Diffed every shared symbol against the released 3.0.3 manifest: the only description change is lints, and the only signature changes are min/max becoming schema-qualified. No collateral.
  • cargo test -p eql-codegen -p eql-domains and the Python generator tests pass; cargo fmt --check and clippy clean.
  • Checked the filter against both single-line and multi-line aggregate forms — a single-line CREATE AGGREGATE x(y) (sfunc = z); cannot latch the body-skip and swallow following declarations.

Two notes for the reviewer:

  • test_xml_to_json.py::test_load_domains was already failing on main — its fixture predated typname and the 3.0.1 renames, so it raised KeyError: 'typname'. It is updated to the current catalog shape and now pins the capability mapping and the json family. A current fixture would have caught bug 3.
  • Still outstanding, not fixed here: all SteVec domains report supportedOperators: [] (hardcoded), so public.eql_v3_json_search claims no operators despite being the domain that supports @> / <@. Populating that needs operator data added to the Rust catalog's SteVec entries, and I did not want to assert an authoritative operator set I could not verify from the catalog.

https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8

Summary by CodeRabbit

  • Bug Fixes

    • Corrected API reference entries for eql_v3.grouped_value and eql_v3.lints().
    • Fixed capability labels so fuzzy matching and encrypted JSON containment are represented accurately.
    • Added the previously missing public.eql_v3_json catalog entry and identified it as storage-only.
    • Improved aggregate documentation extraction, including parameters, return values, and SQL examples.
  • Documentation

    • Improved formatting and preservation of SQL code snippets in generated reference documentation.

Four independent extraction bugs, each of which lost or misdescribed part of
the API surface without failing the docs build.

`@example` deleted the symbol it documented. Doxygen's \example takes a
filename; used as an inline snippet marker it pulls the block out of the
member's documentation. The only two symbols using it were the only two
missing from the manifest: eql_v3.grouped_value lost its whole memberdef and
eql_v3.lints() was left with an empty description, then dropped by the
no-documentation guard. Both now use @code{.sql}/@Endcode.

CREATE AGGREGATE bodies broke the C++ parse. The trailing (sfunc = ...) is a
second parenthesised group with no C++ analogue, so Doxygen misread the
declaration: a bare argument type made it name the member after the TYPE
(grouped_value(jsonb) was extracted as a function called `jsonb`), and a
schema-qualified one truncated the argument list mid-body. Strip aggregate
bodies in the input filter, as it already does for dollar-quoted function
bodies, and read an aggregate's argument types from its declaration and its
return type from @return rather than from the C++ parse.

The `match` capability was derived from the pre-3.0.1 operator spelling.
Fuzzy match became @@ in 3.0.1 but the mapping still tested @>/<@, so NO
domain could be assigned `match`: text_match matched no branch and fell
through to the storage-only default, and text_search silently lost it.
Containment on encrypted JSON keeps @>/<@ and becomes its own capability
instead of being conflated with fuzzy match.

public.eql_v3_json was in no list at all. dump_catalog filtered the json
family's bare storage domain out of `stevec` as scalar, while `types`
iterates scalar_families(), which excludes the mixed json family wholesale.
The domain the SQL materializer creates therefore appeared in neither and
never reached the manifest. Carry it in the json-family inventory behind a
`scalar` flag so consumers do not describe it with the SteVec query surface.

Also renders @code blocks readably: Doxygen encodes each space as an <sp/>
element and the generic walk cleans every nested fragment, welding the SQL
together ("SELECTeql_v3.grouped_value(...)").

Verified against the 3.0.3 tree: 1988 -> 1989 functions, gaining
grouped_value and lints and dropping the phantom `jsonb` function; 52 -> 53
domains, gaining public.eql_v3_json; the `match` capability returns to
text_match and both text_search variants. Diffing every shared symbol
against the released 3.0.3 manifest shows no other description changes.

The stale test fixture in test_xml_to_json.py (which predated `typname` and
the 3.0.1 renames, and had been failing on main) is updated to the current
catalog shape and now pins the capability mapping and the json family.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@coderdan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f6b15f8-1334-4991-a8cd-29d192942fff

📥 Commits

Reviewing files that changed from the base of the PR and between c8ee10f and 8653d06.

📒 Files selected for processing (3)
  • tasks/docs/doxygen-filter.sh
  • tasks/docs/generate/test_xml_to_markdown.py
  • tasks/docs/generate/xml-to-markdown.py
📝 Walkthrough

Walkthrough

The changes correct generated manifest extraction for JSON-family domains, fuzzy matching, containment, aggregate declarations, and SQL documentation snippets. Catalog serialization now preserves scalar JSON storage entries with explicit metadata, while Doxygen preprocessing and XML conversion handle aggregate signatures and code blocks.

Changes

Manifest extraction fixes

Layer / File(s) Summary
Catalog inventory and capability manifest
crates/eql-codegen/src/dump.rs, tasks/docs/generate/xml-to-json.py, tasks/docs/generate/test_xml_to_json.py
Adds scalar metadata for JSON-family catalog entries, separates storage and JSON capabilities, corrects fuzzy-match and containment inference, and updates fixture assertions.
Aggregate preprocessing and extraction
tasks/docs/doxygen-filter.sh, tasks/docs/generate/xml-to-markdown.py
Filters aggregate bodies before Doxygen parsing and extracts aggregate parameters, return types, and verbatim code blocks from XML.
SQL documentation and release metadata
src/v3/aggregates.sql, src/v3/lint/lints.sql, .changeset/manifest-extraction-fixes.md
Replaces @example snippets with typed SQL code blocks and documents the manifest fixes with a patch release entry.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: freshtonic

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main docs fix: restoring symbols that manifest extraction was dropping.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/manifest-extraction-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tasks/docs/generate/xml-to-json.py`:
- Around line 81-92: Update the JSON-domain manifest generation for
public.eql_v3_json_search and eql_v3.query_json to derive capabilities and
supportedOperators from the catalog through _capabilities_from_ops instead of
static storage/json values and empty operator lists. Ensure catalog entries for
@>, <@, ->, and ->> are reflected in the emitted manifest while preserving the
existing @@ match and containment handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ae43972-6d71-435f-ae95-67b784231e19

📥 Commits

Reviewing files that changed from the base of the PR and between d57a2da and c8ee10f.

📒 Files selected for processing (8)
  • .changeset/manifest-extraction-fixes.md
  • crates/eql-codegen/src/dump.rs
  • src/v3/aggregates.sql
  • src/v3/lint/lints.sql
  • tasks/docs/doxygen-filter.sh
  • tasks/docs/generate/test_xml_to_json.py
  • tasks/docs/generate/xml-to-json.py
  • tasks/docs/generate/xml-to-markdown.py

Comment thread tasks/docs/generate/xml-to-json.py
@coderdan

Copy link
Copy Markdown
Contributor Author

Tracked in #428, which also records the one gap left open here (SteVec domains reporting no operators) and some suggested build-time guards. Not using Closes — the issue is broader than this PR, so it should outlive the merge.

@coderdan

Copy link
Copy Markdown
Contributor Author

Review performed by Codex (GPT-5.6-sol).

The recovered documentation contains incorrect source-line metadata and publishes a malformed return type for lints(). Both affect the generated manifest’s accuracy.

  • [P2] Preserve line numbers while skipping aggregate bodiestasks/docs/doxygen-filter.sh:64

    For every multiline aggregate, this branch consumes body rows without printing replacements. Doxygen reports filtered line numbers, so later symbols are shifted; for example, max_sfunc at line 41 is emitted as 36 and max at line 57 as 52. Emit blank lines while skipping to preserve source metadata.

  • [P2] Parse the recovered table return type completelysrc/v3/lint/lints.sql:67

    When this change recovers lints(), Doxygen emits RETURNS TABLE(severity text in argsstring, and the generic parser records the malformed type TABLE(severity in the manifest. Add handling for multiline RETURNS TABLE declarations, with a test covering this newly published entry.

…uncated return type

Addresses both findings from the Codex review.

Skipping CREATE AGGREGATE bodies dropped rows outright. Doxygen reports source
positions against the FILTERED stream, so every symbol after an aggregate was
reported five lines early — max_sfunc at line 41 came through as 36. Emit a
blank line per skipped row, which is how the dollar-quoted body stripper has
always kept its 1:1 mapping. Verified across every SQL file in src/: zero
line-count drift, and all 1926 symbols shared with the released 3.0.3 manifest
now report its exact source lines.

lints() was published with the return type "TABLE(severity". Doxygen reads a
`RETURNS TABLE (col type, ...)` list as a C++ argument list and truncates it at
the first comma, and the return-type regex then matched to the next space.
Joining the declaration onto one line does not help — the truncation is at the
comma, not the line break — so the column list is unrecoverable from the XML.
Stop the match at "(" instead, giving a clean `TABLE`; the full column list is
already carried by the @return tag in returns.description. No other return type
in the corpus contains a paren, and none changed.

Both are regressions this branch introduced by recovering the symbols in the
first place: lints() was absent from the manifest before, so its malformed
return type could not surface, and nothing skipped aggregate bodies.

Adds a regression test for each, including one that runs the filter and asserts
input and output line counts match (confirmed failing without the fix).

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@coderdan

Copy link
Copy Markdown
Contributor Author

Both Codex findings were valid and are fixed in 8653d06. Both were regressions this branch introduced by recovering the symbols in the first place — lints() was absent from the manifest before, so its malformed return type could not surface, and nothing skipped aggregate bodies.

[P2] Line numbers — correct, and the mechanism is exactly as described. The existing dollar-quoted body stripper prints one output line per input line (possibly empty); my aggregate rule used next and printed nothing, so every symbol after an aggregate shifted. Now emits a blank line per skipped row.

Verified across every SQL file under src/: zero line-count drift, and all 1926 symbols shared with the released 3.0.3 manifest now report its exact source lines (previously non-zero drift). Spot-checking the example from the review:

true source:  min_sfunc=14  min=30  max_sfunc=41  max=57
manifest:     min_sfunc=14  min=30  max_sfunc=41  max=57

[P2] RETURNS TABLE — correct that it was being published malformed, though not quite for the reason given. I tried the suggested fix first: joining the multi-line declaration onto one line (with blank padding to preserve the line mapping just fixed). It does not work. Doxygen still yields () RETURNS TABLE(severity text — it reads the column list as a C++ argument list and truncates at the first comma, not at the line break. So the column list is unrecoverable from the XML regardless of how the source is laid out, and I dropped that approach rather than carry dead complexity.

Fixed instead by stopping the return-type match at ( as well as whitespace, which yields a clean TABLE. The full column list is not lost — it is already carried by the @return tag:

"returns": {
  "type": "TABLE",
  "description": "SETOF record (severity text, category text, object_name text, message text)"
}

lints() is the only RETURNS TABLE in the tree, and no return type in the corpus contains a paren, so nothing else is affected — confirmed by diffing return types for all shared symbols against the released manifest: zero changes.

Tests added for both, per the review. The line-numbering one runs the filter and asserts input and output line counts match; I confirmed it fails when the fix is reverted. Generator suite is 11 passed.

@coderdan
coderdan enabled auto-merge July 28, 2026 03:59
@coderdan
coderdan added this pull request to the merge queue Jul 28, 2026
Merged via the queue into main with commit d5a2e17 Jul 28, 2026
20 checks passed
@coderdan
coderdan deleted the docs/manifest-extraction-fixes branch July 28, 2026 04:36
coderdan added a commit to cipherstash/docs that referenced this pull request Jul 28, 2026
EQL 3.0.4 ships the manifest-extraction fixes from
cipherstash/encrypt-query-language#427, so `eql_v3.grouped_value` and
`public.eql_v3_json` now resolve from the catalog and no longer need drift-check
exemptions. Verified against the released manifest: grouped_value(jsonb) and
lints() present, 53 domains (was 52) including public.eql_v3_json, and the
`match` capability restored to text_match and both text_search variants.

Only `eql_v3.query_text_eq` stays on the allowlist — the 40 scalar
query-operand domains created in a `DO ... EXECUTE` block are a separate gap
that 3.0.4 does not address. Removing the other two means a regression in
either now fails the drift check instead of staying quiet.

Also always stamp the pinned tag as the manifest's version rather than only
substituting when it reads "DEV". EQL's release workflow derives that field
from a tag input that can arrive empty, in which case json.sh falls back to
"DEV" — which 3.0.4 shipped, and which would otherwise render as "EQL DEV" in
the banner on every reference page. The existing guard already covered that
case; this extends it to warn when a manifest is stamped with a DIFFERENT
release, which would be a packaging mix-up worth seeing rather than a cosmetic
default worth silencing.

bun run build passes: drift check clean against 3.0.4, 1990 functions,
banner reads EQL 3.0.4.

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants