@open-predicate/open-predicate 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,532 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with the pre-1.0 caveat that a
6
+ minor release may break compatibility, in which case the break is spelled out below.
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Changed
11
+
12
+ - **The npm package is scoped: `@open-predicate/open-predicate`.** 0.6.0 named it `open-predicate`,
13
+ unscoped; it now sits under the `open-predicate` organisation on npmjs.com, which is where the
14
+ project's packages will live. Nothing was ever published under the unscoped name, so there is no
15
+ version to migrate from and no redirect to leave behind. GitHub Packages is unaffected — it still
16
+ carries `@openpredicate/open-predicate`, because that scope has to match the repository owner.
17
+
18
+ One consequence worth knowing: `npx` resolves a *package* name, so the generator is now
19
+ `npx @open-predicate/open-predicate` rather than `npx open-predicate-generate`. The `bin` is
20
+ still named `open-predicate-generate` once the package is installed.
21
+
22
+ The unscoped `open-predicate` is claimed anyway, as a deprecated placeholder holding two files
23
+ and no code, so the name cannot end up on something unrelated to the project.
24
+ `npm install open-predicate` prints a redirect to the scoped package. It is not versioned
25
+ alongside releases and the release pipeline never touches it — see
26
+ [`RELEASING.md`](./RELEASING.md#the-reserved-unscoped-name).
27
+
28
+ ### Added
29
+
30
+ - **Publishing is back on, and npmjs.com authenticates by OIDC.**
31
+ [`.github/workflows/release.yml`](./.github/workflows/release.yml) publishes to both registries
32
+ when a GitHub Release is published. The npmjs job uses npm's [trusted
33
+ publishing](https://docs.npmjs.com/trusted-publishers): it requests `id-token: write` and npm
34
+ exchanges that for a short-lived credential, so there is **no `NPM_TOKEN` secret** in this
35
+ repository and nothing to rotate. Provenance is attached automatically, linking each tarball to
36
+ the workflow run and commit that built it. GitHub Packages stays token-authenticated — it has no
37
+ OIDC equivalent — but `GITHUB_TOKEN` is minted per run and expires with it.
38
+
39
+ `.github/scripts/version-published.sh` is restored alongside, so each job skips a version it has
40
+ already published and a partially-failed release can be re-run safely.
41
+
42
+ **The trust is pinned to the repository and to the workflow filename.** Renaming `release.yml`
43
+ breaks publishing until the trusted publisher is updated to match.
44
+
45
+ **npm cannot mint a package's first version over OIDC**, because a trusted publisher can only be
46
+ attached to a package that already exists. Claiming `@open-predicate/open-predicate` is therefore
47
+ a one-time manual publish, documented in
48
+ [`RELEASING.md`](./RELEASING.md#trusted-publishing-and-the-one-time-bootstrap). Until it has run,
49
+ neither registry has a copy and the README's *Status* table says so.
50
+
51
+ ## [0.6.0] — 2026-09-13
52
+
53
+ **A naming release.** No change to the grammar or to the semantics of evaluation:
54
+ `open-predicate-schema.json` is byte-identical to 0.5.0 apart from three lines — its `$id`, its
55
+ `title` and its `$comment` — and the `$id` still names `v0.4.0`, because the `$id` version tracks
56
+ the grammar and the grammar did not move. Consumers pinning that `$id` have only the new namespace
57
+ to re-point at.
58
+
59
+ ### Changed
60
+
61
+ - **The name is settled, and the project has a dedicated organisation.** **OpenPredicate** is
62
+ stewarded by the [OpenPredicate](https://openpredicate.tech) organisation, whose purpose is to
63
+ carry the grammar to an open standard and push for its adoption. This is the single pass the
64
+ README promised: the repository, both package names, the schema `$id`, the problem-type URIs,
65
+ the CLI and the vendor keyword all derive from the one namespace, so nothing is left
66
+ half-named.
67
+
68
+ | | Name |
69
+ | --- | --- |
70
+ | Repository | [`OpenPredicate/open-predicate`](https://github.com/OpenPredicate/open-predicate) |
71
+ | Schema file | `open-predicate-schema.json` |
72
+ | Schema `$id` | `https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json` |
73
+ | Problem types | `https://openpredicate.tech/problems/…` |
74
+ | npm package | `open-predicate` |
75
+ | GitHub Packages | `@openpredicate/open-predicate` |
76
+ | CLI | `open-predicate-generate` |
77
+ | Vendor keyword | `x-open-predicate` |
78
+ | Generator config | `open-predicate.config.json` |
79
+
80
+ **Migration.** Mechanical, and only for identifiers — no filter valid before this release becomes
81
+ invalid, because the grammar and the evaluation semantics did not move. In a resource schema, the
82
+ vendor keyword the generator reads is `x-open-predicate`; any other spelling is silently ignored,
83
+ so a field you meant to exclude would become queryable. Point any `$ref` or pinned `$id` at the
84
+ `$id` above, and any RFC 9457 `type` matching at the problem base above. A generator config file
85
+ is `open-predicate.config.json`, or pass it explicitly with `--config`. The `$id` still names
86
+ `v0.4.0` — the version tracks the grammar, which is unchanged; only the namespace it sits under
87
+ is new. Nothing was ever served or published under any other namespace or package name, so no
88
+ working deployment can be pinned elsewhere.
89
+
90
+ **On the entries below.** Past releases are written up in these names, so the whole document
91
+ reads in one vocabulary. Released artefacts are unaffected — this names the project, not history.
92
+
93
+ ### Added
94
+
95
+ - **Brand assets, in [`assets/`](./assets/).** The `{ > }` mark — JSON braces around a comparison —
96
+ as SVG and as raster at three sizes, plus a wordmark for light and dark backgrounds.
97
+ [`assets/README.md`](./assets/README.md) states the palette and the usage rules. The README now
98
+ opens with the mark. MIT-licensed with the rest of the repository.
99
+
100
+ ## [0.5.0] — 2026-09-13
101
+
102
+ **A tooling release.** No change to the grammar or to the semantics of evaluation:
103
+ `open-predicate-schema.json` is byte-identical to 0.4.0 apart from its root `description`, and its
104
+ `$id` still names `v0.4.0`, because the `$id` version tracks the grammar and the grammar did not
105
+ move. Consumers pinning that `$id` have nothing to do.
106
+
107
+ What did move is the generator, which is now the thing an API provider uses when implementing
108
+ search: point it at the resource schema, state the slice of the language you can actually serve,
109
+ and get back a filter schema that permits exactly that slice plus a capability document that
110
+ describes it honestly. It also travels with the package for the first time, as a `bin` named
111
+ `open-predicate-generate`, rather than being a file inside a repository nobody installs.
112
+
113
+ [SPEC.md](./SPEC.md) gains two clarifications in service of that, both about what an
114
+ implementation may *claim* rather than about what a filter means:
115
+ [§2.1](./SPEC.md#21-profiles) says out loud that accepting part of a profile is permitted and
116
+ advertising it is not, and [§2.2](./SPEC.md#22-capability-discovery) documents the capability
117
+ document's top-level members. No filter valid under 0.4.0 becomes invalid.
118
+
119
+ ### Added
120
+
121
+ - **The generator selects capabilities, not just profiles**
122
+ ([#11](https://github.com/OpenPredicate/open-predicate/issues/11)). Profiles are the unit
123
+ a server advertises, but three shapes do not fit inside one: a backend with `LIKE` and no
124
+ `POSITION` supports `$like` and not `$contains`; a key-value store cannot implement `$exists` at
125
+ all; a provider compiling to a flat conjunctive index wants one AND level and no shorthand. Six
126
+ new knobs, each available as a flag and as a JS API option — `--operators`, `--drop-operators`,
127
+ `--no-shorthand`, `--max-filter-depth`, `--limits`, and `--config` to hold the combination. What
128
+ is declined is absent from the generated schema, so a client learns it from validation rather
129
+ than from an `unsupported-operator` at runtime. Defaults are unchanged: with none of them given
130
+ the output is byte-identical to before.
131
+ - **`--config <file>`, and `examples/pet.open-predicate.config.json`.** The capability selection is a
132
+ decision about the endpoint, not a shell invocation, so it goes in a JSON file checked in beside
133
+ the resource schema and regenerated from. Its keys are the JS API's option names plus `resource`,
134
+ `out` and `capabilities`; relative paths in it resolve against its own directory, an explicit
135
+ flag beats it, and an unrecognised key is refused rather than ignored — a misspelled key is a
136
+ capability that silently did not apply. `npm run generate:example` now runs through one.
137
+ - **`--max-filter-depth <n>`** caps how deep `$and`/`$or`/`$nor`/`$not` may nest: `1` is a flat
138
+ filter offering no logical operators at all, `2` permits one level of them. JSON Schema
139
+ cannot count how deep an instance already is, so the filter is emitted as a chain of levels:
140
+ level *i* offers the logical operators over level *i+1* and the last level does not offer them
141
+ at all. Every level shares the operand `$defs`, so the cost is *n* copies of a map of `$ref`s.
142
+ Field-level `$not` is bounded to a single application by the same flag — under Kleene logic
143
+ `¬¬X ≡ X` even for UNKNOWN, so a negated negation says nothing the plain constraint does not.
144
+ - **`--limits <json|@file>`** puts the SPEC §7 numbers a provider actually enforces into the
145
+ capability document. They were emitted unconditionally, so every document generated from the CLI
146
+ claimed `maxDepth: 10, maxClauses: 100, maxSetLength: 1000` whether or not that was true. Where
147
+ `--max-filter-depth` is given and `maxDepth` is not, the enforced bound is published.
148
+ - **The generator is part of the package.** `tools/` was not in `package.json` `files` and there
149
+ was no `bin` entry, so the tool the README points readers at could not travel with the package
150
+ at all. It is now a `bin` named `open-predicate-generate`, with `open-predicate/generate` exporting
151
+ `generateFilterSchema` for programmatic use. This repository still publishes no artifacts
152
+ ([RELEASING.md](./RELEASING.md)), so the command is reachable from a clone or a git install and
153
+ not from npmjs; what changed is that it is ready to be, and `npm pack` now contains it.
154
+ - **[SPEC.md §2.2](./SPEC.md#22-capability-discovery) documents the capability document's
155
+ top-level members** — `queryLanguage`, `profiles`, `fields`, `limits` and `filterSchema` — in a
156
+ table beside the existing per-field one. `limits` appeared in the example and in no table, and
157
+ `filterSchema`, `itemValues` and `nullable` were emitted by the generator and described nowhere.
158
+ No normative change to what the members mean.
159
+ - **[SPEC.md §2.1](./SPEC.md#21-profiles) says what a partial profile may and may not do.** The
160
+ rule was already there — a profile other than `core` is implemented in full or not at all — but
161
+ it read as a prohibition on the implementation rather than on the advertisement. An endpoint
162
+ accepting part of a profile is not prohibited from existing; it states what it accepts per path
163
+ and omits the incomplete profile from `profiles`.
164
+ - **`examples/mcp-server/`** — a runnable MCP server whose one tool, `search_pets`, takes a
165
+ filter as its `filter` argument and nothing else. The tool's `inputSchema` is
166
+ `examples/pet.filter.json` inlined verbatim; validation is ajv against that same file, and
167
+ execution is the SQL compiler from `experiments/filter-to-sql` over an in-memory SQLite table,
168
+ so the queries are real. `node examples/mcp-server/demo.mjs` drives it over stdio and prints a
169
+ transcript: two filters that answer, one that shows the `$unknownAs` difference (4 matches
170
+ against 7), and the three valid-but-wrong filters from README §*Exposing search to an agent*
171
+ being rejected with a pointer at the clause. `npm run example:mcp` and
172
+ `npm run example:mcp:demo` are the entry points.
173
+ - The example is also the first place the `$id`-when-inlining hazard is written down: nested
174
+ under `properties.filter`, a bundled schema's self-references resolve against its own `$id`, so
175
+ removing the `$id` breaks it — ajv fails to compile it at all.
176
+
177
+ ### Changed
178
+
179
+ - **The capability document's `profiles` reports coverage rather than the request.** It echoed
180
+ whatever `--profiles` was given; it now lists only the profiles the final operator set covers in
181
+ full, because [SPEC.md §2.1](./SPEC.md#21-profiles) makes a partial profile one an
182
+ implementation may not advertise. `--drop-operators '$contains'` therefore costs the `strings`
183
+ claim, and the per-field `operators` lists carry what is on offer instead — with a warning on
184
+ stderr naming the operator responsible. Declining a `core` operator drops `core` too, and warns
185
+ that the result is not a conforming implementation. Nothing changes for a selection that is
186
+ whole profiles, which is every invocation before this release.
187
+ - **Positioned as one JSON-Schema-described query language with two integration points**, rather
188
+ than as an agent interface. An earlier revision in this same unreleased window led with the MCP
189
+ tool definition and moved §*Exposing search to an agent* ahead of the OpenAPI and generator
190
+ sections; that ordering is reverted and the "search interface for agents" framing is gone from
191
+ the README, the `package.json` description and the repository description. The agent use case
192
+ keeps its section and its runnable server — it is one of the two things the schema is for, not
193
+ the thing the document opens with.
194
+ - **The schema's root `description`** likewise leads with the shared-grammar framing again, and
195
+ mentions inlining as a tool's input schema second. Non-normative prose; no validator behaviour
196
+ changes.
197
+ - **The error format is no longer mandated.** [SPEC.md §8](./SPEC.md#8-errors) required
198
+ [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details with media type
199
+ `application/problem+json`. It now requires only that a rejected filter be answered with
200
+ `400 Bad Request` and that the response say **which** of the five conditions applies —
201
+ `malformed-query`, `unknown-field`, `unsupported-operator`, `invalid-operand`,
202
+ `query-too-complex` — because that is what a client branches on. The envelope is the API's own:
203
+ an API with an established error format should express these conditions in it rather than carry
204
+ a second format for one endpoint. RFC 9457 remains the RECOMMENDED default where there is none,
205
+ and the `type` URIs, the `pointer` member and the recovery members (`queryableFields`,
206
+ `accepted`) are unchanged as its encoding. This relaxes a requirement, so nothing that
207
+ conformed before stops conforming.
208
+
209
+ ### Fixed
210
+
211
+ - **`--max-depth` accepted a value that was not a number.** It was coerced with `Number()` and
212
+ never checked, so `--max-depth deep` became `NaN` and silently stopped the walk at the first
213
+ nested object. It and `--max-filter-depth` are both validated now.
214
+ - **An operator whose dependency was dropped is dropped with it.** `$flags` carries
215
+ `dependentRequired: ["$regex"]` out of the grammar, so `--drop-operators '$regex'` would have
216
+ left `$flags` in `properties` with a rule naming a member `additionalProperties: false` forbids
217
+ — present in the schema and impossible to use. The closure is read off
218
+ `$defs/ConstraintObject`, so a dependency added later is handled by construction.
219
+ - **Generated filter schemas were not a narrowing** ([#8](https://github.com/OpenPredicate/open-predicate/issues/8)).
220
+ `tools/generate-filter-schema.mjs` carried the published constraint object's
221
+ `dependentRequired` rule but not its `dependentSchemas` one, so `{"microchip": {"$unknownAs":
222
+ false}}` — a modifier with nothing to modify — passed a generated schema while
223
+ `open-predicate-schema.json` rejected it. A server following the documented path (generated
224
+ schema as the tool's `inputSchema`, published semantics behind it) then had to evaluate a filter
225
+ with no predicate in it; the SQL compiler in `experiments/filter-to-sql` emitted
226
+ `coalesce((), FALSE)` and the database answered with a syntax error. The generator now reads
227
+ both dependency keywords off `$defs/ConstraintObject` instead of restating either, so a rule
228
+ added there reaches generated schemas with the version that introduces it, and
229
+ `examples/pet.filter.json` is regenerated: 12 of its 18 constraint objects gain the rule — the
230
+ ones offering `$unknownAs`, which is every field that can be absent or null. The `$comment`
231
+ justifying the rule is deliberately not copied along with it: a validator never reads it, and
232
+ one copy per field is charged by the token to whoever inlines the schema in a tool definition.
233
+ No change to the grammar — this is the generator agreeing with it.
234
+ - **The narrowing property is now tested as a property.** `tests/generator.test.mjs` asserted it
235
+ over a hand-written list of fifteen filters, which can only re-check the leaks someone already
236
+ thought of — the keyword above was dropped for as long as the list existed. It now samples
237
+ filters out of each generated schema's own vocabulary (`tests/fuzz.mjs`, seeded, deterministic)
238
+ and asserts that every one the generated schema accepts is valid OpenPredicate, over three generated
239
+ schemas; the run is checked for not being vacuous, in that it must accept a fraction of its
240
+ samples and must reach every operator the schema offers. A second test pins what the generator
241
+ does with each instance-constraining keyword of `$defs/ConstraintObject`, so adding one there
242
+ fails the suite until it is handled.
243
+ - **`experiments/filter-to-sql` rejects a constraint object with no predicate in it** rather than
244
+ emitting an empty expression — `malformed-query`, at the pointer of the offending clause. Both
245
+ schemas already reject these, so this only matters for a compiler reached another way, but the
246
+ failure it replaces was a `500` from the database.
247
+ - The prose in [README §Errors](./README.md#errors), [COMPARISON.md §4](./COMPARISON.md), the
248
+ OpenAPI examples and `experiments/filter-to-sql` follows: they now describe Problem Details as
249
+ the recommended shape rather than the required one, and name the failing *condition* where they
250
+ previously said "problem". The examples still model RFC 9457, since it is still the default a
251
+ greenfield API should pick.
252
+
253
+ ## [0.4.0] — 2026-09-07
254
+
255
+ **Breaking.** The `$id` is now `…/v0.4.0/open-predicate-schema.json`. This release resolves the
256
+ three operator overlaps that an external review and this repository's own
257
+ `experiments/filter-to-sql` flagged independently; the design and the evidence are in
258
+ [`decisions/0001-array-quantifiers-and-unknown-handling.md`](./decisions/0001-array-quantifiers-and-unknown-handling.md).
259
+
260
+ The headline is that the language had **two** unrelated mechanisms for looking inside an array —
261
+ `$elemMatch` and the `[*]` path segment — and one mechanism now does both jobs while naming its
262
+ quantifier. Operator count is unchanged at 34.
263
+
264
+ ### Added
265
+
266
+ - **`$some` and `$every`** (profile `collections`), the element quantifiers. Each takes a `Filter`
267
+ when the elements are objects — paths inside resolve against the element — or a constraint
268
+ object when they are scalars. `$some` is `$elemMatch` renamed; `$every` is new, because
269
+ universal quantification over elements was **not previously expressible**: `$not` over `$some`
270
+ is "no element matches", which is a different predicate.
271
+ - **`$unknownAs`** (profile `core`), a boolean modifier on a constraint object that resolves that
272
+ constraint's UNKNOWN. `{"status": {"$ne": "archived", "$unknownAs": true}}` is the one-clause
273
+ form of the `$or`/`$isNull` longhand this specification prescribed before. It applies last —
274
+ after every sibling operator, including a field-level `$not` — and [SPEC.md
275
+ §4.6](./SPEC.md#46-resolving-unknown--unknownas) gives the scope rules and the nine-case proof
276
+ that resolution distributes over three-valued AND. It requires at least one operator beside it.
277
+ - **A truth-table column for `$nor`** in §4.1, and a note that all three connectives are
278
+ commutative so the six rows cover all nine combinations. `$nor`'s three-valued result previously
279
+ had to be derived, and the derivation was the trap.
280
+ - **`$every` on generated schemas**, and `$unknownAs` on exactly the fields where UNKNOWN is
281
+ reachable — the same rule the generator already applied to `$exists` and `$isNull`. On a
282
+ property that is required all the way up and cannot hold null, the modifier would be a constant,
283
+ so it is omitted and the trap disappears from the tool definition entirely.
284
+
285
+ ### Removed — breaking
286
+
287
+ - **`$elemMatch`.** Renamed to `$some`. Mechanical: `{"items": {"$elemMatch": {…}}}` →
288
+ `{"items": {"$some": {…}}}`.
289
+ - **`$hasAny` and `$hasNone`.** Both were compositions of a quantifier and `$in`, and their
290
+ presence beside whole-value `$in` was the whole `$in`-versus-membership confusion.
291
+ `{"tags": {"$hasAny": ["a"]}}` → `{"tags": {"$some": {"$in": ["a"]}}}`;
292
+ `{"tags": {"$hasNone": ["a"]}}` → `{"tags": {"$not": {"$some": {"$in": ["a"]}}}}`.
293
+ - **The `[*]` wildcard path segment**, from the §3.2 grammar. It expressed nothing the equivalent
294
+ `$some` clauses do not: per-constraint existential scope is exactly what an `$and` of *separate*
295
+ `$some` clauses means. `{"items[*].qty": {"$gt": 2}}` →
296
+ `{"items": {"$some": {"qty": {"$gt": 2}}}}`. Three further reasons it went: living in the path
297
+ grammar made it the only construct present in **every** profile including `core`, so no server
298
+ could decline it; it contradicted §4.2 by revoking `$exists`'s totality; and it cost 1.74× the
299
+ SQL of the equivalent `$elemMatch` plus a table-valued join per clause. The schema now rejects a
300
+ `[*]` path outright, including in `$field` position, so a stale filter is a validation error
301
+ rather than a path read as a literal key name.
302
+ - **`$defs/ScalarSet`.** `$in` and `$nin` now take `$defs/OperandSet`, the same set definition the
303
+ collection operators use. The two definitions had silently diverged — `$hasAny` accepted `$field`
304
+ references and object members while `$in` accepted only scalars — with nothing in the
305
+ specification acknowledging it. The unification is toward the permissive side, so no filter that
306
+ was valid becomes invalid.
307
+
308
+ ### Changed — breaking
309
+
310
+ - **A type-mismatched equality is FALSE, not UNKNOWN.** §4.3 said comparing different JSON types
311
+ yields UNKNOWN; §5.1 defined `$eq` as structural equality, under which a string and a number are
312
+ simply unequal. The two readings are indistinguishable under `$eq` and differ under `$ne`, and
313
+ the specification asserted both. It is now settled as **FALSE for the equality family**
314
+ (`$eq`, `$ne`, `$in`, `$nin`, `$hasAll`) and UNKNOWN for ordering, string and array operators,
315
+ with a table in §4.3. **This changes result sets without changing any filter's shape**, so a
316
+ mechanical rewrite will not surface it: `{"notes": {"$ne": 3}}` now matches a record whose
317
+ `notes` is `"hello"`.
318
+ - **An empty array under a former wildcard clause.** `[*]` on `[]` was UNKNOWN, because the path
319
+ resolved to nothing; `$some` on `[]` is FALSE, because an empty array is a resolved value and
320
+ nothing in it satisfies the condition. `$every` on `[]` is TRUE, vacuously. Observable under
321
+ negation only, and it is the one migration step a codemod cannot claim to preserve.
322
+ - **§3.4 resolution is single-valued.** With no wildcard segment, a path yields zero values or
323
+ exactly one. The sequence model is gone.
324
+ - **Six operator descriptions that contradicted §4.1.** These strings are vendored verbatim into
325
+ generated schemas and MCP tool definitions, so they were a first-order cause of the confusion
326
+ rather than a cosmetic issue. `$nor`'s was outright wrong — "None of the listed filters may
327
+ evaluate TRUE" is the two-valued reading — and `$ne`'s said only "Field does not equal the
328
+ operand". `$nin`, `$nbetween`, `$nlike` and `$nilike` all read as total predicates. Every
329
+ negative operator now states what it does with UNKNOWN.
330
+ - **§1 no longer calls the filter "a boolean function"** while §4.1 makes it three-valued.
331
+ - **`$exists` is documented as unconditionally total.** It always was, except under a wildcard
332
+ path; with those gone the exception is gone.
333
+ - **§3.5 settles whether an index suffix is a separate path.** It is not: `items[0]` is the field
334
+ `items` for queryability, while a named member beneath it (`items[0].sku`) is its own path.
335
+ - **§7 addresses quantifier cost.** `$some` and `$every` are the expensive operators on most
336
+ backends, and a server that cannot afford them can decline the `collections` profile — which is
337
+ precisely what the `[*]` segment made impossible.
338
+
339
+ ### Fixed
340
+
341
+ - **`$some`/`$every`'s operand shape is no longer ambiguous.** `anyOf: [Filter, ConstraintObject]`
342
+ overlaps on a leading `$not`, and nothing said which was meant. §5.8 now gives a decidable rule:
343
+ scan for the first member that can only be one of the two, recursing through `$not`/`$and`/`$or`/
344
+ `$nor` bodies when the outer member is itself ambiguous.
345
+ - **`$field` inside a quantifier resolves against the element**, stated in §5.8 and §5.11. §5.11
346
+ said "the same record" while §5.8 said paths were element-relative; both readings were
347
+ defensible.
348
+
349
+ ## [0.3.1] — 2026-09-04
350
+
351
+ No change to the schema, the grammar or the semantics. `open-predicate-schema.json` is
352
+ byte-identical to 0.3.0 and its `$id` still names `v0.3.0`, because the `$id` version tracks the
353
+ grammar and the grammar did not move. Consumers pinning that `$id` have nothing to do.
354
+
355
+ ### Removed
356
+
357
+ - **Publishing.** The release workflow no longer ships to npmjs.com or GitHub Packages. Neither
358
+ registry ever received a copy, and neither should before the name is final: publishing claims a
359
+ 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 —
360
+ the test suite, and the tag-against-`package.json` check — and uploads nothing. The
361
+ `NPM_TOKEN` secret and `.github/scripts/version-published.sh` are deleted with it.
362
+ [RELEASING.md](./RELEASING.md#turning-publishing-back-on) keeps what the jobs needed, so they
363
+ can be restored from git history rather than rewritten.
364
+
365
+ ### Changed
366
+
367
+ - **README no longer offers an install that does not exist.** The Quickstart opened with
368
+ `npm install --save-dev open-predicate`, which the README's own *Status* table already
369
+ contradicted two screens further down. It now vendors the file by `curl`, which is the only
370
+ way to obtain the schema and always was.
371
+ - **[RELEASING.md](./RELEASING.md) documents the process that exists** — a tag and a GitHub
372
+ Release, carrying notes and a source snapshot and nothing else.
373
+
374
+ ## [0.3.0] — 2026-09-04
375
+
376
+ Guidance for adopters exposing a search endpoint to an LLM agent, the tooling that acts on it,
377
+ and an honest statement of how finished this is. No grammar change: every filter valid under
378
+ v0.2.0 remains valid, and the only edits to `open-predicate-schema.json` are two `description`
379
+ annotations and its version strings.
380
+
381
+ ### Added
382
+
383
+ - **Per-field domains in the capability document.** [SPEC.md §2.2](./SPEC.md#22-capability-discovery)'s
384
+ RECOMMENDED shape now carries `type`, `format`, `values` and `description` alongside
385
+ `operators`, with a table defining each. The grammar cannot express per-field operand
386
+ domains — every path shares one `Constraint` — so a filter naming a real field with an
387
+ out-of-domain value is well-formed and matches nothing. The capability document is the only
388
+ place that domain can be stated.
389
+ - **Recovery members on problem details.** [SPEC.md §8](./SPEC.md#8-errors) now RECOMMENDS that
390
+ `unknown-field` carry `queryableFields` and that `invalid-operand` carry `accepted`, so a
391
+ client that never fetched the capability document can still converge in one round trip
392
+ instead of guessing field names one at a time.
393
+ - **README §*Exposing search to an agent*** — what reaches a tool definition, the three
394
+ valid-but-wrong filters that fail as an empty result set, and the five steps that prevent
395
+ them (bundle rather than remote-`$ref`, narrow `FieldPath`, publish value domains, trim to
396
+ advertised profiles, state the null and `$in` semantics in the tool description).
397
+
398
+ - **`tools/generate-filter-schema.mjs`** — derives a per-resource filter schema from the
399
+ resource's own JSON Schema. The published grammar shares one `Constraint` across every field,
400
+ so it can say `{"status": "Available"}` is well-formed but not that `"Available"` is outside
401
+ `status`'s domain; that is why [SPEC.md §2.2](./SPEC.md#22-capability-discovery) exists. A
402
+ generated schema gives each queryable path its own constraint subschema, carrying only the
403
+ operators that apply to its type and only the operands its domain admits — so the three
404
+ valid-but-wrong filters catalogued in README §*Exposing search to an agent* become validation
405
+ failures instead of empty result sets. The generator emits the §2.2 capability document from
406
+ the same source, and copies operator prose out of the published grammar rather than restating
407
+ it. Generation is narrowing only: every filter a generated schema accepts is valid against the
408
+ published grammar, which `tests/generator.test.mjs` asserts.
409
+ - **`COMPARISON.md`** — how this specification relates to GraphQL, and what a JSON-Schema-native
410
+ alternative to GraphQL would still need. The short version: GraphQL never standardised
411
+ filtering, so the two overlap far less than the question assumes. Also covers OData, JSON:API,
412
+ OGC CQL2 and JSON Hyper-Schema as prior art.
413
+ - **`examples/pet.schema.json`** with its generated `pet.filter.json` and `pet.capabilities.json`
414
+ committed beside it, and `npm run generate:example` to refresh them. A test fails if they drift.
415
+ - **README §*Generating a per-resource filter schema*** — what the generator decides and why, and
416
+ the `x-open-predicate` property annotations that override it.
417
+
418
+ ### Changed
419
+
420
+ - **README framing.** The schema is presented as feeding two integration paths rather than
421
+ one: `$ref`'d from an OpenAPI document, or bundled into an MCP tool's `inputSchema`. The
422
+ *Referencing by URL or by copy* table gains an `MCP inputSchema` row recording that the
423
+ absolute-URL form does not work there at all, since nothing on that path resolves remote
424
+ refs.
425
+ - `$in` and `$nin` descriptions now state that they compare the whole value and do not test
426
+ array membership, naming `$hasAny`/`$hasNone` as the element operators. `$contains` already
427
+ warned about the same crossover; these two did not, and they are the operators a client
428
+ carrying MongoDB habits reaches for first.
429
+ - **`$id` is now `…/v0.3.0/open-predicate-schema.json`.** Consumers pin by `$id`, so the version
430
+ in the path moves with the release. `SPEC.md`, the OpenAPI examples and the generated
431
+ capability document were all still naming v0.2.0; they now agree.
432
+ - **README §*Status* states that this is a work in progress, name included.** The name is not
433
+ final, and every identifier downstream of it — both package names, the `$id`, the URLs in the
434
+ integration examples — is a placeholder, several of which do not
435
+ resolve. Getting them right is deliberately deferred until the name is settled, because a
436
+ rename moves all of them at once. A notice at the top of the README says the same thing before
437
+ a reader reaches an install command that will not work.
438
+ - **`QUERY` now cites [RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)** rather than
439
+ `draft-ietf-httpbis-safe-method-w-body`. The method reached Proposed Standard in June 2026.
440
+ The advice to ship `POST /search` alongside it is unchanged, but the reason is now that
441
+ deployed support trails a fresh RFC, not that the specification is unsettled.
442
+
443
+ ## [0.2.0] — 2026-08-06
444
+
445
+ A structural rewrite. The v0.1.0 file described a grammar but did not enforce one; this
446
+ release makes it a working schema, fixes the grammar's dead ends, and completes the operator
447
+ set. Filters written against v0.1.0 still parse apart from the `$isnull` rename.
448
+
449
+ ### Fixed
450
+
451
+ - **The schema validated nothing.** The root used `"id"` (a draft-04 spelling) rather than
452
+ `"$id"`, and wrapped its definitions in `components.schemas`, which is an OpenAPI container
453
+ and not a JSON Schema keyword. Under draft 2020-12 both were unknown keywords, and the root
454
+ carried no assertion keywords at all — so a validator pointed at the file accepted every
455
+ instance. The root now `$ref`s `#/$defs/Filter`.
456
+ - **`"regex"` is not a JSON Schema keyword** (it is `"pattern"`), and the value it carried —
457
+ `"['\"%?.+%?['\"]"` — was a malformed character class that also expected quote characters
458
+ inside the operand. `$like` and `$nlike` are now plain strings; the wildcard and escape
459
+ grammar is specified in prose ([SPEC.md §5.5](./SPEC.md#55-pattern-matching--like-nlike-ilike-nilike))
460
+ where it belongs.
461
+ - **`examples` was an object** throughout, in the OpenAPI Example-Object style, where JSON
462
+ Schema requires an array of instance values; two schemas used the OpenAPI 3.0 singular
463
+ `example`. Both spellings are now correct, and a test walks the whole document to keep them
464
+ that way.
465
+ - **Operators from different families could not be combined on one field.** The eight-way
466
+ `oneOf` over leaf condition types meant `{"age": {"$gt": 18, "$ne": 30}}` matched no branch
467
+ and was rejected. Sibling operators now AND together.
468
+ - **Ambiguous and empty forms were accepted or accidentally rejected.** `{}` matched all eight
469
+ leaf branches at once; `{"$and": […], "$or": […]}` was accepted with no defined semantics;
470
+ `$and: []` and `$in: []` were accepted. Empty forms are now rejected, and implicit AND across
471
+ siblings is specified.
472
+ - **`$in`/`$nin` excluded booleans and `null`** while `$eq` allowed them.
473
+
474
+ ### Changed — breaking
475
+
476
+ | v0.1.0 | v0.2.0 | Note |
477
+ | --- | --- | --- |
478
+ | `"id": "…/v0.1.0"` | `"$id": "…/v0.2.0/open-predicate-schema.json"` | Correct keyword, versioned path |
479
+ | `#/components/schemas/Query` | `#/$defs/Filter` | Or `$ref` the file itself |
480
+ | `#/components/schemas/Condition` | *(removed)* | Folded into `#/$defs/Filter` |
481
+ | `#/components/schemas/equalCondition`, `notEqualCondition`, `inArrayCondition`, `notInArrayCondition`, `likeCondition`, `notLikeCondition`, `rangeCondition`, `isNullCondition` | *(removed)* | Folded into `#/$defs/ConstraintObject` |
482
+ | `$isnull` | `$isNull` | Renamed for consistency with `$startsWith` &c. |
483
+
484
+ Any OpenAPI document referencing a `#/components/schemas/…` pointer must be repointed. Filter
485
+ *documents* need no change other than `$isnull` → `$isNull`; the v0.1.0 examples are kept as
486
+ test fixtures to prove it.
487
+
488
+ ### Added
489
+
490
+ - **Operators.** `$nor`; `$nbetween`; `$ilike`, `$nilike`, `$startsWith`, `$endsWith`,
491
+ `$contains`; `$regex` with `$flags`; `$exists`; `$type`; `$hasAny`, `$hasAll`, `$hasNone`,
492
+ `$size`, `$elemMatch`; `$search`; a field-level `$not`.
493
+ - **Field-to-field comparison** via `{"$field": "path"}` in operand position — SQL's
494
+ `WHERE price > cost` — with `{"$literal": …}` as the escape for object operands that would
495
+ otherwise read as references.
496
+ - **Scalar shorthand.** `{"status": "open"}` for `{"status": {"$eq": "open"}}`. Restricted to
497
+ strings, numbers, booleans and `null`, so `{"tags": ["a"]}` can never be read ambiguously.
498
+ - **`null`, arrays and objects as `$eq`/`$ne` operands.**
499
+ - **A field path grammar** — dotted paths, array indices, `[*]` wildcards, `\.` dot escaping —
500
+ and a rule for field names beginning with `$`: they are escaped by doubling (`$$price`).
501
+ A single `$` prefix that is not a known operator is now rejected, so `$eqq` is an error
502
+ rather than a field name.
503
+ - **Conformance profiles**, published in the schema as `x-profiles` and specified in
504
+ [SPEC.md §2.1](./SPEC.md#21-profiles), so a server can advertise the subset it implements.
505
+ - **One override point for the queryable field set**, `#/$defs/FieldPath`, reached through
506
+ `propertyNames` so that narrowing it in a bundled copy applies at every nesting level.
507
+ - **[SPEC.md](./SPEC.md)** — three-valued logic, missing-versus-null, coercion rules, per-operator
508
+ semantics, safety limits, and an RFC 9457 problem-type registry.
509
+ - **[README.md](./README.md)** — operator reference and OpenAPI 3.1 / 3.2 integration guidance.
510
+ - **Tests and CI** — ajv under `strict: true`, 30 valid and 22 invalid fixtures, and Redocly
511
+ linting of both OpenAPI examples.
512
+
513
+ ### Notes
514
+
515
+ `$dynamicRef`/`$dynamicAnchor` was evaluated as a way to let consumers restrict the queryable
516
+ field set without copying the file, and rejected: ajv 8.20 does not resolve it correctly even
517
+ for the canonical recursive case, and OpenAPI tooling support is worse. The `$ref`-based
518
+ override described in the README works in every validator.
519
+
520
+ ## [0.1.0] — 2025-02-05
521
+
522
+ Initial research draft: `$and`, `$or`, `$not` over eight leaf condition types
523
+ (`$eq`, `$ne`, `$in`, `$nin`, `$like`, `$nlike`, `$gt`/`$gte`/`$lt`/`$lte`/`$between`, `$isnull`),
524
+ laid out as an OpenAPI `components.schemas` fragment.
525
+
526
+ [0.6.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.5.0...v0.6.0
527
+ [0.5.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.4.0...v0.5.0
528
+ [0.4.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.3.1...v0.4.0
529
+ [0.3.1]: https://github.com/OpenPredicate/open-predicate/compare/v0.3.0...v0.3.1
530
+ [0.3.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.2.0...v0.3.0
531
+ [0.2.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.1.0...v0.2.0
532
+ [0.1.0]: https://github.com/OpenPredicate/open-predicate/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Christos Gkoros
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.