@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 +72 -12
- package/CODE_OF_CONDUCT.md +66 -0
- package/COMPARISON.md +195 -0
- package/CONTRIBUTING.md +134 -0
- package/GOVERNANCE.md +127 -0
- package/README.md +14 -14
- package/SECURITY.md +92 -0
- package/SPEC.md +1 -1
- package/SUPPORT.md +38 -0
- package/package.json +9 -3
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`](
|
|
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`](
|
|
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/`](
|
|
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`](
|
|
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`](
|
|
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`](
|
|
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
|
|
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/`](
|
|
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`](
|
|
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`](
|
|
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](
|
|
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](
|
|
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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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/`](
|
|
28
|
-
- **OpenAPI documents** — [`examples/`](
|
|
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.
|
|
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/`](
|
|
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/`](
|
|
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`](
|
|
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`](
|
|
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`](
|
|
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`](
|
|
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/`](
|
|
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 |
|
|
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.
|
|
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`](
|
|
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/`](
|
|
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`](
|
|
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.
|
|
4
|
-
"description": "OpenPredicate
|
|
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",
|