Skip to content

Releases: OpenPredicate/open-predicate

v0.6.0 — the project is OpenPredicate

Choose a tag to compare

@christosgkoros christosgkoros released this 13 Sep 13:40

A naming release. No change to the grammar or to the semantics of evaluation:
open-predicate-schema.json is byte-identical to 0.5.0 apart from three lines — its $id, its
title and its $comment — and the $id still names v0.4.0, because the $id version tracks
the grammar and the grammar did not move. Consumers pinning that $id have only the new namespace
to re-point at.

Changed

  • The name is settled, and the project has a dedicated organisation. OpenPredicate is
    stewarded by the OpenPredicate organisation, whose purpose is to
    carry the grammar to an open standard and push for its adoption. This is the single pass the
    README promised: the repository, both package names, the schema $id, the problem-type URIs,
    the CLI and the vendor keyword all derive from the one namespace, so nothing is left
    half-named.

    Name
    Repository OpenPredicate/open-predicate
    Schema file open-predicate-schema.json
    Schema $id https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json
    Problem types https://openpredicate.tech/problems/…
    npm package open-predicate
    GitHub Packages @openpredicate/open-predicate
    CLI open-predicate-generate
    Vendor keyword x-open-predicate
    Generator config open-predicate.config.json

    Migration. Mechanical, and only for identifiers — no filter valid before this release becomes
    invalid, because the grammar and the evaluation semantics did not move. In a resource schema, the
    vendor keyword the generator reads is x-open-predicate; any other spelling is silently ignored,
    so a field you meant to exclude would become queryable. Point any $ref or pinned $id at the
    $id above, and any RFC 9457 type matching at the problem base above. A generator config file
    is open-predicate.config.json, or pass it explicitly with --config. The $id still names
    v0.4.0 — the version tracks the grammar, which is unchanged; only the namespace it sits under
    is new. Nothing was ever served or published under any other namespace or package name, so no
    working deployment can be pinned elsewhere.

Added

  • Brand assets, in assets/. The { > } mark — JSON braces around a comparison —
    as SVG and as raster at three sizes, plus a wordmark for light and dark backgrounds.
    assets/README.md states the palette and the usage rules. The README now
    opens with the mark. MIT-licensed with the rest of the repository.

Full changelog: https://github.com/OpenPredicate/open-predicate/blob/v0.6.0/CHANGELOG.md

v0.5.0 — a generator a provider can point at their own API

Choose a tag to compare

@christosgkoros christosgkoros released this 13 Sep 11:29
28567bf

A tooling release. No change to the grammar or to the semantics of evaluation:
query-language-schema.json is byte-identical to 0.4.0 apart from its root description, and its
$id still names v0.4.0, because the $id version tracks the grammar and the grammar did not
move. Consumers pinning that $id have nothing to do.

What did move is the generator, which is now the thing an API provider uses when implementing
search: point it at the resource schema, state the slice of the language you can actually serve,
and get back a filter schema that permits exactly that slice plus a capability document that
describes it honestly. It also travels with the package for the first time, as a bin named
jql-generate, rather than being a file inside a repository nobody installs.

SPEC.md gains two clarifications in service of that, both about what an
implementation may claim rather than about 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. No filter valid under 0.4.0 becomes invalid.

Added

  • The generator selects capabilities, not just profiles
    (#11). Profiles are the unit
    a server advertises, but three shapes do not fit inside one: a backend with LIKE and no
    POSITION supports $like and not $contains; a key-value store cannot implement $exists at
    all; a provider compiling to a flat conjunctive index wants one AND level and no shorthand. Six
    new knobs, each available as a flag and as a JS API option — --operators, --drop-operators,
    --no-shorthand, --max-filter-depth, --limits, and --config to hold the combination. What
    is declined is absent from the generated schema, so a client learns it from validation rather
    than from an unsupported-operator at runtime. Defaults are unchanged: with none of them given
    the output is byte-identical to before.
  • --config <file>, and examples/pet.jql.config.json. The capability selection is a
    decision about the endpoint, not a shell invocation, so it goes in a JSON file checked in beside
    the resource schema and regenerated from. Its keys are the JS API's option names plus resource,
    out and capabilities; relative paths in it resolve against its own directory, an explicit
    flag beats it, and an unrecognised key is refused rather than ignored — a misspelled key is a
    capability that silently did not apply. npm run generate:example now runs through one.
  • --max-filter-depth <n> caps how deep $and/$or/$nor/$not may nest: 1 is a flat
    filter offering no logical operators at all, 2 permits one level of them. JSON Schema
    cannot count how deep an instance already is, so the filter is emitted as a chain of levels:
    level i offers the logical operators over level i+1 and the last level does not offer them
    at all. Every level shares the operand $defs, so the cost is n copies of a map of $refs.
    Field-level $not is bounded to a single application by the same flag — under Kleene logic
    ¬¬X ≡ X even for UNKNOWN, so a negated negation says nothing the plain constraint does not.
  • --limits <json|@file> puts the SPEC §7 numbers a provider actually enforces into the
    capability document. They were emitted unconditionally, so every document generated from the CLI
    claimed maxDepth: 10, maxClauses: 100, maxSetLength: 1000 whether or not that was true. Where
    --max-filter-depth is given and maxDepth is not, the enforced bound is published.
  • The generator is part of the package. tools/ was not in package.json files and there
    was no bin entry, so the tool the README points readers at could not travel with the package
    at all. It is now a bin named jql-generate, with json-query-language/generate exporting
    generateFilterSchema for programmatic use. This repository still publishes no artifacts
    (RELEASING.md), so the command is reachable from a clone or a git install and
    not from npmjs; what changed is that it is ready to be, and npm pack now contains it.
  • SPEC.md §2.2 documents the capability document's
    top-level members
    queryLanguage, profiles, fields, limits and filterSchema — in a
    table beside the existing per-field one. limits appeared in the example and in no table, and
    filterSchema, itemValues and nullable were emitted by the generator and described nowhere.
    No normative change to what the members mean.
  • SPEC.md §2.1 says what a partial profile may and may not do. The
    rule was already there — a profile other than core is implemented in full or not at all — but
    it read as a prohibition on the implementation rather than on the advertisement. An endpoint
    accepting part of a profile is not prohibited from existing; it states what it accepts per path
    and omits the incomplete profile from profiles.
  • examples/mcp-server/ — a runnable MCP server whose one tool, search_pets, takes a
    filter as its filter argument and nothing else. The tool's inputSchema is
    examples/pet.filter.json inlined verbatim; validation is ajv against that same file, and
    execution is the SQL compiler from experiments/filter-to-sql over an in-memory SQLite table,
    so the queries are real. node examples/mcp-server/demo.mjs drives it over stdio and prints a
    transcript: two filters that answer, one that shows the $unknownAs difference (4 matches
    against 7), and the three valid-but-wrong filters from README §Exposing search to an agent
    being rejected with a pointer at the clause. npm run example:mcp and
    npm run example:mcp:demo are the entry points.
  • The example is also the first place the $id-when-inlining hazard is written down: nested
    under properties.filter, a bundled schema's self-references resolve against its own $id, so
    removing the $id breaks it — ajv fails to compile it at all.

Changed

  • The capability document's profiles reports coverage rather than the request. It echoed
    whatever --profiles was given; it now lists only the profiles the final operator set covers in
    full, because SPEC.md §2.1 makes a partial profile one an
    implementation may not advertise. --drop-operators '$contains' therefore costs the strings
    claim, and the per-field operators lists carry what is on offer instead — with a warning on
    stderr naming the operator responsible. Declining a core operator drops core too, and warns
    that the result is not a conforming implementation. Nothing changes for a selection that is
    whole profiles, which is every invocation before this release.
  • Positioned as one JSON-Schema-described query language with two integration points, rather
    than as an agent interface. An earlier revision in this same unreleased window led with the MCP
    tool definition and moved §Exposing search to an agent ahead of the OpenAPI and generator
    sections; that ordering is reverted and the "search interface for agents" framing is gone from
    the README, the package.json description and the repository description. The agent use case
    keeps its section and its runnable server — it is one of the two things the schema is for, not
    the thing the document opens with.
  • The schema's root description likewise leads with the shared-grammar framing again, and
    mentions inlining as a tool's input schema second. Non-normative prose; no validator behaviour
    changes.
  • The error format is no longer mandated. SPEC.md §8 required
    RFC 9457 Problem Details with media type
    application/problem+json. It now requires only that a rejected filter be answered with
    400 Bad Request and that the response say which of the five conditions applies —
    malformed-query, unknown-field, unsupported-operator, invalid-operand,
    query-too-complex — because that is what a client branches on. The envelope is the API's own:
    an API with an established error format should express these conditions in it rather than carry
    a second format for one endpoint. RFC 9457 remains the RECOMMENDED default where there is none,
    and the type URIs, the pointer member and the recovery members (queryableFields,
    accepted) are unchanged as its encoding. This relaxes a requirement, so nothing that
    conformed before stops conforming.

Fixed

  • --max-depth accepted a value that was not a number. It was coerced with Number() and
    never checked, so --max-depth deep became NaN and silently stopped the walk at the first
    nested object. It and --max-filter-depth are both validated now.
  • An operator whose dependency was dropped is dropped with it. $flags carries
    dependentRequired: ["$regex"] out of the grammar, so --drop-operators '$regex' would have
    left $flags in properties with a rule naming a member additionalProperties: false forbids
    — present in the schema and impossible to use. The closure is read off
    $defs/ConstraintObject, so a dependency added later is handled by construction.
  • Generated filter schemas were not a narrowing ([#8](https://github.com/christosgkoros/...
Read more

v0.4.0 — one array quantifier family, and an escape hatch for UNKNOWN

Choose a tag to compare

@christosgkoros christosgkoros released this 09 Sep 01:28
42f3ead

Breaking. The $id is now …/v0.4.0/query-language-schema.json. This release resolves the
three operator overlaps that an external review and this repository's own
experiments/filter-to-sql flagged independently; the design and the evidence are in
decisions/0001-array-quantifiers-and-unknown-handling.md.

The headline is that the language had two unrelated mechanisms for looking inside an array —
$elemMatch and the [*] path segment — and one mechanism now does both jobs while naming its
quantifier. Operator count is unchanged at 34.

Added

  • $some and $every (profile collections), the element quantifiers. Each takes a Filter
    when the elements are objects — paths inside resolve against the element — or a constraint
    object when they are scalars. $some is $elemMatch renamed; $every is new, because
    universal quantification over elements was not previously expressible: $not over $some
    is "no element matches", which is a different predicate.
  • $unknownAs (profile core), a boolean modifier on a constraint object that resolves that
    constraint's UNKNOWN. {"status": {"$ne": "archived", "$unknownAs": true}} is the one-clause
    form of the $or/$isNull longhand this specification prescribed before. It applies last —
    after every sibling operator, including a field-level $not — and SPEC.md
    §4.6
    gives the scope rules and the nine-case proof
    that resolution distributes over three-valued AND. It requires at least one operator beside it.
  • A truth-table column for $nor in §4.1, and a note that all three connectives are
    commutative so the six rows cover all nine combinations. $nor's three-valued result previously
    had to be derived, and the derivation was the trap.
  • $every on generated schemas, and $unknownAs on exactly the fields where UNKNOWN is
    reachable — the same rule the generator already applied to $exists and $isNull. On a
    property that is required all the way up and cannot hold null, the modifier would be a constant,
    so it is omitted and the trap disappears from the tool definition entirely.

Removed — breaking

  • $elemMatch. Renamed to $some. Mechanical: {"items": {"$elemMatch": {…}}}
    {"items": {"$some": {…}}}.
  • $hasAny and $hasNone. Both were compositions of a quantifier and $in, and their
    presence beside whole-value $in was the whole $in-versus-membership confusion.
    {"tags": {"$hasAny": ["a"]}}{"tags": {"$some": {"$in": ["a"]}}};
    {"tags": {"$hasNone": ["a"]}}{"tags": {"$not": {"$some": {"$in": ["a"]}}}}.
  • The [*] wildcard path segment, from the §3.2 grammar. It expressed nothing the equivalent
    $some clauses do not: per-constraint existential scope is exactly what an $and of separate
    $some clauses means. {"items[*].qty": {"$gt": 2}}
    {"items": {"$some": {"qty": {"$gt": 2}}}}. Three further reasons it went: living in the path
    grammar made it the only construct present in every profile including core, so no server
    could decline it; it contradicted §4.2 by revoking $exists's totality; and it cost 1.74× the
    SQL of the equivalent $elemMatch plus a table-valued join per clause. The schema now rejects a
    [*] path outright, including in $field position, so a stale filter is a validation error
    rather than a path read as a literal key name.
  • $defs/ScalarSet. $in and $nin now take $defs/OperandSet, the same set definition the
    collection operators use. The two definitions had silently diverged — $hasAny accepted $field
    references and object members while $in accepted only scalars — with nothing in the
    specification acknowledging it. The unification is toward the permissive side, so no filter that
    was valid becomes invalid.

Changed — breaking

  • A type-mismatched equality is FALSE, not UNKNOWN. §4.3 said comparing different JSON types
    yields UNKNOWN; §5.1 defined $eq as structural equality, under which a string and a number are
    simply unequal. The two readings are indistinguishable under $eq and differ under $ne, and
    the specification asserted both. It is now settled as FALSE for the equality family
    ($eq, $ne, $in, $nin, $hasAll) and UNKNOWN for ordering, string and array operators,
    with a table in §4.3. This changes result sets without changing any filter's shape, so a
    mechanical rewrite will not surface it: {"notes": {"$ne": 3}} now matches a record whose
    notes is "hello".
  • An empty array under a former wildcard clause. [*] on [] was UNKNOWN, because the path
    resolved to nothing; $some on [] is FALSE, because an empty array is a resolved value and
    nothing in it satisfies the condition. $every on [] is TRUE, vacuously. Observable under
    negation only, and it is the one migration step a codemod cannot claim to preserve.
  • §3.4 resolution is single-valued. With no wildcard segment, a path yields zero values or
    exactly one. The sequence model is gone.
  • Six operator descriptions that contradicted §4.1. These strings are vendored verbatim into
    generated schemas and MCP tool definitions, so they were a first-order cause of the confusion
    rather than a cosmetic issue. $nor's was outright wrong — "None of the listed filters may
    evaluate TRUE" is the two-valued reading — and $ne's said only "Field does not equal the
    operand". $nin, $nbetween, $nlike and $nilike all read as total predicates. Every
    negative operator now states what it does with UNKNOWN.
  • §1 no longer calls the filter "a boolean function" while §4.1 makes it three-valued.
  • $exists is documented as unconditionally total. It always was, except under a wildcard
    path; with those gone the exception is gone.
  • §3.5 settles whether an index suffix is a separate path. It is not: items[0] is the field
    items for queryability, while a named member beneath it (items[0].sku) is its own path.
  • §7 addresses quantifier cost. $some and $every are the expensive operators on most
    backends, and a server that cannot afford them can decline the collections profile — which is
    precisely what the [*] segment made impossible.

Fixed

  • $some/$every's operand shape is no longer ambiguous. anyOf: [Filter, ConstraintObject]
    overlaps on a leading $not, and nothing said which was meant. §5.8 now gives a decidable rule:
    scan for the first member that can only be one of the two, recursing through $not/$and/$or/
    $nor bodies when the outer member is itself ambiguous.
  • $field inside a quantifier resolves against the element, stated in §5.8 and §5.11. §5.11
    said "the same record" while §5.8 said paths were element-relative; both readings were
    defensible.

v0.3.1 — a release that publishes nothing

Choose a tag to compare

@christosgkoros christosgkoros released this 04 Sep 12:50

0.3.1 is the release 0.3.0 never became: the 0.3.0 tag was cut against a pipeline that still published to npmjs.com and GitHub Packages, and this project ships no artifacts while its name is a working title. The notes below therefore cover both versions.

Nothing here is downloadable but the source. To use the schema, vendor it:

curl -O https://raw.githubusercontent.com/christosgkoros/json-query-language/v0.3.1/query-language-schema.json

[0.3.1] — 2026-09-04

No change to the schema, the grammar or the semantics. query-language-schema.json is
byte-identical to 0.3.0 and its $id still names v0.3.0, because the $id version tracks the
grammar and the grammar did not move. Consumers pinning that $id have nothing to do.

Removed

  • Publishing. The release workflow no longer ships to npmjs.com or GitHub Packages. Neither
    registry ever received a copy, and while the name is a working title neither should: publishing
    under a placeholder claims the name, and npm blocks a name from reuse permanently once it has
    been published and unpublished. .github/workflows/release.yml now only verifies a release —
    the test suite, and the tag-against-package.json check — and uploads nothing. The
    NPM_TOKEN secret and .github/scripts/version-published.sh are deleted with it.
    RELEASING.md keeps what the jobs needed, so they
    can be restored from git history rather than rewritten.

Changed

  • README no longer offers an install that does not exist. The Quickstart opened with
    npm install --save-dev json-query-language, which the README's own Status table already
    contradicted two screens further down. It now vendors the file by curl, which is the only
    way to obtain the schema and always was.
  • RELEASING.md documents the process that exists — a tag and a GitHub
    Release, carrying notes and a source snapshot and nothing else.

[0.3.0] — 2026-09-04

Guidance for adopters exposing a search endpoint to an LLM agent, the tooling that acts on it,
and an honest statement of how finished this is. No grammar change: every filter valid under
v0.2.0 remains valid, and the only edits to query-language-schema.json are two description
annotations and its version strings.

Added

  • Per-field domains in the capability document. SPEC.md §2.2's
    RECOMMENDED shape now carries type, format, values and description alongside
    operators, with a table defining each. The grammar cannot express per-field operand
    domains — every path shares one Constraint — so a filter naming a real field with an
    out-of-domain value is well-formed and matches nothing. The capability document is the only
    place that domain can be stated.

  • Recovery members on problem details. SPEC.md §8 now RECOMMENDS that
    unknown-field carry queryableFields and that invalid-operand carry accepted, so a
    client that never fetched the capability document can still converge in one round trip
    instead of guessing field names one at a time.

  • README §Exposing search to an agent — what reaches a tool definition, the three
    valid-but-wrong filters that fail as an empty result set, and the five steps that prevent
    them (bundle rather than remote-$ref, narrow FieldPath, publish value domains, trim to
    advertised profiles, state the null and $in semantics in the tool description).

  • tools/generate-filter-schema.mjs — derives a per-resource filter schema from the
    resource's own JSON Schema. The published grammar shares one Constraint across every field,
    so it can say {"status": "Available"} is well-formed but not that "Available" is outside
    status's domain; that is why SPEC.md §2.2 exists. A
    generated schema gives each queryable path its own constraint subschema, carrying only the
    operators that apply to its type and only the operands its domain admits — so the three
    valid-but-wrong filters catalogued in README §Exposing search to an agent become validation
    failures instead of empty result sets. The generator emits the §2.2 capability document from
    the same source, and copies operator prose out of the published grammar rather than restating
    it. Generation is narrowing only: every filter a generated schema accepts is valid against the
    published grammar, which tests/generator.test.mjs asserts.

  • COMPARISON.md — how this specification relates to GraphQL, and what a JSON-Schema-native
    alternative to GraphQL would still need. The short version: GraphQL never standardised
    filtering, so the two overlap far less than the question assumes. Also covers OData, JSON:API,
    OGC CQL2 and JSON Hyper-Schema as prior art.

  • examples/pet.schema.json with its generated pet.filter.json and pet.capabilities.json
    committed beside it, and npm run generate:example to refresh them. A test fails if they drift.

  • README §Generating a per-resource filter schema — what the generator decides and why, and
    the x-jql property annotations that override it.

Changed

  • README framing. The schema is presented as feeding two integration paths rather than
    one: $ref'd from an OpenAPI document, or bundled into an MCP tool's inputSchema. The
    Referencing by URL or by copy table gains an MCP inputSchema row recording that the
    absolute-URL form does not work there at all, since nothing on that path resolves remote
    refs.
  • $in and $nin descriptions now state that they compare the whole value and do not test
    array membership, naming $hasAny/$hasNone as the element operators. $contains already
    warned about the same crossover; these two did not, and they are the operators a client
    carrying MongoDB habits reaches for first.
  • $id is now …/v0.3.0/query-language-schema.json. Consumers pin by $id, so the version
    in the path moves with the release. SPEC.md, the OpenAPI examples and the generated
    capability document were all still naming v0.2.0; they now agree.
  • README §Status states that this is a work in progress, name included. JSON Query
    Language
    is a working title, and every identifier downstream of it — both package names, the
    $id, the URLs in the integration examples — is a placeholder, several of which do not
    resolve. Getting them right is deliberately deferred until the name is settled, because a
    rename moves all of them at once. A notice at the top of the README says the same thing before
    a reader reaches an install command that will not work.
  • QUERY now cites RFC 10008 rather than
    draft-ietf-httpbis-safe-method-w-body. The method reached Proposed Standard in June 2026.
    The advice to ship POST /search alongside it is unchanged, but the reason is now that
    deployed support trails a fresh RFC, not that the specification is unsettled.

v0.2.0 — enforce and complete the grammar

Choose a tag to compare

@christosgkoros christosgkoros released this 06 Aug 08:47

First published release. The v0.1.0 schema described a grammar but did not enforce one: its root used "id" rather than "$id" and wrapped definitions in components.schemas, so under draft 2020-12 the root carried no assertion keywords and accepted every instance.

Fixed — the schema now validates. The eight-way oneOf over leaf conditions collapses into one recursive Filter, which makes {"age": {"$gt": 18, "$ne": 30}} expressible (it was rejected before) and removes the {}-matches-everything ambiguity. "regex" → dropped (not a JSON Schema keyword), examples corrected to arrays, $in/$nin now accept booleans and null.

Added$nor, $nbetween, the string family, $regex/$flags, $exists, $type, the collection operators, $search, field-level $not, and {"$field": "path"} for WHERE price > cost. Plus a field-path grammar, conformance profiles, SPEC.md for the semantics a schema cannot express, and OpenAPI 3.1 / 3.2 integration examples.

Breaking$isnull$isNull; #/components/schemas/*#/$defs/*. Filter documents are otherwise unaffected.

Full detail in CHANGELOG.md.