Skip to content

v0.5.0: let a provider select the operators it serves, and ship the generator - #12

Merged
christosgkoros merged 2 commits into
mainfrom
feature/generator-capability-selection
Sep 13, 2026
Merged

christosgkoros merged 2 commits into
mainfrom
feature/generator-capability-selection

Conversation

@christosgkoros

Copy link
Copy Markdown
Collaborator

Closes #11.

The generator narrowed the value side of a filter well and left the feature side all-or-nothing per profile. Internally it was already operator-granular — every emission site gates on ctx.enabled — but the only thing that could write to that set was the union of whole profiles. A backend with LIKE and no POSITION could not offer $like without also advertising $contains; a store with no notion of key absence could not decline $exists at all.

What this adds

Six knobs, each a CLI flag and a JS API option:

--operators / --drop-operators intersect with, or subtract from, the profile union
--no-shorthand every constraint takes the object form
--max-filter-depth <n> how deep $and/$or/$nor/$not may nest
--limits <json|@file> the SPEC §7 numbers you actually enforce
--config <file> the whole selection, in a file you check in

All three shapes from the issue work. Whatever a provider declines is absent from the generated schema, so a client learns it from validation rather than from a runtime error — or worse, from an empty result set.

--max-filter-depth is the one that restructures output. JSON Schema cannot count how deep an instance already is, so a bounded filter is emitted as a chain: level i offers the logical operators over level i+1 and the last offers none. Levels share the operand $defs, so the cost is n copies of a map of $refs. Unbounded stays the single self-referential object it has always been, and every default is byte-identical to before.

The spec decision

Issue #11 asked whether operator-level narrowing is a tooling change or a tooling-plus-spec one. It takes the first option it lists: a partial profile is demoted. profiles in the capability document now lists only the profiles the final operator set covers in full, the per-field operators lists carry the rest — which §2.2 already described as "a subset of those implied by profiles" — and the demotion is warned about on stderr naming the operator responsible. Declining a core operator says outright that the result is not a conforming implementation.

SPEC.md gains two clarifications, both about what an implementation may claim rather than what a filter means: §2.1 says out loud that accepting part of a profile is permitted and advertising it is not, and §2.2 documents the capability document's top-level members (limits appeared in the example and in no table; filterSchema, itemValues and nullable were emitted by the generator and described nowhere). No grammar change, no $id bump.

Two bugs found on the way

  • --include pruned only the root filter, so with a depth cap every excluded path walked back in one $and down.
  • An operator whose dependency was dropped stayed. --drop-operators $regex left $flags with a dependentRequired rule naming a member additionalProperties: false forbids — present in the schema and impossible to use. The closure is read off $defs/ConstraintObject, so a later dependency is handled by construction.

--max-depth also stopped coercing a non-number to NaN in silence.

Verification

132 tests pass, up from 119. The new shapes are covered by the narrowing property, not a case list: the seeded sampler in tests/fuzz.mjs now runs over dropped operators, --no-shorthand and a capped depth, with its non-vacuity assertions intact. npm run generate:example runs through examples/pet.jql.config.json and the committed outputs are byte-identical; the MCP demo transcript is unchanged.

Also carries 0ba799a, the #8 narrowing fix, which was pushed to its own branch and never opened as a PR. It is already documented in this changelog window.

Generated with Claude Code

christosgkoros and others added 2 commits September 13, 2026 14:23
The generator narrowed the value side of a filter well and left the feature
side all-or-nothing per profile. Internally it was already operator-granular —
every emission site gates on ctx.enabled — but the only thing that could write
to that set was the union of whole profiles, so a backend with LIKE and no
POSITION could not offer $like without also advertising $contains, and a store
with no notion of key absence could not decline $exists at all.

Six knobs reach the set and the emitted shape, each a flag and a JS API option:
--operators, --drop-operators, --no-shorthand, --max-filter-depth, --limits,
and --config to hold the combination in a file checked in beside the resource
schema. What a provider declines is absent from the generated schema, so a
client learns it from validation rather than from a runtime error — or worse,
from an empty result set.

--max-filter-depth is the one that restructures the output. JSON Schema cannot
count how deep an instance already is, so a bounded filter is emitted as a
chain: level i offers the logical operators over level i+1 and the last offers
none. Levels share the operand $defs, so the cost is n copies of a map of
$refs. Unbounded stays the single self-referential object it has always been,
and every default is byte-identical to before.

The capability document stops echoing the request. SPEC §2.1 makes a partial
profile one an implementation may not advertise, so `profiles` now lists only
the profiles the final operator set covers in full, the per-field `operators`
lists carry the rest, and the demotion is warned about on stderr naming the
operator responsible. Declining a core operator says outright that the result
is not conforming. §2.2 gains a table for the top-level members, two of which
the generator already emitted and nothing described.

The tool also travels with the package for the first time. tools/ was not in
package.json `files` and there was no bin entry, so the generator the README
points readers at could not leave the repository. It is now a bin named
jql-generate — reachable from a clone or a git install, since this repository
still publishes nothing to a registry, but present in `npm pack` and ready for
the day it does.

Two bugs surfaced on the way. --include pruned only the root filter, so with a
depth cap every excluded path walked back in one $and down. And an operator
whose dependency was dropped stayed: --drop-operators $regex left $flags with a
dependentRequired rule naming a member additionalProperties: false forbids. The
closure is read off $defs/ConstraintObject so a later dependency is handled by
construction. --max-depth also stopped coercing a non-number to NaN in silence.

The narrowing property is what covers the new shapes: the seeded sampler in
tests/fuzz.mjs now runs over dropped operators, no-shorthand and a capped
depth, because a list of filters can only re-check the leaks someone already
thought of.

Closes #11.

Co-Authored-By: Claude <noreply@anthropic.com>
Cuts the unreleased window as 0.5.0. Minor, and not breaking: no filter valid
under 0.4.0 becomes invalid.

The schema's $id stays at v0.4.0, as it did across 0.3.1, because the $id
tracks the grammar and the grammar did not move — query-language-schema.json
is byte-identical apart from the root description already in this window.
Consumers pinning that $id have nothing to do. SPEC.md's version line stays at
0.4.0 for the same reason.

Co-Authored-By: Claude <noreply@anthropic.com>
@christosgkoros
christosgkoros merged commit 28567bf into main Sep 13, 2026
4 checks passed
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.

Generator: let a provider select the operators and grammar features it supports, not just whole profiles

1 participant