@open-predicate/open-predicate 0.6.1 → 0.6.2

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 CHANGED
@@ -7,6 +7,57 @@ minor release may break compatibility, in which case the break is spelled out be
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.2] — 2026-09-14
11
+
12
+ **A packaging release, again.** The schema is still byte-identical and the `$id` still names
13
+ `v0.4.0`. 0.6.1 fixed the defects an audit of the package found; this one fixes the ones that
14
+ audit had not looked for, and closes the coverage gaps that let the first set through.
15
+
16
+ ### Fixed
17
+
18
+ - **35 links in the shipped documents pointed at files that are not shipped.** The package is a
19
+ subset of the repository, so a relative link into `examples/` reads fine on GitHub and is dead for anyone
20
+ reading the same file in `node_modules`. npmjs.com hides this for the README — it rewrites relative
21
+ links against the repository — but only for the README, and only on that page.
22
+
23
+ Fixed from both ends. `files` now ships the documents a consumer or a would-be contributor
24
+ plausibly wants offline: `COMPARISON.md`, `CONTRIBUTING.md`, `GOVERNANCE.md`, `CODE_OF_CONDUCT.md`,
25
+ `SECURITY.md` and `SUPPORT.md`. Links to things a schema package has no business shipping —
26
+ `examples/`, `assets/`, `.github/`, `tests/`, `experiments/`, the 39 KB decision record and the
27
+ maintainer-only `RELEASING.md` — are now absolute. The tarball grew from 61 KB to 77 KB, which is
28
+ the price of the docs being readable where they are installed.
29
+
30
+ - **`npx open-predicate` printed npm's `could not determine executable to run`.** The unscoped name
31
+ is a deprecated placeholder with no code, so npm had no bin to run and said so in its own terms,
32
+ which tells someone following a stale instruction nothing. It now carries a single executable
33
+ whose only job is to name the scoped package and exit non-zero. The recipe in
34
+ [`RELEASING.md`](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md) is updated
35
+ to reproduce it, and records that `npm deprecate` marks a version rather than a package, so the
36
+ deprecation has to be re-applied after each publish.
37
+
38
+ ### Added
39
+
40
+ - **The packed artifact is now tested under every package manager, on every platform that links a
41
+ bin differently.** The 0.6.0 no-op was a symlink bug, and package managers disagree about
42
+ symlinks: npm and yarn classic symlink, pnpm writes a shell shim, Yarn Berry's PnP has no
43
+ `node_modules` at all, and Windows gets `.cmd` wrappers that pass the real path — which is why the
44
+ bug never existed there. The release gate added in 0.6.1 only ever proved the npm-on-Linux case.
45
+
46
+ [`.github/scripts/smoke-packed-artifact.sh`](https://github.com/OpenPredicate/open-predicate/blob/main/.github/scripts/smoke-packed-artifact.sh)
47
+ installs the tarball and drives the bin, `require()`, the documented ESM import attribute and
48
+ `./generate` under a named package manager, and CI runs it across npm, pnpm, yarn, yarn-pnp and
49
+ bun on Linux plus npm on Windows and macOS. The release workflow calls the same script rather than
50
+ keeping a second copy of the logic.
51
+
52
+ - **`tests/packaging.test.mjs` asserts the claims a tarball can break.** Every relative link in a
53
+ shipped document resolves to a shipped file; no shipped document links outside the package; the
54
+ bin is shipped, non-empty and still has its shebang; and the declared Node floor is the one the
55
+ code actually needs. It reads the file list from `npm pack --dry-run --json` rather than
56
+ extracting a tarball, so it needs no `tar` and runs on Windows unchanged.
57
+
58
+ - **CI declares `permissions: contents: read`.** It had no `permissions` block at all, so it
59
+ inherited the repository default while `release.yml` had been careful to take none.
60
+
10
61
  ## [0.6.1] — 2026-09-14
11
62
 
12
63
  **A packaging release.** No change to the grammar, the schema or the semantics of evaluation:
@@ -41,7 +92,7 @@ changed is that the package now works the way 0.6.0 said it did.
41
92
  - **The release pipeline never exercised the artifact it publishes.** `npm test` and
42
93
  `npm run generate:example` both invoke the generator by path, which is the one way that never
43
94
  crosses the symlink npm installs a `bin` as — so the no-op above passed every check and shipped.
44
- [`.github/workflows/release.yml`](./.github/workflows/release.yml) now packs the tarball, installs
95
+ [`.github/workflows/release.yml`](https://github.com/OpenPredicate/open-predicate/blob/main/.github/workflows/release.yml) now packs the tarball, installs
45
96
  it into a scratch project the way a consumer would, and drives every entry point the README
46
97
  documents: the bin through its symlink, `require()`, the ESM import attribute, and
47
98
  `./generate`. It also asserts the generator stays silent when merely imported. A release cannot
@@ -63,14 +114,22 @@ changed is that the package now works the way 0.6.0 said it did.
63
114
  and no code, so the name cannot end up on something unrelated to the project.
64
115
  `npm install open-predicate` prints a redirect to the scoped package. It is not versioned
65
116
  alongside releases and the release pipeline never touches it — see
66
- [`RELEASING.md`](./RELEASING.md#the-reserved-unscoped-name).
117
+ [`RELEASING.md`](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md#the-reserved-unscoped-name).
67
118
 
68
119
  ### Added
69
120
 
121
+ - **GitHub Packages has its first copy.** `@openpredicate/open-predicate@0.6.1` is the first version
122
+ to reach it; npmjs.com had been the only registry carrying anything. Installing from it still needs
123
+ an `.npmrc` and a token even though it is public, so npmjs.com remains the easier path.
124
+
125
+ - **0.6.1 is the first release with provenance.** Published over OIDC from the workflow rather than
126
+ from a laptop, so the tarball carries a SLSA v1 attestation linking it to the run and commit that
127
+ built it. Verify with `npm view @open-predicate/open-predicate@0.6.1 dist.attestations`.
128
+
70
129
  - **The project has a written governance and contribution process.**
71
130
  [`GOVERNANCE.md`](./GOVERNANCE.md) states how a decision is made and what it costs — editorial,
72
131
  substantive-compatible, or normative, where normative requires a record under
73
- [`decisions/`](./decisions), a migration note and a `$id` bump. It sets out how a disputed design
132
+ [`decisions/`](https://github.com/OpenPredicate/open-predicate/tree/main/decisions), a migration note and a `$id` bump. It sets out how a disputed design
74
133
  call is resolved (answered in writing, then a decision record quoting the objection in the
75
134
  objector's words, then 14-day lazy consensus, then the editor decides **and the dissent is recorded
76
135
  in the record**), what earns commit rights, and an explicit royalty-free patent posture — MIT
@@ -113,10 +172,10 @@ changed is that the package now works the way 0.6.0 said it did.
113
172
  Serving it is a second repository's job, so a release is not finished when the tag is pushed —
114
173
  [`OpenPredicate/openpredicate.tech`](https://github.com/OpenPredicate/openpredicate.tech) vendors
115
174
  the artefacts and has to be synced at the tag. That step is now written down in
116
- [`RELEASING.md`](./RELEASING.md#serving-the-schema-from-its-id).
175
+ [`RELEASING.md`](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md#serving-the-schema-from-its-id).
117
176
 
118
177
  - **Publishing is back on, and npmjs.com authenticates by OIDC.**
119
- [`.github/workflows/release.yml`](./.github/workflows/release.yml) publishes to both registries
178
+ [`.github/workflows/release.yml`](https://github.com/OpenPredicate/open-predicate/blob/main/.github/workflows/release.yml) publishes to both registries
120
179
  when a GitHub Release is published. The npmjs job uses npm's [trusted
121
180
  publishing](https://docs.npmjs.com/trusted-publishers): it requests `id-token: write` and npm
122
181
  exchanges that for a short-lived credential, so there is **no `NPM_TOKEN` secret** in this
@@ -135,14 +194,14 @@ changed is that the package now works the way 0.6.0 said it did.
135
194
  **npm cannot mint a package's first version over OIDC**, because a trusted publisher can only be
136
195
  attached to a package that already exists. Claiming `@open-predicate/open-predicate` was therefore
137
196
  a one-time manual publish, documented along with everything it cost to get right in
138
- [`RELEASING.md`](./RELEASING.md#trusted-publishing-and-the-one-time-bootstrap) — including that
197
+ [`RELEASING.md`](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md#trusted-publishing-and-the-one-time-bootstrap) — including that
139
198
  *configuring* the trusted publisher needs npm >= 12, which fails with an unexplained `E400` on
140
199
  npm 11 because the older client omits the `permissions` field the registry now requires.
141
200
 
142
201
  - **`@open-predicate/open-predicate` is on npmjs.com**, public and installable, from `0.6.0` on. The
143
202
  package is the schema: `require()` it, or `import` it with `{ type: 'json' }`. The trusted
144
203
  publisher is configured, so every release from here is published by the workflow rather than by
145
- hand. GitHub Packages gets its first copy with the next release.
204
+ hand. GitHub Packages got its first copy with 0.6.1.
146
205
 
147
206
  ## [0.6.0] — 2026-09-13
148
207
 
@@ -188,9 +247,9 @@ to re-point at.
188
247
 
189
248
  ### Added
190
249
 
191
- - **Brand assets, in [`assets/`](./assets/).** The `{ > }` mark — JSON braces around a comparison —
250
+ - **Brand assets, in [`assets/`](https://github.com/OpenPredicate/open-predicate/tree/main/assets/).** The `{ > }` mark — JSON braces around a comparison —
192
251
  as SVG and as raster at three sizes, plus a wordmark for light and dark backgrounds.
193
- [`assets/README.md`](./assets/README.md) states the palette and the usage rules. The README now
252
+ [`assets/README.md`](https://github.com/OpenPredicate/open-predicate/blob/main/assets/README.md) states the palette and the usage rules. The README now
194
253
  opens with the mark. MIT-licensed with the rest of the repository.
195
254
 
196
255
  ## [0.5.0] — 2026-09-13
@@ -352,7 +411,7 @@ document's top-level members. No filter valid under 0.4.0 becomes invalid.
352
411
  **Breaking.** The `$id` is now `…/v0.4.0/open-predicate-schema.json`. This release resolves the
353
412
  three operator overlaps that an external review and this repository's own
354
413
  `experiments/filter-to-sql` flagged independently; the design and the evidence are in
355
- [`decisions/0001-array-quantifiers-and-unknown-handling.md`](./decisions/0001-array-quantifiers-and-unknown-handling.md).
414
+ [`decisions/0001-array-quantifiers-and-unknown-handling.md`](https://github.com/OpenPredicate/open-predicate/blob/main/decisions/0001-array-quantifiers-and-unknown-handling.md).
356
415
 
357
416
  The headline is that the language had **two** unrelated mechanisms for looking inside an array —
358
417
  `$elemMatch` and the `[*]` path segment — and one mechanism now does both jobs while naming its
@@ -456,7 +515,7 @@ grammar and the grammar did not move. Consumers pinning that `$id` have nothing
456
515
  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 —
457
516
  the test suite, and the tag-against-`package.json` check — and uploads nothing. The
458
517
  `NPM_TOKEN` secret and `.github/scripts/version-published.sh` are deleted with it.
459
- [RELEASING.md](./RELEASING.md#turning-publishing-back-on) keeps what the jobs needed, so they
518
+ [RELEASING.md](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md#turning-publishing-back-on) keeps what the jobs needed, so they
460
519
  can be restored from git history rather than rewritten.
461
520
 
462
521
  ### Changed
@@ -465,7 +524,7 @@ grammar and the grammar did not move. Consumers pinning that `$id` have nothing
465
524
  `npm install --save-dev open-predicate`, which the README's own *Status* table already
466
525
  contradicted two screens further down. It now vendors the file by `curl`, which is the only
467
526
  way to obtain the schema and always was.
468
- - **[RELEASING.md](./RELEASING.md) documents the process that exists** — a tag and a GitHub
527
+ - **[RELEASING.md](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md) documents the process that exists** — a tag and a GitHub
469
528
  Release, carrying notes and a source snapshot and nothing else.
470
529
 
471
530
  ## [0.3.0] — 2026-09-04
@@ -620,6 +679,7 @@ Initial research draft: `$and`, `$or`, `$not` over eight leaf condition types
620
679
  (`$eq`, `$ne`, `$in`, `$nin`, `$like`, `$nlike`, `$gt`/`$gte`/`$lt`/`$lte`/`$between`, `$isnull`),
621
680
  laid out as an OpenAPI `components.schemas` fragment.
622
681
 
682
+ [0.6.2]: https://github.com/OpenPredicate/open-predicate/compare/v0.6.1...v0.6.2
623
683
  [0.6.1]: https://github.com/OpenPredicate/open-predicate/compare/v0.6.0...v0.6.1
624
684
  [0.6.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.5.0...v0.6.0
625
685
  [0.5.0]: https://github.com/OpenPredicate/open-predicate/compare/v0.4.0...v0.5.0
@@ -0,0 +1,66 @@
1
+ # Code of Conduct
2
+
3
+ ## The short version
4
+
5
+ Attack arguments, not people.
6
+
7
+ This project [asks for disagreement](./CONTRIBUTING.md) and means it. Saying *"this operator is
8
+ redundant and the spec is wrong to include it"* is the contribution, not a breach of conduct. Being
9
+ blunt, unimpressed, or persistent about a technical position is fine. Saying *"only an idiot would
10
+ have designed it this way"* is a different act, and it is the one this document is about.
11
+
12
+ ## Our pledge
13
+
14
+ We pledge to make participation in this project a harassment-free experience for everyone,
15
+ regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender
16
+ identity and expression, level of experience, education, socio-economic status, nationality, personal
17
+ appearance, race, caste, colour, religion, or sexual identity and orientation.
18
+
19
+ ## Expected behaviour
20
+
21
+ - Argue about the specification, the schema, the code and the evidence.
22
+ - Assume the other person has a reason, and ask for it before assuming they do not.
23
+ - Accept a decision you disagree with once it has been answered on the substance — and note that
24
+ [GOVERNANCE.md](./GOVERNANCE.md) requires your dissent to be recorded in the decision record
25
+ rather than discarded, so you do not have to keep re-litigating it to keep it on the record.
26
+ - Give credit, including to people whose objection changed your mind.
27
+
28
+ ## Unacceptable behaviour
29
+
30
+ - Personal attacks, insults, or derogatory comments — including ones dressed as technical criticism.
31
+ - Harassment, public or private; sexualised language or imagery; unwelcome sexual attention.
32
+ - Publishing others' private information without explicit permission.
33
+ - Sustained disruption: reopening a settled decision with no new argument, brigading a thread, or
34
+ demanding a maintainer's time as though it were owed.
35
+ - Any conduct which could reasonably be considered inappropriate in a professional setting.
36
+
37
+ ## Scope
38
+
39
+ This applies in the repositories of the [OpenPredicate organisation](https://github.com/OpenPredicate),
40
+ in issues, pull requests, discussions and commit messages, and anywhere someone is representing the
41
+ project in public.
42
+
43
+ ## Reporting and enforcement
44
+
45
+ Report to **contact@openpredicate.tech**. Reports are read by the maintainers listed in
46
+ [GOVERNANCE.md](./GOVERNANCE.md) — at present that is one person, Christos Gkoros, so a report about
47
+ the maintainer has nowhere independent to go inside the project. That is stated rather than hidden;
48
+ where the complaint is about the maintainer and that is unacceptable to you, GitHub's own
49
+ [abuse reporting](https://github.com/contact/report-abuse) is the escalation path.
50
+
51
+ You will get an acknowledgement within seven days. Reports are kept confidential to the extent
52
+ possible, and the reporter's identity is never disclosed to the person reported without consent.
53
+
54
+ Enforcement is proportionate and escalates: a private correction explaining what was wrong; a
55
+ warning with conditions; a temporary ban from interaction; a permanent ban. Anything beyond a private
56
+ correction is recorded, though not necessarily published. Comments, commits, issues and other
57
+ contributions that breach this document may be edited or removed, with a reason given.
58
+
59
+ ## Attribution
60
+
61
+ Adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, which is
62
+ available at
63
+ [contributor-covenant.org/version/2/1/code_of_conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
64
+ and licensed [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). The opening section, the
65
+ single-maintainer disclosure in *Reporting*, and the dissent clause under *Expected behaviour* are
66
+ specific to this project.
package/COMPARISON.md ADDED
@@ -0,0 +1,195 @@
1
+ # OpenPredicate and GraphQL
2
+
3
+ The question arrives in two forms. *Why not just use GraphQL?* — asked by someone choosing
4
+ between them. And *could this become a JSON-Schema-native alternative to GraphQL?* — asked by
5
+ someone who has noticed that the two overlap somewhere. This document answers both, starting from
6
+ the observation that makes both answerable: they overlap much less than the question assumes.
7
+
8
+ ---
9
+
10
+ ## 1. GraphQL is four things; this is a fifth
11
+
12
+ GraphQL bundles:
13
+
14
+ 1. **A type system with mandatory introspection.** SDL, plus a query every server must answer that
15
+ returns its own schema.
16
+ 2. **Client-specified projection.** The response contains the fields the client asked for and
17
+ nothing else. This is the feature GraphQL is adopted for.
18
+ 3. **Traversal.** Nested selections are joins, executed by resolvers.
19
+ 4. **A transport and execution contract.** One endpoint, variables, operations, an `errors` array
20
+ alongside partial `data`, subscriptions.
21
+
22
+ **Filtering is not on that list.** The GraphQL specification says nothing about `where`. Every
23
+ server invents its own input objects — Hasura's `_eq`/`_in`, Prisma's `equals`/`contains`,
24
+ Postgraphile, Dgraph — with no shared vocabulary, no shared null semantics, and no portability
25
+ between them. A client that knows GraphQL does not thereby know how to filter.
26
+
27
+ That is the gap this specification occupies. [JSON:API](https://jsonapi.org/format/#fetching-filtering)
28
+ has the same hole and says so explicitly: it reserves the `filter` query parameter and declines to
29
+ define its contents. MCP tool definitions have it too — an agent is handed an `inputSchema` and
30
+ left to infer a query syntax from prose.
31
+
32
+ So the first answer is that **this is not GraphQL's competitor; it is the piece GraphQL left
33
+ out.** A GraphQL server could adopt the grammar as the body of its `where` argument tomorrow and
34
+ lose nothing.
35
+
36
+ ---
37
+
38
+ ## 2. Side by side
39
+
40
+ The same request, filtering pets and taking three fields.
41
+
42
+ **GraphQL** — projection is the point; the filter vocabulary is the server's invention:
43
+
44
+ ```graphql
45
+ query {
46
+ pets(where: { _and: [
47
+ { status: { _eq: "available" } },
48
+ { born: { _gte: "2020-01-01" } }
49
+ ]}, limit: 50) {
50
+ id
51
+ name
52
+ shelter { city }
53
+ }
54
+ }
55
+ ```
56
+
57
+ **OpenPredicate** — the filter is standard and schema-validated; projection, ordering and
58
+ paging belong to the enclosing body, where each API defines them:
59
+
60
+ ```http
61
+ QUERY /pets HTTP/1.1
62
+ Content-Type: application/json
63
+ ```
64
+ ```json
65
+ {
66
+ "filter": {
67
+ "status": "available",
68
+ "born": { "$gte": "2020-01-01" }
69
+ },
70
+ "fields": ["id", "name", "shelter.city"],
71
+ "limit": 50
72
+ }
73
+ ```
74
+
75
+ The differences that matter are not the syntax. In the GraphQL version the response type is known
76
+ statically from the selection set, and the filter is unportable. In the second the filter is
77
+ portable and machine-checkable, and `fields` is whatever that API decided it should be.
78
+
79
+ ---
80
+
81
+ ## 3. What GraphQL does that this does not
82
+
83
+ Stated plainly, because these are the reasons to choose GraphQL and they are good ones.
84
+
85
+ | | |
86
+ | --- | --- |
87
+ | **Response shape follows the request** | The single hardest thing to reproduce. See §5. |
88
+ | **One round trip across many resources** | A GraphQL document fetches pets, their shelters and their vaccinations together. Per-resource search endpoints do not. |
89
+ | **One mandatory introspection format** | Every GraphQL server answers the same introspection query, which is why GraphiQL, codegen and federation work everywhere. [SPEC.md §2.2](./SPEC.md#22-capability-discovery)'s capability document is RECOMMENDED, not required, and lives at an unspecified URL. |
90
+ | **Federation** | Composing one graph from many services is a solved, productised problem there and an unsolved one here. |
91
+ | **Subscriptions** | Out of scope entirely. |
92
+ | **Mutations** | Out of scope entirely; this is a predicate language. |
93
+
94
+ ---
95
+
96
+ ## 4. What this does that GraphQL does not
97
+
98
+ | | |
99
+ | --- | --- |
100
+ | **HTTP survives** | Cache-Control and ETags apply, `QUERY` and `GET` stay safe and cacheable, status codes mean what they say, and a rejected filter is a `400` naming which of five conditions failed, with an RFC 6901 pointer at the offending clause, in whatever error format the API already uses ([SPEC.md §8](./SPEC.md#8-errors)). GraphQL's single POST endpoint forfeits intermediary caching; `application/graphql-response+json` recovers status codes but not cache keys. |
101
+ | **Null semantics are specified** | [SPEC.md §4.1](./SPEC.md#41-three-valued-logic) and [§4.2](./SPEC.md#42-missing-versus-null) pin down three-valued logic and the missing-versus-null distinction. GraphQL's null propagation is its most notorious sharp edge, and its filter semantics are per-vendor folklore. |
102
+ | **Complexity limits are normative** | [SPEC.md §7](./SPEC.md#7-safety-limits) bounds depth, clause count, set length and regex execution, and requires rejection rather than truncation. GraphQL query cost is left to each shop to solve. |
103
+ | **Honest partial implementations** | Profiles ([SPEC.md §2.1](./SPEC.md#21-profiles)) let a server advertise the subset it implements and reject the rest, instead of mistranslating it. |
104
+ | **Incremental adoption** | One endpoint can adopt the filter grammar without the rest of the API changing. GraphQL is a parallel stack. |
105
+ | **It is already in the toolchains** | JSON Schema is what OpenAPI 3.1 speaks and what an MCP `inputSchema` is. No new parser, client or IDE plugin. |
106
+
107
+ That last row is worth more in 2026 than it was in 2016. An LLM agent gains little from
108
+ client-specified projection — it did not want the other fields anyway — and a great deal from a
109
+ grammar it can be handed, validated against, and corrected by in one round trip. See
110
+ [README §*Exposing search to an agent*](./README.md#exposing-search-to-an-agent).
111
+
112
+ ---
113
+
114
+ ## 5. What a JSON-Schema-native alternative would actually require
115
+
116
+ Taking the ambition seriously, layer by layer. Each of these is independently useful, which is the
117
+ point: the reason to build this way rather than adopt GraphQL is that adopters can take one layer
118
+ and leave the rest.
119
+
120
+ | Layer | Status | The hard part |
121
+ | --- | --- | --- |
122
+ | **filter** | this specification | done |
123
+ | **capabilities** | [SPEC.md §2.2](./SPEC.md#22-capability-discovery), RECOMMENDED | making it mandatory and locating it, so tooling can rely on it |
124
+ | **select** | not started | deriving the *response* schema from a runtime selection set |
125
+ | **expand / link** | not started | JSON Schema has no relationship vocabulary; [JSON Hyper-Schema](https://json-schema.org/draft/2019-09/json-hyper-schema) tried and stalled after draft-07 |
126
+ | **order / page** | not started | little novelty; cursor opacity is the only real decision |
127
+ | **envelope + errors** | [SPEC.md §8](./SPEC.md#8-errors) | done for filters; would need extending per layer |
128
+
129
+ **The load-bearing difficulty is projection typing.** GraphQL's trick is that the client's query
130
+ document is itself the input to type derivation — that is what Relay and graphql-codegen consume.
131
+ JSON Schema cannot express "the response shape is a function of this request's selection set".
132
+ There are two workable answers and they are not exclusive:
133
+
134
+ - *Normatively:* the response schema is the resource schema with every non-selected property
135
+ optional. A selection then guarantees presence, not typing — the same bargain OData and
136
+ JSON:API's sparse fieldsets strike.
137
+ - *In tooling:* derive the exact response schema client-side from `resource schema ∩ selection`.
138
+ This is codegen, and it is what GraphQL users are actually running anyway.
139
+
140
+ **A relationship vocabulary is the second obstacle**, and the precedent is discouraging. JSON
141
+ Hyper-Schema was the attempt and it did not reach 2019-09. Anything here would have to be new
142
+ work, and cross-resource traversal is the point at which the N+1 problem, authorisation per hop
143
+ and query cost all arrive at once — everything GraphQL servers spend their complexity budget on.
144
+
145
+ ---
146
+
147
+ ## 6. Where the generator fits
148
+
149
+ The gap that most weakens the comparison today is that the published grammar shares one
150
+ `Constraint` across every field, so it can validate that `{"status": "Available"}` is well-formed
151
+ but not that `"Available"` is outside `status`'s domain. GraphQL does not have this problem: it
152
+ *generates* `StringFilter`, `StatusEnumFilter` and so on per field, so the equivalent mistake is a
153
+ compile-time error.
154
+
155
+ [`tools/generate-filter-schema.mjs`](./tools/generate-filter-schema.mjs) closes it the same way,
156
+ from the resource's own JSON Schema — see
157
+ [README §*Generating a per-resource filter schema*](./README.md#generating-a-per-resource-filter-schema).
158
+ `data schema → filter schema` is the same pipeline as GraphQL's `SDL → input types`, with JSON
159
+ Schema as the source of truth instead of SDL. Extending that generator to emit a `select` schema
160
+ and a response schema from the same input is what would turn the phrase "JSON-Schema-native
161
+ alternative to GraphQL" into an architecture rather than a slogan.
162
+
163
+ ---
164
+
165
+ ## 7. Prior art
166
+
167
+ Anyone building this should know what has already been tried.
168
+
169
+ | | |
170
+ | --- | --- |
171
+ | [**OData**](https://www.odata.org/) | The dream, already attempted and standardised (OASIS, ISO/IEC 20802). `$filter`, `$select`, `$expand`, `$orderby`, `$top`/`$skip`, `$metadata`. Worth studying for why it did not displace GraphQL: a string-encoded `$filter` no schema can validate — precisely the defect this grammar fixes — an enormous surface, and CSDL metadata few enjoyed. |
172
+ | [**JSON:API**](https://jsonapi.org/) | Sparse fieldsets and `include` are a working design for the `select` and `expand` layers. Its `filter` is deliberately unspecified. |
173
+ | [**OGC CQL2**](https://docs.ogc.org/is/21-065r2/21-065r2.html) | Direct prior art for the predicate itself, including a JSON encoding. |
174
+ | **MongoDB query language** | The obvious syntactic ancestor of `$eq`/`$in`. The divergences here are deliberate: no implicit coercion, specified three-valued logic, `$in` as whole-value comparison with element access spelled `$some`, and `$elemMatch` renamed to `$some` beside a `$every` MongoDB has no equivalent for. |
175
+ | [**SCIM filters**](https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2), [**AIP-160**](https://google.aip.dev/160) | String-encoded filter grammars, with the same unvalidatable-parameter problem as OData. |
176
+ | [**JSONPath (RFC 9535)**](https://www.rfc-editor.org/rfc/rfc9535) | A standardised path grammar. [SPEC.md §3.2](./SPEC.md#32-path-grammar) uses a deliberately smaller one — smaller again since v0.4.0 dropped the `[*]` wildcard segment in favour of explicit quantifiers, which narrows the divergence to the non-wildcard subset. |
177
+ | **JSON Hyper-Schema** | The cautionary tale for the `expand` layer. |
178
+ | **Hasura / Prisma / Postgraphile `where`** | The de facto GraphQL filter dialects. Between them they are the closest thing to an existing standard, and none of them is one. |
179
+
180
+ ---
181
+
182
+ ## 8. Choosing between them
183
+
184
+ - **Choose GraphQL** if clients need to shape their own responses, if one round trip across many
185
+ resources is a requirement, or if you need federation or subscriptions.
186
+ - **Choose this** if you have HTTP endpoints that already work and want their filters to be
187
+ consistent, validated in CI, typed in generated clients, and usable by an agent — without
188
+ adopting a parallel stack.
189
+ - **Choose both** if you run GraphQL: nothing stops the grammar being the body of your `where`
190
+ argument, and it would make your filter vocabulary portable to the REST endpoints beside it.
191
+
192
+ The honest framing of the ambition: what is published here is one layer, and the most valuable one
193
+ to standardise because it is the one everybody re-invents. Whether the layers above it get built
194
+ is a separate question from whether this layer is worth adopting, and the answer to the second
195
+ does not depend on the first.
@@ -0,0 +1,134 @@
1
+ # Contributing
2
+
3
+ **Disagreement is the most useful contribution at this stage.**
4
+
5
+ That is not politeness. OpenPredicate is pre-1.0 with no known adopters, which means every design
6
+ defect is still free to fix — and after adoption each one is permanent. The most valuable thing you
7
+ can do is find the construct that will be regretted and say so while saying so is cheap.
8
+
9
+ There is precedent. Three operator overlaps — `$in` against `$hasAny`, `$elemMatch` against wildcard
10
+ paths, and three-valued `$not`/`$ne` silently excluding nulls — were removed in v0.4.0 because an
11
+ external survey and this repository's own
12
+ [`experiments/filter-to-sql`](https://github.com/OpenPredicate/open-predicate/tree/main/experiments/filter-to-sql) converged on the same three
13
+ independently. Both reviews were unsolicited. See
14
+ [`decisions/0001`](https://github.com/OpenPredicate/open-predicate/blob/main/decisions/0001-array-quantifiers-and-unknown-handling.md) for what happened to
15
+ them.
16
+
17
+ So: if something here looks wrong, file it. You do not need to propose a replacement, and you do not
18
+ need to be sure.
19
+
20
+ ## Three entry points
21
+
22
+ ### 1. File a design objection
23
+
24
+ Use the *design objection* issue template. A good objection names three things:
25
+
26
+ - **The construct.** Quote the operator, the rule, or the section of [`SPEC.md`](./SPEC.md).
27
+ - **The failure.** What goes wrong — a filter that cannot be expressed, two ways to write the same
28
+ thing, a semantic that surprises, a rule an implementation cannot enforce. A concrete filter and
29
+ the result you expected beats a description.
30
+ - **What it should be instead**, if you have a view. Optional. "This is wrong and I do not know the
31
+ fix" is a legitimate and useful issue.
32
+
33
+ The bar for *adding* an operator is deliberately high, and it is stated in `decisions/0001`:
34
+ **a construct definable in terms of another does not earn a name.** That is why `$none` is not in
35
+ the grammar — it is `$not` over `$some`. An objection that argues a construct is *hard to write
36
+ correctly* in its definable form is the argument that can move this; an objection that it is merely
37
+ absent will not.
38
+
39
+ Out of scope by design, per [SPEC.md §1](./SPEC.md): projection, ordering, pagination, grouping and
40
+ joins. Confining this schema to the predicate is what makes it reusable across endpoints, and
41
+ [`COMPARISON.md`](./COMPARISON.md) §5 sets out what the layers above it would require if they are
42
+ ever built. Proposals to add them now will be declined until the filter layer has external
43
+ implementers.
44
+
45
+ ### 2. Build an implementation — or make implementing it cheaper
46
+
47
+ The specification is written normatively so that independent implementations can agree, rather than
48
+ deferring to a reference implementation. So a second implementation is worth more here than a
49
+ feature, and the evidence says it is small work:
50
+ [`experiments/filter-to-sql`](https://github.com/OpenPredicate/open-predicate/tree/main/experiments/filter-to-sql) covers 33 of 34 operators in one
51
+ dependency-free file, and the part everyone worries about — UNKNOWN — is 23 lines of it.
52
+
53
+ **Be aware of what the test assets currently are and are not.** This matters if you are planning to
54
+ port them:
55
+
56
+ - [`tests/fixtures/`](https://github.com/OpenPredicate/open-predicate/tree/main/tests/fixtures) holds 32 valid and 28 invalid filters as declarative JSON.
57
+ They are portable in shape, and there is a test asserting every operator in the grammar is
58
+ exercised by at least one of them — the operator table and the fixture set are the same list.
59
+ - They check **schema well-formedness only.** There are no records and no expected results, so they
60
+ cannot tell you whether an *evaluator* is correct, which is the part you would actually be writing.
61
+ - The `expectKeyword` member on invalid fixtures names a JSON Schema keyword and is therefore
62
+ ajv-flavoured, not portable.
63
+ - `tests/` is excluded from the published npm package, so today the fixtures reach you only by
64
+ cloning.
65
+
66
+ **The highest-value single contribution available is closing that gap**, and most of the raw material
67
+ exists: `experiments/filter-to-sql/cases.mjs` is already 73 cases over 10 records in the shape
68
+ `{group, id, title, filter, expect}` — an evaluation corpus in everything but name. It is currently
69
+ labelled *"an exercise, not a deliverable"*. Promoting it into a versioned, language-agnostic
70
+ conformance suite with portable rejection cases (keyed to the five error conditions in
71
+ [SPEC.md §8](./SPEC.md) rather than to ajv keywords) and shipping it in the package is open work, and
72
+ it is wanted. Say so in an issue before starting so the shape can be agreed.
73
+
74
+ Where two implementations disagree about a case, that disagreement is a specification defect. Report
75
+ it as a *spec ambiguity* — those are the most valuable issues this project can receive.
76
+
77
+ ### 3. Claim conformance for a library that already exists
78
+
79
+ If you maintain a filter evaluator — anything in the `$eq`/`$in`/`$and` family — you may already be
80
+ most of the way to conforming, and the specification is built to let you say so honestly rather than
81
+ all-or-nothing. [SPEC.md §2.1](./SPEC.md) requires only that you:
82
+
83
+ - implement the `core` profile in full;
84
+ - advertise **only** the profiles you implement completely, stating anything partial per-field
85
+ through the `operators` member of §2.2 instead;
86
+ - reject an operator you do not support with an `unsupported-operator` error — never silently drop
87
+ the clause, because dropping a predicate *widens* the result set, which is the most dangerous
88
+ possible failure mode for an authorization-adjacent filter.
89
+
90
+ File an *implementation report* with what you support and what you had to leave out. The gaps are
91
+ the useful half: a profile boundary that nobody can implement cleanly is a boundary drawn in the
92
+ wrong place, and that is a specification bug.
93
+
94
+ ## Working in the repository
95
+
96
+ ```bash
97
+ npm ci
98
+ npm test # 132 tests: schema meta-validation, fixtures, generator, narrowing property
99
+ ```
100
+
101
+ CI additionally runs the suite on Node 20, 22 and 24, and lints both OpenAPI example documents with
102
+ `@redocly/cli`. Everything is ESM JavaScript on Node with no runtime dependencies; the only
103
+ dev-dependencies are `ajv` and `ajv-formats`. Please keep it that way — a specification repository
104
+ that is expensive to check out is a specification with fewer implementations.
105
+
106
+ A few invariants the tests enforce, so you find out from CI rather than from review: the schema
107
+ meta-validates under `ajv` in strict mode; `x-profiles` covers exactly the operators the grammar
108
+ defines; every operator has at least one valid fixture; the generator's committed output in
109
+ `examples/` must not drift; and a narrowed `$defs/FieldPath` must restrict fields at every nesting
110
+ depth.
111
+
112
+ Commit messages are sentence-shaped and say what the change does to the project, not what was
113
+ edited — *"Stop mandating an error format; mandate the error conditions"*, not *"update SPEC.md"*.
114
+ Normative changes need a [decision record](./GOVERNANCE.md) and a `CHANGELOG.md` entry with a
115
+ migration note; [`GOVERNANCE.md`](./GOVERNANCE.md) has the table of what each class of change costs.
116
+
117
+ ## Adding your own operators without waiting for us
118
+
119
+ You do not need permission, and you should not wait. [SPEC.md §6](./SPEC.md) reserves the bare `$`
120
+ namespace for the specification and gives you the rest: prefix your operator distinctly (`$x_`, or a
121
+ vendor tag such as `$acme_geoWithin`) and document it. An endpoint that adds an operator no longer
122
+ validates against the published schema, so it SHOULD publish an extended schema that `allOf`-composes
123
+ or bundles this one, and MUST NOT advertise the unmodified `$id`.
124
+
125
+ If a vendor-prefixed operator turns out to be one everybody needs, that is exactly the evidence a
126
+ decision record wants, and it is a far stronger case than a proposal with no implementation behind
127
+ it.
128
+
129
+ ## Conduct
130
+
131
+ By participating you agree to the [Code of Conduct](./CODE_OF_CONDUCT.md). It is short, and it makes
132
+ one thing explicit that matters more here than in most projects: blunt technical disagreement is
133
+ welcome and is not incivility. The distinction it draws is between attacking an argument and
134
+ attacking a person.
package/GOVERNANCE.md ADDED
@@ -0,0 +1,127 @@
1
+ # Governance
2
+
3
+ This document says how decisions get made, who makes them, what earns someone a say, and
4
+ what happens when people disagree. It exists because the [stated purpose](./README.md#about-openpredicate)
5
+ of the OpenPredicate organisation is to take this grammar *from a single-author design to an
6
+ open standard*, and a standard whose decision process is undocumented is still a single-author
7
+ design no matter how the specification is written.
8
+
9
+ ## Current state, stated plainly
10
+
11
+ **There is one maintainer:** Christos Gkoros ([@christosgkoros](https://github.com/christosgkoros)),
12
+ who is also the specification editor. Every commit in the history is his. No external contributor
13
+ has yet filed an issue or a pull request.
14
+
15
+ That is a defect, not a design. It is the single largest obstacle to the word *standard* applying
16
+ here, and it ranks above every feature on the roadmap. The rest of this document is written so
17
+ that the process does not have to be invented at the moment a second person shows up.
18
+
19
+ ## Three classes of change
20
+
21
+ What a change is determines what it costs to make. The dividing line is whether it can alter
22
+ what a conforming implementation must do.
23
+
24
+ | Class | Examples | Requires |
25
+ | --- | --- | --- |
26
+ | **Editorial** | Typos, clarifications that cannot change behaviour, examples, tooling, tests, site copy | A pull request. One maintainer approval. |
27
+ | **Substantive, compatible** | A new optional profile member, a new capability-document field, generator features, new fixtures | An issue first, then a pull request that references it. A `CHANGELOG.md` entry. |
28
+ | **Normative** | Adding, removing or renaming an operator; changing semantics, three-valued logic, coercion, error conditions or the safety limits; anything that changes what `MUST` happen | A **decision record** under [`decisions/`](https://github.com/OpenPredicate/open-predicate/tree/main/decisions), merged before or with the implementation. A `CHANGELOG.md` entry with a migration note. A `$id` version bump per [SPEC.md §9](./SPEC.md). |
29
+
30
+ If it is unclear which class a change falls into, it is normative. The cost of over-documenting a
31
+ change is a file nobody needed; the cost of under-documenting one is an implementer who cannot tell
32
+ whether their implementation is still conforming.
33
+
34
+ ## Decision records are the mechanism
35
+
36
+ Significant design decisions are argued in writing under [`decisions/`](https://github.com/OpenPredicate/open-predicate/tree/main/decisions) rather than
37
+ settled by commit message. There is currently **exactly one record** —
38
+ [`0001-array-quantifiers-and-unknown-handling.md`](https://github.com/OpenPredicate/open-predicate/blob/main/decisions/0001-array-quantifiers-and-unknown-handling.md).
39
+ More are expected, and at least two are already owed: the resolution of
40
+ [#1](https://github.com/OpenPredicate/open-predicate/issues/1) (scalar shorthand forcing `anyOf` on
41
+ `Constraint`) and [#2](https://github.com/OpenPredicate/open-predicate/issues/2) (the capability
42
+ document being RECOMMENDED at an unspecified location).
43
+
44
+ A record is numbered, committed, and thereafter immutable in substance — it is superseded by a later
45
+ record rather than edited, so the reasoning available at the time stays legible. `0001` is the
46
+ worked example of the expected shape: **Context** (including what the record does *not* do),
47
+ one section per **Decision**, **Migration** for anything breaking, **Costs and risks**,
48
+ **Open question** for what it deliberately leaves unsettled, **Follow-ups, explicitly out of
49
+ scope**, and **Results**.
50
+
51
+ Two conventions in `0001` are load-bearing and carry forward:
52
+
53
+ - **Record what you decided against, and why.** `0001` spends a section on why `$every` is included
54
+ and `$none` is not, and marks it as the one place its own two criteria pull against each other.
55
+ That is more useful to a future implementer than the decision itself.
56
+ - **Name the evidence.** `0001` resolved three operator overlaps because an external survey and this
57
+ repository's own `experiments/filter-to-sql` converged on the same three independently. A record
58
+ that cites only taste is weaker than one that cites a measurement, and the project has a standing
59
+ bias toward building the measurement.
60
+
61
+ ## How a disputed design call is resolved
62
+
63
+ The project's position is that [disagreement is the most useful contribution at this
64
+ stage](./CONTRIBUTING.md), so the process is built to absorb it rather than to close it quickly.
65
+
66
+ 1. **The objection is filed** as an issue, using the *design objection* template.
67
+ 2. **It gets answered in writing.** A maintainer must respond on the substance — not merely close
68
+ it. "Working as intended" is not an answer unless it says why the intent is right.
69
+ 3. **If it survives that, it becomes a decision record**, and the objection is quoted in the
70
+ Context section in the objector's words rather than paraphrased.
71
+ 4. **Lazy consensus decides it.** A proposed record that has sat for **14 days** with no unresolved
72
+ objection from a maintainer is accepted. Any maintainer may extend that window once by asking.
73
+ 5. **If maintainers still disagree, the specification editor decides — and the dissent is recorded
74
+ in the record itself**, attributed, with its reasoning intact. A standard that hides its
75
+ disagreements makes its own history unusable.
76
+
77
+ Nothing in this process applies to correctness. A demonstrated bug — a filter the schema accepts
78
+ and the spec forbids, or vice versa — is fixed, not debated.
79
+
80
+ ## What earns commit rights
81
+
82
+ Commit rights follow demonstrated judgement about *this* grammar, not volume. Any one of these is
83
+ sufficient to be proposed as a maintainer:
84
+
85
+ - **Two accepted substantive or normative contributions**, at least one of which changed the
86
+ specification text.
87
+ - **An authored decision record that was accepted**, including one that argued successfully against
88
+ the editor.
89
+ - **An independent implementation that is shipped and maintained**, and that passes the fixtures in
90
+ [`tests/fixtures/`](https://github.com/OpenPredicate/open-predicate/tree/main/tests/fixtures). Implementers are the constituency this specification exists
91
+ to serve, and their view of an ambiguity outranks the editor's intent about it.
92
+
93
+ A maintainer is added by consensus of the existing maintainers, announced in a decision record, and
94
+ listed below. Maintainers who have been inactive for twelve months move to emeritus, keeping
95
+ attribution and losing the commit bit; this is bookkeeping, not a judgement, and it reverses on
96
+ request.
97
+
98
+ **Maintainers:** Christos Gkoros ([@christosgkoros](https://github.com/christosgkoros)),
99
+ specification editor.
100
+
101
+ ## Intellectual property and patents
102
+
103
+ The specification, the schema and this repository are [MIT](./LICENSE)-licensed, so the grammar can
104
+ be implemented, vendored, extended and re-specified by anyone, commercially or otherwise, without
105
+ permission and without royalty. MIT settles copyright. It says nothing about patents, and for a
106
+ specification that silence is the thing an adopter's lawyer notices first — so the project's
107
+ position is stated explicitly:
108
+
109
+ **OpenPredicate is intended to be implementable royalty-free by anyone.** The maintainers are aware
110
+ of no patent claims covering the grammar, and will not knowingly accept a contribution encumbered by
111
+ one. By contributing, you confirm that you are entitled to submit the work, and you grant to anyone
112
+ implementing this specification a perpetual, worldwide, non-exclusive, royalty-free licence under
113
+ any patent claims you own or control that are necessarily infringed by implementing your
114
+ contribution. If you know of a claim that would encumber an implementation — yours or anyone's —
115
+ disclose it in the pull request. A contribution that cannot be implemented freely will be declined
116
+ however good the design is.
117
+
118
+ Should the specification enter a formal standards venue, the venue's IPR rules govern instead — for
119
+ the IETF, that is [BCP 78](https://www.rfc-editor.org/info/bcp78) and
120
+ [BCP 79](https://www.rfc-editor.org/info/bcp79) — and this section will be superseded rather than
121
+ reinterpreted.
122
+
123
+ ## Changing this document
124
+
125
+ This document is itself normative about process, so changing it is a normative change: it takes a
126
+ decision record. The first genuinely useful amendment will be the one that removes "there is one
127
+ maintainer" from the top.
package/README.md CHANGED
@@ -24,12 +24,12 @@ One schema, two integration points, because JSON Schema is what both already spe
24
24
  - **Install** — `npm i @open-predicate/open-predicate` (Node >= 20.10) ([npmjs.com](https://www.npmjs.com/package/@open-predicate/open-predicate)). The package *is* the schema: `require()` it, or `import schema from '@open-predicate/open-predicate' with { type: 'json' }` — ESM needs that import attribute, and a bare `import` throws without it.
25
25
  - **Schema** — [`open-predicate-schema.json`](./open-predicate-schema.json) (JSON Schema draft 2020-12)
26
26
  - **Semantics** — [`SPEC.md`](./SPEC.md) — nulls, paths, coercion, errors, limits
27
- - **MCP server** — [`examples/mcp-server/`](./examples/mcp-server) — a search tool with the language as its `inputSchema`, runnable
28
- - **OpenAPI documents** — [`examples/`](./examples) — working 3.1 (`POST /…/search`) and 3.2 (`QUERY`) integrations
27
+ - **MCP server** — [`examples/mcp-server/`](https://github.com/OpenPredicate/open-predicate/tree/main/examples/mcp-server) — a search tool with the language as its `inputSchema`, runnable
28
+ - **OpenAPI documents** — [`examples/`](https://github.com/OpenPredicate/open-predicate/tree/main/examples) — working 3.1 (`POST /…/search`) and 3.2 (`QUERY`) integrations
29
29
  - **Generator** — [`tools/generate-filter-schema.mjs`](./tools/generate-filter-schema.mjs) — turns a resource's JSON Schema, plus the slice of the language you can serve, into a per-field filter schema
30
30
  - **Compared with GraphQL** — [`COMPARISON.md`](./COMPARISON.md) — what this overlaps with, what it does not, and what a JSON-Schema-native alternative would still need
31
31
  - **Stewardship** — the [OpenPredicate](https://openpredicate.tech) organisation — see [About OpenPredicate](#about-openpredicate)
32
- - **Version** — `0.6.1`. The schema's `$id` still names `v0.4.0`: the `$id` tracks the grammar, and neither 0.5.0 nor 0.6.0 touched it. See [`CHANGELOG.md`](./CHANGELOG.md) for this release, and [`decisions/0001`](./decisions/0001-array-quantifiers-and-unknown-handling.md) for the v0.4.0 migration.
32
+ - **Version** — `0.6.2`. The schema's `$id` still names `v0.4.0`: the `$id` tracks the grammar, and neither 0.5.0 nor 0.6.0 touched it. See [`CHANGELOG.md`](./CHANGELOG.md) for this release, and [`decisions/0001`](https://github.com/OpenPredicate/open-predicate/blob/main/decisions/0001-array-quantifiers-and-unknown-handling.md) for the v0.4.0 migration.
33
33
 
34
34
  > **Pre-1.0, and installable.** The name is settled — *OpenPredicate*, stewarded by the [OpenPredicate](https://openpredicate.tech) organisation, with the repository, both package names, the schema `$id` and the problem-type URIs all derived from it — the package is on npm as [`@open-predicate/open-predicate`](https://www.npmjs.com/package/@open-predicate/open-predicate), and the schema is **served at its `$id`** — so `$ref` it by URL, or from a packaged copy, whichever suits. One thing is still provisional: the grammar may still break before 1.0, with each break recorded in [`CHANGELOG.md`](./CHANGELOG.md). See [Status](#status) for what is reachable today.
35
35
 
@@ -54,7 +54,7 @@ is `department = 'sales' AND age >= 18`. That holds for operators on one field t
54
54
 
55
55
  ## Operator reference
56
56
 
57
- Every operator below has a matching fixture in [`tests/fixtures/valid/`](./tests/fixtures/valid) — the table and the test suite are the same list. Full semantics in [SPEC.md §5](./SPEC.md#5-operator-semantics).
57
+ Every operator below has a matching fixture in [`tests/fixtures/valid/`](https://github.com/OpenPredicate/open-predicate/tree/main/tests/fixtures/valid) — the table and the test suite are the same list. Full semantics in [SPEC.md §5](./SPEC.md#5-operator-semantics).
58
58
 
59
59
  ### Logical — profile `core`
60
60
 
@@ -165,7 +165,7 @@ A path addresses one position. To say something about an array's elements, quant
165
165
 
166
166
  ## Using it from OpenAPI
167
167
 
168
- One of the two integration paths this repo exists for; [Exposing search to an agent](#exposing-search-to-an-agent) covers the other. Complete, CI-linted documents live in [`examples/`](./examples).
168
+ One of the two integration paths this repo exists for; [Exposing search to an agent](#exposing-search-to-an-agent) covers the other. Complete, CI-linted documents live in [`examples/`](https://github.com/OpenPredicate/open-predicate/tree/main/examples).
169
169
 
170
170
  ### OpenAPI 3.1 — `POST /…/search`
171
171
 
@@ -197,7 +197,7 @@ components:
197
197
  filter: { $ref: '#/components/schemas/Filter' }
198
198
  ```
199
199
 
200
- → [`examples/openapi-3.1-post-search.yaml`](./examples/openapi-3.1-post-search.yaml)
200
+ → [`examples/openapi-3.1-post-search.yaml`](https://github.com/OpenPredicate/open-predicate/blob/main/examples/openapi-3.1-post-search.yaml)
201
201
 
202
202
  ### OpenAPI 3.2 — the `QUERY` method
203
203
 
@@ -222,7 +222,7 @@ paths:
222
222
  schema: { type: string, format: uri-reference }
223
223
  ```
224
224
 
225
- → [`examples/openapi-3.2-query-method.yaml`](./examples/openapi-3.2-query-method.yaml)
225
+ → [`examples/openapi-3.2-query-method.yaml`](https://github.com/OpenPredicate/open-predicate/blob/main/examples/openapi-3.2-query-method.yaml)
226
226
 
227
227
  `QUERY` ([RFC 10008](https://www.rfc-editor.org/rfc/rfc10008)) is **safe and idempotent** and carries a request body — it says "this is a read" in a way `POST` cannot, so intermediaries may cache it and clients may retry it. Return `Content-Location` when the same representation is also reachable by `GET`.
228
228
 
@@ -306,7 +306,7 @@ node tools/generate-filter-schema.mjs examples/pet.schema.json \
306
306
  --out pet.filter.json
307
307
  ```
308
308
 
309
- Given [`examples/pet.schema.json`](./examples/pet.schema.json), the generated [`pet.filter.json`](./examples/pet.filter.json) turns each of these from an empty result set into a `400`:
309
+ Given [`examples/pet.schema.json`](https://github.com/OpenPredicate/open-predicate/blob/main/examples/pet.schema.json), the generated [`pet.filter.json`](https://github.com/OpenPredicate/open-predicate/blob/main/examples/pet.filter.json) turns each of these from an empty result set into a `400`:
310
310
 
311
311
  | Filter | Rejected because |
312
312
  | --- | --- |
@@ -319,7 +319,7 @@ Given [`examples/pet.schema.json`](./examples/pet.schema.json), the generated [`
319
319
 
320
320
  It also writes the [SPEC.md §2.2](./SPEC.md#22-capability-discovery) capability document from the same source, so the schema and the published domains cannot drift apart.
321
321
 
322
- That file is also what [`examples/mcp-server`](./examples/mcp-server) hands a model: a resource schema in, a tool definition out, with nothing written by hand in between.
322
+ That file is also what [`examples/mcp-server`](https://github.com/OpenPredicate/open-predicate/tree/main/examples/mcp-server) hands a model: a resource schema in, a tool definition out, with nothing written by hand in between.
323
323
 
324
324
  What it decides, and why:
325
325
 
@@ -377,7 +377,7 @@ The tool ships inside the package as a `bin` named `open-predicate-generate`, so
377
377
 
378
378
  ## Exposing search to an agent
379
379
 
380
- The other integration path, and a working server for it lives in [`examples/mcp-server/`](./examples/mcp-server) — one tool, real records, `node examples/mcp-server/demo.mjs` to watch it answer and reject.
380
+ The other integration path, and a working server for it lives in [`examples/mcp-server/`](https://github.com/OpenPredicate/open-predicate/tree/main/examples/mcp-server) — one tool, real records, `node examples/mcp-server/demo.mjs` to watch it answer and reject.
381
381
 
382
382
  The integration is one property of one argument:
383
383
 
@@ -463,7 +463,7 @@ experiments/filter-to-sql/ an exercise: compile a filter to SQL, then judge
463
463
  | --- | --- |
464
464
  | `$id` / `$ref` — `https://openpredicate.tech/schema/v0.4.0/open-predicate-schema.json` | **Served**, as `application/schema+json`, with `Access-Control-Allow-Origin: *` and immutable caching, byte-identical to the copy in this repository. `$ref` it directly, as [Using it from OpenAPI](#using-it-from-openapi) and the capability document examples do. |
465
465
  | [`@open-predicate/open-predicate`](https://www.npmjs.com/package/@open-predicate/open-predicate) on npmjs.com | **Published**, from `v0.6.0` on. Public, no credential needed to install. |
466
- | `@openpredicate/open-predicate` on GitHub Packages | Not published yet — the first copy goes up with the next GitHub Release. Installing from it needs an `.npmrc` and a token even though it is public, so npmjs.com is the easier path. |
466
+ | `@openpredicate/open-predicate` on GitHub Packages | **Published**, from `0.6.1` on — the first copy went up with that release. Installing from it needs an `.npmrc` and a token even though it is public, so npmjs.com is the easier path. |
467
467
  | The version line at the top, and the version inside the `$id` | May lag the latest tag. `CHANGELOG.md` is authoritative. |
468
468
  | `npx @open-predicate/open-predicate` | Works from `0.6.1`. **Broken in `0.6.0`**, where the `bin` exited 0 having printed nothing — the entry guard compared `process.argv[1]` against `import.meta.url`, and npm installs a `bin` as a *symlink*. Use the package name, not the bin name `open-predicate-generate`, which is not a package. |
469
469
 
@@ -488,10 +488,10 @@ npm i @open-predicate/open-predicate
488
488
  or with no package manager in the way — pinning a tag rather than `main`:
489
489
 
490
490
  ```bash
491
- curl -O https://raw.githubusercontent.com/OpenPredicate/open-predicate/v0.6.1/open-predicate-schema.json
491
+ curl -O https://raw.githubusercontent.com/OpenPredicate/open-predicate/v0.6.2/open-predicate-schema.json
492
492
  ```
493
493
 
494
- The served copy and the packaged copy are the same bytes: the site vendors the schema out of this repository at a tag, so a release moves both together. See [`RELEASING.md`](./RELEASING.md).
494
+ The served copy and the packaged copy are the same bytes: the site vendors the schema out of this repository at a tag, so a release moves both together. See [`RELEASING.md`](https://github.com/OpenPredicate/open-predicate/blob/main/RELEASING.md).
495
495
 
496
496
  **What is stable enough to review.** The grammar, the operator set and profile grouping, the null and three-valued semantics, and the error model. Those are what the schema, [`SPEC.md`](./SPEC.md) and the test suite pin down, and they are what feedback is most useful on. The grammar may still change before 1.0; each break is recorded in [`CHANGELOG.md`](./CHANGELOG.md) with a migration note.
497
497
 
@@ -499,7 +499,7 @@ The served copy and the packaged copy are the same bytes: the site vendors the s
499
499
 
500
500
  OpenPredicate is stewarded by the [**OpenPredicate**](https://openpredicate.tech) organisation, at [github.com/OpenPredicate](https://github.com/OpenPredicate). Its purpose is to take this predicate grammar from a single-author design to an **open standard**, and to push for its adoption at the places APIs are already described: `$ref`-ed from OpenAPI documents, inlined as MCP tool `inputSchema`s, and carried as the body of the HTTP `QUERY` method.
501
501
 
502
- That goal sets the terms of the work. The grammar is specified normatively in [`SPEC.md`](./SPEC.md) rather than left to a reference implementation, so that independent implementations can agree; every breaking change is recorded with a migration note; and each significant design decision is argued in writing under [`decisions/`](./decisions) rather than settled by commit. Adoption arguments belong in the open too — [`COMPARISON.md`](./COMPARISON.md) is where the case against the nearest alternative is made and its gaps admitted.
502
+ That goal sets the terms of the work. The grammar is specified normatively in [`SPEC.md`](./SPEC.md) rather than left to a reference implementation, so that independent implementations can agree; every breaking change is recorded with a migration note; and each significant design decision is argued in writing under [`decisions/`](https://github.com/OpenPredicate/open-predicate/tree/main/decisions) rather than settled by commit. Adoption arguments belong in the open too — [`COMPARISON.md`](./COMPARISON.md) is where the case against the nearest alternative is made and its gaps admitted.
503
503
 
504
504
  [`GOVERNANCE.md`](./GOVERNANCE.md) is where that process is written down: what each class of change costs, how a disputed design call is resolved — including that dissent is recorded in the decision record rather than discarded — what earns commit rights, and the project's royalty-free patent posture. It also states plainly that there is currently one maintainer, and treats that as the defect it is.
505
505
 
package/SECURITY.md ADDED
@@ -0,0 +1,92 @@
1
+ # Security policy
2
+
3
+ ## What this project is, and what that means for a report
4
+
5
+ OpenPredicate is a **specification and a JSON Schema**, plus a generator and some examples. It has no
6
+ runtime dependencies and executes nothing on a server. So a security report here is usually not
7
+ "this code is exploitable" but one of two other things, and both are in scope:
8
+
9
+ 1. **A defect in the specification that makes conforming implementations unsafe.** A rule that, if
10
+ followed, produces a vulnerability. These are the serious ones, because they replicate into every
11
+ implementation.
12
+ 2. **A vulnerability in the shipped tooling** — `tools/generate-filter-schema.mjs`, the published npm
13
+ package, the examples, or the release pipeline.
14
+
15
+ ## In scope
16
+
17
+ A filter is user-supplied input that becomes a query plan, and [SPEC.md §7](./SPEC.md) bounds it
18
+ normatively. Reports that land squarely in scope include:
19
+
20
+ - **A filter that stays within the §7 limits and is still superlinear.** The recommended defaults are
21
+ depth 10, 100 clauses, 1000 set members, 64 KiB body and 100 ms per `$regex` per record. A filter
22
+ that satisfies all five and still degrades a reasonable backend is a specification bug — the limits
23
+ are drawn in the wrong place.
24
+ - **A `$regex` operand that defeats the 100 ms guidance**, or a case where the advice to prefer a
25
+ non-backtracking engine is insufficient rather than merely inconvenient.
26
+ - **Anything that causes a predicate to be dropped, widened or truncated.** This is the failure mode
27
+ the specification treats as most dangerous: §2.1 forbids silently ignoring an unsupported operator
28
+ and §7 forbids truncating an over-limit filter, both because a filter that matches *more* than
29
+ asked is an authorization bypass wherever filters carry tenancy or visibility. If you find a path
30
+ through the spec where a conforming implementation ends up widening a result set, report it here
31
+ rather than as a design objection.
32
+ - **An injection or escaping defect** in the filter-to-SQL compiler under
33
+ [`experiments/filter-to-sql/`](https://github.com/OpenPredicate/open-predicate/tree/main/experiments/filter-to-sql), even though it is explicitly an
34
+ exercise and not a deliverable. It is the thing people will read to learn how to compile a filter,
35
+ so an unsafe pattern in it propagates.
36
+ - **Schema-level resource exhaustion**: a document that makes a conforming validator behave
37
+ pathologically, including through the recursive `$ref` structure or the generator's emitted output.
38
+ - **Anything in the supply chain**: the trusted-publisher configuration, the release workflow, or a
39
+ published tarball that does not match the repository.
40
+
41
+ ## Out of scope
42
+
43
+ - Vulnerabilities in *your* server that come from not implementing §7 at all. Bounding a filter is
44
+ `MUST`; not doing it is a bug in the implementation, and the answer is to implement the limits.
45
+ - Denial of service from unindexed fields where the capability document was not used to restrict
46
+ expensive operators to indexed ones — §7 already says to do this. Report it if the guidance is
47
+ *wrong*, not if it was skipped.
48
+ - Design objections with no safety consequence. Those are welcome, but as a
49
+ [design objection](./CONTRIBUTING.md) issue, in public.
50
+ - Reports against `npm install open-predicate`, the unscoped deprecated placeholder, which holds two
51
+ files and no code.
52
+
53
+ ## Supported versions
54
+
55
+ Pre-1.0, **only the latest release is supported.** Fixes land on `main` and ship in the next release;
56
+ there are no backports. Versioned schema `$id`s are immutable by policy ([SPEC.md §9](./SPEC.md)), so
57
+ a security fix that changes the grammar produces a *new* version rather than altering a published
58
+ one — an implementation pinned to an old `$id` must move to get the fix, and the `CHANGELOG.md`
59
+ migration note will say so.
60
+
61
+ | Version | Supported |
62
+ | --- | --- |
63
+ | 0.6.x | Yes |
64
+ | < 0.6 | No |
65
+
66
+ ## Reporting
67
+
68
+ **Do not open a public issue for anything in the *In scope* list above.**
69
+
70
+ Use GitHub's private vulnerability reporting —
71
+ [**Report a vulnerability**](https://github.com/OpenPredicate/open-predicate/security/advisories/new)
72
+ — which keeps the report private until an advisory is published. If that is unavailable to you, email
73
+ **contact@openpredicate.tech**; say in the subject line that it is a security report, and do not
74
+ include exploit details in the first message if you would rather establish the channel first.
75
+
76
+ Please include the filter, the schema version, and what a conforming implementation does with it.
77
+ A reproducing filter is worth more than a description of one.
78
+
79
+ ## What happens next
80
+
81
+ - **Acknowledgement within 7 days**, and an initial assessment within 14. There is one maintainer;
82
+ if you have heard nothing after 14 days, send a reminder rather than assuming it was ignored.
83
+ - **A fix or a decision, in writing.** If a report turns out to be a specification defect, the
84
+ resolution goes through a [decision record](./GOVERNANCE.md) like any other normative change — with
85
+ the difference that the record is published when the fix ships, not while it is embargoed.
86
+ - **Coordinated disclosure.** We will agree a date with you, defaulting to 90 days or the fix,
87
+ whichever is sooner. Because this is a specification, a defect in class (1) above may need other
88
+ implementers notified before it is public; if that applies we will tell you, and say who.
89
+ - **Credit** in the advisory and the changelog, under whatever name you choose, or none.
90
+
91
+ There is no bug bounty. This is an unfunded single-maintainer project and pretending otherwise would
92
+ waste your time.
package/SPEC.md CHANGED
@@ -399,7 +399,7 @@ is TRUE only when a **single** item has both `qty > 2` and an `A` SKU.
399
399
 
400
400
  is TRUE when *some* item has `qty > 2` and *some* item has an `A` SKU — **possibly different items**, because each `$some` chooses its own element.
401
401
 
402
- Both readings are expressible and the nesting says which is meant. Versions before v0.4.0 offered a second mechanism for the second reading — a `[*]` wildcard path segment — which expressed nothing the two-clause form does not, could not be declined by a server through its profiles, and left the quantifier's scope to be inferred from a path shape. It was removed; see [`decisions/0001`](./decisions/0001-array-quantifiers-and-unknown-handling.md).
402
+ Both readings are expressible and the nesting says which is meant. Versions before v0.4.0 offered a second mechanism for the second reading — a `[*]` wildcard path segment — which expressed nothing the two-clause form does not, could not be declined by a server through its profiles, and left the quantifier's scope to be inferred from a path shape. It was removed; see [`decisions/0001`](https://github.com/OpenPredicate/open-predicate/blob/main/decisions/0001-array-quantifiers-and-unknown-handling.md).
403
403
 
404
404
  ### 5.10 Presence and type — `$exists`, `$isNull`, `$type`
405
405
 
package/SUPPORT.md ADDED
@@ -0,0 +1,38 @@
1
+ # Getting help
2
+
3
+ **One maintainer, no service-level agreement.** Everything here is best-effort, usually within a week.
4
+ Knowing that up front is more useful than a promise that does not hold.
5
+
6
+ ## Where to go
7
+
8
+ | You want to | Go to |
9
+ | --- | --- |
10
+ | Try a filter and see it validate | The [playground](https://openpredicate.tech/playground/) — runs the real schema in your browser and shows the error a conforming server would return |
11
+ | Know what an operator means, normatively | [`SPEC.md`](./SPEC.md), or the generated [operator reference](https://openpredicate.tech/operators/) |
12
+ | Ask how to do something | [Discussions → Q&A](https://github.com/OpenPredicate/open-predicate/discussions) |
13
+ | Argue that a design decision is wrong | An issue, using the *design objection* template — see [`CONTRIBUTING.md`](./CONTRIBUTING.md) |
14
+ | Report that the schema and the spec disagree | An issue, using the *spec ambiguity* template |
15
+ | Report a security or denial-of-service concern | [`SECURITY.md`](./SECURITY.md) — **not** a public issue |
16
+ | Say you have implemented it | An issue, using the *implementation report* template. These are the most welcome issues the project receives |
17
+ | Understand why a decision was made | [`decisions/`](https://github.com/OpenPredicate/open-predicate/tree/main/decisions) and [`CHANGELOG.md`](./CHANGELOG.md), which carries a migration note for every break |
18
+ | Know how this compares to GraphQL, OData, CQL2, Mongo or JSONPath | [`COMPARISON.md`](./COMPARISON.md), including the gaps it admits |
19
+
20
+ ## Questions that already have answers
21
+
22
+ Three things surprise almost everyone, and all three are deliberate:
23
+
24
+ - **Sibling members AND together, and a bare scalar means `$eq`.** `{"status": "open", "rank": 3}`
25
+ is `status = 'open' AND rank = 3`. You only need `$and` to repeat a field, or to nest inside `$or`.
26
+ - **A comparison against `null` or an absent field is UNKNOWN, not FALSE — and only TRUE matches.**
27
+ So `{"status": {"$ne": "archived"}}` does **not** match a record whose status is missing. That is
28
+ SQL's behaviour. When you want those records, say so: `{"$ne": "archived", "$unknownAs": true}`.
29
+ - **`$in` does not look inside arrays.** It is whole-value comparison. Element membership is
30
+ `{"tags": {"$some": {"$in": [...]}}}`. This differs from MongoDB on purpose — overloading `$in`
31
+ makes the meaning depend on data a validator cannot see.
32
+
33
+ ## Versions
34
+
35
+ Pre-1.0: only the latest release is supported, and the grammar may still break before 1.0. Pin an
36
+ exact version if you depend on it. The schema `$id` is versioned and immutable — `v0.4.0` will always
37
+ return the bytes it returned the day it was published — so `$ref`-ing a versioned `$id` is the
38
+ safe thing to do. There is deliberately no `latest` URL.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@open-predicate/open-predicate",
3
- "version": "0.6.1",
4
- "description": "OpenPredicate an open standard for JSON-encoded, SQL-flavoured predicates, described by a single JSON Schema: $ref it from OpenAPI, or inline it into an MCP tool's inputSchema.",
3
+ "version": "0.6.2",
4
+ "description": "OpenPredicate \u2014 an open standard for JSON-encoded, SQL-flavoured predicates, described by a single JSON Schema: $ref it from OpenAPI, or inline it into an MCP tool's inputSchema.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "engines": {
@@ -48,7 +48,13 @@
48
48
  "tools/",
49
49
  "SPEC.md",
50
50
  "README.md",
51
- "CHANGELOG.md"
51
+ "CHANGELOG.md",
52
+ "COMPARISON.md",
53
+ "CONTRIBUTING.md",
54
+ "GOVERNANCE.md",
55
+ "CODE_OF_CONDUCT.md",
56
+ "SECURITY.md",
57
+ "SUPPORT.md"
52
58
  ],
53
59
  "scripts": {
54
60
  "test": "node --test tests/*.test.mjs",