From 8f41c122431f318bf3137fda580f19575c2b3011 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 17:39:36 -0600 Subject: [PATCH 1/9] Add differential conformance harness for Harper vs. the RQL 2.0 parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs a 349-case corpus through both Harper's REST query parser and the reference parser, maps Harper's output into the canonical model (§6) with an adapter, diffs the two, and classifies every difference as an existing row of Harper's divergence ledger (harper#2440, which spec Appendix D links to), a NEW divergence with a proposed row, or a reference-parser bug. An unclassified divergence fails the run, so the report cannot quietly go stale. Record/replay, per the ruling on the planning gate: `--record` gives every Harper parse its own short-lived process (Harper's parser holds module-global state) and writes a provenance-stamped fixture; ordinary runs replay it, so CI needs no Harper checkout and `--check` asserts the committed report is reproduced byte for byte. Replay uses one persistent reference worker, killed and replaced on a timeout. `conformance/ledger.json` is a refreshable cache of harper#2440, not a second source of truth. First run: 190 agree, 33 map to ledger rows, 117 are new divergences across 24 proposed rows, 9 are reference-parser bugs, 0 unclassified. Nothing in specification/ or src/ is touched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- .gitignore | 1 + conformance/README.md | 103 + conformance/canonical.ts | 149 + conformance/classify.ts | 572 + conformance/compare.ts | 140 + conformance/conformance-report.md | 2528 +++ conformance/corpus.ts | 452 + conformance/design.md | 106 + conformance/fixtures/harper-parse.json | 22293 +++++++++++++++++++++++ conformance/harperAdapter.ts | 280 + conformance/ledger.json | 96 + conformance/report.ts | 345 + conformance/tagged.ts | 146 + conformance/tsconfig.json | 10 + package-lock.json | 18 + package.json | 12 +- scripts/conformance-diff.mjs | 323 + scripts/conformance-record-worker.mjs | 64 + scripts/conformance-ref-worker.mjs | 55 + scripts/refresh-ledger.mjs | 72 + test/conformance/adapter.test.ts | 316 + test/conformance/canonical.test.ts | 90 + test/conformance/classify.test.ts | 144 + test/conformance/corpus.test.ts | 65 + test/conformance/replay.test.ts | 116 + test/conformance/tagged.test.ts | 115 + 26 files changed, 28609 insertions(+), 2 deletions(-) create mode 100644 conformance/README.md create mode 100644 conformance/canonical.ts create mode 100644 conformance/classify.ts create mode 100644 conformance/compare.ts create mode 100644 conformance/conformance-report.md create mode 100644 conformance/corpus.ts create mode 100644 conformance/design.md create mode 100644 conformance/fixtures/harper-parse.json create mode 100644 conformance/harperAdapter.ts create mode 100644 conformance/ledger.json create mode 100644 conformance/report.ts create mode 100644 conformance/tagged.ts create mode 100644 conformance/tsconfig.json create mode 100644 scripts/conformance-diff.mjs create mode 100644 scripts/conformance-record-worker.mjs create mode 100644 scripts/conformance-ref-worker.mjs create mode 100755 scripts/refresh-ledger.mjs create mode 100644 test/conformance/adapter.test.ts create mode 100644 test/conformance/canonical.test.ts create mode 100644 test/conformance/classify.test.ts create mode 100644 test/conformance/corpus.test.ts create mode 100644 test/conformance/replay.test.ts create mode 100644 test/conformance/tagged.test.ts diff --git a/.gitignore b/.gitignore index db6fa17..11ba232 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ downloaded-modules node_modules npm-debug.log +conformance/*.actual diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 0000000..5eea832 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,103 @@ +# Differential conformance harness + +Runs a corpus of RQL query strings through **both** Harper's REST query parser and the RQL 2.0 +reference parser in `src/`, maps Harper's output into the canonical model (§6) with an adapter, +diffs the two, and classifies every difference. + +The output is [`conformance-report.md`](./conformance-report.md), which is committed. Spec +Appendix D deliberately tracks no vendor rows — it links to each implementation's own public +ledger — so this harness is what turns Harper's ledger +([HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440)) from a hand-written +list into an exhaustive, re-runnable one. + +## Running it + +```bash +npm run conformance # replay the recorded fixture and regenerate the report +npm run conformance:check # fail if the committed report is out of date (CI) +npm test # unit tests + an end-to-end replay assertion +npm run typecheck:conformance # type-checks conformance/, test/conformance/ and src/ +``` + +None of those need a Harper checkout. Only re-recording does: + +```bash +HARPER_PATH=../harper npm run conformance:record +``` + +`HARPER_PATH` must point at a Harper checkout that has been built (`npm install && npm run build` +there) — the harness imports `dist/resources/search.js` and `dist/resources/RequestTarget.js`, and +that import graph pulls in Table/rocksdb-js, so a full install is required. There is no default +path and no implicit search: recording is an explicit act against a named checkout, and the +checkout's commit is stamped into the fixture. + +Refresh the cached ledger before trusting a classification: + +```bash +npm run conformance:refresh-ledger # re-reads harper#2440 with the gh CLI +``` + +## Why record and replay + +Harper's parser keeps **module-global state** (`lastIndex` / `currentQuery` / `queryString` in +`resources/search.ts`), so two parses must never interleave. Recording gives every query its own +short-lived process, which makes that structural rather than a convention and contains a crash or +a hang to the one case that caused it. Ordinary runs then replay the committed fixture, which +means: + +- CI needs no cross-repo dependency and no build of Harper; +- the report is reproducible — `conformance:check` asserts replay reproduces it byte for byte; +- the adapter is exercised against real recorded host shapes rather than invented ones; +- the Harper revision every classification was made against is recorded, not assumed. + +Replay uses **one** persistent reference-parser worker (the reference parser holds no global +state), killed and replaced if a parse exceeds its wall-clock budget, so one pathological case +costs that case and not the run. + +## What is in here + +| File | | +|---|---| +| `corpus.ts` | The declared corpus. Every case carries the grammar features it exercises and the ledger rows it witnesses. Ids are content hashes, so inserting a case never renumbers the others. | +| `harperAdapter.ts` | Harper's parse output → the canonical model. Structural only: it resolves Harper's comparator vocabulary and shapes, and never re-interprets a value Harper already decoded. | +| `tagged.ts` | Lossless, deterministic encoding of raw Harper output for the fixture — `URLSearchParams`, `Date`, `NaN`, `undefined`, arrays carrying marker properties. | +| `canonical.ts` | Deterministic canonical JSON for a `ParseResult`, plus the structural differ the classification rules match on. | +| `classify.ts` | The rules. Each maps the *shape* of a disagreement (or a named set of witness queries) to a ledger row, a proposed new row, or a reference-parser bug. | +| `compare.ts` | Assembles a run from the fixture plus reference outcomes. Shared by the runner and the tests. | +| `report.ts` | Markdown rendering. Reads only committed data, never the wall clock, so the report is stable. | +| `ledger.json` | A provenance-stamped **cache** of harper#2440. The issue stays canonical. | +| `fixtures/harper-parse.json` | Recorded raw Harper output, stamped with the Harper commit, the reference commit, the Node version and the corpus digest. | +| `../scripts/` | The CLI (`conformance-diff.mjs`), the two workers, and the ledger refresher. | + +## Adding a corpus case + +Add a `draft(...)` to the right group in `corpus.ts` with the grammar features it exercises, then: + +```bash +HARPER_PATH=../harper npm run conformance:record +``` + +Replay refuses to run against a fixture recorded for a different corpus — the digest is checked — +so there is no way to compare a new case against a stale recording. + +If the new case diverges for a reason no rule covers, the run **fails** and prints it. That is the +design: a divergence nobody has classified is not allowed to sit quietly in the report. + +## Adding a classification rule + +Rules live in `classify.ts` and are tried in order. Prefer a rule that matches the *shape* of the +disagreement (which pointers differ, and how) — it then covers every future case with the same +root cause. Pin a rule to witness queries only where the shape is not distinctive. + +Each rule states a rationale citing the spec clause, and a `new` verdict must propose the ledger +row to add. **The specification is never edited from here**; the report proposes rows and a human +carries them to the ledger issue. + +Every rule must match at least one corpus case — `test/conformance/replay.test.ts` fails on a rule +that matches nothing, so a rule made obsolete by a Harper fix has to be deleted along with it. + +## Scope + +This compares **parse** results. Value coercion against a table's declared attribute types, +comparator evaluation and result materialization all happen later, in Harper's `resources/Table.ts`, +and no case here can witness them — the report says so for the ledger rows that describe them. diff --git a/conformance/canonical.ts b/conformance/canonical.ts new file mode 100644 index 0000000..6d146cb --- /dev/null +++ b/conformance/canonical.ts @@ -0,0 +1,149 @@ +/** + * Deterministic serialization of the canonical model, and a structural differ over it. + * + * Both parsers produce `ParseResult`s (Harper's via the adapter). Comparing them needs a + * form that is stable byte-for-byte across runs — object key order is not — and a diff + * that names *where* two results disagree, because the classification rules key off the + * shape of the disagreement rather than off the query string. + */ + +import type { ParseResult, Value } from '../src/types.ts'; + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +/** + * Outcome of running one query string through one parser. A successful parse is carried as + * its canonical JSON view rather than as a `ParseResult`, so that an outcome crossing the + * worker IPC boundary is the same value the comparison and the report work on. + */ +export type Outcome = + | { status: 'parsed'; canonical: Json } + /** Rejected at parse — the canonical behavior for a syntax violation (§6.1). */ + | { status: 'rejected'; error: string } + /** Rejected, but only into the pipeline: Harper's deferred mode (§6.1, ledger row 8). */ + | { status: 'deferred-error'; error: string; canonical: Json } + /** The parse did not finish inside its wall-clock budget. */ + | { status: 'timeout'; ms: number } + /** The parse produced a shape the adapter cannot place in the canonical model. */ + | { status: 'adapter-gap'; error: string } + /** The parser process itself failed (crash, unusable output). */ + | { status: 'harness-error'; error: string }; + +/** Whether an outcome means "this query was not accepted", however that was signalled. */ +export function isRejection(outcome: Outcome): boolean { + return outcome.status === 'rejected' || outcome.status === 'deferred-error'; +} + +function canonicalValue(value: Value | undefined): Json { + if (value === undefined) return { $absent: true }; + if (value === null) return null; + if (value instanceof Date) + return Number.isNaN(value.getTime()) ? { $date: 'invalid' } : { $date: value.toISOString() }; + if (Array.isArray(value)) return value.map(canonicalValue); + if (typeof value === 'number' && !Number.isFinite(value)) return { $number: String(value) }; + return value; +} + +/** + * Canonical JSON view of a ParseResult. Absent optional members stay absent (they are not + * the same as present-and-empty), and values keep their type so `"3"` never reads as `3`. + */ +export function canonicalize(result: ParseResult): Json { + const out: { [key: string]: Json } = {}; + if (result.filter !== undefined) out.filter = canonicalTerm(result.filter); + if (result.sort !== undefined) out.sort = result.sort.map((key) => ({ path: key.path, direction: key.direction })); + if (result.select !== undefined) out.select = canonicalProjection(result.select); + if (result.limit !== undefined) out.limit = canonicalValue(result.limit as Value); + if (result.offset !== undefined) out.offset = canonicalValue(result.offset as Value); + return out; +} + +function canonicalProjection(projection: NonNullable): Json { + return { + mode: projection.mode, + fields: projection.fields.map((field) => { + const out: { [key: string]: Json } = { path: field.path }; + if (field.projection) out.projection = canonicalProjection(field.projection); + return out; + }), + }; +} + +type Term = NonNullable['terms'][number] | NonNullable; + +function canonicalTerm(term: Term): Json { + if ('terms' in term) return { kind: 'group', operator: term.operator, terms: term.terms.map(canonicalTerm) }; + if ('some' in term) { + const out: { [key: string]: Json } = { kind: 'elementMatch', path: term.path }; + if (term.negated) out.negated = true; + out.some = canonicalTerm(term.some); + return out; + } + const out: { [key: string]: Json } = { kind: 'condition', path: term.path, comparator: term.comparator }; + if (term.negated) out.negated = true; + out.value = canonicalValue(term.value); + return out; +} + +/** JSON with object keys emitted in sorted order, so equal structures produce equal bytes. */ +export function stableStringify(value: Json, indent = 0): string { + const render = (node: Json, depth: number): string => { + const pad = indent ? '\n' + ' '.repeat(indent * (depth + 1)) : ''; + const closePad = indent ? '\n' + ' '.repeat(indent * depth) : ''; + if (node === null || typeof node !== 'object') return JSON.stringify(node); + if (Array.isArray(node)) { + if (node.length === 0) return '[]'; + return '[' + node.map((item) => pad + render(item, depth + 1)).join(',') + closePad + ']'; + } + const keys = Object.keys(node).sort(); + if (keys.length === 0) return '{}'; + return '{' + keys.map((key) => pad + JSON.stringify(key) + ':' + (indent ? ' ' : '') + render(node[key], depth + 1)).join(',') + closePad + '}'; + }; + return render(value, 0); +} + +export type Difference = { + /** JSON-pointer-ish location, e.g. `/filter/terms/0/value`. */ + at: string; + kind: 'value' | 'type' | 'ref-only' | 'harper-only'; + ref?: Json; + harper?: Json; +}; + +const typeOf = (node: Json): string => (node === null ? 'null' : Array.isArray(node) ? 'array' : typeof node); + +/** Structural difference list between the reference result and the adapted Harper result. */ +export function diffCanonical(ref: Json, harper: Json): Difference[] { + const differences: Difference[] = []; + walk('', ref, harper, differences); + return differences; +} + +function walk(at: string, ref: Json, harper: Json, out: Difference[]): void { + if (typeOf(ref) !== typeOf(harper)) { + out.push({ at, kind: 'type', ref, harper }); + return; + } + if (ref === null || typeof ref !== 'object') { + if (ref !== harper) out.push({ at, kind: 'value', ref, harper }); + return; + } + if (Array.isArray(ref) && Array.isArray(harper)) { + const length = Math.max(ref.length, harper.length); + for (let index = 0; index < length; index++) { + const location = `${at}/${index}`; + if (index >= harper.length) out.push({ at: location, kind: 'ref-only', ref: ref[index] }); + else if (index >= ref.length) out.push({ at: location, kind: 'harper-only', harper: harper[index] }); + else walk(location, ref[index], harper[index], out); + } + return; + } + const refObject = ref as { [key: string]: Json }; + const harperObject = harper as { [key: string]: Json }; + for (const key of [...new Set([...Object.keys(refObject), ...Object.keys(harperObject)])].sort()) { + const location = `${at}/${key}`; + if (!(key in harperObject)) out.push({ at: location, kind: 'ref-only', ref: refObject[key] }); + else if (!(key in refObject)) out.push({ at: location, kind: 'harper-only', harper: harperObject[key] }); + else walk(location, refObject[key], harperObject[key], out); + } +} diff --git a/conformance/classify.ts b/conformance/classify.ts new file mode 100644 index 0000000..416a5a3 --- /dev/null +++ b/conformance/classify.ts @@ -0,0 +1,572 @@ +/** + * Divergence classification. + * + * Every disagreement between Harper and the reference parser has to land in exactly one of + * three buckets: a row of Harper's divergence ledger (spec Appendix D links to it), a NEW + * divergence the ledger does not carry yet, or a bug in the reference parser. Nothing may + * fall through — an unmatched disagreement is reported as `unclassified` and fails the run, + * which is what keeps the report honest as the corpus grows. + * + * Two mechanisms do the work: + * + * 1. **A residual filter.** Harper's interpreted-mode values are never typed (§5.2.2), so + * that one divergence shows up as noise on top of many others. Differences it explains + * are removed first and recorded as `alsoUninterpretedValues`, and the remaining + * differences are what the rules match on. A case with nothing left over is classified + * as that divergence alone. + * + * 2. **Ordered rules.** Each rule matches either the *shape* of the residual disagreement + * (so a newly added corpus case with the same root cause is classified without being + * hand-labelled) or an explicit list of witness queries where the shape is not + * distinctive enough to be worth guessing at. The first matching rule wins. + * + * Where BOTH parsers deviate from the specification, the verdict names the side that has to + * change for them to agree *conformantly* — normally Harper — and the rationale records the + * reference parser's own gap so it is not lost. + */ + +import type { Case } from './corpus.ts'; +import type { Difference, Json, Outcome } from './canonical.ts'; +import { isRejection } from './canonical.ts'; + +export type Proposal = { + /** A proposed ledger row, in the columns Harper's ledger issue uses. */ + divergence: string; + class: 'bug' | 'feature gap' | 'permitted' | 'tolerance' | 'divergence'; + action: string; +}; + +export type Classification = + | { verdict: 'agrees' } + | { verdict: 'ledger'; rule: string; row: number; rationale: string } + | { verdict: 'new'; rule: string; rationale: string; proposal: Proposal } + | { verdict: 'reference-bug'; rule: string; rationale: string } + | { verdict: 'unclassified' }; + +export type Comparison = { + case: Case; + ref: Outcome; + harper: Outcome; + /** Differences left after the interpreted-value filter; empty when the two agree. */ + differences: Difference[]; + /** True when Harper answered from its non-parsing fast path (ledger row 1). */ + harperFastPath: boolean; + /** True when some differences were explained by `value-not-interpreted` and removed. */ + alsoUninterpretedValues?: boolean; +}; + +type Rule = { + id: string; + /** Witness queries this rule applies to; omit to match on shape alone. */ + queries?: readonly string[]; + when?: (comparison: Comparison) => boolean; + classify: () => Omit, 'rule'>; +}; + +// ── shape predicates ──────────────────────────────────────────────────────── + +const isValuePointer = (at: string): boolean => at === '/value' || at.endsWith('/value') || at.includes('/value/'); + +/** True when `harper` is the plain decimal/boolean/null *spelling* of `ref`'s typed literal. */ +export function isUninterpretedSpelling(ref: Json | undefined, harper: Json | undefined): boolean { + if (typeof harper !== 'string') return false; + if (ref === null) return harper === 'null'; + if (typeof ref === 'number' || typeof ref === 'boolean') return String(ref) === harper; + return false; +} + +const explainedByValueMode = (difference: Difference): boolean => + isValuePointer(difference.at) && + (difference.kind === 'value' || difference.kind === 'type') && + isUninterpretedSpelling(difference.ref, difference.harper); + +const everyDifference = (comparison: Comparison, predicate: (difference: Difference) => boolean): boolean => + comparison.differences.length > 0 && comparison.differences.every(predicate); + +const bothParsed = (comparison: Comparison): boolean => + comparison.ref.status === 'parsed' && comparison.harper.status === 'parsed'; + +const harperRejectsOnly = (comparison: Comparison): boolean => + comparison.ref.status === 'parsed' && isRejection(comparison.harper); + +const refRejectsOnly = (comparison: Comparison): boolean => + comparison.harper.status === 'parsed' && isRejection(comparison.ref); + +const hasFeature = (comparison: Comparison, feature: string): boolean => comparison.case.features.includes(feature); +const hasLedgerTag = (comparison: Comparison, row: number): boolean => (comparison.case.ledger ?? []).includes(row); +const refErrorIs = (comparison: Comparison, name: string): boolean => + isRejection(comparison.ref) && (comparison.ref as { error: string }).error.startsWith(name); + +// ── the rules, in order ───────────────────────────────────────────────────── + +const RULES: readonly Rule[] = [ + { + id: 'reference-uri-error-not-a-client-error', + when: (comparison) => refErrorIs(comparison, 'URIError'), + classify: () => ({ + verdict: 'reference-bug', + rationale: + 'The reference parser calls `decodeURIComponent` on the raw token and lets the resulting `URIError` escape. §4.2 makes percent-decoding part of parsing and §6.1 makes a structural violation a client error, so a malformed escape must surface as a `SyntaxViolation` (HTTP 400), not as a bare `URIError` with no status. Harper answers from its fast path here and leaves the malformed escape in place (ledger row 1).', + }), + }, + { + id: 'harper-fast-path-form-decoding', + when: (comparison) => + comparison.harperFastPath && + everyDifference( + comparison, + (difference) => + isValuePointer(difference.at) && + typeof difference.ref === 'string' && + typeof difference.harper === 'string' && + difference.ref.includes('+') && + difference.ref.replace(/\+/g, ' ') === difference.harper + ), + classify: () => ({ + verdict: 'new', + rationale: + 'The fast path builds a `URLSearchParams`, which decodes `+` as a space (HTML form encoding). §4.2 defines decoding as percent-decoding per token, where `+` is a literal. Harper therefore reads `a=1+2` two different ways depending on whether anything else in the query forced the parsing path — `"1 2"` on the fast path, `"1+2"` on the parsing path.', + proposal: { + divergence: 'The fast path decodes values with HTML form rules, so a raw `+` becomes a space — and means something different once any structural character forces the parsing path', + class: 'bug', + action: 'decode fast-path names/values with `decodeURIComponent` (§4.2), or route every query through the parser', + }, + }), + }, + { + id: 'ledger-1-fast-path-skips-parsing', + when: (comparison) => comparison.harperFastPath && (refRejectsOnly(comparison) || comparison.differences.length > 0), + classify: () => ({ + verdict: 'ledger', + row: 1, + rationale: + 'The query contains none of the characters that trigger Harper’s parser, so it is never parsed: `URLSearchParams` splits it into name/value pairs and any structure in it becomes literal text. That makes Harper accept strings §4 rejects (a bare name, a value containing `=` or `,`, `{`/`}`). Where the reference parser also accepts such a string, it does so by silently DROPPING the malformed term rather than rejecting it, which is a gap of its own.', + }), + }, + { + id: 'ledger-13-no-not-expression', + when: (comparison) => hasFeature(comparison, 'not-expr') && !bothParsed(comparison), + classify: () => ({ + verdict: 'ledger', + row: 13, + rationale: + '`not` is read as a call-function name, and it is not in Harper’s call namespace, so the query is rejected instead of desugaring to leaf/scope negation (§5.4).', + }), + }, + { + id: 'ledger-14-element-conditions', + when: (comparison) => hasFeature(comparison, 'elem-cond') && harperRejectsOnly(comparison), + classify: () => ({ + verdict: 'ledger', + row: 14, + rationale: + 'An element condition (§5.3) has an empty relative path. Harper requires an attribute before every comparator, so `prop[=ge=10]` is rejected — it supports only the `prop[cond&cond]` half of scoped matching.', + }), + }, + { + id: 'ledger-2-nameless-chain-leg', + queries: ['ratings=ge=3&=5'], + classify: () => ({ + verdict: 'ledger', + row: 2, + rationale: + 'Harper lets a nameless chain leg inherit the preceding comparator. §4’s `chained-cond` requires a comparator name on every leg, so the reference parser rejects it; the ledger already marks this for deprecation.', + }), + }, + { + id: 'ledger-6-lenient-value-scan', + when: (comparison) => hasLedgerTag(comparison, 6), + classify: () => ({ + verdict: 'ledger', + row: 6, + rationale: + '§4.1 lets a consumer scan a value leniently and take `(`, `)`, `<`, `>` and `!` as literal characters. Harper does; the reference parser does so only after an interpreting comparator. Both readings are conformant — this is a tolerance, not a semantic difference — and producers must percent-encode these characters regardless.', + }), + }, + { + id: 'ledger-7-repeated-array-parameter', + when: (comparison) => hasLedgerTag(comparison, 7), + classify: () => ({ + verdict: 'ledger', + row: 7, + rationale: + '`prop[]=v` is a host-framework accommodation (Appendix B), not RQL grammar: Harper turns it into a membership condition, while the reference parser reads `[` structurally and rejects the empty scope body.', + }), + }, + { + id: 'reference-gap-value-list-inside-a-group', + queries: ['(a=in=(1,2)&b=2)', '[a=in=(1,2)&b=2]', 'x[a=in=(1,2)]'], + classify: () => ({ + verdict: 'reference-bug', + rationale: + 'Known reference-parser gap. Inside a group or scope body the reference parser always tokenizes with the structural pattern, so a `(v1,v2)` value list is read as a nested group and rejected. §5.1.2 puts no such restriction on where a value list may appear; Harper is right here.', + }), + }, + { + id: 'reference-rejects-chaining-after-between', + queries: ['a=between=(1,5)&=ne=3'], + classify: () => ({ + verdict: 'reference-bug', + rationale: + '`between` is a condition in surface terms (`prop=fiql-name=value-list`), so §4’s `chained-cond` — "MUST directly follow a condition or another chained-cond" — permits chaining onto it. The reference parser has already desugared it to an ElementMatch by then and refuses to chain onto a non-Condition. Harper extends the same element scope, which is what §5.3 describes.', + }), + }, + { + id: 'reference-wildcard-on-negated-equality', + queries: ['name!=Jo*'], + classify: () => ({ + verdict: 'reference-bug', + rationale: + '§5.1.2: "The trailing-`*` wildcard applies only to `==`". The reference parser applies it to every non-verbatim equality, so `!=` becomes a negated `starts_with`. Harper keeps the `*` as a literal, which is what the spec says.', + }), + }, + { + id: 'reference-missing-camelcase-alias', + queries: ['price=notEqual=10'], + classify: () => ({ + verdict: 'reference-bug', + rationale: + 'Appendix B’s alias list ends in "…", and the reference parser implements the camelCase forms for the ordered comparators (`lessThan`, `greaterThanEqual`, …) but not `notEqual`, which falls through to the open vocabulary. Harper resolves it to `ne`. Either the reference parser should carry the alias or Appendix B should close the list explicitly.', + }), + }, + { + id: 'reference-accepts-a-leading-conjunction', + queries: ['|', '|a=1'], + classify: () => ({ + verdict: 'reference-bug', + rationale: + '§4’s `query` production has a term before every conjunction, so a leading `|` is a syntax error. The reference parser records the group operator and moves on, accepting the query; Harper rejects it.', + }), + }, + { + id: 'harper-not-prefix-only-for-a-fixed-base-set', + when: (comparison) => + bothParsed(comparison) && + comparison.differences.length > 0 && + comparison.differences.every( + (difference) => + (difference.at.endsWith('/comparator') && + typeof difference.ref === 'string' && + difference.harper === `not_${difference.ref}`) || + (difference.at.endsWith('/negated') && difference.ref === true) + ), + classify: () => ({ + verdict: 'new', + rationale: + '§5.1.1 makes `not_` a uniform prefix over *any* comparator, including open-vocabulary names. Harper’s `resolveComparator` strips it only when the base is one of `in`, `between`, `starts_with`, `ends_with`, `contains`, `equals`, so `not_lt`, `not_ge` and `not_` survive as opaque comparator names that no executor implements — a silent no-match rather than a negation.', + proposal: { + divergence: 'The `not_` prefix is recognized only for a fixed base set, so `not_lt`/`not_le`/`not_gt`/`not_ge` and `not_` are not negations', + class: 'bug', + action: 'strip `not_` from any comparator name and set `negated` (§5.1.1); leave semantic validation of the base name to execution', + }, + }), + }, + { + id: 'harper-includes-is-in-not-contains', + queries: ['price=includes=10'], + classify: () => ({ + verdict: 'new', + rationale: + 'Appendix B desugars `includes` to `contains`; Harper’s `ALTERNATE_COMPARATOR_NAMES` maps it to `in`. The two mean different things — substring containment versus membership in a supplied list — so the same query returns different records.', + proposal: { + divergence: '`includes` is an alias for `in`, but Appendix B assigns it to `contains`', + class: 'divergence', + action: 'decide: realign `includes` with Appendix B, or record it as a Harper-lineage exception and have the spec note the conflict', + }, + }), + }, + { + id: 'harper-missing-out-alias', + queries: ['a=out=(1,2)'], + classify: () => ({ + verdict: 'new', + rationale: + 'Appendix B carries the RQL 1.x `out` alias for `not_in`. Harper does not know the name, so it is left as an opaque comparator and its `(1,2)` argument is never recognized as a value list — the condition becomes an equality against the literal string `"(1,2)"`.', + proposal: { + divergence: 'The RQL 1.x `out` alias is not accepted; the value list is left as the literal string `(v1,v2)`', + class: 'feature gap', + action: 'add `out` → `not_in` to `ALTERNATE_COMPARATOR_NAMES` (Appendix B), or reject unknown comparators at parse', + }, + }), + }, + { + id: 'harper-wildcard-on-fiql-equality', + queries: ['name=eq=Jo*'], + classify: () => ({ + verdict: 'new', + rationale: + '§5.1.2 restricts the trailing-`*` wildcard to the `==` surface form. Harper applies it whenever the raw comparator name is `eq`, so the FIQL spelling `=eq=` also wildcards. Worth confirming with the spec editor rather than only with Harper: `==` and `=eq=` desugar to the same comparator and the same value mode, so the asymmetry may be a spec gap rather than an implementation one.', + proposal: { + divergence: 'The trailing-`*` wildcard is applied to the FIQL form `=eq=` as well as to `==`', + class: 'divergence', + action: 'confirm §5.1.2’s intent for `=eq=`; then either restrict the wildcard to `==` or widen the spec to both spellings', + }, + }), + }, + { + id: 'harper-alias-value-mode-is-always-interpreted', + queries: ['a=equals=null', 'a=equal=null', 'a=not_equal=null'], + classify: () => ({ + verdict: 'new', + rationale: + 'Appendix B desugars `equals` / `equal` / `not_equal` to a VERBATIM value (they are the FIQL spelling of `===` and `!==`). Harper decodes every FIQL value with `typedDecoding`, so the strict comparators still interpret `null` — the one literal `typedDecoding` does convert — and a query asking for the four-character string `null` matches the null value instead.', + proposal: { + divergence: 'The strict aliases `equals`/`equal`/`not_equal` decode their value in interpreted mode, so `=equals=null` matches null rather than the string "null"', + class: 'bug', + action: 'decode the value mode from the comparator (§5.2): verbatim for `=`, `===`, `!==` and their aliases; interpreted otherwise', + }, + }), + }, + { + id: 'harper-malformed-typed-literals-are-coerced', + queries: ['a=eq=number:abc', 'a=eq=number:', 'a=eq=boolean:yes', 'a=eq=date:notadate'], + classify: () => ({ + verdict: 'new', + rationale: + '§5.2.2 makes a malformed typed literal a syntax error (HTTP 400), and §6 requires every value in the model to be well-formed — "no NaN, no invalid timestamps". Harper coerces instead: `number:abc` becomes `NaN`, `number:` becomes `0`, `boolean:yes` becomes `false`, and `date:notadate` becomes an Invalid Date. Each of those silently changes which records match.', + proposal: { + divergence: 'Malformed typed literals are coerced rather than rejected — `number:abc` → NaN, `number:` → 0, `boolean:yes` → false, `date:notadate` → Invalid Date', + class: 'bug', + action: 'reject a malformed typed literal with a 400 (§5.2.2); never admit NaN or an invalid Date into a condition', + }, + }), + }, + { + id: 'harper-between-arguments-unvalidated', + queries: ['a=between=(1,5,9)', 'a=between=1'], + classify: () => ({ + verdict: 'new', + rationale: + 'Appendix B defines `between=(lo,hi)` as exactly two bounds desugaring to `ge` AND `le`. Harper accepts any argument shape: three values produce a three-element list and a bare scalar produces a string, both of which reach execution as a `between` comparator that cannot be evaluated as a range.', + proposal: { + divergence: '`between` accepts anything as its argument — a bare scalar or a list of any length — instead of requiring exactly two bounds', + class: 'bug', + action: 'reject a `between` argument that is not a two-element value list (Appendix B)', + }, + }), + }, + { + id: 'harper-chain-supports-only-one-leg', + queries: ['ratings=ge=3&=le=4&=ne=5'], + classify: () => ({ + verdict: 'new', + rationale: + '`parseBlock` switches to the value pattern (the one that recognizes `&=` and `|=` as single tokens) only while an attribute is pending. After the first chained leg the attribute is the empty string, so the second `&=` is tokenized as `&` followed by `=` and the query is rejected. §5.3 puts no bound on the number of chained legs, and two-sided ranges plus an exclusion (`=ge=3&=le=9&=ne=5`) are the obvious case.', + proposal: { + divergence: 'Only ONE chained leg is supported: a second `&=` / `|=` is tokenized as `&` + `=` and rejected', + class: 'bug', + action: 'keep using the value pattern across chained legs so `prop=ge=1&=le=9&=ne=5` parses (§5.3)', + }, + }), + }, + { + id: 'harper-empty-group-body-accepted', + queries: ['()', '[]', '(a=1&())', 'a[]'], + classify: () => ({ + verdict: 'new', + rationale: + '§4’s `group-body` requires at least one term, so `()`, `[]` and `prop[]` are syntax errors. Harper builds an empty condition group instead, and an empty AND group is vacuously true — a query that should be rejected becomes one that matches everything. (The reference parser accepts these too, but drops the empty group rather than keeping it; it should reject.)', + proposal: { + divergence: 'An empty group or scope body (`()`, `[]`, `prop[]`) is accepted and becomes an empty condition group, which matches every record', + class: 'bug', + action: 'reject an empty `group-body` at parse (§4)', + }, + }), + }, + { + id: 'harper-unbalanced-groups-accepted', + queries: ['((a=1)', '[a=1)', 'a[b=1'], + classify: () => ({ + verdict: 'new', + rationale: + '`parseBlock` reports a missing terminator only for the block that is waiting for one, so a group left open at the end of the string — or closed with the wrong delimiter — parses as if it had been closed. The query silently means something other than what was written.', + proposal: { + divergence: 'Unterminated or mismatched group delimiters (`((a=1)`, `[a=1)`, `a[b=1`) parse as if the group had been closed correctly', + class: 'bug', + action: 'require every opened group/scope to be closed by its own delimiter before the end of the query', + }, + }), + }, + { + id: 'harper-duplicate-call-functions-accepted', + queries: ['select(a)&select(b)', 'limit(1)&limit(2)', 'sort(a)&sort(b)'], + classify: () => ({ + verdict: 'new', + rationale: + '§5.6: "A call function appearing more than once in a query is a syntax error." Harper overwrites the earlier call, so a duplicate silently wins on last-write — which is exactly the shape a request-smuggling or cache-key bug takes when two layers each append a `limit(...)`.', + proposal: { + divergence: 'A repeated call function is accepted, last one wins, instead of being a syntax error', + class: 'bug', + action: 'reject a second occurrence of `select`, `sort` or `limit` in one query (§5.6)', + }, + }), + }, + { + id: 'harper-limit-arguments-unvalidated', + queries: ['limit(x)', 'limit(-1)', 'limit(2,1)', 'limit(1.5)', 'limit(01)'], + classify: () => ({ + verdict: 'new', + rationale: + '§5.6 requires non-negative decimal integers with end ≥ start, and §6 requires `limit`/`offset` to be validated non-negative integers. Harper applies unary `+` and keeps whatever comes out: `limit(x)` becomes `NaN`, `limit(-1)` a negative limit, and `limit(2,1)` an offset of 2 with a limit of −1.', + proposal: { + divergence: '`limit` arguments are coerced with unary `+` and never validated — `limit(x)` → NaN, `limit(-1)` → −1, `limit(2,1)` → offset 2 / limit −1', + class: 'bug', + action: 'validate both arguments as non-negative decimal integers with end ≥ start, and reject otherwise (§5.6)', + }, + }), + }, + { + id: 'harper-sort-direction-marker-read-after-decoding', + queries: ['sort(%2Bname)', 'sort(%2Dname)'], + classify: () => ({ + verdict: 'new', + rationale: + '§4.2 rule 4 recognizes the `+`/`-` sort marker on the RAW token, so `%2B`/`%2D` are literal name characters — `sort(%2Bname)` sorts by the property named `+name`. Harper decodes the token first and then inspects its first character, so the escape is indistinguishable from the marker and a property whose name starts with `+` or `-` cannot be sorted on at all.', + proposal: { + divergence: 'The `+`/`-` sort-direction marker is read AFTER percent-decoding, so `sort(%2Bname)` is treated as ascending `name` rather than the property `+name`', + class: 'bug', + action: 'read the direction marker on the raw token before decoding (§4.2 rule 4)', + }, + }), + }, + { + id: 'harper-trailing-comma-yields-an-empty-key', + queries: ['sort(a,)', 'select(a,b,)'], + classify: () => ({ + verdict: 'new', + rationale: + 'A trailing comma is significant only for `select`, where it forces records mode for a single field (§5.7). Harper pushes the empty token as another argument, producing a sort key on the property named "" and a selected field named "" — neither of which exists.', + proposal: { + divergence: 'A trailing comma in a call adds an empty argument: `sort(a,)` gains a sort key on "" and `select(a,b,)` a field named ""', + class: 'bug', + action: 'drop a trailing empty argument; keep its only meaning, the single-field records form `select(a,)` (§5.7)', + }, + }), + }, + { + id: 'harper-dotted-select-path-collides-with-tuple-form', + queries: ['select(a.b)', 'select(a.b,c)'], + classify: () => ({ + verdict: 'new', + rationale: + 'A dotted select path is decoded into a segment array, which is the same representation Harper uses for the `[a,b]` tuple form — so `select(a.b)` is indistinguishable from `select([a,b])` and is projected as a two-column tuple of the top-level properties `a` and `b` rather than as the nested property `a.b`. §5.7’s `select-item` is a `prop-path`, and §5.5 makes dotted paths nested-property addressing.', + proposal: { + divergence: 'A dotted path in `select` is represented as a segment array and read as the `[a,b]` tuple form, so `select(a.b)` projects two top-level properties instead of the nested one', + class: 'bug', + action: 'give a nested select path a distinct representation from the tuple form (e.g. `{ name: [...] }` vs. an `asArray` list)', + }, + }), + }, + { + id: 'harper-reserved-nested-tuple-accepted', + queries: ['select(rel{[a,b]})'], + classify: () => ({ + verdict: 'new', + rationale: + '§5.7 reserves the nested tuple form: "A nested `[x,y]` tuple form (`rel{[x,y]}`) is reserved and currently a syntax error." Harper accepts it and produces a nested list whose single element is an unnamed array, which no consumer reads as a projection.', + proposal: { + divergence: 'The reserved nested tuple form `rel{[a,b]}` is accepted and produces an unnamed nested array', + class: 'bug', + action: 'reject a `[...]` item inside a `{...}` nested projection until the form is specified (§5.7)', + }, + }), + }, + { + id: 'harper-empty-sort-rejected', + queries: ['sort()'], + classify: () => ({ + verdict: 'new', + rationale: + '§4’s `call` production makes the argument list optional, so `sort()` is grammatical and means "no ordering". Harper reaches `toSortObject` with an empty argument list and reports "Unknown sort type undefined". Low impact, but it is a rejection of a grammatical query; if the intent is that `sort` requires at least one key, §5.6 should say so.', + proposal: { + divergence: '`sort()` with no keys is rejected, though §4 makes a call’s argument list optional', + class: 'divergence', + action: 'accept `sort()` as a no-op, or have §5.6 require at least one sort key', + }, + }), + }, + { + id: 'harper-empty-value-drops-the-condition', + queries: ['a==', 'a!==', 'a>='], + classify: () => ({ + verdict: 'new', + rationale: + '§4’s `plain-value` is `*vchar` — the empty value is legal, and `a==` is a condition against the empty string. Harper’s value pattern requires at least one character, so at the end of the string the pending condition is never built and the whole filter disappears: `?a==` returns every record instead of the ones whose `a` is empty. (`a=` takes the fast path and does produce the condition, so the two spellings disagree inside Harper.)', + proposal: { + divergence: 'A comparator followed by an empty value at end-of-string silently drops the condition, so `?a==` returns everything', + class: 'bug', + action: 'allow an empty value token on the parsing path (§4 `plain-value = *vchar`), matching what the fast path already does for `a=`', + }, + }), + }, + { + id: 'harper-re-anchors-on-a-second-operator', + queries: ['a===1===2'], + classify: () => ({ + verdict: 'new', + rationale: + 'Two comparators in one term are a syntax error (§4 rule 2: a raw `=` never occurs inside a value). Harper takes the second operator as the start of a new condition and builds one on the property named `1`; the reference parser instead keeps the first condition and silently discards the rest, which is its own gap. Neither rejects.', + proposal: { + divergence: 'A second comparator in one term re-anchors the condition (`a===1===2` becomes a condition on the property `1`) instead of being a syntax error', + class: 'bug', + action: 'reject a second comparator within one term (§4 rule 2)', + }, + }), + }, + { + id: 'harper-trailing-conjunction-drops-the-operator', + queries: ['a=1|'], + classify: () => ({ + verdict: 'new', + rationale: + 'A trailing conjunction with no following term is a syntax error under §4. Both parsers accept it; they differ in what the resulting one-term group says: Harper assigns the group operator only when a second condition arrives, so the group reports `and` where the query said `|`. With a single term the operator carries no meaning, but the two representations are not equal.', + proposal: { + divergence: 'A dangling conjunction is accepted, and the group operator it named is dropped (`a=1|` yields an `and` group)', + class: 'bug', + action: 'reject a conjunction with no following term (§4)', + }, + }), + }, +]; + +/** + * Split a difference list into the part explained by Harper's uninterpreted values and the + * part that still needs a rule. + */ +export function partitionDifferences(differences: Difference[]): { residual: Difference[]; valueMode: boolean } { + const residual = differences.filter((difference) => !explainedByValueMode(difference)); + return { residual, valueMode: residual.length !== differences.length }; +} + +const VALUE_MODE_CLASSIFICATION: Extract = { + verdict: 'new', + rule: 'value-not-interpreted', + rationale: + 'Harper’s `typedDecoding` interprets only `null` and the explicit `type:` prefixes, so a bare `3`, `true` or `false` in interpreted position stays the string it was written as. §5.2.2 makes a round-trip decimal numeral a number and `true`/`false` booleans in EVERY interpreted mode — `==`, `!=`, the ordered symbolic operators, and all FIQL named comparators. Downstream, `Table.ts` re-coerces against the column type where a schema attribute is known, which is why this is usually invisible; it stops being invisible for schema-less values, for chained legs (whose attribute is null — ledger row 10) and for any consumer that reads the parsed condition directly.', + proposal: { + divergence: + 'Interpreted-mode value literals are not typed at parse: a bare numeral stays a string and `true`/`false` stay strings (only `null` and the `type:` prefixes are honored)', + class: 'bug', + action: 'apply §5.2.2 in `typedDecoding` — round-trip decimal numerals become numbers, `true`/`false` become booleans', + }, +}; + +export function classify(comparison: Comparison): Classification { + if (isRejection(comparison.ref) && isRejection(comparison.harper)) return { verdict: 'agrees' }; + if (comparison.ref.status === comparison.harper.status && comparison.differences.length === 0 && !comparison.alsoUninterpretedValues) + return { verdict: 'agrees' }; + + for (const rule of RULES) { + if (rule.queries && !rule.queries.includes(comparison.case.query)) continue; + if (rule.when && !rule.when(comparison)) continue; + if (!rule.queries && !rule.when) continue; + return { ...rule.classify(), rule: rule.id } as Classification; + } + + // Nothing structural left over: the whole disagreement is the value-mode divergence. + if (comparison.differences.length === 0 && comparison.alsoUninterpretedValues) return VALUE_MODE_CLASSIFICATION; + + return { verdict: 'unclassified' }; +} + +export const RULE_IDS: readonly string[] = [...RULES.map((rule) => rule.id), VALUE_MODE_CLASSIFICATION.rule]; diff --git a/conformance/compare.ts b/conformance/compare.ts new file mode 100644 index 0000000..e9da633 --- /dev/null +++ b/conformance/compare.ts @@ -0,0 +1,140 @@ +/** + * Turning one recorded Harper outcome plus one reference outcome into a classified + * comparison. Kept out of the CLI so that the end-to-end test builds its comparisons + * through exactly the same code the runner uses, rather than through a second copy of it. + */ + +import type { Case } from './corpus.ts'; +import { CORPUS } from './corpus.ts'; +import type { Outcome } from './canonical.ts'; +import { canonicalize, diffCanonical } from './canonical.ts'; +import { decodeTagged, type Tagged } from './tagged.ts'; +import { AdapterError, adaptHarperResult } from './harperAdapter.ts'; +import { type Classification, type Comparison, classify, partitionDifferences } from './classify.ts'; + +/** One case as the fixture stores it. */ +export type RecordedCase = { + query: string; + timedOut?: boolean; + timeoutMs?: number; + /** `parseQuery(query)` — the throwing entry point. */ + strict?: RecordedOutcome; + /** `new RequestTarget('?' + query)` — the production entry point, which defers errors. */ + target?: RecordedOutcome; +}; + +export type RecordedOutcome = { status: 'ok'; raw: Tagged } | { status: 'error'; name: string; message: string }; + +/** Map one recorded raw Harper result into the canonical model. */ +export function harperOutcome(recorded: RecordedOutcome | undefined, label: string): Outcome { + if (!recorded) return { status: 'harness-error', error: `the fixture has no "${label}" outcome for this case` }; + if (recorded.status === 'error') return { status: 'rejected', error: `${recorded.name}: ${recorded.message}` }; + if (recorded.status !== 'ok') + return { status: 'harness-error', error: `unknown recorded status ${String((recorded as { status: string }).status)}` }; + + try { + const adapted = adaptHarperResult(decodeTagged(recorded.raw)); + return adapted.status === 'parsed' + ? { status: 'parsed', canonical: canonicalize(adapted.result) } + : { status: 'deferred-error', error: adapted.message, canonical: canonicalize(adapted.partial) }; + } catch (error) { + if (error instanceof AdapterError) return { status: 'adapter-gap', error: error.message }; + return { status: 'harness-error', error: error instanceof Error ? error.message : String(error) }; + } +} + +/** + * A recorded `URLSearchParams` (rather than a `Query`) means Harper never parsed the + * query at all — its fast path, ledger row 1. + */ +export function tookFastPath(recorded: RecordedCase): boolean { + const strict = recorded.strict; + return !recorded.timedOut && strict?.status === 'ok' && typeof strict.raw === 'object' && strict.raw !== null && (strict.raw as { $?: string }).$ === 'usp'; +} + +export function buildComparison(item: Case, reference: Outcome, recorded: RecordedCase): Comparison & { classification: Classification } { + const harper: Outcome = recorded.timedOut + ? { status: 'timeout', ms: recorded.timeoutMs ?? 0 } + : harperOutcome(recorded.strict, 'strict'); + + const raw = + reference.status === 'parsed' && harper.status === 'parsed' ? diffCanonical(reference.canonical, harper.canonical) : []; + // Harper's uninterpreted values would otherwise decorate every other divergence; take + // them out first and classify what is left (see classify.ts). + const { residual, valueMode } = partitionDifferences(raw); + + const comparison: Comparison = { + case: item, + ref: reference, + harper, + differences: residual, + harperFastPath: tookFastPath(recorded), + alsoUninterpretedValues: valueMode, + }; + return { ...comparison, classification: classify(comparison) }; +} + +// ── the shape of one whole run ────────────────────────────────────────────── + +export type LedgerRow = { row: number; divergence: string; class: string; action: string }; + +export type Ledger = { + source: { repo: string; issue: number; url: string; title: string }; + fetchedAt: string; + note?: string; + rows: LedgerRow[]; +}; + +export type Provenance = { + harper: { commit: string; describe?: string; dirty?: boolean; version?: string }; + /** The reference-parser revision the fixture was recorded against. */ + reference: { commit: string; describe?: string; dirty?: boolean }; + recordedAt: string; + corpusDigest: string; + node: string; +}; + +export type Fixture = { schema: number; provenance: Provenance; cases: Record }; + +/** A Harper outcome recorded from the deferring production entry point, and its reference peer. */ +export type Observation = { case: Case; harper: Outcome; ref: Outcome }; + +export type RunResult = { + comparisons: (Comparison & { classification: Classification })[]; + deferred: Observation[]; + provenance: Provenance; + ledger: Ledger; +}; + +/** Reference-parser outcomes for one query, in both of §6.1's error modes. */ +export type ReferenceOutcomes = { strict: Outcome; deferred: Outcome }; + +/** + * Assemble a whole run from the recorded fixture plus reference outcomes the caller has + * already obtained (from the worker in the runner, in-process in the tests). + */ +export function buildRun( + fixture: Fixture, + ledger: Ledger, + referenceOf: (item: Case) => ReferenceOutcomes, + cases: readonly Case[] = CORPUS +): RunResult { + const comparisons: RunResult['comparisons'] = []; + const deferred: Observation[] = []; + + for (const item of cases) { + const recorded = fixture.cases[item.id]; + const reference = referenceOf(item); + const comparison = buildComparison(item, reference.strict, recorded); + comparisons.push(comparison); + + // Ledger rows 8 and 9: the production entry point defers errors instead of throwing. + // Record it wherever deferring changes what Harper reports. + if (!recorded.timedOut) { + const target = harperOutcome(recorded.target, 'target'); + if (target.status !== comparison.harper.status) deferred.push({ case: item, harper: target, ref: reference.deferred }); + } + } + + return { comparisons, deferred, provenance: fixture.provenance, ledger }; +} diff --git a/conformance/conformance-report.md b/conformance/conformance-report.md new file mode 100644 index 0000000..ba9f766 --- /dev/null +++ b/conformance/conformance-report.md @@ -0,0 +1,2528 @@ +# RQL 2.0 differential conformance report + +Generated by `npm run conformance` — do not edit by hand. + +Every corpus query is parsed by both Harper’s REST query parser and the RQL 2.0 +reference parser, Harper’s output is mapped to the canonical model (§6) by +`conformance/harperAdapter.ts`, and the two canonical results are compared. Every +disagreement is classified below; an unclassified disagreement fails the run. + +### How to read this + +- A disagreement is classified by RULE, not case by case — the rules live in + `conformance/classify.ts` and each names the witnesses it covers. +- The verdict names **the side that has to change** for the two parsers to agree + *conformantly*. Where both deviate from the spec, the rationale says so. +- Harper’s uninterpreted values (§5.2.2) are stripped from a case’s difference list + before the other rules see it, so one pervasive divergence does not mask the rest. + A case where they were stripped is marked "+ uninterpreted values". +- Scope: this compares PARSE results only. Value coercion against a table’s declared + attribute types, comparator evaluation and result materialization all happen later, + in `resources/Table.ts`, and no case here can witness them. + +## Provenance + +| | | +|---|---| +| Harper commit | `11a1c489168dfb00c6a66472cef14cb3f875286f` (v5.2.2-14-g11a1c4891) | +| Harper version | `5.2.2` | +| Reference parser commit | `ab665de98490b53dbcc017857a8e1320d67c704b` (v0.3.3-23-gab665de) **+ uncommitted changes** | +| Fixture recorded | 2026-09-01T23:38:56.709Z | +| Recorded on Node | `v26.2.0` | +| Corpus digest | `349-6b64d3e9` | +| Ledger snapshot | [HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440), 14 rows, fetched 2026-09-01T23:16:18.888Z | +| Snapshot vs fixture | snapshot fetched the same day as the fixture recording | + +The ledger snapshot is a cache. [HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440) stays canonical — +refresh with `npm run conformance:refresh-ledger` before trusting a classification. + +## Totals + +| Outcome | Cases | +|---|---:| +| Corpus size | 349 | +| Agree (identical canonical result, or both reject) | 190 | +| **Diverge** | **159** | +| — classified to a ledger row | 33 | +| — NEW divergence (proposed rows below) | 117 | +| — reference-parser bug | 9 | +| — **unclassified (must be 0)** | **0** | + +| Reference outcome / Harper outcome | Cases | +|---|---:| +| parsed / parsed | 256 | +| rejected / parsed | 40 | +| rejected / rejected | 35 | +| parsed / rejected | 18 | + +## Grammar feature coverage + +Each corpus case carries the grammar features it exercises; every feature of §4–§5.7 +is required to have at least one case. + +| Feature | Cases | Agree | Diverge | +|---|---:|---:|---:| +| `adversarial` | 83 | 40 | 43 | +| `call-error` | 9 | 6 | 3 | +| `chain` | 15 | 3 | 12 | +| `dotted-path` | 12 | 4 | 8 | +| `elem-cond` | 6 | 0 | 6 | +| `fiql-alias` | 28 | 5 | 23 | +| `fiql-core` | 12 | 1 | 11 | +| `form-encoding` | 25 | 20 | 5 | +| `grouping-bracket` | 7 | 4 | 3 | +| `grouping-nested` | 8 | 7 | 1 | +| `grouping-paren` | 5 | 4 | 1 | +| `limit` | 14 | 9 | 5 | +| `not-expr` | 11 | 2 | 9 | +| `not-prefix` | 19 | 10 | 9 | +| `pct-encoded-path` | 14 | 6 | 8 | +| `pct-encoded-value` | 16 | 12 | 4 | +| `scoped-match` | 14 | 8 | 6 | +| `select` | 24 | 20 | 4 | +| `sort` | 16 | 12 | 4 | +| `symbol-op` | 55 | 36 | 19 | +| `tolerance` | 12 | 5 | 7 | +| `typed-value` | 32 | 20 | 12 | +| `value-list` | 18 | 6 | 12 | +| `wildcard` | 10 | 8 | 2 | + +## Divergence-ledger coverage + +Rows of [HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440), and the corpus cases that witness them. +A row marked _execution-level_ describes behavior after parsing, which a parser-only +harness cannot observe. + +| Row | Class | Divergence | Witnesses | Observed | +|---:|---|---|---:|---| +| 1 | permitted | Simple queries (no structural chars) skip parsing; conditions surface as raw name/value p… | 24 | confirmed — 11 diverging case(s) | +| 2 | representational + quirk | Chains attach as `chainedConditions`; nameless legs (`a=ge=1&=5`) inherit the previous co… | 9 | confirmed — 1 diverging case(s) | +| 3 | permitted | Strict/coercive modeled as distinct comparators (`equals`/`not_equal` vs `eq`/`ne`) | 39 | witnessed; the difference observed is classified as `harper-alias-value-mode-is-always-interpreted`, `value-not-interpreted`, `reference-missing-camelcase-alias`, `harper-includes-is-in-not-contains` | +| 4 | permitted | `between` is a first-class comparator | 6 | witnessed; the difference observed is classified as `value-not-interpreted`, `harper-between-arguments-unvalidated`, `reference-rejects-chaining-after-between` | +| 5 | permitted | Sort is a linked list; select is a polymorphic array with marker props | 24 | witnessed; the difference observed is classified as `harper-trailing-comma-yields-an-empty-key`, `harper-dotted-select-path-collides-with-tuple-form` | +| 6 | tolerance | `(4)` on a non-list comparator is the literal string `"(4)"` | 9 | confirmed — 4 diverging case(s) | +| 7 | tolerance | `prop[]=v` repeated array params | 3 | confirmed — 3 diverging case(s) | +| 8 | permitted | Parse errors deferred into the pipeline (`parseError`) | 4 | confirmed in deferred-error mode — see below | +| 9 | bug | `group-by(...)` accepted at parse and falls through into `sort` (missing `break`) | 1 | confirmed in deferred-error mode — see below | +| 10 | bug | Chained legs' values never type-coerced on the REST path → mixed-type ranges, silent supe… | 1 | witnessed; the difference observed is classified as `value-not-interpreted` | +| 11 | bug | Indexed `elements` attributes return one result per matching element (duplicates) | 0 | _execution-level_ — duplicate results from indexed `elements` attributes are produced by query execution, not by parsing | +| 12 | divergence | `contains` matches numbers via decimal-string coercion | 0 | _execution-level_ — `contains` coercing a number to its decimal string happens inside the comparator, at execution | +| 13 | feature gap | No `not(...)` group/scope negation | 11 | confirmed — 9 diverging case(s) | +| 14 | partial | Element conditions (`prop[=ge=10\|=le=2]`) and scoped sub-queries per §5.3 surface | 9 | confirmed — 5 diverging case(s) | + +## NEW divergences + +Not yet in Harper’s ledger. Each block proposes a row to add — the spec is not edited here. + +### harper-fast-path-form-decoding — 1 case(s) + +The fast path builds a `URLSearchParams`, which decodes `+` as a space (HTML form encoding). §4.2 defines decoding as percent-decoding per token, where `+` is a literal. Harper therefore reads `a=1+2` two different ways depending on whether anything else in the query forced the parsing path — `"1 2"` on the fast path, `"1+2"` on the parsing path. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The fast path decodes values with HTML form rules, so a raw `+` becomes a space — and means something different once any structural character forces the parsing path | bug | decode fast-path names/values with `decodeURIComponent` (§4.2), or route every query through the parser | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=1+2` | parsed | parsed | + +
Worked example: `a=1+2` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1+2" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1 2" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/value` | value | `"1+2"` | `"1 2"` | + +
+ +### value-not-interpreted — 70 case(s) + +Harper’s `typedDecoding` interprets only `null` and the explicit `type:` prefixes, so a bare `3`, `true` or `false` in interpreted position stays the string it was written as. §5.2.2 makes a round-trip decimal numeral a number and `true`/`false` booleans in EVERY interpreted mode — `==`, `!=`, the ordered symbolic operators, and all FIQL named comparators. Downstream, `Table.ts` re-coerces against the column type where a schema attribute is known, which is why this is usually invisible; it stops being invisible for schema-less values, for chained legs (whose attribute is null — ledger row 10) and for any consumer that reads the parsed condition directly. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| Interpreted-mode value literals are not typed at parse: a bare numeral stays a string and `true`/`false` stay strings (only `null` and the `type:` prefixes are honored) | bug | apply §5.2.2 in `typedDecoding` — round-trip decimal numerals become numbers, `true`/`false` become booleans | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=1+2&b<3` | parsed | parsed + uninterpreted values | +| `a=in=(1,2)` | parsed | parsed + uninterpreted values | +| `a=in=(1)` | parsed | parsed + uninterpreted values | +| `a=in=(1,2,3,4,5)` | parsed | parsed + uninterpreted values | +| `a=not_in=(1,2)` | parsed | parsed + uninterpreted values | +| `a=between=(1,5)` | parsed | parsed + uninterpreted values | +| `a=not_between=(1,5)` | parsed | parsed + uninterpreted values | +| `a=ge=1&=le=5` | parsed | parsed + uninterpreted values | +| `a==3` | parsed | parsed + uninterpreted values | +| `a==-5` | parsed | parsed + uninterpreted values | +| `a==2.5` | parsed | parsed + uninterpreted values | +| `a==true` | parsed | parsed + uninterpreted values | +| `a==false` | parsed | parsed + uninterpreted values | +| `a.b.c==1` | parsed | parsed + uninterpreted values | +| `a.b.c.d.e==1` | parsed | parsed + uninterpreted values | +| `a%2Eb==1` | parsed | parsed + uninterpreted values | +| `a%2Eb.c==1` | parsed | parsed + uninterpreted values | +| `a.b%2Ec==1` | parsed | parsed + uninterpreted values | +| `%2E==1` | parsed | parsed + uninterpreted values | +| `a-b_c~d==1` | parsed | parsed + uninterpreted values | +| `a%2Db==1` | parsed | parsed + uninterpreted values | +| `ratings=ge=3&=le=4` | parsed | parsed + uninterpreted values | +| `ratings=ge=3&ratings=le=4` | parsed | parsed + uninterpreted values | +| `ratings=ge=3\|=le=4` | parsed | parsed + uninterpreted values | +| `ratings=ge=3&=not_eq=4` | parsed | parsed + uninterpreted values | +| `ratings=ge=3&=le=4&b=2` | parsed | parsed + uninterpreted values | +| `a==1&=le=5` | parsed | parsed + uninterpreted values | +| `a=1&=le=5` | parsed | parsed + uninterpreted values | +| `a=in=(1,2)&=le=4` | parsed | parsed + uninterpreted values | +| `reviews[rating=ge=4&helpful=ge=10]` | parsed | parsed + uninterpreted values | +| `reviews[rating=ge=4\|helpful=ge=10]` | parsed | parsed + uninterpreted values | +| `a=in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,2…` | parsed | parsed + uninterpreted values | +| `price==5` | parsed | parsed + uninterpreted values | +| `price==true` | parsed | parsed + uninterpreted values | +| `price==2.5` | parsed | parsed + uninterpreted values | +| `price!=5` | parsed | parsed + uninterpreted values | +| `price!=true` | parsed | parsed + uninterpreted values | +| `price!=2.5` | parsed | parsed + uninterpreted values | +| `price<5` | parsed | parsed + uninterpreted values | +| `price5` | parsed | parsed + uninterpreted values | +| `price>true` | parsed | parsed + uninterpreted values | +| `price>2.5` | parsed | parsed + uninterpreted values | +| `price>=5` | parsed | parsed + uninterpreted values | +| `price>=true` | parsed | parsed + uninterpreted values | +| `price>=2.5` | parsed | parsed + uninterpreted values | +| `price=eq=10` | parsed | parsed + uninterpreted values | +| `price=lt=10` | parsed | parsed + uninterpreted values | +| `price=le=10` | parsed | parsed + uninterpreted values | +| `price=gt=10` | parsed | parsed + uninterpreted values | +| `price=ge=10` | parsed | parsed + uninterpreted values | +| `price=contains=10` | parsed | parsed + uninterpreted values | +| `price=starts_with=10` | parsed | parsed + uninterpreted values | +| `price=ends_with=10` | parsed | parsed + uninterpreted values | +| `price=ne=10` | parsed | parsed + uninterpreted values | +| `price=sw=10` | parsed | parsed + uninterpreted values | +| `price=ew=10` | parsed | parsed + uninterpreted values | +| `price=ct=10` | parsed | parsed + uninterpreted values | +| `price=less_than=10` | parsed | parsed + uninterpreted values | +| `price=lessThan=10` | parsed | parsed + uninterpreted values | +| `price=less_than_equal=10` | parsed | parsed + uninterpreted values | +| `price=lessThanEqual=10` | parsed | parsed + uninterpreted values | +| `price=greater_than=10` | parsed | parsed + uninterpreted values | +| `price=greaterThan=10` | parsed | parsed + uninterpreted values | +| `price=greater_than_equal=10` | parsed | parsed + uninterpreted values | +| `price=greaterThanEqual=10` | parsed | parsed + uninterpreted values | + +
Worked example: `a=1+2&b<3` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1+2" + }, + { + "comparator": "lt", + "kind": "condition", + "path": [ + "b" + ], + "value": 3 + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1+2" + }, + { + "comparator": "lt", + "kind": "condition", + "path": [ + "b" + ], + "value": "3" + } + ] + } +} +``` + +
+ +### harper-wildcard-on-fiql-equality — 1 case(s) + +§5.1.2 restricts the trailing-`*` wildcard to the `==` surface form. Harper applies it whenever the raw comparator name is `eq`, so the FIQL spelling `=eq=` also wildcards. Worth confirming with the spec editor rather than only with Harper: `==` and `=eq=` desugar to the same comparator and the same value mode, so the asymmetry may be a spec gap rather than an implementation one. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The trailing-`*` wildcard is applied to the FIQL form `=eq=` as well as to `==` | divergence | confirm §5.1.2’s intent for `=eq=`; then either restrict the wildcard to `==` or widen the spec to both spellings | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `name=eq=Jo*` | parsed | parsed | + +
Worked example: `name=eq=Jo*` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "name" + ], + "value": "Jo*" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "starts_with", + "kind": "condition", + "path": [ + "name" + ], + "value": "Jo" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/comparator` | value | `"eq"` | `"starts_with"` | +| `/filter/terms/0/value` | value | `"Jo*"` | `"Jo"` | + +
+ +### harper-missing-out-alias — 1 case(s) + +Appendix B carries the RQL 1.x `out` alias for `not_in`. Harper does not know the name, so it is left as an opaque comparator and its `(1,2)` argument is never recognized as a value list — the condition becomes an equality against the literal string `"(1,2)"`. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The RQL 1.x `out` alias is not accepted; the value list is left as the literal string `(v1,v2)` | feature gap | add `out` → `not_in` to `ALTERNATE_COMPARATOR_NAMES` (Appendix B), or reject unknown comparators at parse | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=out=(1,2)` | parsed | parsed | + +
Worked example: `a=out=(1,2)` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "in", + "kind": "condition", + "negated": true, + "path": [ + "a" + ], + "value": [ + 1, + 2 + ] + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "out", + "kind": "condition", + "path": [ + "a" + ], + "value": "(1,2)" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/comparator` | value | `"in"` | `"out"` | +| `/filter/terms/0/negated` | ref-only | `true` | — | +| `/filter/terms/0/value` | type | `[1,2]` | `"(1,2)"` | + +
+ +### harper-between-arguments-unvalidated — 2 case(s) + +Appendix B defines `between=(lo,hi)` as exactly two bounds desugaring to `ge` AND `le`. Harper accepts any argument shape: three values produce a three-element list and a bare scalar produces a string, both of which reach execution as a `between` comparator that cannot be evaluated as a range. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| `between` accepts anything as its argument — a bare scalar or a list of any length — instead of requiring exactly two bounds | bug | reject a `between` argument that is not a two-element value list (Appendix B) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=between=(1,5,9)` | rejected | parsed | +| `a=between=1` | rejected | parsed | + +
Worked example: `a=between=(1,5,9)` + +Reference: + +> SyntaxViolation: between requires exactly two values + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "between", + "kind": "condition", + "path": [ + "a" + ], + "value": [ + "1", + "5", + "9" + ] + } + ] + } +} +``` + +
+ +### harper-malformed-typed-literals-are-coerced — 4 case(s) + +§5.2.2 makes a malformed typed literal a syntax error (HTTP 400), and §6 requires every value in the model to be well-formed — "no NaN, no invalid timestamps". Harper coerces instead: `number:abc` becomes `NaN`, `number:` becomes `0`, `boolean:yes` becomes `false`, and `date:notadate` becomes an Invalid Date. Each of those silently changes which records match. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| Malformed typed literals are coerced rather than rejected — `number:abc` → NaN, `number:` → 0, `boolean:yes` → false, `date:notadate` → Invalid Date | bug | reject a malformed typed literal with a 400 (§5.2.2); never admit NaN or an invalid Date into a condition | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=eq=number:abc` | rejected | parsed | +| `a=eq=number:` | rejected | parsed | +| `a=eq=boolean:yes` | rejected | parsed | +| `a=eq=date:notadate` | rejected | parsed | + +
Worked example: `a=eq=number:abc` + +Reference: + +> QueryError: malformed number literal 'number:abc' + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": { + "$number": "NaN" + } + } + ] + } +} +``` + +
+ +### harper-alias-value-mode-is-always-interpreted — 3 case(s) + +Appendix B desugars `equals` / `equal` / `not_equal` to a VERBATIM value (they are the FIQL spelling of `===` and `!==`). Harper decodes every FIQL value with `typedDecoding`, so the strict comparators still interpret `null` — the one literal `typedDecoding` does convert — and a query asking for the four-character string `null` matches the null value instead. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The strict aliases `equals`/`equal`/`not_equal` decode their value in interpreted mode, so `=equals=null` matches null rather than the string "null" | bug | decode the value mode from the comparator (§5.2): verbatim for `=`, `===`, `!==` and their aliases; interpreted otherwise | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=equals=null` | parsed | parsed | +| `a=equal=null` | parsed | parsed | +| `a=not_equal=null` | parsed | parsed | + +
Worked example: `a=equals=null` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "null" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": null + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/value` | type | `"null"` | `null` | + +
+ +### harper-empty-group-body-accepted — 4 case(s) + +§4’s `group-body` requires at least one term, so `()`, `[]` and `prop[]` are syntax errors. Harper builds an empty condition group instead, and an empty AND group is vacuously true — a query that should be rejected becomes one that matches everything. (The reference parser accepts these too, but drops the empty group rather than keeping it; it should reject.) + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| An empty group or scope body (`()`, `[]`, `prop[]`) is accepted and becomes an empty condition group, which matches every record | bug | reject an empty `group-body` at parse (§4) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `()` | parsed | parsed | +| `[]` | parsed | parsed | +| `(a=1&())` | parsed | parsed | +| `a[]` | rejected | parsed | + +
Worked example: `()` + +Reference: + +```json +{} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "group", + "operator": "and", + "terms": [] + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter` | harper-only | — | `{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","terms":[]}]}` | + +
+ +### harper-chain-supports-only-one-leg — 1 case(s) + +`parseBlock` switches to the value pattern (the one that recognizes `&=` and `|=` as single tokens) only while an attribute is pending. After the first chained leg the attribute is the empty string, so the second `&=` is tokenized as `&` followed by `=` and the query is rejected. §5.3 puts no bound on the number of chained legs, and two-sided ranges plus an exclusion (`=ge=3&=le=9&=ne=5`) are the obvious case. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| Only ONE chained leg is supported: a second `&=` / `\|=` is tokenized as `&` + `=` and rejected | bug | keep using the value pattern across chained legs so `prop=ge=1&=le=9&=ne=5` parses (§5.3) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `ratings=ge=3&=le=4&=ne=5` | parsed | rejected | + +
Worked example: `ratings=ge=3&=le=4&=ne=5` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "elementMatch", + "path": [ + "ratings" + ], + "some": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "ge", + "kind": "condition", + "path": [], + "value": 3 + }, + { + "comparator": "le", + "kind": "condition", + "path": [], + "value": 4 + }, + { + "comparator": "eq", + "kind": "condition", + "negated": true, + "path": [], + "value": 5 + } + ] + } + } + ] + } +} +``` + +Harper, adapted: + +> TypeError: Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 24 in 'ratings=ge=3&=le=4&=ne=5', attribute must be specified before equality comparator at position 20 + +
+ +### harper-sort-direction-marker-read-after-decoding — 2 case(s) + +§4.2 rule 4 recognizes the `+`/`-` sort marker on the RAW token, so `%2B`/`%2D` are literal name characters — `sort(%2Bname)` sorts by the property named `+name`. Harper decodes the token first and then inspects its first character, so the escape is indistinguishable from the marker and a property whose name starts with `+` or `-` cannot be sorted on at all. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The `+`/`-` sort-direction marker is read AFTER percent-decoding, so `sort(%2Bname)` is treated as ascending `name` rather than the property `+name` | bug | read the direction marker on the raw token before decoding (§4.2 rule 4) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `sort(%2Bname)` | parsed | parsed | +| `sort(%2Dname)` | parsed | parsed | + +
Worked example: `sort(%2Bname)` + +Reference: + +```json +{ + "sort": [ + { + "direction": "asc", + "path": [ + "+name" + ] + } + ] +} +``` + +Harper, adapted: + +```json +{ + "sort": [ + { + "direction": "asc", + "path": [ + "name" + ] + } + ] +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/sort/0/path/0` | value | `"+name"` | `"name"` | + +
+ +### harper-empty-sort-rejected — 1 case(s) + +§4’s `call` production makes the argument list optional, so `sort()` is grammatical and means "no ordering". Harper reaches `toSortObject` with an empty argument list and reports "Unknown sort type undefined". Low impact, but it is a rejection of a grammatical query; if the intent is that `sort` requires at least one key, §5.6 should say so. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| `sort()` with no keys is rejected, though §4 makes a call’s argument list optional | divergence | accept `sort()` as a no-op, or have §5.6 require at least one sort key | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `sort()` | parsed | rejected | + +
Worked example: `sort()` + +Reference: + +```json +{ + "sort": [] +} +``` + +Harper, adapted: + +> TypeError: Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in 'sort()', Unknown sort type undefined at position 6 + +
+ +### harper-trailing-comma-yields-an-empty-key — 2 case(s) + +A trailing comma is significant only for `select`, where it forces records mode for a single field (§5.7). Harper pushes the empty token as another argument, producing a sort key on the property named "" and a selected field named "" — neither of which exists. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| A trailing comma in a call adds an empty argument: `sort(a,)` gains a sort key on "" and `select(a,b,)` a field named "" | bug | drop a trailing empty argument; keep its only meaning, the single-field records form `select(a,)` (§5.7) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `sort(a,)` | parsed | parsed | +| `select(a,b,)` | parsed | parsed | + +
Worked example: `sort(a,)` + +Reference: + +```json +{ + "sort": [ + { + "direction": "asc", + "path": [ + "a" + ] + } + ] +} +``` + +Harper, adapted: + +```json +{ + "sort": [ + { + "direction": "asc", + "path": [ + "a" + ] + }, + { + "direction": "asc", + "path": [ + "" + ] + } + ] +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/sort/1` | harper-only | — | `{"direction":"asc","path":[""]}` | + +
+ +### harper-reserved-nested-tuple-accepted — 1 case(s) + +§5.7 reserves the nested tuple form: "A nested `[x,y]` tuple form (`rel{[x,y]}`) is reserved and currently a syntax error." Harper accepts it and produces a nested list whose single element is an unnamed array, which no consumer reads as a projection. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The reserved nested tuple form `rel{[a,b]}` is accepted and produces an unnamed nested array | bug | reject a `[...]` item inside a `{...}` nested projection until the form is specified (§5.7) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `select(rel{[a,b]})` | rejected | parsed | + +
Worked example: `select(rel{[a,b]})` + +Reference: + +> SyntaxViolation: Unable to parse query: nested '[...]' tuple inside '{}' is reserved at position 17 + +Harper, adapted: + +```json +{ + "select": { + "fields": [ + { + "path": [ + "rel" + ], + "projection": { + "fields": [ + { + "path": [], + "projection": { + "fields": [ + { + "path": [ + "a" + ] + }, + { + "path": [ + "b" + ] + } + ], + "mode": "records" + } + } + ], + "mode": "records" + } + } + ], + "mode": "records" + } +} +``` + +
+ +### harper-dotted-select-path-collides-with-tuple-form — 2 case(s) + +A dotted select path is decoded into a segment array, which is the same representation Harper uses for the `[a,b]` tuple form — so `select(a.b)` is indistinguishable from `select([a,b])` and is projected as a two-column tuple of the top-level properties `a` and `b` rather than as the nested property `a.b`. §5.7’s `select-item` is a `prop-path`, and §5.5 makes dotted paths nested-property addressing. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| A dotted path in `select` is represented as a segment array and read as the `[a,b]` tuple form, so `select(a.b)` projects two top-level properties instead of the nested one | bug | give a nested select path a distinct representation from the tuple form (e.g. `{ name: [...] }` vs. an `asArray` list) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `select(a.b)` | parsed | parsed | +| `select(a.b,c)` | parsed | parsed | + +
Worked example: `select(a.b)` + +Reference: + +```json +{ + "select": { + "fields": [ + { + "path": [ + "a", + "b" + ] + } + ], + "mode": "values" + } +} +``` + +Harper, adapted: + +```json +{ + "select": { + "fields": [ + { + "path": [ + "a" + ] + }, + { + "path": [ + "b" + ] + } + ], + "mode": "tuples" + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/select/fields/0/path/1` | ref-only | `"b"` | — | +| `/select/fields/1` | harper-only | — | `{"path":["b"]}` | +| `/select/mode` | value | `"values"` | `"tuples"` | + +
+ +### harper-limit-arguments-unvalidated — 5 case(s) + +§5.6 requires non-negative decimal integers with end ≥ start, and §6 requires `limit`/`offset` to be validated non-negative integers. Harper applies unary `+` and keeps whatever comes out: `limit(x)` becomes `NaN`, `limit(-1)` a negative limit, and `limit(2,1)` an offset of 2 with a limit of −1. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| `limit` arguments are coerced with unary `+` and never validated — `limit(x)` → NaN, `limit(-1)` → −1, `limit(2,1)` → offset 2 / limit −1 | bug | validate both arguments as non-negative decimal integers with end ≥ start, and reject otherwise (§5.6) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `limit(x)` | rejected | parsed | +| `limit(-1)` | rejected | parsed | +| `limit(2,1)` | rejected | parsed | +| `limit(1.5)` | rejected | parsed | +| `limit(01)` | rejected | parsed | + +
Worked example: `limit(x)` + +Reference: + +> SyntaxViolation: Unable to parse query: limit argument must be a non-negative integer: 'x' at position 8 + +Harper, adapted: + +```json +{ + "limit": { + "$number": "NaN" + } +} +``` + +
+ +### harper-duplicate-call-functions-accepted — 3 case(s) + +§5.6: "A call function appearing more than once in a query is a syntax error." Harper overwrites the earlier call, so a duplicate silently wins on last-write — which is exactly the shape a request-smuggling or cache-key bug takes when two layers each append a `limit(...)`. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| A repeated call function is accepted, last one wins, instead of being a syntax error | bug | reject a second occurrence of `select`, `sort` or `limit` in one query (§5.6) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `select(a)&select(b)` | rejected | parsed | +| `limit(1)&limit(2)` | rejected | parsed | +| `sort(a)&sort(b)` | rejected | parsed | + +
Worked example: `select(a)&select(b)` + +Reference: + +> SyntaxViolation: Unable to parse query: duplicate select() at position 19 + +Harper, adapted: + +```json +{ + "select": { + "fields": [ + { + "path": [ + "b" + ] + } + ], + "mode": "values" + } +} +``` + +
+ +### harper-unbalanced-groups-accepted — 3 case(s) + +`parseBlock` reports a missing terminator only for the block that is waiting for one, so a group left open at the end of the string — or closed with the wrong delimiter — parses as if it had been closed. The query silently means something other than what was written. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| Unterminated or mismatched group delimiters (`((a=1)`, `[a=1)`, `a[b=1`) parse as if the group had been closed correctly | bug | require every opened group/scope to be closed by its own delimiter before the end of the query | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `((a=1)` | rejected | parsed | +| `[a=1)` | rejected | parsed | +| `a[b=1` | rejected | parsed | + +
Worked example: `((a=1)` + +Reference: + +> SyntaxViolation: Unable to parse query: expected ')', got end of string at position 6 + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1" + } + ] + } + ] + } + ] + } +} +``` + +
+ +### harper-trailing-conjunction-drops-the-operator — 1 case(s) + +A trailing conjunction with no following term is a syntax error under §4. Both parsers accept it; they differ in what the resulting one-term group says: Harper assigns the group operator only when a second condition arrives, so the group reports `and` where the query said `|`. With a single term the operator carries no meaning, but the two representations are not equal. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| A dangling conjunction is accepted, and the group operator it named is dropped (`a=1\|` yields an `and` group) | bug | reject a conjunction with no following term (§4) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=1\|` | parsed | parsed | + +
Worked example: `a=1\|` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "or", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/operator` | value | `"or"` | `"and"` | + +
+ +### harper-empty-value-drops-the-condition — 3 case(s) + +§4’s `plain-value` is `*vchar` — the empty value is legal, and `a==` is a condition against the empty string. Harper’s value pattern requires at least one character, so at the end of the string the pending condition is never built and the whole filter disappears: `?a==` returns every record instead of the ones whose `a` is empty. (`a=` takes the fast path and does produce the condition, so the two spellings disagree inside Harper.) + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| A comparator followed by an empty value at end-of-string silently drops the condition, so `?a==` returns everything | bug | allow an empty value token on the parsing path (§4 `plain-value = *vchar`), matching what the fast path already does for `a=` | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a==` | parsed | parsed | +| `a!==` | parsed | parsed | +| `a>=` | parsed | parsed | + +
Worked example: `a==` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "" + } + ] + } +} +``` + +Harper, adapted: + +```json +{} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter` | ref-only | `{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition","path":["a"],"value":""}]}` | — | + +
+ +### harper-re-anchors-on-a-second-operator — 1 case(s) + +Two comparators in one term are a syntax error (§4 rule 2: a raw `=` never occurs inside a value). Harper takes the second operator as the start of a new condition and builds one on the property named `1`; the reference parser instead keeps the first condition and silently discards the rest, which is its own gap. Neither rejects. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| A second comparator in one term re-anchors the condition (`a===1===2` becomes a condition on the property `1`) instead of being a syntax error | bug | reject a second comparator within one term (§4 rule 2) | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a===1===2` | parsed | parsed | + +
Worked example: `a===1===2` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "1" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "1" + ], + "value": "2" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/path/0` | value | `"a"` | `"1"` | +| `/filter/terms/0/value` | value | `"1"` | `"2"` | + +
+ +### harper-includes-is-in-not-contains — 1 case(s) + +Appendix B desugars `includes` to `contains`; Harper’s `ALTERNATE_COMPARATOR_NAMES` maps it to `in`. The two mean different things — substring containment versus membership in a supplied list — so the same query returns different records. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| `includes` is an alias for `in`, but Appendix B assigns it to `contains` | divergence | decide: realign `includes` with Appendix B, or record it as a Harper-lineage exception and have the spec note the conflict | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `price=includes=10` | parsed | parsed + uninterpreted values | + +
Worked example: `price=includes=10` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "contains", + "kind": "condition", + "path": [ + "price" + ], + "value": 10 + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "in", + "kind": "condition", + "path": [ + "price" + ], + "value": "10" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/comparator` | value | `"contains"` | `"in"` | + +
+ +### harper-not-prefix-only-for-a-fixed-base-set — 5 case(s) + +§5.1.1 makes `not_` a uniform prefix over *any* comparator, including open-vocabulary names. Harper’s `resolveComparator` strips it only when the base is one of `in`, `between`, `starts_with`, `ends_with`, `contains`, `equals`, so `not_lt`, `not_ge` and `not_` survive as opaque comparator names that no executor implements — a silent no-match rather than a negation. + +**Proposed ledger row** + +| Divergence | Class | Action | +|---|---|---| +| The `not_` prefix is recognized only for a fixed base set, so `not_lt`/`not_le`/`not_gt`/`not_ge` and `not_` are not negations | bug | strip `not_` from any comparator name and set `negated` (§5.1.1); leave semantic validation of the base name to execution | + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `labels=not_lt=urgent` | parsed | parsed | +| `labels=not_le=urgent` | parsed | parsed | +| `labels=not_gt=urgent` | parsed | parsed | +| `labels=not_ge=urgent` | parsed | parsed | +| `labels=not_frobnicate=urgent` | parsed | parsed | + +
Worked example: `labels=not_lt=urgent` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "lt", + "kind": "condition", + "negated": true, + "path": [ + "labels" + ], + "value": "urgent" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "not_lt", + "kind": "condition", + "path": [ + "labels" + ], + "value": "urgent" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/comparator` | value | `"lt"` | `"not_lt"` | +| `/filter/terms/0/negated` | ref-only | `true` | — | + +
+ +## Divergences matching an existing ledger row + +Already tracked; cited by row number. + +### row 1 — 11 case(s) + +Ledger row 1 (permitted): Simple queries (no structural chars) skip parsing; conditions surface as raw name/value pairs (dual shape) + +Tracked action: adapter maps to canonical model + +The query contains none of the characters that trigger Harper’s parser, so it is never parsed: `URLSearchParams` splits it into name/value pairs and any structure in it becomes literal text. That makes Harper accept strings §4 rejects (a bare name, a value containing `=` or `,`, `{`/`}`). Where the reference parser also accepts such a string, it does so by silently DROPPING the malformed term rather than rejecting it, which is a gap of its own. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a` | parsed | parsed | +| `a&b` | parsed | parsed | +| `=1` | rejected | parsed | +| `a=%2E=1` | rejected | parsed | +| `a=has-dash=1` | rejected | parsed | +| `a=,=1` | rejected | parsed | +| `a,b=1` | rejected | parsed | +| `{a}` | rejected | parsed | +| `a{b}=1` | rejected | parsed | +| `a=1&b` | parsed | parsed | +| `a=1#b=2` | rejected | parsed | + +
Worked example: `a` + +Reference: + +```json +{} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter` | harper-only | — | `{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition","path":["a"],"value":""}]}` | + +
+ +### row 6 — 4 case(s) + +Ledger row 6 (tolerance): `(4)` on a non-list comparator is the literal string `"(4)"` + +Tracked action: keep; producers must not rely on it + +§4.1 lets a consumer scan a value leniently and take `(`, `)`, `<`, `>` and `!` as literal characters. Harper does; the reference parser does so only after an interpreting comparator. Both readings are conformant — this is a tolerance, not a semantic difference — and producers must percent-encode these characters regardless. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=(4)` | rejected | parsed | +| `foo=ba)r` | rejected | parsed | +| `foo=ba(r` | rejected | parsed | +| `a=1)` | rejected | parsed | + +
Worked example: `a=(4)` + +Reference: + +> SyntaxViolation: Unable to parse query: unexpected value without path '4' at position 5 + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "a" + ], + "value": "(4)" + } + ] + } +} +``` + +
+ +### row 2 — 1 case(s) + +Ledger row 2 (representational + quirk): Chains attach as `chainedConditions`; nameless legs (`a=ge=1&=5`) inherit the previous comparator + +Tracked action: adapter; nameless legs are a syntax error in 2.0 — deprecate + +Harper lets a nameless chain leg inherit the preceding comparator. §4’s `chained-cond` requires a comparator name on every leg, so the reference parser rejects it; the ledger already marks this for deprecation. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `ratings=ge=3&=5` | rejected | parsed | + +
Worked example: `ratings=ge=3&=5` + +Reference: + +> SyntaxViolation: Unable to parse query: chain leg requires a comparator name before '5' at position 15 + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "elementMatch", + "path": [ + "ratings" + ], + "some": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "ge", + "kind": "condition", + "path": [], + "value": "3" + }, + { + "comparator": "ge", + "kind": "condition", + "path": [], + "value": "5" + } + ] + } + } + ] + } +} +``` + +
+ +### row 14 — 5 case(s) + +Ledger row 14 (partial): Element conditions (`prop[=ge=10|=le=2]`) and scoped sub-queries per §5.3 surface + +Tracked action: Harper has `prop[cond&cond]`; elem-cond (empty relative path) not supported — fold into not() work or file separately + +An element condition (§5.3) has an empty relative path. Harper requires an attribute before every comparator, so `prop[=ge=10]` is rejected — it supports only the `prop[cond&cond]` half of scoped matching. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `scores[=ge=10\|=le=2]` | parsed | rejected | +| `scores[=ge=10&=le=20]` | parsed | rejected | +| `scores[=ge=10]` | parsed | rejected | +| `tags[=not_eq=urgent]` | parsed | rejected | +| `reviews[=ge=4&author.name=kim]` | parsed | rejected | + +
Worked example: `scores[=ge=10\|=le=2]` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "elementMatch", + "path": [ + "scores" + ], + "some": { + "kind": "group", + "operator": "or", + "terms": [ + { + "comparator": "ge", + "kind": "condition", + "path": [], + "value": 10 + }, + { + "comparator": "le", + "kind": "condition", + "path": [], + "value": 2 + } + ] + } + } + ] + } +} +``` + +Harper, adapted: + +> TypeError: Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'scores[=ge=10\|=le=2]', attribute must be specified before equality comparator at position 8 + +
+ +### row 13 — 9 case(s) + +Ledger row 13 (feature gap): No `not(...)` group/scope negation + +Tracked action: sub-issue below + +`not` is read as a call-function name, and it is not in Harper’s call namespace, so the query is rejected instead of desugaring to leaf/scope negation (§5.4). + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `not(a=1)` | parsed | rejected | +| `not(a=1&b=2)` | parsed | rejected | +| `not(a=1\|b=2)` | parsed | rejected | +| `not(not(a=1))` | parsed | rejected | +| `status=open¬(tag=urgent\|tag=blocked)` | parsed | rejected | +| `not(scores[=ge=10&=le=20])` | parsed | rejected | +| `not(reviews[rating=ge=4])` | parsed | rejected | +| `a=1¬(b=2)` | parsed | rejected | +| `[not(a=1)\|b=2]` | parsed | rejected | + +
Worked example: `not(a=1)` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "negated": true, + "path": [ + "a" + ], + "value": "1" + } + ] + } +} +``` + +Harper, adapted: + +> TypeError: Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'not(a=1)', unknown query function call not at position 8 + +
+ +### row 7 — 3 case(s) + +Ledger row 7 (tolerance): `prop[]=v` repeated array params + +Tracked action: spec Appendix B host accommodation + +`prop[]=v` is a host-framework accommodation (Appendix B), not RQL grammar: Harper turns it into a membership condition, while the reference parser reads `[` structurally and rejects the empty scope body. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `itemIds[]=1` | rejected | parsed | +| `itemIds[]=1&itemIds[]=2` | rejected | parsed | +| `itemIds[]=1&itemIds[]=2&b=3` | rejected | parsed | + +
Worked example: `itemIds[]=1` + +Reference: + +> SyntaxViolation: Unable to parse query: empty bracket group for 'itemIds' at position 9, path required before = at position 10 + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "itemIds" + ], + "value": "1" + } + ] + } +} +``` + +
+ +## Reference-parser bugs + +The reference implementation, not Harper, is wrong here. + +### reference-wildcard-on-negated-equality — 1 case(s) + +§5.1.2: "The trailing-`*` wildcard applies only to `==`". The reference parser applies it to every non-verbatim equality, so `!=` becomes a negated `starts_with`. Harper keeps the `*` as a literal, which is what the spec says. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `name!=Jo*` | parsed | parsed | + +
Worked example: `name!=Jo*` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "starts_with", + "kind": "condition", + "negated": true, + "path": [ + "name" + ], + "value": "Jo" + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "negated": true, + "path": [ + "name" + ], + "value": "Jo*" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/comparator` | value | `"starts_with"` | `"eq"` | +| `/filter/terms/0/value` | value | `"Jo"` | `"Jo*"` | + +
+ +### reference-gap-value-list-inside-a-group — 2 case(s) + +Known reference-parser gap. Inside a group or scope body the reference parser always tokenizes with the structural pattern, so a `(v1,v2)` value list is read as a nested group and rejected. §5.1.2 puts no such restriction on where a value list may appear; Harper is right here. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `[a=in=(1,2)&b=2]` | rejected | parsed | +| `x[a=in=(1,2)]` | rejected | parsed | + +
Worked example: `[a=in=(1,2)&b=2]` + +Reference: + +> SyntaxViolation: Unable to parse query: unexpected ',' at position 9, unexpected value without path '2' at position 11 + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "in", + "kind": "condition", + "path": [ + "a" + ], + "value": [ + "1", + "2" + ] + }, + { + "comparator": "eq", + "kind": "condition", + "path": [ + "b" + ], + "value": "2" + } + ] + } + ] + } +} +``` + +
+ +### reference-rejects-chaining-after-between — 1 case(s) + +`between` is a condition in surface terms (`prop=fiql-name=value-list`), so §4’s `chained-cond` — "MUST directly follow a condition or another chained-cond" — permits chaining onto it. The reference parser has already desugared it to an ElementMatch by then and refuses to chain onto a non-Condition. Harper extends the same element scope, which is what §5.3 describes. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `a=between=(1,5)&=ne=3` | rejected | parsed | + +
Worked example: `a=between=(1,5)&=ne=3` + +Reference: + +> SyntaxViolation: Unable to parse query: no preceding Condition to chain onto at position 17 + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "kind": "elementMatch", + "path": [ + "a" + ], + "some": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "ge", + "kind": "condition", + "path": [], + "value": "1" + }, + { + "comparator": "le", + "kind": "condition", + "path": [], + "value": "5" + }, + { + "comparator": "eq", + "kind": "condition", + "negated": true, + "path": [], + "value": "3" + } + ] + } + } + ] + } +} +``` + +
+ +### reference-accepts-a-leading-conjunction — 2 case(s) + +§4’s `query` production has a term before every conjunction, so a leading `|` is a syntax error. The reference parser records the group operator and moves on, accepting the query; Harper rejects it. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `\|` | parsed | rejected | +| `\|a=1` | parsed | rejected | + +
Worked example: `\|` + +Reference: + +```json +{} +``` + +Harper, adapted: + +> TypeError: Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 1 in '\|', no comparison specified before '\|' at position 1 + +
+ +### reference-uri-error-not-a-client-error — 2 case(s) + +The reference parser calls `decodeURIComponent` on the raw token and lets the resulting `URIError` escape. §4.2 makes percent-decoding part of parsing and §6.1 makes a structural violation a client error, so a malformed escape must surface as a `SyntaxViolation` (HTTP 400), not as a bare `URIError` with no status. Harper answers from its fast path here and leaves the malformed escape in place (ledger row 1). + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `%ZZ=1` | rejected | parsed | +| `a=%ZZ` | rejected | parsed | + +
Worked example: `%ZZ=1` + +Reference: + +> URIError: URI malformed + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "path": [ + "%ZZ" + ], + "value": "1" + } + ] + } +} +``` + +
+ +### reference-missing-camelcase-alias — 1 case(s) + +Appendix B’s alias list ends in "…", and the reference parser implements the camelCase forms for the ordered comparators (`lessThan`, `greaterThanEqual`, …) but not `notEqual`, which falls through to the open vocabulary. Harper resolves it to `ne`. Either the reference parser should carry the alias or Appendix B should close the list explicitly. + +| Query | Reference | Harper (adapted) | +|---|---|---| +| `price=notEqual=10` | parsed | parsed + uninterpreted values | + +
Worked example: `price=notEqual=10` + +Reference: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "notEqual", + "kind": "condition", + "path": [ + "price" + ], + "value": 10 + } + ] + } +} +``` + +Harper, adapted: + +```json +{ + "filter": { + "kind": "group", + "operator": "and", + "terms": [ + { + "comparator": "eq", + "kind": "condition", + "negated": true, + "path": [ + "price" + ], + "value": "10" + } + ] + } +} +``` + +Differences: + +| At | Kind | Reference | Harper | +|---|---|---|---| +| `/filter/terms/0/comparator` | value | `"notEqual"` | `"eq"` | +| `/filter/terms/0/negated` | harper-only | — | `true` | + +
+ +## Unclassified + +Every entry here is a hole in `conformance/classify.ts` and fails the run. + +_None._ + +## Deferred-error mode (ledger rows 8 and 9) + +Harper’s production entry point is `new RequestTarget("?" + query)`, which never throws: +a parse error is deferred into `parseError` for the request pipeline to raise (§6.1 +permits this). These cases show what that path produces where the throwing entry point +rejects — including row 9, where a reserved call name is rejected *and* still falls +through into `sort`. + +| Query | Reference (deferred) | Harper (deferred) | Harper still produced | +|---|---|---|---| +| `name==*Jo` | rejected | rejected (deferred) | `{}` | +| `a=eq=unknown:x` | rejected | rejected (deferred) | `{}` | +| `a=1&b=2\|c=3` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `(a=1&b=2\|c=3)` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `(a=in=(1,2)&b=2)` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `ratings=ge=3&=le=4&=ne=5` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["ratin…` | +| `ratings=ge=3&=le=4\|=gt=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["ratin…` | +| `&=le=4` | rejected (deferred) | rejected (deferred) | `{}` | +| `(a=1)&=le=4` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `scores[=ge=10\|=le=2]` | parsed | rejected (deferred) | `{}` | +| `scores[=ge=10&=le=20]` | parsed | rejected (deferred) | `{}` | +| `scores[=ge=10]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["score…` | +| `tags[=not_eq=urgent]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["tags"…` | +| `reviews[=ge=4&author.name=kim]` | parsed | rejected (deferred) | `{}` | +| `not(a=1)` | parsed | rejected (deferred) | `{}` | +| `not(a=1&b=2)` | parsed | rejected (deferred) | `{}` | +| `not(a=1\|b=2)` | parsed | rejected (deferred) | `{}` | +| `not(not(a=1))` | parsed | rejected (deferred) | `{}` | +| `status=open¬(tag=urgent\|tag=blocked)` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `not(scores[=ge=10&=le=20])` | parsed | rejected (deferred) | `{}` | +| `not(reviews[rating=ge=4])` | parsed | rejected (deferred) | `{}` | +| `a=1¬(b=2)` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `[not(a=1)\|b=2]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `not()` | rejected (deferred) | rejected (deferred) | `{}` | +| `not(a=1&b=2\|c=3)` | rejected (deferred) | rejected (deferred) | `{}` | +| `sort()` | parsed | rejected (deferred) | `{}` | +| `sort(a` | rejected (deferred) | rejected (deferred) | `{}` | +| `select(a` | rejected (deferred) | rejected (deferred) | `{"select":{"fields":[],"mode":"records"}}` | +| `select([a,b)` | rejected (deferred) | rejected (deferred) | `{"select":{"fields":[{"path":["a"]}],"mode":"tuples"}}` | +| `limit()` | rejected (deferred) | rejected (deferred) | `{}` | +| `limit(1,2,3)` | rejected (deferred) | rejected (deferred) | `{}` | +| `limit(5,10` | rejected (deferred) | rejected (deferred) | `{"limit":5}` | +| `unknown(1)` | rejected (deferred) | rejected (deferred) | `{}` | +| `lt(price,10)` | rejected (deferred) | rejected (deferred) | `{}` | +| `group-by(a)` | rejected (deferred) | rejected (deferred) | `{"sort":[{"direction":"asc","path":["a"]}]}` | +| `count(a)` | rejected (deferred) | rejected (deferred) | `{}` | +| `distinct(a)` | rejected (deferred) | rejected (deferred) | `{}` | +| `aggregate(a)` | rejected (deferred) | rejected (deferred) | `{}` | +| `(` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `)` | rejected (deferred) | rejected (deferred) | `{}` | +| `[` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `]` | rejected (deferred) | rejected (deferred) | `{}` | +| `(a=1))` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `(a=1]` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | +| `\|` | parsed | rejected (deferred) | `{}` | +| `\|a=1` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `==1` | rejected (deferred) | rejected (deferred) | `{}` | +| `a=1=2` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `a=9lives=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `a<>1` | rejected (deferred) | rejected (deferred) | `{}` | +| `a]b=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | +| `a==%ZZ` | rejected | rejected (deferred) | `{}` | +| `a==%E0%A4%A` | rejected | rejected (deferred) | `{}` | + +## Every case + +All 349 corpus cases, in corpus order, with the verdict applied to each. + +| # | Query | Features | Reference | Harper | Verdict | +|---:|---|---|---|---|---| +| 1 | _(empty string)_ | `form-encoding` | parsed | parsed | agrees | +| 2 | `a=1` | `form-encoding` | parsed | parsed | agrees | +| 3 | `a=1&b=2` | `form-encoding` | parsed | parsed | agrees | +| 4 | `a=1&a=2` | `form-encoding` | parsed | parsed | agrees | +| 5 | `a=` | `form-encoding` | parsed | parsed | agrees | +| 6 | `a` | `form-encoding` | parsed | parsed | ledger row 1 | +| 7 | `a&b` | `form-encoding` | parsed | parsed | ledger row 1 | +| 8 | `a=1&` | `form-encoding` | parsed | parsed | agrees | +| 9 | `a=1&&b=2` | `form-encoding` `adversarial` | parsed | parsed | agrees | +| 10 | `&` | `form-encoding` `adversarial` | parsed | parsed | agrees | +| 11 | `=1` | `form-encoding` `adversarial` | rejected | parsed | ledger row 1 | +| 12 | `a=1+2` | `form-encoding` `pct-encoded-value` | parsed | parsed | NEW (harper-fast-path-form-decoding) | +| 13 | `a=1+2&b<3` | `form-encoding` `pct-encoded-value` `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 14 | `a=b%26c` | `form-encoding` `pct-encoded-value` | parsed | parsed | agrees | +| 15 | `a%20b=1` | `form-encoding` `pct-encoded-path` | parsed | parsed | agrees | +| 16 | `a=%20` | `form-encoding` `pct-encoded-value` | parsed | parsed | agrees | +| 17 | `%61=1` | `form-encoding` `pct-encoded-path` | parsed | parsed | agrees | +| 18 | `name==Jo*` | `wildcard` | parsed | parsed | agrees | +| 19 | `name=eq=Jo*` | `wildcard` | parsed | parsed | NEW (harper-wildcard-on-fiql-equality) | +| 20 | `name==*` | `wildcard` | parsed | parsed | agrees | +| 21 | `name==*Jo` | `wildcard` `adversarial` | rejected | rejected | agrees | +| 22 | `name==*Jo*` | `wildcard` `adversarial` | parsed | parsed | agrees | +| 23 | `name==12*` | `wildcard` | parsed | parsed | agrees | +| 24 | `name=starts_with=Jo*` | `wildcard` | parsed | parsed | agrees | +| 25 | `name===Jo*` | `wildcard` | parsed | parsed | agrees | +| 26 | `name==Jo%2A` | `wildcard` `pct-encoded-value` | parsed | parsed | agrees | +| 27 | `name!=Jo*` | `wildcard` | parsed | parsed | reference bug (reference-wildcard-on-negated-equality) | +| 28 | `a=in=(1,2)` | `value-list` `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 29 | `a=in=(1)` | `value-list` `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 30 | `a=in=()` | `value-list` `fiql-core` | parsed | parsed | agrees | +| 31 | `a=in=(1,2,3,4,5)` | `value-list` `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 32 | `a=in=(a,b%2Cc)` | `value-list` `pct-encoded-value` | parsed | parsed | agrees | +| 33 | `a=not_in=(1,2)` | `value-list` `not-prefix` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 34 | `a=out=(1,2)` | `value-list` `fiql-alias` | parsed | parsed | NEW (harper-missing-out-alias) | +| 35 | `a=in=(number:1,boolean:true)` | `value-list` `typed-value` | parsed | parsed | agrees | +| 36 | `a=eq=(1,2)` | `value-list` `adversarial` | parsed | parsed | agrees | +| 37 | `a=(4)` | `value-list` `tolerance` | rejected | parsed | ledger row 6 | +| 38 | `a=lt=(4)` | `value-list` `tolerance` | parsed | parsed | agrees | +| 39 | `a=between=(1,5)` | `fiql-alias` `value-list` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 40 | `a=not_between=(1,5)` | `fiql-alias` `value-list` `not-prefix` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 41 | `a=between=(1,5,9)` | `fiql-alias` `adversarial` | rejected | parsed | NEW (harper-between-arguments-unvalidated) | +| 42 | `a=between=1` | `fiql-alias` `adversarial` | rejected | parsed | NEW (harper-between-arguments-unvalidated) | +| 43 | `a=between=(number:1,number:5)` | `fiql-alias` `typed-value` | parsed | parsed | agrees | +| 44 | `a=ge=1&=le=5` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 45 | `a==3` | `typed-value` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 46 | `a==-5` | `typed-value` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 47 | `a==2.5` | `typed-value` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 48 | `a==1e3` | `typed-value` | parsed | parsed | agrees | +| 49 | `a==01` | `typed-value` | parsed | parsed | agrees | +| 50 | `a==1.50` | `typed-value` | parsed | parsed | agrees | +| 51 | `a==null` | `typed-value` | parsed | parsed | agrees | +| 52 | `a==true` | `typed-value` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 53 | `a==false` | `typed-value` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 54 | `a=null` | `typed-value` `form-encoding` | parsed | parsed | agrees | +| 55 | `a=eq=number:5` | `typed-value` | parsed | parsed | agrees | +| 56 | `a=eq=number:$ff` | `typed-value` | parsed | parsed | agrees | +| 57 | `a=eq=number:-2.5` | `typed-value` | parsed | parsed | agrees | +| 58 | `a=eq=number:abc` | `typed-value` `adversarial` | rejected | parsed | NEW (harper-malformed-typed-literals-are-coerced) | +| 59 | `a=eq=number:` | `typed-value` `adversarial` | rejected | parsed | NEW (harper-malformed-typed-literals-are-coerced) | +| 60 | `a=eq=boolean:true` | `typed-value` | parsed | parsed | agrees | +| 61 | `a=eq=boolean:false` | `typed-value` | parsed | parsed | agrees | +| 62 | `a=eq=boolean:yes` | `typed-value` `adversarial` | rejected | parsed | NEW (harper-malformed-typed-literals-are-coerced) | +| 63 | `a=eq=date:2020-01-01` | `typed-value` | parsed | parsed | agrees | +| 64 | `a=eq=date:2024-01-05T20%3A07%3A27.955Z` | `typed-value` `pct-encoded-value` | parsed | parsed | agrees | +| 65 | `a=eq=date:1602872124871` | `typed-value` | parsed | parsed | agrees | +| 66 | `a=eq=date:notadate` | `typed-value` `adversarial` | rejected | parsed | NEW (harper-malformed-typed-literals-are-coerced) | +| 67 | `a=eq=string:null` | `typed-value` | parsed | parsed | agrees | +| 68 | `a=equals=null` | `typed-value` `fiql-alias` | parsed | parsed | NEW (harper-alias-value-mode-is-always-interpreted) | +| 69 | `a=equal=null` | `typed-value` `fiql-alias` | parsed | parsed | NEW (harper-alias-value-mode-is-always-interpreted) | +| 70 | `a=not_equal=null` | `typed-value` `fiql-alias` | parsed | parsed | NEW (harper-alias-value-mode-is-always-interpreted) | +| 71 | `a=ne=null` | `typed-value` `fiql-alias` | parsed | parsed | agrees | +| 72 | `a=eq=string:3` | `typed-value` | parsed | parsed | agrees | +| 73 | `a=eq=unknown:x` | `typed-value` `adversarial` | rejected | rejected | agrees | +| 74 | `a==string%3Anull` | `typed-value` `pct-encoded-value` | parsed | parsed | agrees | +| 75 | `a==%2C` | `pct-encoded-value` | parsed | parsed | agrees | +| 76 | `a==%26` | `pct-encoded-value` | parsed | parsed | agrees | +| 77 | `a==%28x%29` | `pct-encoded-value` | parsed | parsed | agrees | +| 78 | `a==a%2Fb` | `pct-encoded-value` | parsed | parsed | agrees | +| 79 | `brand.name=Microsoft` | `dotted-path` | parsed | parsed | agrees | +| 80 | `a.b.c==1` | `dotted-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 81 | `a.b.c.d.e==1` | `dotted-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 82 | `a%2Eb=1` | `pct-encoded-path` `form-encoding` | parsed | parsed | agrees | +| 83 | `a%2Eb==1` | `pct-encoded-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 84 | `a%2Eb.c==1` | `pct-encoded-path` `dotted-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 85 | `a.b%2Ec==1` | `pct-encoded-path` `dotted-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 86 | `%2E==1` | `pct-encoded-path` `adversarial` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 87 | `a-b_c~d==1` | `dotted-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 88 | `a%2Db==1` | `pct-encoded-path` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 89 | `a=1\|b=2` | `grouping-paren` | parsed | parsed | agrees | +| 90 | `(a=1\|b=2)&c=3` | `grouping-paren` | parsed | parsed | agrees | +| 91 | `[a=1\|b=2]&c=3` | `grouping-bracket` | parsed | parsed | agrees | +| 92 | `a=1&[b=2\|c=3]` | `grouping-bracket` | parsed | parsed | agrees | +| 93 | `(a=1)` | `grouping-paren` | parsed | parsed | agrees | +| 94 | `[a=1]` | `grouping-bracket` | parsed | parsed | agrees | +| 95 | `a=1&(b=2\|(c=3&d=4))` | `grouping-nested` | parsed | parsed | agrees | +| 96 | `[a=1&[b=2\|[c=3\|d=4]]]` | `grouping-nested` | parsed | parsed | agrees | +| 97 | `(a=1&b=2)\|(c=3&d=4)` | `grouping-nested` | parsed | parsed | agrees | +| 98 | `((a=1))` | `grouping-nested` | parsed | parsed | agrees | +| 99 | `a=1&b=2\|c=3` | `adversarial` | rejected | rejected | agrees | +| 100 | `(a=1&b=2\|c=3)` | `adversarial` | rejected | rejected | agrees | +| 101 | `()` | `grouping-paren` `adversarial` | parsed | parsed | NEW (harper-empty-group-body-accepted) | +| 102 | `[]` | `grouping-bracket` `adversarial` | parsed | parsed | NEW (harper-empty-group-body-accepted) | +| 103 | `(a=1&())` | `grouping-nested` `adversarial` | parsed | parsed | NEW (harper-empty-group-body-accepted) | +| 104 | `(a=in=(1,2)&b=2)` | `grouping-paren` `value-list` | rejected | rejected | agrees | +| 105 | `[a=in=(1,2)&b=2]` | `grouping-bracket` `value-list` | rejected | parsed | reference bug (reference-gap-value-list-inside-a-group) | +| 106 | `x[a=in=(1,2)]` | `scoped-match` `value-list` | rejected | parsed | reference bug (reference-gap-value-list-inside-a-group) | +| 107 | `ratings=ge=3&=le=4` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 108 | `ratings=ge=3&ratings=le=4` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 109 | `ratings=ge=3\|=le=4` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 110 | `ratings=ge=3&=not_eq=4` | `chain` `not-prefix` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 111 | `ratings=ge=3&=le=4&=ne=5` | `chain` | parsed | rejected | NEW (harper-chain-supports-only-one-leg) | +| 112 | `ratings=ge=3&=le=4&b=2` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 113 | `a==1&=le=5` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 114 | `a=1&=le=5` | `chain` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 115 | `ratings=ge=3&=5` | `chain` `adversarial` | rejected | parsed | ledger row 2 | +| 116 | `ratings=ge=3&=le=4\|=gt=1` | `chain` `adversarial` | rejected | rejected | agrees | +| 117 | `&=le=4` | `chain` `adversarial` | rejected | rejected | agrees | +| 118 | `(a=1)&=le=4` | `chain` `adversarial` | rejected | rejected | agrees | +| 119 | `a=in=(1,2)&=le=4` | `chain` `value-list` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 120 | `a=between=(1,5)&=ne=3` | `chain` `fiql-alias` | rejected | parsed | reference bug (reference-rejects-chaining-after-between) | +| 121 | `reviews[rating=ge=4&helpful=ge=10]` | `scoped-match` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 122 | `reviews[rating=ge=4\|helpful=ge=10]` | `scoped-match` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 123 | `orders[status=open]` | `scoped-match` | parsed | parsed | agrees | +| 124 | `orders[status=not_eq=open]` | `scoped-match` `not-prefix` | parsed | parsed | agrees | +| 125 | `a.b[c=1]` | `scoped-match` `dotted-path` | parsed | parsed | agrees | +| 126 | `a%2Eb[c=1]` | `scoped-match` `pct-encoded-path` | parsed | parsed | agrees | +| 127 | `a[b[c=1]]` | `scoped-match` `grouping-nested` | parsed | parsed | agrees | +| 128 | `a[b=1&c[d=2]]` | `scoped-match` `grouping-nested` | parsed | parsed | agrees | +| 129 | `x[a=1&b=2]\|y=3` | `scoped-match` `grouping-bracket` | parsed | parsed | agrees | +| 130 | `a[]` | `scoped-match` `adversarial` | rejected | parsed | NEW (harper-empty-group-body-accepted) | +| 131 | `scores[=ge=10\|=le=2]` | `elem-cond` | parsed | rejected | ledger row 14 | +| 132 | `scores[=ge=10&=le=20]` | `elem-cond` | parsed | rejected | ledger row 14 | +| 133 | `scores[=ge=10]` | `elem-cond` | parsed | rejected | ledger row 14 | +| 134 | `tags[=not_eq=urgent]` | `elem-cond` `not-prefix` | parsed | rejected | ledger row 14 | +| 135 | `tags=not_eq=urgent` | `not-prefix` | parsed | parsed | agrees | +| 136 | `reviews[=ge=4&author.name=kim]` | `elem-cond` `scoped-match` `dotted-path` | parsed | rejected | ledger row 14 | +| 137 | `not(a=1)` | `not-expr` | parsed | rejected | ledger row 13 | +| 138 | `not(a=1&b=2)` | `not-expr` | parsed | rejected | ledger row 13 | +| 139 | `not(a=1\|b=2)` | `not-expr` | parsed | rejected | ledger row 13 | +| 140 | `not(not(a=1))` | `not-expr` | parsed | rejected | ledger row 13 | +| 141 | `status=open¬(tag=urgent\|tag=blocked)` | `not-expr` | parsed | rejected | ledger row 13 | +| 142 | `not(scores[=ge=10&=le=20])` | `not-expr` `elem-cond` | parsed | rejected | ledger row 13 | +| 143 | `not(reviews[rating=ge=4])` | `not-expr` `scoped-match` | parsed | rejected | ledger row 13 | +| 144 | `a=1¬(b=2)` | `not-expr` | parsed | rejected | ledger row 13 | +| 145 | `[not(a=1)\|b=2]` | `not-expr` `grouping-bracket` | parsed | rejected | ledger row 13 | +| 146 | `not()` | `not-expr` `adversarial` | rejected | rejected | agrees | +| 147 | `not(a=1&b=2\|c=3)` | `not-expr` `adversarial` | rejected | rejected | agrees | +| 148 | `sort(a)` | `sort` | parsed | parsed | agrees | +| 149 | `sort(+a)` | `sort` | parsed | parsed | agrees | +| 150 | `sort(-a)` | `sort` | parsed | parsed | agrees | +| 151 | `sort(+a,-b)` | `sort` | parsed | parsed | agrees | +| 152 | `sort(a,b,c)` | `sort` | parsed | parsed | agrees | +| 153 | `sort(a.b)` | `sort` `dotted-path` | parsed | parsed | agrees | +| 154 | `sort(-a.b)` | `sort` `dotted-path` | parsed | parsed | agrees | +| 155 | `sort(%2Bname)` | `sort` `pct-encoded-path` | parsed | parsed | NEW (harper-sort-direction-marker-read-after-decoding) | +| 156 | `sort(%2Dname)` | `sort` `pct-encoded-path` | parsed | parsed | NEW (harper-sort-direction-marker-read-after-decoding) | +| 157 | `sort(a%2Eb)` | `sort` `pct-encoded-path` | parsed | parsed | agrees | +| 158 | `sort()` | `sort` `adversarial` | parsed | rejected | NEW (harper-empty-sort-rejected) | +| 159 | `sort(a,)` | `sort` `adversarial` | parsed | parsed | NEW (harper-trailing-comma-yields-an-empty-key) | +| 160 | `sort(a` | `sort` `adversarial` | rejected | rejected | agrees | +| 161 | `a=1&sort(b)` | `sort` | parsed | parsed | agrees | +| 162 | `select(a)` | `select` | parsed | parsed | agrees | +| 163 | `select(a,b)` | `select` | parsed | parsed | agrees | +| 164 | `select(a,)` | `select` | parsed | parsed | agrees | +| 165 | `select(a,b,)` | `select` | parsed | parsed | NEW (harper-trailing-comma-yields-an-empty-key) | +| 166 | `select([a,b])` | `select` | parsed | parsed | agrees | +| 167 | `select([a])` | `select` | parsed | parsed | agrees | +| 168 | `select([a,b],c)` | `select` | parsed | parsed | agrees | +| 169 | `select(rel{x,y})` | `select` | parsed | parsed | agrees | +| 170 | `select(rel{x})` | `select` | parsed | parsed | agrees | +| 171 | `select(rel{x,y},id)` | `select` | parsed | parsed | agrees | +| 172 | `select(rel{x},rel2{y})` | `select` | parsed | parsed | agrees | +| 173 | `select(rel[select(x,y)])` | `select` | parsed | parsed | agrees | +| 174 | `select(related{name,otherTable{other_name}},id,name)` | `select` | parsed | parsed | agrees | +| 175 | `select(related[select(name,otherTable[select(other_name,)])],id,name)` | `select` | parsed | parsed | agrees | +| 176 | `select(rel{[a,b]})` | `select` `adversarial` | rejected | parsed | NEW (harper-reserved-nested-tuple-accepted) | +| 177 | `select(a.b)` | `select` `dotted-path` | parsed | parsed | NEW (harper-dotted-select-path-collides-with-tuple-form) | +| 178 | `select(a.b,c)` | `select` `dotted-path` | parsed | parsed | NEW (harper-dotted-select-path-collides-with-tuple-form) | +| 179 | `select(a%2Eb)` | `select` `pct-encoded-path` | parsed | parsed | agrees | +| 180 | `select()` | `select` `adversarial` | parsed | parsed | agrees | +| 181 | `select(a` | `select` `adversarial` | rejected | rejected | agrees | +| 182 | `select([a,b)` | `select` `adversarial` | rejected | rejected | agrees | +| 183 | `select(a)&sort(b)&limit(2)` | `select` `sort` `limit` | parsed | parsed | agrees | +| 184 | `limit(10)` | `limit` | parsed | parsed | agrees | +| 185 | `limit(0)` | `limit` | parsed | parsed | agrees | +| 186 | `limit(5,10)` | `limit` | parsed | parsed | agrees | +| 187 | `limit(0,0)` | `limit` | parsed | parsed | agrees | +| 188 | `limit(x)` | `limit` `adversarial` | rejected | parsed | NEW (harper-limit-arguments-unvalidated) | +| 189 | `limit(-1)` | `limit` `adversarial` | rejected | parsed | NEW (harper-limit-arguments-unvalidated) | +| 190 | `limit(2,1)` | `limit` `adversarial` | rejected | parsed | NEW (harper-limit-arguments-unvalidated) | +| 191 | `limit(1.5)` | `limit` `adversarial` | rejected | parsed | NEW (harper-limit-arguments-unvalidated) | +| 192 | `limit(01)` | `limit` `adversarial` | rejected | parsed | NEW (harper-limit-arguments-unvalidated) | +| 193 | `limit()` | `limit` `adversarial` | rejected | rejected | agrees | +| 194 | `limit(1,2,3)` | `limit` `adversarial` | rejected | rejected | agrees | +| 195 | `limit(5,10` | `limit` `adversarial` | rejected | rejected | agrees | +| 196 | `unknown(1)` | `call-error` | rejected | rejected | agrees | +| 197 | `lt(price,10)` | `call-error` | rejected | rejected | agrees | +| 198 | `group-by(a)` | `call-error` | rejected | rejected | agrees | +| 199 | `count(a)` | `call-error` | rejected | rejected | agrees | +| 200 | `distinct(a)` | `call-error` | rejected | rejected | agrees | +| 201 | `aggregate(a)` | `call-error` | rejected | rejected | agrees | +| 202 | `select(a)&select(b)` | `call-error` | rejected | parsed | NEW (harper-duplicate-call-functions-accepted) | +| 203 | `limit(1)&limit(2)` | `call-error` | rejected | parsed | NEW (harper-duplicate-call-functions-accepted) | +| 204 | `sort(a)&sort(b)` | `call-error` | rejected | parsed | NEW (harper-duplicate-call-functions-accepted) | +| 205 | `a=1&select(b)&sort(c)&limit(2)` | `select` `sort` `limit` | parsed | parsed | agrees | +| 206 | `select(b)&a=1` | `select` | parsed | parsed | agrees | +| 207 | `foo=ba)r` | `tolerance` | rejected | parsed | ledger row 6 | +| 208 | `foo==ba)r` | `tolerance` | parsed | parsed | agrees | +| 209 | `foo==ba(r` | `tolerance` | parsed | parsed | agrees | +| 210 | `foo=ba(r` | `tolerance` | rejected | parsed | ledger row 6 | +| 211 | `foo==a=` | `adversarial` | parsed | parsed | NEW (harper-empty-value-drops-the-condition) | +| 243 | `a<>1` | `adversarial` | rejected | rejected | agrees | +| 244 | `a=1#b=2` | `adversarial` | rejected | parsed | ledger row 1 | +| 245 | `a=1&limit(2)&` | `adversarial` | parsed | parsed | agrees | +| 246 | `a[b=1` | `adversarial` | rejected | parsed | NEW (harper-unbalanced-groups-accepted) | +| 247 | `a]b=1` | `adversarial` | rejected | rejected | agrees | +| 248 | `%ZZ=1` | `adversarial` `pct-encoded-path` | rejected | parsed | reference bug (reference-uri-error-not-a-client-error) | +| 249 | `a=%ZZ` | `adversarial` `pct-encoded-value` | rejected | parsed | reference bug (reference-uri-error-not-a-client-error) | +| 250 | `a==%ZZ` | `adversarial` `pct-encoded-value` | rejected | rejected | agrees | +| 251 | `a==%E0%A4%A` | `adversarial` `pct-encoded-value` | rejected | rejected | agrees | +| 252 | `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…` | `adversarial` | parsed | parsed | agrees | +| 253 | `a=1111111111111111111111111111111111111111111111111111111111111111111…` | `adversarial` | parsed | parsed | agrees | +| 254 | `(((((((((((a=1)))))))))))` | `adversarial` `grouping-nested` | parsed | parsed | agrees | +| 255 | `a=in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,2…` | `adversarial` `value-list` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 256 | `a[b[c[d[e=1]]]]` | `adversarial` `scoped-match` | parsed | parsed | agrees | +| 257 | `a=1&f0=0&f1=1&f2=2&f3=3&f4=4&f5=5&f6=6&f7=7&f8=8&f9=9&f10=10&f11=11&f…` | `adversarial` | parsed | parsed | agrees | +| 258 | `price=5` | `symbol-op` `form-encoding` | parsed | parsed | agrees | +| 259 | `price=abc` | `symbol-op` `form-encoding` | parsed | parsed | agrees | +| 260 | `price=null` | `symbol-op` `form-encoding` | parsed | parsed | agrees | +| 261 | `price=true` | `symbol-op` `form-encoding` | parsed | parsed | agrees | +| 262 | `price=2.5` | `symbol-op` `form-encoding` | parsed | parsed | agrees | +| 263 | `price=x%20y` | `symbol-op` `form-encoding` | parsed | parsed | agrees | +| 264 | `price==5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 265 | `price==abc` | `symbol-op` | parsed | parsed | agrees | +| 266 | `price==null` | `symbol-op` | parsed | parsed | agrees | +| 267 | `price==true` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 268 | `price==2.5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 269 | `price==x%20y` | `symbol-op` | parsed | parsed | agrees | +| 270 | `price===5` | `symbol-op` | parsed | parsed | agrees | +| 271 | `price===abc` | `symbol-op` | parsed | parsed | agrees | +| 272 | `price===null` | `symbol-op` | parsed | parsed | agrees | +| 273 | `price===true` | `symbol-op` | parsed | parsed | agrees | +| 274 | `price===2.5` | `symbol-op` | parsed | parsed | agrees | +| 275 | `price===x%20y` | `symbol-op` | parsed | parsed | agrees | +| 276 | `price!=5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 277 | `price!=abc` | `symbol-op` | parsed | parsed | agrees | +| 278 | `price!=null` | `symbol-op` | parsed | parsed | agrees | +| 279 | `price!=true` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 280 | `price!=2.5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 281 | `price!=x%20y` | `symbol-op` | parsed | parsed | agrees | +| 282 | `price!==5` | `symbol-op` | parsed | parsed | agrees | +| 283 | `price!==abc` | `symbol-op` | parsed | parsed | agrees | +| 284 | `price!==null` | `symbol-op` | parsed | parsed | agrees | +| 285 | `price!==true` | `symbol-op` | parsed | parsed | agrees | +| 286 | `price!==2.5` | `symbol-op` | parsed | parsed | agrees | +| 287 | `price!==x%20y` | `symbol-op` | parsed | parsed | agrees | +| 288 | `price<5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 289 | `price5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 301 | `price>abc` | `symbol-op` | parsed | parsed | agrees | +| 302 | `price>null` | `symbol-op` | parsed | parsed | agrees | +| 303 | `price>true` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 304 | `price>2.5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 305 | `price>x%20y` | `symbol-op` | parsed | parsed | agrees | +| 306 | `price>=5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 307 | `price>=abc` | `symbol-op` | parsed | parsed | agrees | +| 308 | `price>=null` | `symbol-op` | parsed | parsed | agrees | +| 309 | `price>=true` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 310 | `price>=2.5` | `symbol-op` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 311 | `price>=x%20y` | `symbol-op` | parsed | parsed | agrees | +| 312 | `price=eq=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 313 | `price=lt=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 314 | `price=le=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 315 | `price=gt=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 316 | `price=ge=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 317 | `price=contains=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 318 | `price=starts_with=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 319 | `price=ends_with=10` | `fiql-core` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 320 | `price=ne=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 321 | `price=equals=10` | `fiql-alias` | parsed | parsed | agrees | +| 322 | `price=equal=10` | `fiql-alias` | parsed | parsed | agrees | +| 323 | `price=not_equal=10` | `fiql-alias` | parsed | parsed | agrees | +| 324 | `price=notEqual=10` | `fiql-alias` | parsed | parsed + uninterpreted values | reference bug (reference-missing-camelcase-alias) | +| 325 | `price=sw=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 326 | `price=ew=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 327 | `price=ct=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 328 | `price=includes=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (harper-includes-is-in-not-contains) | +| 329 | `price=less_than=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 330 | `price=lessThan=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 331 | `price=less_than_equal=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 332 | `price=lessThanEqual=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 333 | `price=greater_than=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 334 | `price=greaterThan=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 335 | `price=greater_than_equal=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 336 | `price=greaterThanEqual=10` | `fiql-alias` | parsed | parsed + uninterpreted values | NEW (value-not-interpreted) | +| 337 | `labels=not_eq=urgent` | `not-prefix` | parsed | parsed | agrees | +| 338 | `labels=not_in=(a,b)` | `not-prefix` | parsed | parsed | agrees | +| 339 | `labels=not_contains=urgent` | `not-prefix` | parsed | parsed | agrees | +| 340 | `labels=not_ct=urgent` | `not-prefix` | parsed | parsed | agrees | +| 341 | `labels=not_starts_with=urgent` | `not-prefix` | parsed | parsed | agrees | +| 342 | `labels=not_sw=urgent` | `not-prefix` | parsed | parsed | agrees | +| 343 | `labels=not_ends_with=urgent` | `not-prefix` | parsed | parsed | agrees | +| 344 | `labels=not_lt=urgent` | `not-prefix` | parsed | parsed | NEW (harper-not-prefix-only-for-a-fixed-base-set) | +| 345 | `labels=not_le=urgent` | `not-prefix` | parsed | parsed | NEW (harper-not-prefix-only-for-a-fixed-base-set) | +| 346 | `labels=not_gt=urgent` | `not-prefix` | parsed | parsed | NEW (harper-not-prefix-only-for-a-fixed-base-set) | +| 347 | `labels=not_ge=urgent` | `not-prefix` | parsed | parsed | NEW (harper-not-prefix-only-for-a-fixed-base-set) | +| 348 | `labels=not_equals=urgent` | `not-prefix` | parsed | parsed | agrees | +| 349 | `labels=not_frobnicate=urgent` | `not-prefix` | parsed | parsed | NEW (harper-not-prefix-only-for-a-fixed-base-set) | diff --git a/conformance/corpus.ts b/conformance/corpus.ts new file mode 100644 index 0000000..543ed83 --- /dev/null +++ b/conformance/corpus.ts @@ -0,0 +1,452 @@ +/** + * The differential corpus: query strings exercising every feature of the RQL 2.0 grammar + * (specification/rql-2.0.md §4–§5.7), the Appendix B compatibility aliases, the rows of + * Harper's divergence ledger, and a body of adversarial strings. + * + * Cases are declared, not randomly generated, so the corpus is reviewable and a recorded + * fixture stays addressable across edits: each case's id is a hash of its query string, so + * inserting a case never renumbers the others. + */ + +export type Case = { + /** Stable, content-derived id — also the fixture key. */ + id: string; + query: string; + features: readonly string[]; + /** Rows of the Harper divergence ledger this case is a witness for. */ + ledger?: readonly number[]; + note?: string; +}; + +/** Every feature tag the corpus is required to cover; the report enumerates these. */ +export const REQUIRED_FEATURES = [ + 'form-encoding', + 'symbol-op', + 'fiql-core', + 'fiql-alias', + 'not-prefix', + 'value-list', + 'typed-value', + 'wildcard', + 'grouping-paren', + 'grouping-bracket', + 'grouping-nested', + 'chain', + 'scoped-match', + 'elem-cond', + 'not-expr', + 'dotted-path', + 'pct-encoded-path', + 'pct-encoded-value', + 'select', + 'sort', + 'limit', + 'call-error', + 'tolerance', + 'adversarial', +] as const; + +function fnv1a(input: string): string { + let hash = 0x811c9dc5; + for (let index = 0; index < input.length; index++) { + hash ^= input.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return hash.toString(16).padStart(8, '0'); +} + +type Draft = { query: string; features: readonly string[]; ledger?: readonly number[]; note?: string }; + +const draft = (query: string, features: readonly string[], extra: Omit = {}): Draft => ({ + query, + features, + ...extra, +}); + +// ── §4 / §5.1.2 symbolic operators × representative value tokens ───────────── +const SYMBOL_OPS = ['=', '==', '===', '!=', '!==', '<', '<=', '>', '>='] as const; +const SYMBOL_VALUES = ['5', 'abc', 'null', 'true', '2.5', 'x%20y'] as const; + +const symbolMatrix: Draft[] = SYMBOL_OPS.flatMap((op) => + SYMBOL_VALUES.map((value) => + draft(`price${op}${value}`, op === '=' ? ['symbol-op', 'form-encoding'] : ['symbol-op'], { + ledger: op === '=' ? [1, 3] : op === '===' || op === '!==' ? [3] : undefined, + }) + ) +); + +// ── §5.1.1 core comparators and Appendix B aliases in FIQL form ────────────── +const CORE_FIQL = ['eq', 'lt', 'le', 'gt', 'ge', 'contains', 'starts_with', 'ends_with'] as const; +const ALIAS_FIQL = [ + 'ne', 'equals', 'equal', 'not_equal', 'notEqual', 'sw', 'ew', 'ct', 'includes', + 'less_than', 'lessThan', 'less_than_equal', 'lessThanEqual', + 'greater_than', 'greaterThan', 'greater_than_equal', 'greaterThanEqual', +] as const; + +const fiqlMatrix: Draft[] = [ + ...CORE_FIQL.map((name) => draft(`price=${name}=10`, ['fiql-core'])), + ...ALIAS_FIQL.map((name) => draft(`price=${name}=10`, ['fiql-alias'], { ledger: [3] })), +]; + +const NOT_PREFIXES = [ + 'not_eq', 'not_in', 'not_contains', 'not_ct', 'not_starts_with', 'not_sw', + 'not_ends_with', 'not_lt', 'not_le', 'not_gt', 'not_ge', 'not_equals', 'not_frobnicate', +] as const; + +const notPrefixMatrix: Draft[] = NOT_PREFIXES.map((name) => + draft(name === 'not_in' ? `labels=${name}=(a,b)` : `labels=${name}=urgent`, ['not-prefix']) +); + +const DRAFTS: Draft[] = [ + // ── §3.2 form encoding — Harper's non-parsing fast path (ledger row 1) ────── + draft('', ['form-encoding'], { note: 'empty query' }), + draft('a=1', ['form-encoding'], { ledger: [1] }), + draft('a=1&b=2', ['form-encoding'], { ledger: [1] }), + draft('a=1&a=2', ['form-encoding'], { ledger: [1], note: 'repeated parameter, no brackets' }), + draft('a=', ['form-encoding'], { ledger: [1], note: 'empty value' }), + draft('a', ['form-encoding'], { ledger: [1], note: 'name with no comparator' }), + draft('a&b', ['form-encoding'], { ledger: [1] }), + draft('a=1&', ['form-encoding'], { ledger: [1], note: 'trailing conjunction' }), + draft('a=1&&b=2', ['form-encoding', 'adversarial'], { ledger: [1] }), + draft('&', ['form-encoding', 'adversarial'], { ledger: [1] }), + draft('=1', ['form-encoding', 'adversarial'], { ledger: [1], note: 'value with no property path' }), + draft('a=1+2', ['form-encoding', 'pct-encoded-value'], { ledger: [1], note: 'raw + — form decoding vs percent decoding' }), + draft('a=1+2&b<3', ['form-encoding', 'pct-encoded-value', 'symbol-op'], { note: 'the same + token, but on the parsing path' }), + draft('a=b%26c', ['form-encoding', 'pct-encoded-value'], { ledger: [1] }), + draft('a%20b=1', ['form-encoding', 'pct-encoded-path'], { ledger: [1] }), + draft('a=%20', ['form-encoding', 'pct-encoded-value'], { ledger: [1] }), + draft('%61=1', ['form-encoding', 'pct-encoded-path'], { ledger: [1] }), + + // ── §5.1.2 wildcards ──────────────────────────────────────────────────────── + draft('name==Jo*', ['wildcard']), + draft('name=eq=Jo*', ['wildcard']), + draft('name==*', ['wildcard'], { note: 'bare wildcard' }), + draft('name==*Jo', ['wildcard', 'adversarial'], { note: 'leading wildcard — §5.1.2 syntax error' }), + draft('name==*Jo*', ['wildcard', 'adversarial'], { note: 'embedded wildcard' }), + draft('name==12*', ['wildcard'], { note: 'the stem is the decoded string, never interpreted' }), + draft('name=starts_with=Jo*', ['wildcard'], { note: 'wildcards apply only to ==' }), + draft('name===Jo*', ['wildcard'], { note: 'verbatim equality does not take a wildcard' }), + draft('name==Jo%2A', ['wildcard', 'pct-encoded-value'], { note: '§4.2 rule 4 — an encoded marker is literal' }), + draft('name!=Jo*', ['wildcard'], { note: 'negated equality with a wildcard stem' }), + + // ── §5.1.2 value lists ────────────────────────────────────────────────────── + draft('a=in=(1,2)', ['value-list', 'fiql-core']), + draft('a=in=(1)', ['value-list', 'fiql-core']), + draft('a=in=()', ['value-list', 'fiql-core'], { note: 'the empty list' }), + draft('a=in=(1,2,3,4,5)', ['value-list', 'fiql-core']), + draft('a=in=(a,b%2Cc)', ['value-list', 'pct-encoded-value']), + draft('a=not_in=(1,2)', ['value-list', 'not-prefix']), + draft('a=out=(1,2)', ['value-list', 'fiql-alias'], { note: 'Appendix B: out desugars to not_in' }), + draft('a=in=(number:1,boolean:true)', ['value-list', 'typed-value'], { note: 'list elements may be typed' }), + draft('a=eq=(1,2)', ['value-list', 'adversarial'], { note: 'a list for a non-list comparator is a syntax error' }), + draft('a=(4)', ['value-list', 'tolerance'], { ledger: [6], note: 'literal "(4)" on a non-list comparator' }), + draft('a=lt=(4)', ['value-list', 'tolerance'], { ledger: [6] }), + + // ── Appendix B between / not_between (ledger row 4) ───────────────────────── + draft('a=between=(1,5)', ['fiql-alias', 'value-list'], { ledger: [4] }), + draft('a=not_between=(1,5)', ['fiql-alias', 'value-list', 'not-prefix'], { ledger: [4] }), + draft('a=between=(1,5,9)', ['fiql-alias', 'adversarial'], { ledger: [4], note: 'between needs exactly two bounds' }), + draft('a=between=1', ['fiql-alias', 'adversarial'], { ledger: [4], note: 'between needs a value list' }), + draft('a=between=(number:1,number:5)', ['fiql-alias', 'typed-value'], { ledger: [4] }), + draft('a=ge=1&=le=5', ['chain'], { ledger: [2], note: 'the canonical spelling of the between alias' }), + + // ── §5.2.2 typed and interpreted values ──────────────────────────────────── + draft('a==3', ['typed-value'], { note: 'round-trip decimal numeral becomes a number' }), + draft('a==-5', ['typed-value']), + draft('a==2.5', ['typed-value']), + draft('a==1e3', ['typed-value'], { note: 'non-round-trip spelling stays a string' }), + draft('a==01', ['typed-value'], { note: 'non-round-trip spelling stays a string' }), + draft('a==1.50', ['typed-value'], { note: 'non-round-trip spelling stays a string' }), + draft('a==null', ['typed-value']), + draft('a==true', ['typed-value']), + draft('a==false', ['typed-value']), + draft('a=null', ['typed-value', 'form-encoding'], { ledger: [1], note: 'verbatim mode keeps the string' }), + draft('a=eq=number:5', ['typed-value']), + draft('a=eq=number:$ff', ['typed-value'], { note: 'base-36 numeral' }), + draft('a=eq=number:-2.5', ['typed-value']), + draft('a=eq=number:abc', ['typed-value', 'adversarial'], { note: 'malformed numeric literal' }), + draft('a=eq=number:', ['typed-value', 'adversarial']), + draft('a=eq=boolean:true', ['typed-value']), + draft('a=eq=boolean:false', ['typed-value']), + draft('a=eq=boolean:yes', ['typed-value', 'adversarial'], { note: 'malformed boolean literal' }), + draft('a=eq=date:2020-01-01', ['typed-value']), + draft('a=eq=date:2024-01-05T20%3A07%3A27.955Z', ['typed-value', 'pct-encoded-value']), + draft('a=eq=date:1602872124871', ['typed-value'], { note: 'epoch millis' }), + draft('a=eq=date:notadate', ['typed-value', 'adversarial'], { note: 'unparseable date literal' }), + draft('a=eq=string:null', ['typed-value'], { note: 'the string: prefix suppresses further interpretation' }), + draft('a=equals=null', ['typed-value', 'fiql-alias'], { ledger: [3], note: 'Appendix B reads equals in verbatim mode' }), + draft('a=equal=null', ['typed-value', 'fiql-alias'], { ledger: [3] }), + draft('a=not_equal=null', ['typed-value', 'fiql-alias'], { ledger: [3] }), + draft('a=ne=null', ['typed-value', 'fiql-alias'], { ledger: [3], note: 'ne is interpreted in both' }), + draft('a=eq=string:3', ['typed-value']), + draft('a=eq=unknown:x', ['typed-value', 'adversarial'], { note: 'unknown type prefix — syntax error' }), + draft('a==string%3Anull', ['typed-value', 'pct-encoded-value'], { note: '§4.2 rule 4 — an encoded marker is literal' }), + draft('a==%2C', ['pct-encoded-value']), + draft('a==%26', ['pct-encoded-value']), + draft('a==%28x%29', ['pct-encoded-value']), + draft('a==a%2Fb', ['pct-encoded-value']), + + // ── §5.5 property paths ──────────────────────────────────────────────────── + draft('brand.name=Microsoft', ['dotted-path']), + draft('a.b.c==1', ['dotted-path']), + draft('a.b.c.d.e==1', ['dotted-path']), + draft('a%2Eb=1', ['pct-encoded-path', 'form-encoding'], { ledger: [1], note: '%2E is a literal dot inside one segment' }), + draft('a%2Eb==1', ['pct-encoded-path']), + draft('a%2Eb.c==1', ['pct-encoded-path', 'dotted-path'], { note: 'segment "a.b" then segment "c"' }), + draft('a.b%2Ec==1', ['pct-encoded-path', 'dotted-path']), + draft('%2E==1', ['pct-encoded-path', 'adversarial'], { note: 'a property literally named "."' }), + draft('a-b_c~d==1', ['dotted-path'], { note: 'unreserved segment characters' }), + draft('a%2Db==1', ['pct-encoded-path']), + + // ── §5.4 grouping ────────────────────────────────────────────────────────── + draft('a=1|b=2', ['grouping-paren'], { ledger: [1] }), + draft('(a=1|b=2)&c=3', ['grouping-paren']), + draft('[a=1|b=2]&c=3', ['grouping-bracket']), + draft('a=1&[b=2|c=3]', ['grouping-bracket']), + draft('(a=1)', ['grouping-paren'], { note: 'a group of one — §6 does not flatten it' }), + draft('[a=1]', ['grouping-bracket']), + draft('a=1&(b=2|(c=3&d=4))', ['grouping-nested']), + draft('[a=1&[b=2|[c=3|d=4]]]', ['grouping-nested']), + draft('(a=1&b=2)|(c=3&d=4)', ['grouping-nested']), + draft('((a=1))', ['grouping-nested']), + draft('a=1&b=2|c=3', ['adversarial'], { ledger: [8], note: '§5.4 forbids mixing & and | in one group' }), + draft('(a=1&b=2|c=3)', ['adversarial'], { note: 'mixing inside a group' }), + draft('()', ['grouping-paren', 'adversarial'], { note: 'empty group' }), + draft('[]', ['grouping-bracket', 'adversarial']), + draft('(a=1&())', ['grouping-nested', 'adversarial']), + draft('(a=in=(1,2)&b=2)', ['grouping-paren', 'value-list'], { note: 'a value list inside a parenthesized group' }), + draft('[a=in=(1,2)&b=2]', ['grouping-bracket', 'value-list']), + draft('x[a=in=(1,2)]', ['scoped-match', 'value-list']), + + // ── §5.3 chaining ────────────────────────────────────────────────────────── + draft('ratings=ge=3&=le=4', ['chain'], { ledger: [2, 10] }), + draft('ratings=ge=3&ratings=le=4', ['chain'], { note: 'contrast: two independent conditions' }), + draft('ratings=ge=3|=le=4', ['chain'], { ledger: [2] }), + draft('ratings=ge=3&=not_eq=4', ['chain', 'not-prefix'], { ledger: [2] }), + draft('ratings=ge=3&=le=4&=ne=5', ['chain'], { ledger: [2], note: 'three-leg chain' }), + draft('ratings=ge=3&=le=4&b=2', ['chain'], { ledger: [2], note: 'a chain followed by an independent condition' }), + draft('a==1&=le=5', ['chain'], { ledger: [2], note: 'chain onto a symbolic-operator head' }), + draft('a=1&=le=5', ['chain'], { ledger: [2], note: 'chain onto a verbatim-equality head' }), + draft('ratings=ge=3&=5', ['chain', 'adversarial'], { ledger: [2], note: 'a nameless leg — a syntax error in 2.0' }), + draft('ratings=ge=3&=le=4|=gt=1', ['chain', 'adversarial'], { note: 'mixing & and | within one chain' }), + draft('&=le=4', ['chain', 'adversarial'], { note: 'a chain with no preceding condition' }), + draft('(a=1)&=le=4', ['chain', 'adversarial'], { note: 'a chain onto a group' }), + draft('a=in=(1,2)&=le=4', ['chain', 'value-list']), + draft('a=between=(1,5)&=ne=3', ['chain', 'fiql-alias'], { ledger: [4] }), + + // ── §5.3 scoped sub-queries and element conditions ───────────────────────── + draft('reviews[rating=ge=4&helpful=ge=10]', ['scoped-match'], { ledger: [14] }), + draft('reviews[rating=ge=4|helpful=ge=10]', ['scoped-match'], { ledger: [14] }), + draft('orders[status=open]', ['scoped-match'], { note: '§5.3 normalizes a single non-negated inner condition' }), + draft('orders[status=not_eq=open]', ['scoped-match', 'not-prefix'], { note: 'a negated inner condition is never flattened' }), + draft('a.b[c=1]', ['scoped-match', 'dotted-path']), + draft('a%2Eb[c=1]', ['scoped-match', 'pct-encoded-path']), + draft('a[b[c=1]]', ['scoped-match', 'grouping-nested']), + draft('a[b=1&c[d=2]]', ['scoped-match', 'grouping-nested']), + draft('x[a=1&b=2]|y=3', ['scoped-match', 'grouping-bracket']), + draft('a[]', ['scoped-match', 'adversarial'], { note: 'an empty scope body' }), + draft('scores[=ge=10|=le=2]', ['elem-cond'], { ledger: [14] }), + draft('scores[=ge=10&=le=20]', ['elem-cond'], { ledger: [14] }), + draft('scores[=ge=10]', ['elem-cond'], { ledger: [14], note: 'equivalent to scores=ge=10 after §5.3 normalization' }), + draft('tags[=not_eq=urgent]', ['elem-cond', 'not-prefix'], { ledger: [14], note: 'some tag differs — contrast tags=not_eq=urgent' }), + draft('tags=not_eq=urgent', ['not-prefix'], { note: 'no tag equals — negation scopes over the existential' }), + draft('reviews[=ge=4&author.name=kim]', ['elem-cond', 'scoped-match', 'dotted-path'], { ledger: [14] }), + + // ── §5.4 not(...) ────────────────────────────────────────────────────────── + draft('not(a=1)', ['not-expr'], { ledger: [13] }), + draft('not(a=1&b=2)', ['not-expr'], { ledger: [13], note: 'De Morgan' }), + draft('not(a=1|b=2)', ['not-expr'], { ledger: [13] }), + draft('not(not(a=1))', ['not-expr'], { ledger: [13], note: 'a nested not cancels' }), + draft('status=open¬(tag=urgent|tag=blocked)', ['not-expr'], { ledger: [13] }), + draft('not(scores[=ge=10&=le=20])', ['not-expr', 'elem-cond'], { ledger: [13, 14] }), + draft('not(reviews[rating=ge=4])', ['not-expr', 'scoped-match'], { ledger: [13, 14] }), + draft('a=1¬(b=2)', ['not-expr'], { ledger: [13] }), + draft('[not(a=1)|b=2]', ['not-expr', 'grouping-bracket'], { ledger: [13] }), + draft('not()', ['not-expr', 'adversarial'], { ledger: [13], note: 'an empty body' }), + draft('not(a=1&b=2|c=3)', ['not-expr', 'adversarial'], { ledger: [13], note: 'mixing still applies inside' }), + + // ── §5.6 sort ────────────────────────────────────────────────────────────── + draft('sort(a)', ['sort'], { ledger: [5] }), + draft('sort(+a)', ['sort'], { ledger: [5] }), + draft('sort(-a)', ['sort'], { ledger: [5] }), + draft('sort(+a,-b)', ['sort'], { ledger: [5] }), + draft('sort(a,b,c)', ['sort'], { ledger: [5] }), + draft('sort(a.b)', ['sort', 'dotted-path'], { ledger: [5] }), + draft('sort(-a.b)', ['sort', 'dotted-path'], { ledger: [5] }), + draft('sort(%2Bname)', ['sort', 'pct-encoded-path'], { note: '§4.2 rule 4 — %2B is a literal name character' }), + draft('sort(%2Dname)', ['sort', 'pct-encoded-path']), + draft('sort(a%2Eb)', ['sort', 'pct-encoded-path']), + draft('sort()', ['sort', 'adversarial'], { note: 'no sort keys' }), + draft('sort(a,)', ['sort', 'adversarial'], { note: 'a trailing comma' }), + draft('sort(a', ['sort', 'adversarial'], { note: 'an unterminated call' }), + draft('a=1&sort(b)', ['sort']), + + // ── §5.7 select ──────────────────────────────────────────────────────────── + draft('select(a)', ['select'], { ledger: [5], note: 'a single field is values mode' }), + draft('select(a,b)', ['select'], { ledger: [5], note: 'records mode' }), + draft('select(a,)', ['select'], { ledger: [5], note: 'a trailing comma forces records mode for one field' }), + draft('select(a,b,)', ['select'], { ledger: [5] }), + draft('select([a,b])', ['select'], { ledger: [5], note: 'tuple rows' }), + draft('select([a])', ['select'], { ledger: [5] }), + draft('select([a,b],c)', ['select'], { ledger: [5], note: 'a tuple item inside a record projection' }), + draft('select(rel{x,y})', ['select'], { ledger: [5], note: 'a nested projection, brace form' }), + draft('select(rel{x})', ['select'], { ledger: [5] }), + draft('select(rel{x,y},id)', ['select'], { ledger: [5] }), + draft('select(rel{x},rel2{y})', ['select'], { ledger: [5] }), + draft('select(rel[select(x,y)])', ['select'], { ledger: [5], note: 'a nested projection, bracket form' }), + draft('select(related{name,otherTable{other_name}},id,name)', ['select'], { ledger: [5], note: 'two levels of nesting' }), + draft('select(related[select(name,otherTable[select(other_name,)])],id,name)', ['select'], { ledger: [5] }), + draft('select(rel{[a,b]})', ['select', 'adversarial'], { note: '§5.7 reserves the nested tuple form' }), + draft('select(a.b)', ['select', 'dotted-path'], { ledger: [5], note: 'a single dotted field' }), + draft('select(a.b,c)', ['select', 'dotted-path'], { ledger: [5] }), + draft('select(a%2Eb)', ['select', 'pct-encoded-path'], { ledger: [5] }), + draft('select()', ['select', 'adversarial'], { note: 'no fields' }), + draft('select(a', ['select', 'adversarial'], { note: 'an unterminated call' }), + draft('select([a,b)', ['select', 'adversarial'], { note: 'mismatched brackets' }), + draft('select(a)&sort(b)&limit(2)', ['select', 'sort', 'limit']), + + // ── §5.6 limit ───────────────────────────────────────────────────────────── + draft('limit(10)', ['limit']), + draft('limit(0)', ['limit']), + draft('limit(5,10)', ['limit'], { note: 'start/end bounds — offset 5, at most 5 records' }), + draft('limit(0,0)', ['limit']), + draft('limit(x)', ['limit', 'adversarial'], { note: 'a non-integer argument is a §5.6 syntax error' }), + draft('limit(-1)', ['limit', 'adversarial'], { note: 'a negative bound' }), + draft('limit(2,1)', ['limit', 'adversarial'], { note: 'end below start' }), + draft('limit(1.5)', ['limit', 'adversarial']), + draft('limit(01)', ['limit', 'adversarial'], { note: 'a non-canonical integer spelling' }), + draft('limit()', ['limit', 'adversarial']), + draft('limit(1,2,3)', ['limit', 'adversarial'], { note: 'too many arguments' }), + draft('limit(5,10', ['limit', 'adversarial'], { note: 'an unterminated call' }), + + // ── §5.6 the call-function namespace ─────────────────────────────────────── + draft('unknown(1)', ['call-error'], { ledger: [8], note: 'call names are a closed set' }), + draft('lt(price,10)', ['call-error'], { note: 'Appendix A — comparators have no call form in 2.0' }), + draft('group-by(a)', ['call-error'], { ledger: [8, 9], note: 'an Appendix C reserved name' }), + draft('count(a)', ['call-error'], { note: 'an Appendix C reserved name' }), + draft('distinct(a)', ['call-error'], { note: 'an Appendix C reserved name' }), + draft('aggregate(a)', ['call-error'], { note: 'an Appendix C reserved name' }), + draft('select(a)&select(b)', ['call-error'], { note: 'a call function may appear only once' }), + draft('limit(1)&limit(2)', ['call-error']), + draft('sort(a)&sort(b)', ['call-error']), + draft('a=1&select(b)&sort(c)&limit(2)', ['select', 'sort', 'limit']), + draft('select(b)&a=1', ['select'], { note: 'a call before a condition' }), + + // ── §4.1 tolerances and host accommodations ──────────────────────────────── + draft('foo=ba)r', ['tolerance'], { ledger: [6], note: '§4.1 — a lenient value scan may take ) literally' }), + draft('foo==ba)r', ['tolerance'], { ledger: [6] }), + draft('foo==ba(r', ['tolerance'], { ledger: [6] }), + draft('foo=ba(r', ['tolerance'], { ledger: [6] }), + draft('foo==a=', ['adversarial']), + draft('a<>1', ['adversarial'], { note: 'not an RQL operator' }), + draft('a=1#b=2', ['adversarial'], { note: 'a fragment delimiter inside a query component' }), + draft('a=1&limit(2)&', ['adversarial'], { ledger: [8] }), + draft('a[b=1', ['adversarial'], { note: 'an unterminated scope' }), + draft('a]b=1', ['adversarial']), + draft('%ZZ=1', ['adversarial', 'pct-encoded-path'], { note: 'a malformed percent escape' }), + draft('a=%ZZ', ['adversarial', 'pct-encoded-value'], { note: 'a malformed percent escape in a value' }), + draft('a==%ZZ', ['adversarial', 'pct-encoded-value']), + draft('a==%E0%A4%A', ['adversarial', 'pct-encoded-value'], { note: 'a truncated UTF-8 escape' }), + draft('a'.repeat(200) + '=1', ['adversarial'], { note: 'a long property name' }), + draft('a=' + '1'.repeat(500), ['adversarial'], { note: 'a long value' }), + draft('(((((((((((a=1)))))))))))', ['adversarial', 'grouping-nested'], { note: 'deep nesting' }), + draft('a=in=(' + Array.from({ length: 50 }, (_, index) => index).join(',') + ')', ['adversarial', 'value-list'], { note: 'a long value list' }), + draft('a[b[c[d[e=1]]]]', ['adversarial', 'scoped-match'], { note: 'deep scoping' }), + draft('a=1&' + Array.from({ length: 40 }, (_, index) => `f${index}=${index}`).join('&'), ['adversarial'], { note: 'many conditions' }), +]; + +function build(): readonly Case[] { + const drafts = [...DRAFTS, ...symbolMatrix, ...fiqlMatrix, ...notPrefixMatrix]; + const seen = new Map(); + const cases: Case[] = []; + for (const item of drafts) { + const id = `q${fnv1a(item.query)}`; + const previous = seen.get(id); + if (previous !== undefined) + throw new Error(`corpus id collision between ${JSON.stringify(previous)} and ${JSON.stringify(item.query)}`); + seen.set(id, item.query); + cases.push({ + id, + query: item.query, + features: item.features, + ...(item.ledger ? { ledger: item.ledger } : {}), + ...(item.note ? { note: item.note } : {}), + }); + } + // Corpus order is declaration order; the fixture and the report both follow it. + return Object.freeze(cases); +} + +/** + * Ledger rows that describe behavior AFTER parsing. A parser-only harness cannot witness + * them, so the report says so instead of leaving them looking untested. + */ +export const EXECUTION_LEVEL_LEDGER_ROWS: Readonly> = { + 11: 'duplicate results from indexed `elements` attributes are produced by query execution, not by parsing', + 12: '`contains` coercing a number to its decimal string happens inside the comparator, at execution', +}; + +export const CORPUS: readonly Case[] = build(); + +/** A fingerprint of the corpus, stamped into the fixture so replay can detect drift. */ +export function corpusDigest(cases: readonly Case[] = CORPUS): string { + let hash = ''; + for (const item of cases) hash = fnv1a(hash + ' ' + item.id + ' ' + item.query); + return `${cases.length}-${hash}`; +} + +export function featureCoverage(cases: readonly Case[] = CORPUS): Map { + const coverage = new Map(); + for (const feature of REQUIRED_FEATURES) coverage.set(feature, []); + for (const item of cases) + for (const feature of item.features) { + const bucket = coverage.get(feature); + if (bucket) bucket.push(item); + else coverage.set(feature, [item]); + } + return coverage; +} + +export function ledgerCoverage(cases: readonly Case[] = CORPUS): Map { + const coverage = new Map(); + for (const item of cases) + for (const row of item.ledger ?? []) { + const bucket = coverage.get(row); + if (bucket) bucket.push(item); + else coverage.set(row, [item]); + } + return coverage; +} diff --git a/conformance/design.md b/conformance/design.md new file mode 100644 index 0000000..cb94736 --- /dev/null +++ b/conformance/design.md @@ -0,0 +1,106 @@ +# Design note — differential conformance harness + +## Intent + +Give the RQL 2.0 specification a repeatable, single-command comparison between Harper's REST +query parser and the reference parser in `src/`, so that Harper's divergence ledger +([HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440), which spec Appendix D +links to) can be exhaustive rather than hand-assembled, and so that the same command can be re-run +as Harper converges. + +## Design assessment + +The change adds a tooling directory and touches neither parser's API nor any production +behaviour. One judgement area is load-bearing: **adapter mapping fidelity** — how faithfully +Harper's internal shapes become the canonical model of `src/types.ts`, since an adapter that is +too generous makes divergences disappear and one that is too literal invents them. The planning +gate therefore applied. + +## Invariant + +For every corpus query, both parsers see exactly the query string; Harper's output is mapped +structurally without reinterpreting any value it already decoded; equal canonical outcomes compare +equal after deterministic serialization; and every unequal outcome appears exactly once in the +generated report under exactly one classification, with an unclassified outcome failing the run. + +## Approaches considered + +### Different layer — make Harper emit the canonical model + +Change `resources/search.ts` so its public parse result already matches `src/types.ts`, and +compare without an adapter. **Rejected:** Harper's parse output is consumed by its request +pipeline in host-specific forms, and changing that production API would fold convergence work into +what is meant to be a diagnostic. Spec §8 explicitly provides for the adapter route. + +### Deeper cause — have Harper consume the reference parser + +Eliminate drift at the source. **Rejected:** the specification is deliberately an independent +*ideal* definition and Harper converges toward it through a ledger, not by adopting the reference +implementation. Sharing the implementation would also destroy the differential independence that +makes the harness worth running. + +### Do less — one witness per known ledger row + +Snapshot a single example for each existing row and stop there. **Rejected:** that cannot reach +the grammar coverage the task requires and, by construction, can never surface an *unknown* +divergence — which is most of what the first run actually found. + +### Chosen — isolated recording plus deterministic replay + +An explicit `--record` command runs each Harper parse in its own short-lived process against a +named `HARPER_PATH` checkout and writes a losslessly tagged fixture stamped with that checkout's +commit. Ordinary runs replay the fixture through a pure adapter, parse the same corpus with a +persistent reference worker (restarted on timeout), diff, classify, and render the committed +report. + +## Planning-gate history + +The first two `--mode plan` reviews did not clear: + +1. `option-set-too-narrow` — the option set had not considered record/replay against live-only. + Escalated; **ruling: adopt record/replay with a shared corpus and per-case timeouts.** +2. `better-alternative-exists` — record/replay agreed, topology refined. Escalated; **ruling: + adopt all three refinements** — + - commit a provenance-stamped snapshot of harper#2440 as a *cache*, with an explicit refresh + command, treating the issue as canonical and reporting the snapshot's age; + - per-case subprocesses **only** for live Harper recording; one persistent reference worker, + restarted on timeout, for replay; + - deterministic timeout handling, fixture provenance checks, a dedicated child IPC channel, + negative controls, and package scripts that actually typecheck and run the new tests. + +All of that is implemented; nothing about the framing is carried into review as an open +disagreement. + +## Components + +- `conformance/corpus.ts` — declared, feature-tagged cases with content-hash ids and a digest. +- `conformance/harperAdapter.ts` — structural mapping to `src/types.ts`; five stated rules, the + load-bearing one being that values pass through untouched. +- `conformance/tagged.ts` — lossless deterministic encoding of raw host values. +- `conformance/canonical.ts` — canonical JSON plus the pointer-addressed differ. +- `conformance/classify.ts` — the residual filter and the ordered rule set. +- `conformance/compare.ts` — run assembly, shared by the runner and the tests. +- `conformance/report.ts` — Markdown rendering from committed data only. +- `scripts/conformance-diff.mjs` — the CLI; `scripts/conformance-record-worker.mjs` and + `scripts/conformance-ref-worker.mjs` — the two process boundaries; + `scripts/refresh-ledger.mjs` — the ledger cache refresher. + +## Behaviour traces + +- Harper is driven through **both** of its entry points. `parseQuery(query)` (no target) throws on + a recorded error and is what the diff uses — it is also how Harper's own unit tests drive it. + `new RequestTarget('?' + query)` is the production path: a `URLSearchParams` that also carries + the parsed query and defers parse errors into `parseError`. Recording both is what lets the + report confirm ledger rows 8 and 9, neither of which is observable through the throwing path. +- The reference parser is driven in both of §6.1's modes for the same reason. +- Determinism: the report contains no wall-clock value. Provenance timestamps come from the + fixture and the ledger snapshot, both committed, so `--check` can assert byte equality. + +## Failure handling + +Recording against a missing or unbuilt checkout fails with an actionable message; replay never +searches for Harper implicitly. A fixture whose schema, corpus digest or case set does not match +fails with the exact refresh command rather than silently mixing revisions. Fixture writes are +atomic. Timeouts are outcomes, not aborts. Tagged encoding refuses cyclic input rather than +looping. An adapter shape that cannot be placed raises `AdapterError` and surfaces as an adapter +gap. Any unclassified divergence exits non-zero. diff --git a/conformance/fixtures/harper-parse.json b/conformance/fixtures/harper-parse.json new file mode 100644 index 0000000..23a7264 --- /dev/null +++ b/conformance/fixtures/harper-parse.json @@ -0,0 +1,22293 @@ +{ + "schema": 1, + "provenance": { + "harper": { + "commit": "11a1c489168dfb00c6a66472cef14cb3f875286f", + "describe": "v5.2.2-14-g11a1c4891", + "dirty": false, + "version": "5.2.2" + }, + "reference": { + "commit": "ab665de98490b53dbcc017857a8e1320d67c704b", + "describe": "v0.3.3-23-gab665de", + "dirty": true + }, + "recordedAt": "2026-09-01T23:38:56.709Z", + "corpusDigest": "349-6b64d3e9", + "node": "v26.2.0" + }, + "cases": { + "q5bb13bf6": { + "query": "a=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1", + "sort": null + } + } + } + }, + "q811c9dc5": { + "query": "", + "strict": { + "status": "ok", + "raw": { + "$": "undefined" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "", + "sort": null + } + } + } + }, + "qd3bb1579": { + "query": "a=1&b=2", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&b=2", + "sort": null + } + } + } + }, + "q7532ec38": { + "query": "a=1&a=2", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ], + [ + "a", + "2" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "a", + "2" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&a=2", + "sort": null + } + } + } + }, + "qe40c292c": { + "query": "a", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a", + "sort": null + } + } + } + }, + "q1024a5c3": { + "query": "a=", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=", + "sort": null + } + } + } + }, + "q28012870": { + "query": "a=1&", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&", + "sort": null + } + } + } + }, + "q94f4e31b": { + "query": "=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "=1", + "sort": null + } + } + } + }, + "qe80fa86f": { + "query": "a=1&&b=2", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&&b=2", + "sort": null + } + } + } + }, + "q409fc854": { + "query": "a&b", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "" + ], + [ + "b", + "" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "" + ], + [ + "b", + "" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a&b", + "sort": null + } + } + } + }, + "q230c8c59": { + "query": "&", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "&", + "sort": null + } + } + } + }, + "q45f2c34f": { + "query": "a=1+2", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1 2" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1 2" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1+2", + "sort": null + } + } + } + }, + "q080b25d2": { + "query": "a=1+2&b<3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1+2" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "lt", + "value": "3" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1 2" + ], + [ + "b<3", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1+2" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "lt", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "a=1+2&b<3", + "sort": null + } + } + } + }, + "q1f8304f0": { + "query": "a=%20", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + " " + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + " " + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=%20", + "sort": null + } + } + } + }, + "qe93d2c03": { + "query": "a=b%26c", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "b&c" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "b&c" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=b%26c", + "sort": null + } + } + } + }, + "q5306c41f": { + "query": "name==Jo*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "Jo" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "=Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "Jo" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name==Jo*", + "sort": null + } + } + } + }, + "qac7f53f7": { + "query": "a%20b=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a b", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a b", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a%20b=1", + "sort": null + } + } + } + }, + "q674214f7": { + "query": "%61=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "%61=1", + "sort": null + } + } + } + }, + "q233cabe9": { + "query": "name=eq=Jo*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "Jo" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "eq=Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "Jo" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name=eq=Jo*", + "sort": null + } + } + } + }, + "q08fed3ba": { + "query": "name==*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "=*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name==*", + "sort": null + } + } + } + }, + "qae4e8a85": { + "query": "name==*Jo*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "*Jo" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "=*Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "*Jo" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name==*Jo*", + "sort": null + } + } + } + }, + "q7a7ae687": { + "query": "name==12*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "12" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "=12*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "12" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name==12*", + "sort": null + } + } + } + }, + "qed82cd6f": { + "query": "name=starts_with=Jo*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "Jo*" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "starts_with=Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "starts_with", + "value": "Jo*" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name=starts_with=Jo*", + "sort": null + } + } + } + }, + "q586532ad": { + "query": "name==*Jo", + "strict": { + "status": "error", + "name": "Error", + "message": "Unable to parse query, wildcard can only be used at the end of a string at position 9 in 'name==*Jo'" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "=*Jo" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "Error", + "message": "Unable to parse query, wildcard can only be used at the end of a string at position 9 in 'name==*Jo'" + }, + "pathname": { + "$": "undefined" + }, + "search": "name==*Jo", + "sort": null + } + } + } + }, + "q073d97da": { + "query": "name===Jo*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "equals", + "value": "Jo*" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "==Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "equals", + "value": "Jo*" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name===Jo*", + "sort": null + } + } + } + }, + "qe3339e45": { + "query": "a=in=(1,2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(1,2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(1,2)", + "sort": null + } + } + } + }, + "q9973b94c": { + "query": "a=in=()", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=()" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=()", + "sort": null + } + } + } + }, + "q788134ff": { + "query": "name==Jo%2A", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "eq", + "value": "Jo*" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name", + "=Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "eq", + "value": "Jo*" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name==Jo%2A", + "sort": null + } + } + } + }, + "q5c1fc2b3": { + "query": "name!=Jo*", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "ne", + "value": "Jo*" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "name!", + "Jo*" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "name", + "comparator": "ne", + "value": "Jo*" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "name!=Jo*", + "sort": null + } + } + } + }, + "q96686d07": { + "query": "a=in=(1)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(1)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(1)", + "sort": null + } + } + } + }, + "q0849860f": { + "query": "a=in=(1,2,3,4,5)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2", + "3", + "4", + "5" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(1,2,3,4,5)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2", + "3", + "4", + "5" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(1,2,3,4,5)", + "sort": null + } + } + } + }, + "qf7be7d1a": { + "query": "a=in=(a,b%2Cc)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "a", + "b,c" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(a,b,c)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "a", + "b,c" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(a,b%2Cc)", + "sort": null + } + } + } + }, + "q9b80a978": { + "query": "a=in=(number:1,boolean:true)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + 1, + true + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(number:1,boolean:true)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + 1, + true + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(number:1,boolean:true)", + "sort": null + } + } + } + }, + "q377f0f8c": { + "query": "a=out=(1,2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "out", + "value": "(1,2)" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "out=(1,2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "out", + "value": "(1,2)" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=out=(1,2)", + "sort": null + } + } + } + }, + "q1460191b": { + "query": "a=not_in=(1,2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "negated": true, + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "not_in=(1,2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "negated": true, + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=not_in=(1,2)", + "sort": null + } + } + } + }, + "q25715cf0": { + "query": "a=eq=(1,2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "(1,2)" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=(1,2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "(1,2)" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=(1,2)", + "sort": null + } + } + } + }, + "qe4750702": { + "query": "a=(4)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "(4)" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "(4)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "(4)" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=(4)", + "sort": null + } + } + } + }, + "qfa0c65b5": { + "query": "a=lt=(4)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "lt", + "value": "(4)" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "lt=(4)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "lt", + "value": "(4)" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=lt=(4)", + "sort": null + } + } + } + }, + "qd5eb65da": { + "query": "a=between=(1,5,9)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": { + "$": "array", + "items": [ + "1", + "5", + "9" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "between=(1,5,9)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": { + "$": "array", + "items": [ + "1", + "5", + "9" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=between=(1,5,9)", + "sort": null + } + } + } + }, + "q03bea649": { + "query": "a=not_between=(1,5)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "negated": true, + "value": { + "$": "array", + "items": [ + "1", + "5" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "not_between=(1,5)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "negated": true, + "value": { + "$": "array", + "items": [ + "1", + "5" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=not_between=(1,5)", + "sort": null + } + } + } + }, + "q38c1ff5b": { + "query": "a=between=(1,5)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": { + "$": "array", + "items": [ + "1", + "5" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "between=(1,5)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": { + "$": "array", + "items": [ + "1", + "5" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=between=(1,5)", + "sort": null + } + } + } + }, + "q58f18283": { + "query": "a=between=1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "between=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=between=1", + "sort": null + } + } + } + }, + "q4a30839b": { + "query": "a=between=(number:1,number:5)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": { + "$": "array", + "items": [ + 1, + 5 + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "between=(number:1,number:5)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "between", + "value": { + "$": "array", + "items": [ + 1, + 5 + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=between=(number:1,number:5)", + "sort": null + } + } + } + }, + "q240a700d": { + "query": "a=ge=1&=le=5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "5" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "ge=1" + ], + [ + "", + "le=5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "5" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=ge=1&=le=5", + "sort": null + } + } + } + }, + "q5a492aa0": { + "query": "a==-5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "-5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=-5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "-5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==-5", + "sort": null + } + } + } + }, + "q251f38cb": { + "query": "a==3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==3", + "sort": null + } + } + } + }, + "q583a02b5": { + "query": "a==2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==2.5", + "sort": null + } + } + } + }, + "q305bb0dd": { + "query": "a==1e3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "1e3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=1e3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "1e3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==1e3", + "sort": null + } + } + } + }, + "q7228fcbd": { + "query": "a==01", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "01" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=01" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "01" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==01", + "sort": null + } + } + } + }, + "q6bc7edc8": { + "query": "a==1.50", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "1.50" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=1.50" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "1.50" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==1.50", + "sort": null + } + } + } + }, + "q9103046b": { + "query": "a==null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==null", + "sort": null + } + } + } + }, + "qfe0aea46": { + "query": "a==true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==true", + "sort": null + } + } + } + }, + "q335ca3a1": { + "query": "a==false", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "false" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=false" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "false" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==false", + "sort": null + } + } + } + }, + "q81917910": { + "query": "a=eq=number:5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": 5 + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=number:5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": 5 + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=number:5", + "sort": null + } + } + } + }, + "q482500b6": { + "query": "a=null", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "null" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "null" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=null", + "sort": null + } + } + } + }, + "q800f614b": { + "query": "a=eq=number:$ff", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": 555 + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=number:$ff" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": 555 + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=number:$ff", + "sort": null + } + } + } + }, + "q835c351f": { + "query": "a=eq=number:-2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": -2.5 + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=number:-2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": -2.5 + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=number:-2.5", + "sort": null + } + } + } + }, + "qfb7055cb": { + "query": "a=eq=number:abc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "number", + "v": "NaN" + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=number:abc" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "number", + "v": "NaN" + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=number:abc", + "sort": null + } + } + } + }, + "q2cfc8c85": { + "query": "a=eq=number:", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": 0 + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=number:" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": 0 + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=number:", + "sort": null + } + } + } + }, + "q854d8fba": { + "query": "a=eq=boolean:true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": true + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=boolean:true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": true + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=boolean:true", + "sort": null + } + } + } + }, + "q72bb948d": { + "query": "a=eq=boolean:false", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": false + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=boolean:false" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": false + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=boolean:false", + "sort": null + } + } + } + }, + "qed51cdb1": { + "query": "a=eq=boolean:yes", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": false + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=boolean:yes" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": false + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=boolean:yes", + "sort": null + } + } + } + }, + "qb5435e86": { + "query": "a=eq=date:2020-01-01", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "iso": "2020-01-01T00:00:00.000Z" + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=date:2020-01-01" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "iso": "2020-01-01T00:00:00.000Z" + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=date:2020-01-01", + "sort": null + } + } + } + }, + "q7f89d8d1": { + "query": "a=eq=date:2024-01-05T20%3A07%3A27.955Z", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "iso": "2024-01-05T20:07:27.955Z" + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=date:2024-01-05T20:07:27.955Z" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "iso": "2024-01-05T20:07:27.955Z" + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=date:2024-01-05T20%3A07%3A27.955Z", + "sort": null + } + } + } + }, + "q2d7831b2": { + "query": "a=eq=date:notadate", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "invalid": true + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=date:notadate" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "invalid": true + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=date:notadate", + "sort": null + } + } + } + }, + "q0fda2074": { + "query": "a=eq=string:null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "null" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=string:null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "null" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=string:null", + "sort": null + } + } + } + }, + "qb79e442b": { + "query": "a=eq=date:1602872124871", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "iso": "2020-10-16T18:15:24.871Z" + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=date:1602872124871" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": { + "$": "date", + "iso": "2020-10-16T18:15:24.871Z" + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=date:1602872124871", + "sort": null + } + } + } + }, + "q3d3ac588": { + "query": "a=equals=null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "equals=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=equals=null", + "sort": null + } + } + } + }, + "q0a807839": { + "query": "a=equal=null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equal", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "equal=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equal", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=equal=null", + "sort": null + } + } + } + }, + "q45a984c3": { + "query": "a=not_equal=null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "not_equal", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "not_equal=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "not_equal", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=not_equal=null", + "sort": null + } + } + } + }, + "q90449a32": { + "query": "a=eq=string:3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=string:3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=string:3", + "sort": null + } + } + } + }, + "qf9eaba00": { + "query": "a=ne=null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "ne", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "ne=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "ne", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=ne=null", + "sort": null + } + } + } + }, + "q4326264a": { + "query": "a==%2C", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "," + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=," + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "," + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==%2C", + "sort": null + } + } + } + }, + "q1e1697cc": { + "query": "a=eq=unknown:x", + "strict": { + "status": "error", + "name": "Error", + "message": "Unable to parse query, Unknown type unknown at position 14 in 'a=eq=unknown:x'" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "eq=unknown:x" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "Error", + "message": "Unable to parse query, Unknown type unknown at position 14 in 'a=eq=unknown:x'" + }, + "pathname": { + "$": "undefined" + }, + "search": "a=eq=unknown:x", + "sort": null + } + } + } + }, + "qb9a06117": { + "query": "a==string%3Anull", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "string:null" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=string:null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "string:null" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==string%3Anull", + "sort": null + } + } + } + }, + "q89911502": { + "query": "a==a%2Fb", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "a/b" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=a/b" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "a/b" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==a%2Fb", + "sort": null + } + } + } + }, + "q25571d4d": { + "query": "a==%28x%29", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "(x)" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=(x)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "(x)" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==%28x%29", + "sort": null + } + } + } + }, + "q641af072": { + "query": "brand.name=Microsoft", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "brand", + "name" + ] + }, + "comparator": "equals", + "value": "Microsoft" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "brand.name", + "Microsoft" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "brand", + "name" + ] + }, + "comparator": "equals", + "value": "Microsoft" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "brand.name=Microsoft", + "sort": null + } + } + } + }, + "q30260861": { + "query": "a==%26", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "&" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=&" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "eq", + "value": "&" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==%26", + "sort": null + } + } + } + }, + "q7cc95261": { + "query": "a.b.c.d.e==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b.c.d.e", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a.b.c.d.e==1", + "sort": null + } + } + } + }, + "qf11ddcd0": { + "query": "a.b.c==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b", + "c" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b.c", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b", + "c" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a.b.c==1", + "sort": null + } + } + } + }, + "qf192f27a": { + "query": "a%2Eb=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a.b", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a%2Eb=1", + "sort": null + } + } + } + }, + "q26a398dc": { + "query": "a%2Eb.c==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a.b", + "c" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b.c", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a.b", + "c" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a%2Eb.c==1", + "sort": null + } + } + } + }, + "q965d8d84": { + "query": "a.b%2Ec==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b.c" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b.c", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b.c" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a.b%2Ec==1", + "sort": null + } + } + } + }, + "q674f6d06": { + "query": "%2E==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": ".", + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + ".", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": ".", + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "%2E==1", + "sort": null + } + } + } + }, + "q0d3613e5": { + "query": "a%2Eb==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a.b", + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a.b", + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a%2Eb==1", + "sort": null + } + } + } + }, + "q15d295c0": { + "query": "a-b_c~d==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a-b_c~d", + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a-b_c~d", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a-b_c~d", + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a-b_c~d==1", + "sort": null + } + } + } + }, + "qf8d78ec2": { + "query": "a%2Db==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a-b", + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a-b", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a-b", + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a%2Db==1", + "sort": null + } + } + } + }, + "qbb613ffb": { + "query": "a=1|b=2", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1|b=2" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "or", + "pathname": { + "$": "undefined" + }, + "search": "a=1|b=2", + "sort": null + } + } + } + }, + "q3443fcf3": { + "query": "(a=1|b=2)&c=3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1|b=2)" + ], + [ + "c", + "3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "(a=1|b=2)&c=3", + "sort": null + } + } + } + }, + "q6e2b8e36": { + "query": "[a=1|b=2]&c=3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[a", + "1|b=2]" + ], + [ + "c", + "3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "[a=1|b=2]&c=3", + "sort": null + } + } + } + }, + "qbc437312": { + "query": "a=1&[b=2|c=3]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "[b", + "2|c=3]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "a=1&[b=2|c=3]", + "sort": null + } + } + } + }, + "q7c6e4647": { + "query": "(a=1)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "(a=1)", + "sort": null + } + } + } + }, + "q1e32a7c3": { + "query": "a=1&(b=2|(c=3&d=4))", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "(b", + "2|(c=3" + ], + [ + "d", + "4))" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "a=1&(b=2|(c=3&d=4))", + "sort": null + } + } + } + }, + "qcbff4926": { + "query": "[a=1]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[a", + "1]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "[a=1]", + "sort": null + } + } + } + }, + "q8a0d7034": { + "query": "((a=1))", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "((a", + "1))" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "((a=1))", + "sort": null + } + } + } + }, + "q13ec207b": { + "query": "(a=1&b=2)|(c=3&d=4)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1" + ], + [ + "b", + "2)|(c=3" + ], + [ + "d", + "4)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "operator": "or", + "pathname": { + "$": "undefined" + }, + "search": "(a=1&b=2)|(c=3&d=4)", + "sort": null + } + } + } + }, + "q377f6f88": { + "query": "a=1&b=2|c=3", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'a=1&b=2|c=3', Can not mix operators within a condition grouping at position 11" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2|c=3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "Can not mix operators within a condition grouping at position 11" + }, + "parseErrorMessage": "Can not mix operators within a condition grouping at position 11", + "pathname": { + "$": "undefined" + }, + "search": "a=1&b=2|c=3", + "sort": null + } + } + } + }, + "q3d53b309": { + "query": "[a=1&[b=2|[c=3|d=4]]]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[a", + "1" + ], + [ + "[b", + "2|[c=3|d=4]]]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "[a=1&[b=2|[c=3|d=4]]]", + "sort": null + } + } + } + }, + "q28d9f59a": { + "query": "()", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "()", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "()", + "sort": null + } + } + } + }, + "q741638a5": { + "query": "[]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[]", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "[]", + "sort": null + } + } + } + }, + "q9a53d080": { + "query": "(a=1&())", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1" + ], + [ + "())", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "(a=1&())", + "sort": null + } + } + } + }, + "qd754d591": { + "query": "(a=1&b=2|c=3)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 13 in '(a=1&b=2|c=3)', Can not mix operators within a condition grouping at position 13" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1" + ], + [ + "b", + "2|c=3)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "Can not mix operators within a condition grouping at position 13" + }, + "parseErrorMessage": "Can not mix operators within a condition grouping at position 13", + "pathname": { + "$": "undefined" + }, + "search": "(a=1&b=2|c=3)", + "sort": null + } + } + } + }, + "qac905c3b": { + "query": "(a=in=(1,2)&b=2)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 16 in '(a=in=(1,2)&b=2)', conditions/comparisons are not allowed in a property list at position 9, no attribute or comparison specified at position 11" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "in=(1,2)" + ], + [ + "b", + "2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "conditions/comparisons are not allowed in a property list at position 9, no attribute or comparison specified at position 11" + }, + "parseErrorMessage": "conditions/comparisons are not allowed in a property list at position 9, no attribute or comparison specified at position 11", + "pathname": { + "$": "undefined" + }, + "search": "(a=in=(1,2)&b=2)", + "sort": null + } + } + } + }, + "qfe472964": { + "query": "[a=in=(1,2)&b=2]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[a", + "in=(1,2)" + ], + [ + "b", + "2]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "[a=in=(1,2)&b=2]", + "sort": null + } + } + } + }, + "q61f480b7": { + "query": "x[a=in=(1,2)]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + }, + "name": "x" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "x[a", + "in=(1,2)]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + }, + "name": "x" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "x[a=in=(1,2)]", + "sort": null + } + } + } + }, + "q93b260ab": { + "query": "ratings=ge=3&=le=4", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=le=4", + "sort": null + } + } + } + }, + "qabc26be9": { + "query": "ratings=ge=3|=le=4", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "or", + "value": "3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3|=le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "or", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3|=le=4", + "sort": null + } + } + } + }, + "q0b1d9bdd": { + "query": "ratings=ge=3&ratings=le=4", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "comparator": "ge", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "ratings", + "comparator": "le", + "value": "4" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "ratings", + "le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "comparator": "ge", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "ratings", + "comparator": "le", + "value": "4" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&ratings=le=4", + "sort": null + } + } + } + }, + "q8fbf7454": { + "query": "ratings=ge=3&=not_eq=4", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "eq", + "negated": true, + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "not_eq=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "eq", + "negated": true, + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=not_eq=4", + "sort": null + } + } + } + }, + "qd6bc9379": { + "query": "ratings=ge=3&=le=4&=ne=5", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 24 in 'ratings=ge=3&=le=4&=ne=5', attribute must be specified before equality comparator at position 20" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "le=4" + ], + [ + "", + "ne=5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + }, + { + "$": "object", + "props": { + "attribute": null, + "comparator": "ne", + "value": "5" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "attribute must be specified before equality comparator at position 20" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 20", + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=le=4&=ne=5", + "sort": null + } + } + } + }, + "q5dd20ac5": { + "query": "a==1&=le=5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "5" + } + } + ] + }, + "comparator": "eq", + "operator": "and", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=1" + ], + [ + "", + "le=5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "5" + } + } + ] + }, + "comparator": "eq", + "operator": "and", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a==1&=le=5", + "sort": null + } + } + } + }, + "q48dce74c": { + "query": "ratings=ge=3&=le=4&b=2", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "le=4" + ], + [ + "b", + "2" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=le=4&b=2", + "sort": null + } + } + } + }, + "qd7cacd58": { + "query": "ratings=ge=3&=5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "ge", + "value": "5" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "ge", + "value": "5" + } + } + ] + }, + "comparator": "ge", + "operator": "and", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=5", + "sort": null + } + } + } + }, + "q7a1e03c2": { + "query": "a=1&=le=5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "5" + } + } + ] + }, + "comparator": "equals", + "operator": "and", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "", + "le=5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "5" + } + } + ] + }, + "comparator": "equals", + "operator": "and", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&=le=5", + "sort": null + } + } + } + }, + "qd25b0a59": { + "query": "ratings=ge=3&=le=4|=gt=1", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 24 in 'ratings=ge=3&=le=4|=gt=1', attribute must be specified before equality comparator at position 20" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "le=4|=gt=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + }, + { + "$": "object", + "props": { + "attribute": null, + "comparator": "gt", + "value": "1" + } + } + ] + }, + "comparator": "ge", + "operator": "or", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "attribute must be specified before equality comparator at position 20" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 20", + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=le=4|=gt=1", + "sort": null + } + } + } + }, + "qbad87f96": { + "query": "a=in=(1,2)&=le=4", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "in", + "operator": "and", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(1,2)" + ], + [ + "", + "le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "in", + "operator": "and", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(1,2)&=le=4", + "sort": null + } + } + } + }, + "qf67d8316": { + "query": "&=le=4", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 6 in '&=le=4', no comparison specified before '&' at position 1, attribute must be specified before equality comparator at position 2" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "", + "le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 6 in '&=le=4', no comparison specified before '&' at position 1, attribute must be specified before equality comparator at position 2" + }, + "parseErrorMessage": "no comparison specified before '&' at position 1, attribute must be specified before equality comparator at position 2", + "pathname": { + "$": "undefined" + }, + "search": "&=le=4", + "sort": null + } + } + } + }, + "q584be364": { + "query": "(a=1)&=le=4", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in '(a=1)&=le=4', attribute must be specified before equality comparator at position 7" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1)" + ], + [ + "", + "le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "attribute must be specified before equality comparator at position 7" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 7", + "pathname": { + "$": "undefined" + }, + "search": "(a=1)&=le=4", + "sort": null + } + } + } + }, + "qbc933def": { + "query": "a=between=(1,5)&=ne=3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "ne", + "value": "3" + } + } + ] + }, + "comparator": "between", + "operator": "and", + "value": { + "$": "array", + "items": [ + "1", + "5" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "between=(1,5)" + ], + [ + "", + "ne=3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "ne", + "value": "3" + } + } + ] + }, + "comparator": "between", + "operator": "and", + "value": { + "$": "array", + "items": [ + "1", + "5" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=between=(1,5)&=ne=3", + "sort": null + } + } + } + }, + "q758cc252": { + "query": "reviews[rating=ge=4&helpful=ge=10]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "rating", + "comparator": "ge", + "value": "4" + } + }, + { + "$": "object", + "props": { + "attribute": "helpful", + "comparator": "ge", + "value": "10" + } + } + ] + }, + "name": "reviews", + "operator": "and" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "reviews[rating", + "ge=4" + ], + [ + "helpful", + "ge=10]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "rating", + "comparator": "ge", + "value": "4" + } + }, + { + "$": "object", + "props": { + "attribute": "helpful", + "comparator": "ge", + "value": "10" + } + } + ] + }, + "name": "reviews", + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "reviews[rating=ge=4&helpful=ge=10]", + "sort": null + } + } + } + }, + "qf928b50c": { + "query": "reviews[rating=ge=4|helpful=ge=10]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "rating", + "comparator": "ge", + "value": "4" + } + }, + { + "$": "object", + "props": { + "attribute": "helpful", + "comparator": "ge", + "value": "10" + } + } + ] + }, + "name": "reviews", + "operator": "or" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "reviews[rating", + "ge=4|helpful=ge=10]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "rating", + "comparator": "ge", + "value": "4" + } + }, + { + "$": "object", + "props": { + "attribute": "helpful", + "comparator": "ge", + "value": "10" + } + } + ] + }, + "name": "reviews", + "operator": "or" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "reviews[rating=ge=4|helpful=ge=10]", + "sort": null + } + } + } + }, + "q2a49ec41": { + "query": "orders[status=open]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "status", + "comparator": "equals", + "value": "open" + } + } + ] + }, + "name": "orders" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "orders[status", + "open]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "status", + "comparator": "equals", + "value": "open" + } + } + ] + }, + "name": "orders" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "orders[status=open]", + "sort": null + } + } + } + }, + "q862308b0": { + "query": "orders[status=not_eq=open]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "status", + "comparator": "eq", + "negated": true, + "value": "open" + } + } + ] + }, + "name": "orders" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "orders[status", + "not_eq=open]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "status", + "comparator": "eq", + "negated": true, + "value": "open" + } + } + ] + }, + "name": "orders" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "orders[status=not_eq=open]", + "sort": null + } + } + } + }, + "qf3bd4dcb": { + "query": "a.b[c=1]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "a.b" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b[c", + "1]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "a.b" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a.b[c=1]", + "sort": null + } + } + } + }, + "qc2cdcedf": { + "query": "a%2Eb[c=1]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "a%2Eb" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b[c", + "1]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "a%2Eb" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a%2Eb[c=1]", + "sort": null + } + } + } + }, + "qf97a5a16": { + "query": "a[b=1&c[d=2]]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "name": "c" + }, + "ctor": "Query" + } + ] + }, + "name": "a", + "operator": "and" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a[b", + "1" + ], + [ + "c[d", + "2]]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "name": "c" + }, + "ctor": "Query" + } + ] + }, + "name": "a", + "operator": "and" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a[b=1&c[d=2]]", + "sort": null + } + } + } + }, + "q13b19473": { + "query": "a[b[c=1]]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "b" + }, + "ctor": "Query" + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a[b[c", + "1]]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "b" + }, + "ctor": "Query" + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a[b[c=1]]", + "sort": null + } + } + } + }, + "qb1e2d91e": { + "query": "x[a=1&b=2]|y=3", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "name": "x", + "operator": "and" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "y", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "x[a", + "1" + ], + [ + "b", + "2]|y=3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + }, + "name": "x", + "operator": "and" + }, + "ctor": "Query" + }, + { + "$": "object", + "props": { + "attribute": "y", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "or", + "pathname": { + "$": "undefined" + }, + "search": "x[a=1&b=2]|y=3", + "sort": null + } + } + } + }, + "q04ad3e98": { + "query": "a[]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a[]", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a[]", + "sort": null + } + } + } + }, + "q10b36efe": { + "query": "scores[=ge=10|=le=2]", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'scores[=ge=10|=le=2]', attribute must be specified before equality comparator at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "scores[", + "ge=10|=le=2]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'scores[=ge=10|=le=2]', attribute must be specified before equality comparator at position 8" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 8", + "pathname": { + "$": "undefined" + }, + "search": "scores[=ge=10|=le=2]", + "sort": null + } + } + } + }, + "qf4ef4d26": { + "query": "scores[=ge=10&=le=20]", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'scores[=ge=10&=le=20]', attribute must be specified before equality comparator at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "scores[", + "ge=10" + ], + [ + "", + "le=20]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'scores[=ge=10&=le=20]', attribute must be specified before equality comparator at position 8" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 8", + "pathname": { + "$": "undefined" + }, + "search": "scores[=ge=10&=le=20]", + "sort": null + } + } + } + }, + "q099d768f": { + "query": "scores[=ge=10]", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 14 in 'scores[=ge=10]', attribute must be specified before equality comparator at position 8, no attribute or comparison specified at position 14" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "scores[", + "ge=10]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "scores" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "attribute must be specified before equality comparator at position 8, no attribute or comparison specified at position 14" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 8, no attribute or comparison specified at position 14", + "pathname": { + "$": "undefined" + }, + "search": "scores[=ge=10]", + "sort": null + } + } + } + }, + "q9bc2ab21": { + "query": "tags[=not_eq=urgent]", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 20 in 'tags[=not_eq=urgent]', attribute must be specified before equality comparator at position 6, no attribute or comparison specified at position 20" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "tags[", + "not_eq=urgent]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "tags" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "attribute must be specified before equality comparator at position 6, no attribute or comparison specified at position 20" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 6, no attribute or comparison specified at position 20", + "pathname": { + "$": "undefined" + }, + "search": "tags[=not_eq=urgent]", + "sort": null + } + } + } + }, + "q8c0fbb8b": { + "query": "tags=not_eq=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "tags", + "comparator": "eq", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "tags", + "not_eq=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "tags", + "comparator": "eq", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "tags=not_eq=urgent", + "sort": null + } + } + } + }, + "qcaef3596": { + "query": "not(a=1)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'not(a=1)', unknown query function call not at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(a", + "1)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call not at position 8" + }, + "parseErrorMessage": "unknown query function call not at position 8", + "pathname": { + "$": "undefined" + }, + "search": "not(a=1)", + "sort": null + } + } + } + }, + "qfb9f25ea": { + "query": "reviews[=ge=4&author.name=kim]", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'reviews[=ge=4&author.name=kim]', attribute must be specified before equality comparator at position 9" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "reviews[", + "ge=4" + ], + [ + "author.name", + "kim]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'reviews[=ge=4&author.name=kim]', attribute must be specified before equality comparator at position 9" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 9", + "pathname": { + "$": "undefined" + }, + "search": "reviews[=ge=4&author.name=kim]", + "sort": null + } + } + } + }, + "q7040a9ef": { + "query": "not(a=1&b=2)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 8 in 'not(a=1&b=2)', conditions/comparisons are not allowed in a property list at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(a", + "1" + ], + [ + "b", + "2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 8 in 'not(a=1&b=2)', conditions/comparisons are not allowed in a property list at position 8" + }, + "parseErrorMessage": "conditions/comparisons are not allowed in a property list at position 8", + "pathname": { + "$": "undefined" + }, + "search": "not(a=1&b=2)", + "sort": null + } + } + } + }, + "qbf8d8c65": { + "query": "not(a=1|b=2)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 8 in 'not(a=1|b=2)', conditions/comparisons are not allowed in a property list at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(a", + "1|b=2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 8 in 'not(a=1|b=2)', conditions/comparisons are not allowed in a property list at position 8" + }, + "parseErrorMessage": "conditions/comparisons are not allowed in a property list at position 8", + "pathname": { + "$": "undefined" + }, + "search": "not(a=1|b=2)", + "sort": null + } + } + } + }, + "q0e094248": { + "query": "not(not(a=1))", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 13 in 'not(not(a=1))', unknown query function call not at position 12, unknown query function call not at position 13" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(not(a", + "1))" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call not at position 12, unknown query function call not at position 13" + }, + "parseErrorMessage": "unknown query function call not at position 12, unknown query function call not at position 13", + "pathname": { + "$": "undefined" + }, + "search": "not(not(a=1))", + "sort": null + } + } + } + }, + "qd6a5533b": { + "query": "status=open¬(tag=urgent|tag=blocked)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 27 in 'status=open¬(tag=urgent|tag=blocked)', conditions/comparisons are not allowed in a property list at position 27" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "status", + "open" + ], + [ + "not(tag", + "urgent|tag=blocked)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "status", + "comparator": "equals", + "value": "open" + } + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 27 in 'status=open¬(tag=urgent|tag=blocked)', conditions/comparisons are not allowed in a property list at position 27" + }, + "parseErrorMessage": "conditions/comparisons are not allowed in a property list at position 27", + "pathname": { + "$": "undefined" + }, + "search": "status=open¬(tag=urgent|tag=blocked)", + "sort": null + } + } + } + }, + "q1822eb86": { + "query": "not(scores[=ge=10&=le=20])", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 18 in 'not(scores[=ge=10&=le=20])', attribute must be specified before equality comparator at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(scores[", + "ge=10" + ], + [ + "", + "le=20])" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 18 in 'not(scores[=ge=10&=le=20])', attribute must be specified before equality comparator at position 12" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 12", + "pathname": { + "$": "undefined" + }, + "search": "not(scores[=ge=10&=le=20])", + "sort": null + } + } + } + }, + "q0b9f743b": { + "query": "not(reviews[rating=ge=4])", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 25 in 'not(reviews[rating=ge=4])', expected operator, but encountered 'ge' at position 22, expected ')', but encountered ']' at position 24, unexpected operator ']' at position 24, unknown query function call not at position 25" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(reviews[rating", + "ge=4])" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "expected operator, but encountered 'ge' at position 22, expected ')', but encountered ']' at position 24, unexpected operator ']' at position 24, unknown query function call not at position 25" + }, + "parseErrorMessage": "expected operator, but encountered 'ge' at position 22, expected ')', but encountered ']' at position 24, unexpected operator ']' at position 24, unknown query function call not at position 25", + "pathname": { + "$": "undefined" + }, + "search": "not(reviews[rating=ge=4])", + "sort": null + } + } + } + }, + "q40f8695b": { + "query": "a=1¬(b=2)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'a=1¬(b=2)', unknown query function call not at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "not(b", + "2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call not at position 12" + }, + "parseErrorMessage": "unknown query function call not at position 12", + "pathname": { + "$": "undefined" + }, + "search": "a=1¬(b=2)", + "sort": null + } + } + } + }, + "q77fb6d87": { + "query": "[not(a=1)|b=2]", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 14 in '[not(a=1)|b=2]', unknown query function call not at position 9" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[not(a", + "1)|b=2]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call not at position 9" + }, + "parseErrorMessage": "unknown query function call not at position 9", + "pathname": { + "$": "undefined" + }, + "search": "[not(a=1)|b=2]", + "sort": null + } + } + } + }, + "q2cc2b2fd": { + "query": "not()", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 5 in 'not()', unknown query function call not at position 5" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not()", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call not at position 5" + }, + "parseErrorMessage": "unknown query function call not at position 5", + "pathname": { + "$": "undefined" + }, + "search": "not()", + "sort": null + } + } + } + }, + "q7b11cacc": { + "query": "not(a=1&b=2|c=3)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 8 in 'not(a=1&b=2|c=3)', conditions/comparisons are not allowed in a property list at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "not(a", + "1" + ], + [ + "b", + "2|c=3)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'length') at position 8 in 'not(a=1&b=2|c=3)', conditions/comparisons are not allowed in a property list at position 8" + }, + "parseErrorMessage": "conditions/comparisons are not allowed in a property list at position 8", + "pathname": { + "$": "undefined" + }, + "search": "not(a=1&b=2|c=3)", + "sort": null + } + } + } + }, + "q261f5cc5": { + "query": "sort(a)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(a)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false + } + } + } + } + } + }, + "qe37e525e": { + "query": "sort(+a)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort( a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(+a)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false + } + } + } + } + } + }, + "q0ce60830": { + "query": "sort(-a)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": true + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(-a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(-a)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": true + } + } + } + } + } + }, + "q75b2152d": { + "query": "sort(+a,-b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "b", + "descending": true + } + } + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort( a,-b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(+a,-b)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "b", + "descending": true + } + } + } + } + } + } + } + }, + "qa2ed9670": { + "query": "sort(a,b,c)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "b", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "c", + "descending": false + } + } + } + } + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a,b,c)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(a,b,c)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "b", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "c", + "descending": false + } + } + } + } + } + } + } + } + } + }, + "q8903effd": { + "query": "sort(a.b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a.b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(a.b)", + "sort": { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "descending": false + } + } + } + } + } + }, + "qc6433d54": { + "query": "sort(-a.b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "descending": true + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(-a.b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(-a.b)", + "sort": { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "descending": true + } + } + } + } + } + }, + "qab8e546c": { + "query": "sort(%2Bname)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "name", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(+name)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(%2Bname)", + "sort": { + "$": "object", + "props": { + "attribute": "name", + "descending": false + } + } + } + } + } + }, + "q4120e2d2": { + "query": "sort(%2Dname)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "name", + "descending": true + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(-name)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(%2Dname)", + "sort": { + "$": "object", + "props": { + "attribute": "name", + "descending": true + } + } + } + } + } + }, + "qaa960731": { + "query": "sort(a%2Eb)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a.b", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a.b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(a%2Eb)", + "sort": { + "$": "object", + "props": { + "attribute": "a.b", + "descending": false + } + } + } + } + } + }, + "qf35281dd": { + "query": "sort(a,)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "", + "descending": false + } + } + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a,)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(a,)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false, + "next": { + "$": "object", + "props": { + "attribute": "", + "descending": false + } + } + } + } + } + } + } + }, + "qf2add796": { + "query": "sort()", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in 'sort()', Unknown sort type undefined at position 6" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort()", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "Unknown sort type undefined at position 6" + }, + "parseErrorMessage": "Unknown sort type undefined at position 6", + "pathname": { + "$": "undefined" + }, + "search": "sort()", + "sort": { + "$": "undefined" + } + } + } + } + }, + "qbaad7f6e": { + "query": "sort(a", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in 'sort(a', expected ')', but encountered end of string} at position 6, no comparison specified before end of string at position 6, Unknown sort type undefined at position 6" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "expected ')', but encountered end of string} at position 6, no comparison specified before end of string at position 6, Unknown sort type undefined at position 6" + }, + "parseErrorMessage": "expected ')', but encountered end of string} at position 6, no comparison specified before end of string at position 6, Unknown sort type undefined at position 6", + "pathname": { + "$": "undefined" + }, + "search": "sort(a", + "sort": { + "$": "undefined" + } + } + } + } + }, + "q3275c249": { + "query": "select(a)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": "a" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a)", + "select": "a", + "sort": null + } + } + } + }, + "qcb148f59": { + "query": "a=1&sort(b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "sort": { + "$": "object", + "props": { + "attribute": "b", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "sort(b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&sort(b)", + "sort": { + "$": "object", + "props": { + "attribute": "b", + "descending": false + } + } + } + } + } + }, + "qc612622b": { + "query": "select(a,b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "a", + "b" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a,b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a,b)", + "select": { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "sort": null + } + } + } + }, + "q536888b1": { + "query": "select(a,)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "a" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a,)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a,)", + "select": { + "$": "array", + "items": [ + "a" + ] + }, + "sort": null + } + } + } + }, + "q54f7d4b7": { + "query": "select(a,b,)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "a", + "b", + "" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a,b,)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a,b,)", + "select": { + "$": "array", + "items": [ + "a", + "b", + "" + ] + }, + "sort": null + } + } + } + }, + "qb06ba3d0": { + "query": "select([a,b],c)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "c" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select([a,b],c)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select([a,b],c)", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "c" + ] + }, + "sort": null + } + } + } + }, + "qc425c8df": { + "query": "select([a,b])", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "a", + "b" + ], + "props": { + "asArray": true + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select([a,b])", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select([a,b])", + "select": { + "$": "array", + "items": [ + "a", + "b" + ], + "props": { + "asArray": true + } + }, + "sort": null + } + } + } + }, + "qa938e8b5": { + "query": "select([a])", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "a" + ], + "props": { + "asArray": true + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select([a])", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select([a])", + "select": { + "$": "array", + "items": [ + "a" + ], + "props": { + "asArray": true + } + }, + "sort": null + } + } + } + }, + "q7e542578": { + "query": "select(rel{x,y})", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "x", + "y" + ], + "props": { + "name": "rel" + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(rel{x,y})", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(rel{x,y})", + "select": { + "$": "array", + "items": [ + "x", + "y" + ], + "props": { + "name": "rel" + } + }, + "sort": null + } + } + } + }, + "qa2275843": { + "query": "select(rel{x})", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "x" + ], + "props": { + "name": "rel" + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(rel{x})", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(rel{x})", + "select": { + "$": "array", + "items": [ + "x" + ], + "props": { + "name": "rel" + } + }, + "sort": null + } + } + } + }, + "qf0f79389": { + "query": "select(rel{x,y},id)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "x", + "y" + ], + "props": { + "name": "rel" + } + }, + "id" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(rel{x,y},id)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(rel{x,y},id)", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "x", + "y" + ], + "props": { + "name": "rel" + } + }, + "id" + ] + }, + "sort": null + } + } + } + }, + "q2d187d59": { + "query": "select(rel[select(x,y)])", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "rel", + "select": { + "$": "array", + "items": [ + "x", + "y" + ] + } + }, + "ctor": "Query" + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(rel[select(x,y)])", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(rel[select(x,y)])", + "select": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "rel", + "select": { + "$": "array", + "items": [ + "x", + "y" + ] + } + }, + "ctor": "Query" + }, + "sort": null + } + } + } + }, + "q9bcaf069": { + "query": "select(rel{x},rel2{y})", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "x" + ], + "props": { + "name": "rel" + } + }, + { + "$": "array", + "items": [ + "y" + ], + "props": { + "name": "rel2" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(rel{x},rel2{y})", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(rel{x},rel2{y})", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "x" + ], + "props": { + "name": "rel" + } + }, + { + "$": "array", + "items": [ + "y" + ], + "props": { + "name": "rel2" + } + } + ] + }, + "sort": null + } + } + } + }, + "q055a7984": { + "query": "select(related{name,otherTable{other_name}},id,name)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "name", + { + "$": "array", + "items": [ + "other_name" + ], + "props": { + "name": "otherTable" + } + } + ], + "props": { + "name": "related" + } + }, + "id", + "name" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(related{name,otherTable{other_name}},id,name)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(related{name,otherTable{other_name}},id,name)", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "name", + { + "$": "array", + "items": [ + "other_name" + ], + "props": { + "name": "otherTable" + } + } + ], + "props": { + "name": "related" + } + }, + "id", + "name" + ] + }, + "sort": null + } + } + } + }, + "qf101a79c": { + "query": "select(related[select(name,otherTable[select(other_name,)])],id,name)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "related", + "select": { + "$": "array", + "items": [ + "name", + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "otherTable", + "select": { + "$": "array", + "items": [ + "other_name" + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + }, + "id", + "name" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(related[select(name,otherTable[select(other_name,)])],id,name)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(related[select(name,otherTable[select(other_name,)])],id,name)", + "select": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "related", + "select": { + "$": "array", + "items": [ + "name", + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "name": "otherTable", + "select": { + "$": "array", + "items": [ + "other_name" + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + }, + "id", + "name" + ] + }, + "sort": null + } + } + } + }, + "q757437ce": { + "query": "select(rel{[a,b]})", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + } + ], + "props": { + "name": "rel" + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(rel{[a,b]})", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(rel{[a,b]})", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + } + ], + "props": { + "name": "rel" + } + }, + "sort": null + } + } + } + }, + "q0f11fc49": { + "query": "select(a.b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + "a", + "b" + ], + "props": { + "asArray": true + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a.b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a.b)", + "select": { + "$": "array", + "items": [ + "a", + "b" + ], + "props": { + "asArray": true + } + }, + "sort": null + } + } + } + }, + "q285e7865": { + "query": "select(a%2Eb)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": "a.b" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a.b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a%2Eb)", + "select": "a.b", + "sort": null + } + } + } + }, + "q4dcca36e": { + "query": "select(a.b,c)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "c" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a.b,c)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a.b,c)", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "c" + ] + }, + "sort": null + } + } + } + }, + "q8e645e42": { + "query": "select()", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select()", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select()", + "select": { + "$": "array", + "items": [] + }, + "sort": null + } + } + } + }, + "q5664061a": { + "query": "select(a", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'select(a', expected ')', but encountered end of string} at position 8, no comparison specified before end of string at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "expected ')', but encountered end of string} at position 8, no comparison specified before end of string at position 8" + }, + "parseErrorMessage": "expected ')', but encountered end of string} at position 8, no comparison specified before end of string at position 8", + "pathname": { + "$": "undefined" + }, + "search": "select(a", + "select": { + "$": "array", + "items": [] + }, + "sort": null + } + } + } + }, + "q6d485cc8": { + "query": "select(a)&sort(b)&limit(2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 2, + "select": "a", + "sort": { + "$": "object", + "props": { + "attribute": "b", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a)", + "" + ], + [ + "sort(b)", + "" + ], + [ + "limit(2)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 2, + "pathname": { + "$": "undefined" + }, + "search": "select(a)&sort(b)&limit(2)", + "select": "a", + "sort": { + "$": "object", + "props": { + "attribute": "b", + "descending": false + } + } + } + } + } + }, + "q8b202070": { + "query": "select([a,b)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'select([a,b)', expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select([a,b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12" + }, + "parseErrorMessage": "expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12", + "pathname": { + "$": "undefined" + }, + "search": "select([a,b)", + "select": { + "$": "array", + "items": [ + "a" + ], + "props": { + "asArray": true + } + }, + "sort": null + } + } + } + }, + "q70a1675c": { + "query": "limit(10)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 10 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(10)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 10, + "pathname": { + "$": "undefined" + }, + "search": "limit(10)", + "sort": null + } + } + } + }, + "q1c91063f": { + "query": "limit(5,10)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 5, + "offset": 5 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(5,10)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 5, + "offset": 5, + "pathname": { + "$": "undefined" + }, + "search": "limit(5,10)", + "sort": null + } + } + } + }, + "qfc3b985f": { + "query": "limit(0)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 0 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(0)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 0, + "pathname": { + "$": "undefined" + }, + "search": "limit(0)", + "sort": null + } + } + } + }, + "q34c278bb": { + "query": "limit(0,0)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 0, + "offset": 0 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(0,0)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 0, + "offset": 0, + "pathname": { + "$": "undefined" + }, + "search": "limit(0,0)", + "sort": null + } + } + } + }, + "qfcee83d7": { + "query": "limit(x)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": { + "$": "number", + "v": "NaN" + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(x)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": { + "$": "number", + "v": "NaN" + }, + "pathname": { + "$": "undefined" + }, + "search": "limit(x)", + "sort": null + } + } + } + }, + "qf6a48d35": { + "query": "limit(-1)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": -1 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(-1)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": -1, + "pathname": { + "$": "undefined" + }, + "search": "limit(-1)", + "sort": null + } + } + } + }, + "q981b5e08": { + "query": "limit(2,1)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": -1, + "offset": 2 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(2,1)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": -1, + "offset": 2, + "pathname": { + "$": "undefined" + }, + "search": "limit(2,1)", + "sort": null + } + } + } + }, + "q7928c071": { + "query": "limit(1.5)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": { + "$": "number", + "v": "NaN" + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(1.5)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": { + "$": "number", + "v": "NaN" + }, + "pathname": { + "$": "undefined" + }, + "search": "limit(1.5)", + "sort": null + } + } + } + }, + "qe80c300a": { + "query": "limit(01)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 1 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(01)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 1, + "pathname": { + "$": "undefined" + }, + "search": "limit(01)", + "sort": null + } + } + } + }, + "q2a72a507": { + "query": "limit()", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 7 in 'limit()', limit must have 1 or 2 arguments at position 7" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit()", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "limit must have 1 or 2 arguments at position 7" + }, + "parseErrorMessage": "limit must have 1 or 2 arguments at position 7", + "pathname": { + "$": "undefined" + }, + "search": "limit()", + "sort": null + } + } + } + }, + "qaadb840c": { + "query": "limit(5,10", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 10 in 'limit(5,10', expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(5,10", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 5, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10" + }, + "parseErrorMessage": "expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10", + "pathname": { + "$": "undefined" + }, + "search": "limit(5,10", + "sort": null + } + } + } + }, + "q1a35685f": { + "query": "limit(1,2,3)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'limit(1,2,3)', limit must have 1 or 2 arguments at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(1,2,3)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "limit must have 1 or 2 arguments at position 12" + }, + "parseErrorMessage": "limit must have 1 or 2 arguments at position 12", + "pathname": { + "$": "undefined" + }, + "search": "limit(1,2,3)", + "sort": null + } + } + } + }, + "q27c2778d": { + "query": "unknown(1)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 10 in 'unknown(1)', unknown query function call unknown at position 10" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "unknown(1)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call unknown at position 10" + }, + "parseErrorMessage": "unknown query function call unknown at position 10", + "pathname": { + "$": "undefined" + }, + "search": "unknown(1)", + "sort": null + } + } + } + }, + "q1ef8b8e4": { + "query": "lt(price,10)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'lt(price,10)', unknown query function call lt at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "lt(price,10)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call lt at position 12" + }, + "parseErrorMessage": "unknown query function call lt at position 12", + "pathname": { + "$": "undefined" + }, + "search": "lt(price,10)", + "sort": null + } + } + } + }, + "qe50ac660": { + "query": "group-by(a)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'group-by(a)', group by is not implemented yet at position 11" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "group-by(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "group by is not implemented yet at position 11" + }, + "parseErrorMessage": "group by is not implemented yet at position 11", + "pathname": { + "$": "undefined" + }, + "search": "group-by(a)", + "sort": { + "$": "object", + "props": { + "attribute": "a", + "descending": false + } + } + } + } + } + }, + "q1d97c032": { + "query": "count(a)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'count(a)', unknown query function call count at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "count(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call count at position 8" + }, + "parseErrorMessage": "unknown query function call count at position 8", + "pathname": { + "$": "undefined" + }, + "search": "count(a)", + "sort": null + } + } + } + }, + "qc2dfd8a9": { + "query": "distinct(a)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'distinct(a)', unknown query function call distinct at position 11" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "distinct(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call distinct at position 11" + }, + "parseErrorMessage": "unknown query function call distinct at position 11", + "pathname": { + "$": "undefined" + }, + "search": "distinct(a)", + "sort": null + } + } + } + }, + "q9c84ee42": { + "query": "select(a)&select(b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": "b" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a)", + "" + ], + [ + "select(b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a)&select(b)", + "select": "b", + "sort": null + } + } + } + }, + "qf0e814ee": { + "query": "aggregate(a)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'aggregate(a)', unknown query function call aggregate at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "aggregate(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call aggregate at position 12" + }, + "parseErrorMessage": "unknown query function call aggregate at position 12", + "pathname": { + "$": "undefined" + }, + "search": "aggregate(a)", + "sort": null + } + } + } + }, + "q9d259a34": { + "query": "limit(1)&limit(2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "limit": 2 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "limit(1)", + "" + ], + [ + "limit(2)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "limit": 2, + "pathname": { + "$": "undefined" + }, + "search": "limit(1)&limit(2)", + "sort": null + } + } + } + }, + "qa7064e62": { + "query": "sort(a)&sort(b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "sort": { + "$": "object", + "props": { + "attribute": "b", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort(a)", + "" + ], + [ + "sort(b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "sort(a)&sort(b)", + "sort": { + "$": "object", + "props": { + "attribute": "b", + "descending": false + } + } + } + } + } + }, + "q2bdef6db": { + "query": "a=1&select(b)&sort(c)&limit(2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "limit": 2, + "select": "b", + "sort": { + "$": "object", + "props": { + "attribute": "c", + "descending": false + } + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "select(b)", + "" + ], + [ + "sort(c)", + "" + ], + [ + "limit(2)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "limit": 2, + "pathname": { + "$": "undefined" + }, + "search": "a=1&select(b)&sort(c)&limit(2)", + "select": "b", + "sort": { + "$": "object", + "props": { + "attribute": "c", + "descending": false + } + } + } + } + } + }, + "qfe0e59e0": { + "query": "foo=ba)r", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "equals", + "value": "ba)r" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "foo", + "ba)r" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "equals", + "value": "ba)r" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "foo=ba)r", + "sort": null + } + } + } + }, + "qc3934795": { + "query": "select(b)&a=1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "select": "b" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(b)", + "" + ], + [ + "a", + "1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(b)&a=1", + "select": "b", + "sort": null + } + } + } + }, + "q640d92fb": { + "query": "foo==ba)r", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "eq", + "value": "ba)r" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "foo", + "=ba)r" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "eq", + "value": "ba)r" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "foo==ba)r", + "sort": null + } + } + } + }, + "q0810a835": { + "query": "foo=ba(r", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "equals", + "value": "ba(r" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "foo", + "ba(r" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "equals", + "value": "ba(r" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "foo=ba(r", + "sort": null + } + } + } + }, + "q6209be3e": { + "query": "foo==ba(r", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "eq", + "value": "ba(r" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "foo", + "=ba(r" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "foo", + "comparator": "eq", + "value": "ba(r" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "foo==ba(r", + "sort": null + } + } + } + }, + "qe2408cba": { + "query": "foo==a=", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a>", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a>=", + "sort": null + } + } + } + }, + "q12a5c004": { + "query": "a=1#b=2", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1#b=2" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1#b=2" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1#b=2", + "sort": null + } + } + } + }, + "q395242e1": { + "query": "a<>1", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 4 in 'a<>1', unexpected operator '<>' at position 3, no comparison specified before end of string at position 4" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a<>1", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unexpected operator '<>' at position 3, no comparison specified before end of string at position 4" + }, + "parseErrorMessage": "unexpected operator '<>' at position 3, no comparison specified before end of string at position 4", + "pathname": { + "$": "undefined" + }, + "search": "a<>1", + "sort": null + } + } + } + }, + "q8f5d2232": { + "query": "a=1&limit(2)&", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "limit": 2 + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "limit(2)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "limit": 2, + "pathname": { + "$": "undefined" + }, + "search": "a=1&limit(2)&", + "sort": null + } + } + } + }, + "qb79cd2eb": { + "query": "a[b=1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a[b", + "1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a[b=1", + "sort": null + } + } + } + }, + "q715fbe41": { + "query": "a]b=1", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 5 in 'a]b=1', unexpected token ']' at position 2, unexpected operator ']' at position 2" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a]b", + "1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unexpected token ']' at position 2, unexpected operator ']' at position 2" + }, + "parseErrorMessage": "unexpected token ']' at position 2, unexpected operator ']' at position 2", + "pathname": { + "$": "undefined" + }, + "search": "a]b=1", + "sort": null + } + } + } + }, + "q34c67586": { + "query": "%ZZ=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "%ZZ", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "%ZZ", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "%ZZ=1", + "sort": null + } + } + } + }, + "qc9e5b726": { + "query": "a=%ZZ", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "%ZZ" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "%ZZ" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=%ZZ", + "sort": null + } + } + } + }, + "q4bc2fadd": { + "query": "a==%ZZ", + "strict": { + "status": "error", + "name": "URIError", + "message": "Unable to parse query, URI malformed at position 6 in 'a==%ZZ'" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=%ZZ" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "URIError", + "message": "Unable to parse query, URI malformed at position 6 in 'a==%ZZ'" + }, + "pathname": { + "$": "undefined" + }, + "search": "a==%ZZ", + "sort": null + } + } + } + }, + "q98b2469c": { + "query": "a==%E0%A4%A", + "strict": { + "status": "error", + "name": "URIError", + "message": "Unable to parse query, URI malformed at position 11 in 'a==%E0%A4%A'" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "=�%A" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "URIError", + "message": "Unable to parse query, URI malformed at position 11 in 'a==%E0%A4%A'" + }, + "pathname": { + "$": "undefined" + }, + "search": "a==%E0%A4%A", + "sort": null + } + } + } + }, + "qa9136993": { + "query": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=1", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "1" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "1" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=1", + "sort": null + } + } + } + }, + "q785e1727": { + "query": "a=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "sort": null + } + } + } + }, + "q5c245819": { + "query": "(((((((((((a=1)))))))))))", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(((((((((((a", + "1)))))))))))" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "(((((((((((a=1)))))))))))", + "sort": null + } + } + } + }, + "qad45ce67": { + "query": "a=in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49)", + "sort": null + } + } + } + }, + "q08129664": { + "query": "a=1&f0=0&f1=1&f2=2&f3=3&f4=4&f5=5&f6=6&f7=7&f8=8&f9=9&f10=10&f11=11&f12=12&f13=13&f14=14&f15=15&f16=16&f17=17&f18=18&f19=19&f20=20&f21=21&f22=22&f23=23&f24=24&f25=25&f26=26&f27=27&f28=28&f29=29&f30=30&f31=31&f32=32&f33=33&f34=34&f35=35&f36=36&f37=37&f38=38&f39=39", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ], + [ + "f0", + "0" + ], + [ + "f1", + "1" + ], + [ + "f2", + "2" + ], + [ + "f3", + "3" + ], + [ + "f4", + "4" + ], + [ + "f5", + "5" + ], + [ + "f6", + "6" + ], + [ + "f7", + "7" + ], + [ + "f8", + "8" + ], + [ + "f9", + "9" + ], + [ + "f10", + "10" + ], + [ + "f11", + "11" + ], + [ + "f12", + "12" + ], + [ + "f13", + "13" + ], + [ + "f14", + "14" + ], + [ + "f15", + "15" + ], + [ + "f16", + "16" + ], + [ + "f17", + "17" + ], + [ + "f18", + "18" + ], + [ + "f19", + "19" + ], + [ + "f20", + "20" + ], + [ + "f21", + "21" + ], + [ + "f22", + "22" + ], + [ + "f23", + "23" + ], + [ + "f24", + "24" + ], + [ + "f25", + "25" + ], + [ + "f26", + "26" + ], + [ + "f27", + "27" + ], + [ + "f28", + "28" + ], + [ + "f29", + "29" + ], + [ + "f30", + "30" + ], + [ + "f31", + "31" + ], + [ + "f32", + "32" + ], + [ + "f33", + "33" + ], + [ + "f34", + "34" + ], + [ + "f35", + "35" + ], + [ + "f36", + "36" + ], + [ + "f37", + "37" + ], + [ + "f38", + "38" + ], + [ + "f39", + "39" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "f0", + "0" + ], + [ + "f1", + "1" + ], + [ + "f2", + "2" + ], + [ + "f3", + "3" + ], + [ + "f4", + "4" + ], + [ + "f5", + "5" + ], + [ + "f6", + "6" + ], + [ + "f7", + "7" + ], + [ + "f8", + "8" + ], + [ + "f9", + "9" + ], + [ + "f10", + "10" + ], + [ + "f11", + "11" + ], + [ + "f12", + "12" + ], + [ + "f13", + "13" + ], + [ + "f14", + "14" + ], + [ + "f15", + "15" + ], + [ + "f16", + "16" + ], + [ + "f17", + "17" + ], + [ + "f18", + "18" + ], + [ + "f19", + "19" + ], + [ + "f20", + "20" + ], + [ + "f21", + "21" + ], + [ + "f22", + "22" + ], + [ + "f23", + "23" + ], + [ + "f24", + "24" + ], + [ + "f25", + "25" + ], + [ + "f26", + "26" + ], + [ + "f27", + "27" + ], + [ + "f28", + "28" + ], + [ + "f29", + "29" + ], + [ + "f30", + "30" + ], + [ + "f31", + "31" + ], + [ + "f32", + "32" + ], + [ + "f33", + "33" + ], + [ + "f34", + "34" + ], + [ + "f35", + "35" + ], + [ + "f36", + "36" + ], + [ + "f37", + "37" + ], + [ + "f38", + "38" + ], + [ + "f39", + "39" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1&f0=0&f1=1&f2=2&f3=3&f4=4&f5=5&f6=6&f7=7&f8=8&f9=9&f10=10&f11=11&f12=12&f13=13&f14=14&f15=15&f16=16&f17=17&f18=18&f19=19&f20=20&f21=21&f22=22&f23=23&f24=24&f25=25&f26=26&f27=27&f28=28&f29=29&f30=30&f31=31&f32=32&f33=33&f34=34&f35=35&f36=36&f37=37&f38=38&f39=39", + "sort": null + } + } + } + }, + "qe71af900": { + "query": "price=5", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "price", + "5" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "5" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=5", + "sort": null + } + } + } + }, + "q5d587d3e": { + "query": "a[b[c[d[e=1]]]]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "e", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "d" + }, + "ctor": "Query" + } + ] + }, + "name": "c" + }, + "ctor": "Query" + } + ] + }, + "name": "b" + }, + "ctor": "Query" + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a[b[c[d[e", + "1]]]]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "e", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "d" + }, + "ctor": "Query" + } + ] + }, + "name": "c" + }, + "ctor": "Query" + } + ] + }, + "name": "b" + }, + "ctor": "Query" + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a[b[c[d[e=1]]]]", + "sort": null + } + } + } + }, + "qe5c09edb": { + "query": "price=abc", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "price", + "abc" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "abc" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=abc", + "sort": null + } + } + } + }, + "qcf2043b5": { + "query": "price=true", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "price", + "true" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "true" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=true", + "sort": null + } + } + } + }, + "q7898e334": { + "query": "price=null", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "price", + "null" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "null" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=null", + "sort": null + } + } + } + }, + "q5b72eb57": { + "query": "price=x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "price", + "x y" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "x y" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=x%20y", + "sort": null + } + } + } + }, + "q1489ef57": { + "query": "price==5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "=5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price==5", + "sort": null + } + } + } + }, + "qc666eccc": { + "query": "price=2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=2.5", + "sort": null + } + } + } + }, + "q2e986eb5": { + "query": "price==null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price==null", + "sort": null + } + } + } + }, + "qeddac7a8": { + "query": "price==abc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "abc" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "=abc" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "abc" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price==abc", + "sort": null + } + } + } + }, + "q07719cd0": { + "query": "price==true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "=true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price==true", + "sort": null + } + } + } + }, + "qc870f494": { + "query": "price==x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "x y" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "=x y" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "x y" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price==x%20y", + "sort": null + } + } + } + }, + "qf5c9433f": { + "query": "price==2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "=2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price==2.5", + "sort": null + } + } + } + }, + "q470f9f3e": { + "query": "price===5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "==5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price===5", + "sort": null + } + } + } + }, + "qf555dbf2": { + "query": "price===null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "null" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "==null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "null" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price===null", + "sort": null + } + } + } + }, + "q8351b6f3": { + "query": "price===true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "==true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price===true", + "sort": null + } + } + } + }, + "q0387a731": { + "query": "price===abc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "abc" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "==abc" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "abc" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price===abc", + "sort": null + } + } + } + }, + "q6531bbea": { + "query": "price===2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "==2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price===2.5", + "sort": null + } + } + } + }, + "qe1f2f9f1": { + "query": "price===x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "x y" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "==x y" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "x y" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price===x%20y", + "sort": null + } + } + } + }, + "qea6c1b23": { + "query": "price!=5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!=5", + "sort": null + } + } + } + }, + "q6a758c0c": { + "query": "price!=abc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "abc" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "abc" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "abc" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!=abc", + "sort": null + } + } + } + }, + "q9e683851": { + "query": "price!=null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!=null", + "sort": null + } + } + } + }, + "q1f957484": { + "query": "price!=true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!=true", + "sort": null + } + } + } + }, + "qb775abe3": { + "query": "price!=2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!=2.5", + "sort": null + } + } + } + }, + "q97219f70": { + "query": "price!=x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "x y" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "x y" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "x y" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!=x%20y", + "sort": null + } + } + } + }, + "q2e42448a": { + "query": "price!==5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "=5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!==5", + "sort": null + } + } + } + }, + "qd23eee5e": { + "query": "price!==null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "null" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "=null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "null" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!==null", + "sort": null + } + } + } + }, + "qf6f90b47": { + "query": "price!==true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "=true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!==true", + "sort": null + } + } + } + }, + "qbf0c45de": { + "query": "price!==2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "=2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!==2.5", + "sort": null + } + } + } + }, + "q19fc39ed": { + "query": "price!==x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "x y" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "=x y" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "x y" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!==x%20y", + "sort": null + } + } + } + }, + "qd6b3aa45": { + "query": "price!==abc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "abc" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price!", + "=abc" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "abc" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price!==abc", + "sort": null + } + } + } + }, + "qd425c346": { + "query": "priceabc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "abc" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>abc", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "abc" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>abc", + "sort": null + } + } + } + }, + "q6e8fd8bc": { + "query": "price>true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>true", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>true", + "sort": null + } + } + } + }, + "q77138ceb": { + "query": "price>5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>5", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>5", + "sort": null + } + } + } + }, + "q27c49b9b": { + "query": "price>2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>2.5", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>2.5", + "sort": null + } + } + } + }, + "q0ed17279": { + "query": "price>null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>null", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>null", + "sort": null + } + } + } + }, + "qee48cab8": { + "query": "price>x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "x y" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>x y", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "x y" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>x%20y", + "sort": null + } + } + } + }, + "q0d90140d": { + "query": "price>=abc", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "abc" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>", + "abc" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "abc" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>=abc", + "sort": null + } + } + } + }, + "q41b32092": { + "query": "price>=5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>", + "5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>=5", + "sort": null + } + } + } + }, + "qc05992bf": { + "query": "price>=true", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "true" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>", + "true" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "true" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>=true", + "sort": null + } + } + } + }, + "q7cdecfa6": { + "query": "price>=null", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": null + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>", + "null" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": null + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>=null", + "sort": null + } + } + } + }, + "qa969c956": { + "query": "price>=2.5", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "2.5" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>", + "2.5" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "2.5" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>=2.5", + "sort": null + } + } + } + }, + "q8973ca55": { + "query": "price>=x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "x y" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price>", + "x y" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "x y" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price>=x%20y", + "sort": null + } + } + } + }, + "qb158a5e3": { + "query": "price=eq=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "eq=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "eq", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=eq=10", + "sort": null + } + } + } + }, + "qe03ec969": { + "query": "price=lt=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "lt", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "lt=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "lt", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=lt=10", + "sort": null + } + } + } + }, + "qd4e28da4": { + "query": "price=le=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "le", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "le=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "le", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=le=10", + "sort": null + } + } + } + }, + "q8661f080": { + "query": "price=gt=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "gt=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "gt", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=gt=10", + "sort": null + } + } + } + }, + "qb0a3c665": { + "query": "price=ge=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "ge=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ge", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=ge=10", + "sort": null + } + } + } + }, + "qbba5295e": { + "query": "price=contains=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "contains", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "contains=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "contains", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=contains=10", + "sort": null + } + } + } + }, + "qf8a42b32": { + "query": "price=ne=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "ne=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ne", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=ne=10", + "sort": null + } + } + } + }, + "q1e9bfe51": { + "query": "price=starts_with=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "starts_with", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "starts_with=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "starts_with", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=starts_with=10", + "sort": null + } + } + } + }, + "q9a1e46ea": { + "query": "price=equals=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "equals=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equals", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=equals=10", + "sort": null + } + } + } + }, + "q6e29a4da": { + "query": "price=ends_with=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ends_with", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "ends_with=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ends_with", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=ends_with=10", + "sort": null + } + } + } + }, + "q71629c9f": { + "query": "price=equal=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equal", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "equal=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "equal", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=equal=10", + "sort": null + } + } + } + }, + "qcd6bce71": { + "query": "price=not_equal=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "not_equal=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "not_equal", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=not_equal=10", + "sort": null + } + } + } + }, + "qf1e52808": { + "query": "price=notEqual=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "notEqual", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "notEqual=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "notEqual", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=notEqual=10", + "sort": null + } + } + } + }, + "q204577bf": { + "query": "price=sw=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "sw", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "sw=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "sw", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=sw=10", + "sort": null + } + } + } + }, + "qd66bc7b2": { + "query": "price=includes=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "includes", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "includes=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "includes", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=includes=10", + "sort": null + } + } + } + }, + "qc4e0ff4c": { + "query": "price=ct=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ct", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "ct=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ct", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=ct=10", + "sort": null + } + } + } + }, + "q5802bcf1": { + "query": "price=ew=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ew", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "ew=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "ew", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=ew=10", + "sort": null + } + } + } + }, + "q75c5c014": { + "query": "price=less_than=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "less_than", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "less_than=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "less_than", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=less_than=10", + "sort": null + } + } + } + }, + "q193df6a3": { + "query": "price=lessThan=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "lessThan", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "lessThan=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "lessThan", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=lessThan=10", + "sort": null + } + } + } + }, + "q494a69a1": { + "query": "price=less_than_equal=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "less_than_equal", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "less_than_equal=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "less_than_equal", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=less_than_equal=10", + "sort": null + } + } + } + }, + "qe099d5d5": { + "query": "price=lessThanEqual=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "lessThanEqual", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "lessThanEqual=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "lessThanEqual", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=lessThanEqual=10", + "sort": null + } + } + } + }, + "qc1629475": { + "query": "price=greater_than=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greater_than", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "greater_than=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greater_than", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=greater_than=10", + "sort": null + } + } + } + }, + "q8c4cdb68": { + "query": "price=greaterThan=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greaterThan", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "greaterThan=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greaterThan", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=greaterThan=10", + "sort": null + } + } + } + }, + "q40c81b60": { + "query": "price=greater_than_equal=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greater_than_equal", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "greater_than_equal=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greater_than_equal", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=greater_than_equal=10", + "sort": null + } + } + } + }, + "q600ed57c": { + "query": "price=greaterThanEqual=10", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greaterThanEqual", + "value": "10" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "greaterThanEqual=10" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "price", + "comparator": "greaterThanEqual", + "value": "10" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=greaterThanEqual=10", + "sort": null + } + } + } + }, + "q798912e5": { + "query": "labels=not_eq=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "eq", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_eq=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "eq", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_eq=urgent", + "sort": null + } + } + } + }, + "q97b07289": { + "query": "labels=not_in=(a,b)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "in", + "negated": true, + "value": { + "$": "array", + "items": [ + "a", + "b" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_in=(a,b)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "in", + "negated": true, + "value": { + "$": "array", + "items": [ + "a", + "b" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_in=(a,b)", + "sort": null + } + } + } + }, + "q9487c806": { + "query": "labels=not_ct=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "ct", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_ct=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "ct", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_ct=urgent", + "sort": null + } + } + } + }, + "q5c41a384": { + "query": "labels=not_contains=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "contains", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_contains=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "contains", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_contains=urgent", + "sort": null + } + } + } + }, + "qace820af": { + "query": "labels=not_starts_with=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "starts_with", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_starts_with=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "starts_with", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_starts_with=urgent", + "sort": null + } + } + } + }, + "qc6437439": { + "query": "labels=not_sw=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "sw", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_sw=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "sw", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_sw=urgent", + "sort": null + } + } + } + }, + "q245f5958": { + "query": "labels=not_ends_with=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "ends_with", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_ends_with=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "ends_with", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_ends_with=urgent", + "sort": null + } + } + } + }, + "q55d3d0a7": { + "query": "labels=not_lt=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_lt", + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_lt=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_lt", + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_lt=urgent", + "sort": null + } + } + } + }, + "qe72db062": { + "query": "labels=not_gt=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_gt", + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_gt=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_gt", + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_gt=urgent", + "sort": null + } + } + } + }, + "qcd061832": { + "query": "labels=not_le=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_le", + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_le=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_le", + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_le=urgent", + "sort": null + } + } + } + }, + "q7cd9aef7": { + "query": "labels=not_ge=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_ge", + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_ge=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_ge", + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_ge=urgent", + "sort": null + } + } + } + }, + "q933dac88": { + "query": "labels=not_equals=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "equals", + "negated": true, + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_equals=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "equals", + "negated": true, + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_equals=urgent", + "sort": null + } + } + } + }, + "qacc677aa": { + "query": "labels=not_frobnicate=urgent", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_frobnicate", + "value": "urgent" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "labels", + "not_frobnicate=urgent" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "labels", + "comparator": "not_frobnicate", + "value": "urgent" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "labels=not_frobnicate=urgent", + "sort": null + } + } + } + } + } +} diff --git a/conformance/harperAdapter.ts b/conformance/harperAdapter.ts new file mode 100644 index 0000000..f1f1bcb --- /dev/null +++ b/conformance/harperAdapter.ts @@ -0,0 +1,280 @@ +/** + * Adapter: Harper's REST parse output → the RQL 2.0 canonical model (`src/types.ts`). + * + * Spec §8 lets an implementation with a different internal representation conform "by + * supplying an adapter from its internal form to the canonical model". This is that + * adapter, and the rules it follows are deliberately narrow: + * + * 1. **Structural only — values pass through untouched.** Harper has already decoded and + * (per its own rules) typed every value token. Re-interpreting `"3"` as `3` here would + * hide exactly the value-model divergences the harness exists to find. + * 2. **Alias resolution follows *Harper's* vocabulary, not the spec's.** `includes` maps to + * `in` because that is what Harper's `ALTERNATE_COMPARATOR_NAMES` means by it, even + * though Appendix B assigns `includes` to `contains`. The diff then reports the real + * disagreement instead of an adapter that quietly agrees. + * 3. **§6 normalization is applied**, because it is part of the canonical model rather than + * of either parser: a non-negated element-scoped match holding exactly one non-negated + * condition collapses to a plain condition on the concatenated path (§5.3). + * 4. **Parse-time shape only.** Where Harper's *executor* later reads a shape in a + * surprising way, that is recorded in the report as an observation, not folded in here. + * 5. **Total, except for genuinely unknown shapes.** Anything the adapter cannot place + * raises `AdapterError`, which the report surfaces as an adapter gap rather than + * silently dropping. + */ + +import type { Condition, ElementMatch, Field, Group, ParseResult, Projection, SortKey, Value } from '../src/types.ts'; + +export class AdapterError extends Error {} + +export type HarperOutcome = + | { status: 'parsed'; result: ParseResult } + /** Harper's deferred-error mode (§6.1, ledger row 8): a result object carrying `parseError`. */ + | { status: 'deferred-error'; message: string; partial: ParseResult }; + +type Dict = Record; + +const isDict = (value: unknown): value is Dict => typeof value === 'object' && value !== null; + +/** Harper comparator name → canonical comparator plus whether the alias itself negates. */ +const COMPARATOR_ALIASES: Readonly> = { + // Symbolic-operator outputs (`SYMBOL_OPERATORS` in resources/search.ts). + eq: { comparator: 'eq' }, + equals: { comparator: 'eq' }, + ne: { comparator: 'eq', negated: true }, + not_equal: { comparator: 'eq', negated: true }, + // `ALTERNATE_COMPARATOR_NAMES` — resolved at execution in Harper, at adaptation here. + equal: { comparator: 'eq' }, + notEqual: { comparator: 'eq', negated: true }, + greater_than: { comparator: 'gt' }, + greaterThan: { comparator: 'gt' }, + greater_than_equal: { comparator: 'ge' }, + greaterThanEqual: { comparator: 'ge' }, + less_than: { comparator: 'lt' }, + lessThan: { comparator: 'lt' }, + less_than_equal: { comparator: 'le' }, + lessThanEqual: { comparator: 'le' }, + sw: { comparator: 'starts_with' }, + startsWith: { comparator: 'starts_with' }, + ew: { comparator: 'ends_with' }, + endsWith: { comparator: 'ends_with' }, + ct: { comparator: 'contains' }, + // Rule 2: Harper reads `includes` as `in`; Appendix B reads it as `contains`. + includes: { comparator: 'in' }, +}; + +/** A property path Harper has already decoded (`decodeProperty`): string, or segment array. */ +function decodedPath(attribute: unknown): string[] { + if (attribute === null || attribute === undefined) return []; + if (Array.isArray(attribute)) return attribute.map((segment) => String(segment)); + if (typeof attribute === 'string') return [attribute]; + throw new AdapterError(`unsupported attribute shape: ${Object.prototype.toString.call(attribute)}`); +} + +/** + * A property path Harper kept RAW — the `name` of a scoped match or of a nested select, + * which `parseBlock` assigns straight from the token. Split on literal `.` first so that + * `%2E` stays inside one segment (§4.2). + */ +function rawPath(name: unknown): string[] { + if (typeof name !== 'string') throw new AdapterError(`unsupported raw path shape: ${String(name)}`); + return name.split('.').map((segment) => decodeURIComponent(segment)); +} + +const isConditionShape = (term: Dict): boolean => 'comparator' in term || 'attribute' in term || 'value' in term; +const isGroupShape = (term: Dict): boolean => Array.isArray(term.conditions); + +/** + * §5.3 / §6: a non-negated element-scoped match with exactly one non-negated inner + * condition is the same thing as a plain condition on the concatenated path, and the + * canonical model states it that way. Mirrors `pushElementMatch` in the reference parser. + */ +function normalizeElementMatch(match: ElementMatch): Condition | ElementMatch { + const terms = match.some.terms; + if (match.negated || terms.length !== 1) return match; + const only = terms[0]; + if (!('comparator' in only) || only.negated) return match; + const merged: Condition = { path: [...match.path, ...only.path], comparator: only.comparator, value: only.value }; + return merged; +} + +function relativeLeg(raw: Dict, scopePath: string[]): Condition { + const leg = adaptCondition(raw); + if ('some' in leg) throw new AdapterError('a chained leg cannot itself be an element match'); + // Harper stores a chained leg with `attribute: null`; a `between` head keeps its own path. + return { ...leg, path: leg.path.slice(scopePath.length) }; +} + +function adaptCondition(raw: Dict): Condition | ElementMatch { + const rawComparator = raw.comparator === undefined || raw.comparator === null ? 'equals' : String(raw.comparator); + const path = decodedPath(raw.attribute); + const negatedByFlag = raw.negated === true; + + if (rawComparator === 'between') { + const bounds = raw.value; + // Appendix B: `between=(lo,hi)` is an element-scoped `ge` AND `le` (ledger row 4). + if (Array.isArray(bounds) && bounds.length === 2) { + const match: ElementMatch = { + path, + some: { + operator: 'and', + terms: [ + { path: [], comparator: 'ge', value: bounds[0] as Value }, + { path: [], comparator: 'le', value: bounds[1] as Value }, + ], + }, + }; + if (negatedByFlag) match.negated = true; + return withChain(raw, match, path); + } + // Not a two-element list: keep Harper's literal condition so the diff can say so. + const literal: Condition = { path, comparator: 'between', value: bounds as Value }; + if (negatedByFlag) literal.negated = true; + return withChain(raw, literal, path); + } + + const alias = COMPARATOR_ALIASES[rawComparator]; + const condition: Condition = { + path, + comparator: alias ? alias.comparator : rawComparator, + value: raw.value as Value, + }; + if (negatedByFlag !== (alias?.negated === true)) condition.negated = true; + return withChain(raw, condition, path); +} + +/** + * `chainedConditions` (ledger row 2) is Harper's spelling of an element-scoped match: the + * head condition plus its legs all bind to one element at the head's path. + */ +function withChain(raw: Dict, head: Condition | ElementMatch, path: string[]): Condition | ElementMatch { + const chained = raw.chainedConditions; + if (!Array.isArray(chained) || chained.length === 0) return head; + + const terms: (Condition | Group | ElementMatch)[] = []; + if ('some' in head) terms.push(...head.some.terms); + else terms.push({ ...head, path: head.path.slice(path.length) }); + for (const leg of chained) { + if (!isDict(leg)) throw new AdapterError('unsupported chained condition shape'); + terms.push(relativeLeg(leg, path)); + } + + const match: ElementMatch = { path, some: { operator: raw.operator === 'or' ? 'or' : 'and', terms } }; + if ('some' in head && head.negated) match.negated = true; + return normalizeElementMatch(match); +} + +function adaptTerm(raw: unknown): Condition | Group | ElementMatch { + if (!isDict(raw)) throw new AdapterError(`unsupported query term: ${String(raw)}`); + if (isGroupShape(raw)) { + const group = adaptGroup(raw); + // `prop[...]` — Harper marks a scoped match by hanging the raw property name on the group. + if (typeof raw.name === 'string') return normalizeElementMatch({ path: rawPath(raw.name), some: group }); + return group; + } + if (isConditionShape(raw)) return adaptCondition(raw); + throw new AdapterError(`unrecognized query term with keys [${Object.keys(raw).join(', ')}]`); +} + +function adaptGroup(raw: Dict): Group { + const conditions = raw.conditions as unknown[]; + return { operator: raw.operator === 'or' ? 'or' : 'and', terms: conditions.map(adaptTerm) }; +} + +/** Harper's sort is a linked list of `{attribute, descending, next}` (ledger row 5). */ +function adaptSort(raw: unknown): SortKey[] { + const keys: SortKey[] = []; + let node = raw; + while (node !== undefined && node !== null) { + if (!isDict(node)) throw new AdapterError(`unsupported sort node: ${String(node)}`); + keys.push({ + path: node.attribute === '' ? [''] : decodedPath(node.attribute), + direction: node.descending === true ? 'desc' : 'asc', + }); + node = node.next; + } + return keys; +} + +const nestedList = (item: unknown): unknown[] | undefined => { + if (Array.isArray(item)) return item; + if (isDict(item) && item.select !== undefined) return Array.isArray(item.select) ? item.select : [item.select]; + return undefined; +}; + +function adaptField(item: unknown): Field { + if (typeof item === 'string') return { path: [item] }; + const nested = nestedList(item); + if (nested === undefined) throw new AdapterError(`unsupported select item: ${String(item)}`); + const name = (item as Dict).name; + // §5.7: nested projections are always `records`. + const projection: Projection = { mode: 'records', fields: nested.map(adaptField) }; + // A nameless nested list is Harper's `[a,b]` tuple item; it has no property path. + return name === undefined ? { path: [], projection } : { path: rawPath(name), projection }; +} + +/** + * Harper's `select` is polymorphic (ledger row 5) and its mode follows the dispatch in + * `Table.transformEntryForSelect`: a bare string is the single-value form, an array marked + * `asArray` is a tuple, anything else trims the record to an object. + */ +function adaptSelect(raw: unknown): Projection { + if (typeof raw === 'string') return { mode: 'values', fields: [{ path: [raw] }] }; + if (Array.isArray(raw)) { + const named = (raw as unknown as Dict).name; + if (named !== undefined) return { mode: 'records', fields: [adaptField(raw)] }; + if ((raw as unknown as Dict).asArray === true) return { mode: 'tuples', fields: raw.map(adaptField) }; + return { mode: 'records', fields: raw.map(adaptField) }; + } + if (isDict(raw) && raw.select !== undefined) return { mode: 'records', fields: [adaptField(raw)] }; + throw new AdapterError(`unsupported select shape: ${String(raw)}`); +} + +function adaptNumber(raw: unknown, field: string): number { + if (typeof raw !== 'number') throw new AdapterError(`${field} is not a number: ${String(raw)}`); + return raw; +} + +function adaptQueryBody(raw: Dict): ParseResult { + const result: ParseResult = {}; + const conditions = raw.conditions; + if (Array.isArray(conditions) && conditions.length > 0) result.filter = adaptGroup(raw); + if (raw.sort !== undefined && raw.sort !== null) result.sort = adaptSort(raw.sort); + if (raw.select !== undefined) result.select = adaptSelect(raw.select); + if (raw.limit !== undefined) result.limit = adaptNumber(raw.limit, 'limit'); + if (raw.offset !== undefined) result.offset = adaptNumber(raw.offset, 'offset'); + return result; +} + +/** + * Ledger row 1: a query with no structural characters skips parsing entirely and its + * conditions surface as raw `URLSearchParams` name/value pairs. Those names are always a + * single segment — a literal `.` would have forced the parsing path — and both name and + * value are already form-decoded, so neither is split or decoded again here. + */ +function adaptSearchParams(params: URLSearchParams): ParseResult { + const terms: (Condition | Group | ElementMatch)[] = []; + for (const [name, value] of params) terms.push({ path: [name], comparator: 'eq', value }); + return terms.length === 0 ? {} : { filter: { operator: 'and', terms } }; +} + +export function adaptHarperResult(raw: unknown): HarperOutcome { + // `parseQuery('')` returns undefined — an unfiltered, unshaped query. + if (raw === undefined || raw === null) return { status: 'parsed', result: {} }; + + if (raw instanceof URLSearchParams) { + // A RequestTarget is a URLSearchParams that ALSO carries the parsed query when the + // parsing path ran (the dual shape of ledger row 1). + const carrier = raw as unknown as Dict; + if (carrier.conditions === undefined) return finish(adaptSearchParams(raw), carrier); + return finish(adaptQueryBody(carrier), carrier); + } + if (!isDict(raw)) throw new AdapterError(`unsupported parse result: ${String(raw)}`); + return finish(adaptQueryBody(raw), raw); +} + +function finish(result: ParseResult, raw: Dict): HarperOutcome { + const parseError = raw.parseError; + if (parseError === undefined || parseError === null) return { status: 'parsed', result }; + const message = parseError instanceof Error ? parseError.message : String(parseError); + return { status: 'deferred-error', message, partial: result }; +} diff --git a/conformance/ledger.json b/conformance/ledger.json new file mode 100644 index 0000000..82a987f --- /dev/null +++ b/conformance/ledger.json @@ -0,0 +1,96 @@ +{ + "source": { + "repo": "HarperFast/harper", + "issue": 2440, + "url": "https://github.com/HarperFast/harper/issues/2440", + "title": "RQL 2.0 conformance: divergence tracking" + }, + "fetchedAt": "2026-09-01T23:16:18.888Z", + "note": "Snapshot of Harper’s divergence ledger, cached so the harness can classify offline. The issue is canonical; refresh with `npm run conformance:refresh-ledger`.", + "rows": [ + { + "row": 1, + "divergence": "Simple queries (no structural chars) skip parsing; conditions surface as raw name/value pairs (dual shape)", + "class": "permitted", + "action": "adapter maps to canonical model" + }, + { + "row": 2, + "divergence": "Chains attach as `chainedConditions`; nameless legs (`a=ge=1&=5`) inherit the previous comparator", + "class": "representational + quirk", + "action": "adapter; nameless legs are a syntax error in 2.0 — deprecate" + }, + { + "row": 3, + "divergence": "Strict/coercive modeled as distinct comparators (`equals`/`not_equal` vs `eq`/`ne`)", + "class": "permitted", + "action": "adapter normalizes to value-literal modes (§5.2)" + }, + { + "row": 4, + "divergence": "`between` is a first-class comparator", + "class": "permitted", + "action": "spec Appendix B alias; desugars to element-scoped `ge`+`le`" + }, + { + "row": 5, + "divergence": "Sort is a linked list; select is a polymorphic array with marker props", + "class": "permitted", + "action": "adapter" + }, + { + "row": 6, + "divergence": "`(4)` on a non-list comparator is the literal string `\"(4)\"`", + "class": "tolerance", + "action": "keep; producers must not rely on it" + }, + { + "row": 7, + "divergence": "`prop[]=v` repeated array params", + "class": "tolerance", + "action": "spec Appendix B host accommodation" + }, + { + "row": 8, + "divergence": "Parse errors deferred into the pipeline (`parseError`)", + "class": "permitted", + "action": "§6.1 allows deferred mode" + }, + { + "row": 9, + "divergence": "`group-by(...)` accepted at parse and falls through into `sort` (missing `break`)", + "class": "bug", + "action": "fix in flight (dispatch `harper-groupby-fallthrough`); 2.0 parse-rejects reserved call names" + }, + { + "row": 10, + "divergence": "Chained legs' values never type-coerced on the REST path → mixed-type ranges, silent superset/empty results", + "class": "bug", + "action": "#2433" + }, + { + "row": 11, + "divergence": "Indexed `elements` attributes return one result per matching element (duplicates)", + "class": "bug", + "action": "#2434" + }, + { + "row": 12, + "divergence": "`contains` matches numbers via decimal-string coercion", + "class": "divergence", + "action": "spec §5.1.1: string comparators match only strings; decide keep-as-divergence vs align" + }, + { + "row": 13, + "divergence": "No `not(...)` group/scope negation", + "class": "feature gap", + "action": "sub-issue below" + }, + { + "row": 14, + "divergence": "Element conditions (`prop[=ge=10|=le=2]`) and scoped sub-queries per §5.3 surface", + "class": "partial", + "action": "Harper has `prop[cond&cond]`; elem-cond (empty relative path) not supported — fold into not() work or file separately" + } + ] +} diff --git a/conformance/report.ts b/conformance/report.ts new file mode 100644 index 0000000..f923811 --- /dev/null +++ b/conformance/report.ts @@ -0,0 +1,345 @@ +/** + * Markdown rendering of a conformance run. + * + * The report is committed, so it is rendered from committed data only — the fixture, the + * ledger snapshot and the corpus — and never from the wall clock. `--check` can then assert + * that replaying the fixture reproduces the committed report byte for byte. + */ + +import type { Case } from './corpus.ts'; +import { EXECUTION_LEVEL_LEDGER_ROWS, REQUIRED_FEATURES, featureCoverage, ledgerCoverage } from './corpus.ts'; +import type { Difference, Json, Outcome } from './canonical.ts'; +import { stableStringify } from './canonical.ts'; +import type { Classification, Comparison } from './classify.ts'; +import type { Ledger, Provenance, RunResult } from './compare.ts'; + +export type { Ledger, Provenance, RunResult } from './compare.ts'; + +const escapeCell = (text: string): string => text.replace(/\|/g, '\\|').replace(/\n/g, ' '); +const code = (text: string): string => '`' + escapeCell(text.replace(/`/g, 'ˋ')) + '`'; +const queryCell = (query: string): string => (query === '' ? '_(empty string)_' : code(truncate(query, 70))); + +function truncate(text: string, limit: number): string { + return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`; +} + +function describeOutcome(outcome: Outcome): string { + switch (outcome.status) { + case 'parsed': return 'parsed'; + case 'rejected': return 'rejected'; + case 'deferred-error': return 'rejected (deferred)'; + case 'timeout': return `timeout after ${outcome.ms}ms`; + case 'adapter-gap': return 'adapter gap'; + case 'harness-error': return 'harness error'; + } +} + +/** The canonical result an outcome carries, if it produced one. */ +export function canonicalOf(outcome: Outcome): Json | undefined { + return outcome.status === 'parsed' || outcome.status === 'deferred-error' ? outcome.canonical : undefined; +} + +function outcomeDetail(outcome: Outcome): string { + const json = canonicalOf(outcome); + if (json !== undefined) { + const body = '```json\n' + stableStringify(json, 2) + '\n```'; + return outcome.status === 'deferred-error' ? `> ${escapeCell(outcome.error)}\n\n${body}` : body; + } + if (outcome.status === 'rejected') return `> ${escapeCell(outcome.error)}`; + if (outcome.status === 'adapter-gap' || outcome.status === 'harness-error') return `> ${escapeCell(outcome.error)}`; + if (outcome.status === 'timeout') return `> timed out after ${outcome.ms}ms`; + return ''; +} + +/** Days between two ISO timestamps, floored — a stable function of committed data. */ +function daysBetween(from: string, to: string): number { + return Math.floor((Date.parse(to) - Date.parse(from)) / 86_400_000); +} + +/** Divergences are grouped by what explains them: a ledger row, or the rule that matched. */ +function groupKey(classification: Classification): string { + switch (classification.verdict) { + case 'ledger': return `row ${classification.row}`; + case 'new': + case 'reference-bug': return classification.rule; + default: return classification.verdict; + } +} + +export function renderReport(run: RunResult): string { + const { comparisons, ledger, provenance } = run; + const lines: string[] = []; + const write = (text = ''): void => void lines.push(text); + + const byVerdict = (verdict: T) => + comparisons.filter((comparison) => comparison.classification.verdict === verdict); + + const agrees = byVerdict('agrees'); + const ledgerHits = byVerdict('ledger'); + const news = byVerdict('new'); + const referenceBugs = byVerdict('reference-bug'); + const unclassified = byVerdict('unclassified'); + const divergences = comparisons.length - agrees.length; + + write('# RQL 2.0 differential conformance report'); + write(); + write('Generated by `npm run conformance` — do not edit by hand.'); + write(); + write('Every corpus query is parsed by both Harper’s REST query parser and the RQL 2.0'); + write('reference parser, Harper’s output is mapped to the canonical model (§6) by'); + write('`conformance/harperAdapter.ts`, and the two canonical results are compared. Every'); + write('disagreement is classified below; an unclassified disagreement fails the run.'); + write(); + write('### How to read this'); + write(); + write('- A disagreement is classified by RULE, not case by case — the rules live in'); + write(' `conformance/classify.ts` and each names the witnesses it covers.'); + write('- The verdict names **the side that has to change** for the two parsers to agree'); + write(' *conformantly*. Where both deviate from the spec, the rationale says so.'); + write('- Harper’s uninterpreted values (§5.2.2) are stripped from a case’s difference list'); + write(' before the other rules see it, so one pervasive divergence does not mask the rest.'); + write(' A case where they were stripped is marked "+ uninterpreted values".'); + write('- Scope: this compares PARSE results only. Value coercion against a table’s declared'); + write(' attribute types, comparator evaluation and result materialization all happen later,'); + write(' in `resources/Table.ts`, and no case here can witness them.'); + write(); + + write('## Provenance'); + write(); + write('| | |'); + write('|---|---|'); + write(`| Harper commit | ${code(provenance.harper.commit)}${provenance.harper.describe ? ` (${escapeCell(provenance.harper.describe)})` : ''}${provenance.harper.dirty ? ' **+ uncommitted changes**' : ''} |`); + write(`| Harper version | ${provenance.harper.version ? code(provenance.harper.version) : '_unknown_'} |`); + write( + `| Reference parser commit | ${code(provenance.reference.commit)}${provenance.reference.describe ? ` (${escapeCell(provenance.reference.describe)})` : ''}${provenance.reference.dirty ? ' **+ uncommitted changes**' : ''} |` + ); + write(`| Fixture recorded | ${provenance.recordedAt} |`); + write(`| Recorded on Node | ${code(provenance.node)} |`); + write(`| Corpus digest | ${code(provenance.corpusDigest)} |`); + write(`| Ledger snapshot | [${escapeCell(ledger.source.repo)}#${ledger.source.issue}](${ledger.source.url}), ${ledger.rows.length} rows, fetched ${ledger.fetchedAt} |`); + const drift = daysBetween(ledger.fetchedAt, provenance.recordedAt); + write( + `| Snapshot vs fixture | snapshot fetched ${drift === 0 ? 'the same day as' : drift > 0 ? `${drift} day(s) before` : `${-drift} day(s) after`} the fixture recording |` + ); + write(); + write('The ledger snapshot is a cache. [' + escapeCell(`${ledger.source.repo}#${ledger.source.issue}`) + '](' + ledger.source.url + ') stays canonical —'); + write('refresh with `npm run conformance:refresh-ledger` before trusting a classification.'); + write(); + + write('## Totals'); + write(); + write('| Outcome | Cases |'); + write('|---|---:|'); + write(`| Corpus size | ${comparisons.length} |`); + write(`| Agree (identical canonical result, or both reject) | ${agrees.length} |`); + write(`| **Diverge** | **${divergences}** |`); + write(`| — classified to a ledger row | ${ledgerHits.length} |`); + write(`| — NEW divergence (proposed rows below) | ${news.length} |`); + write(`| — reference-parser bug | ${referenceBugs.length} |`); + write(`| — **unclassified (must be 0)** | **${unclassified.length}** |`); + write(); + + const statusCounts = new Map(); + for (const comparison of comparisons) { + const key = `${describeOutcome(comparison.ref)} / ${describeOutcome(comparison.harper)}`; + statusCounts.set(key, (statusCounts.get(key) ?? 0) + 1); + } + write('| Reference outcome / Harper outcome | Cases |'); + write('|---|---:|'); + for (const [key, count] of [...statusCounts].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))) + write(`| ${key} | ${count} |`); + write(); + + write('## Grammar feature coverage'); + write(); + write('Each corpus case carries the grammar features it exercises; every feature of §4–§5.7'); + write('is required to have at least one case.'); + write(); + write('| Feature | Cases | Agree | Diverge |'); + write('|---|---:|---:|---:|'); + const verdictById = new Map(comparisons.map((comparison) => [comparison.case.id, comparison.classification.verdict])); + const coverage = featureCoverage(comparisons.map((comparison) => comparison.case)); + for (const feature of [...coverage.keys()].sort()) { + const cases = coverage.get(feature) ?? []; + const agreeCount = cases.filter((item) => verdictById.get(item.id) === 'agrees').length; + const required = (REQUIRED_FEATURES as readonly string[]).includes(feature); + write(`| ${code(feature)}${required ? '' : ' _(extra)_'} | ${cases.length} | ${agreeCount} | ${cases.length - agreeCount} |`); + } + write(); + + write('## Divergence-ledger coverage'); + write(); + write(`Rows of [${escapeCell(`${ledger.source.repo}#${ledger.source.issue}`)}](${ledger.source.url}), and the corpus cases that witness them.`); + write('A row marked _execution-level_ describes behavior after parsing, which a parser-only'); + write('harness cannot observe.'); + write(); + write('| Row | Class | Divergence | Witnesses | Observed |'); + write('|---:|---|---|---:|---|'); + const witnesses = ledgerCoverage(comparisons.map((comparison) => comparison.case)); + const ledgerObserved = new Map(); + for (const comparison of comparisons) + if (comparison.classification.verdict === 'ledger') + ledgerObserved.set(comparison.classification.row, (ledgerObserved.get(comparison.classification.row) ?? 0) + 1); + const deferredQueries = new Set(run.deferred.map((observation) => observation.case.query)); + const otherRules = new Map>(); + for (const comparison of comparisons) { + const verdict = comparison.classification; + if (verdict.verdict !== 'new' && verdict.verdict !== 'reference-bug') continue; + const rule = verdict.rule; + for (const row of comparison.case.ledger ?? []) { + const bucket = otherRules.get(row) ?? new Set(); + bucket.add(rule); + otherRules.set(row, bucket); + } + } + for (const row of ledger.rows) { + const cases = witnesses.get(row.row) ?? []; + const observed = ledgerObserved.get(row.row) ?? 0; + const executionLevel = EXECUTION_LEVEL_LEDGER_ROWS[row.row]; + let state: string; + if (observed > 0) state = `confirmed — ${observed} diverging case(s)`; + else if (executionLevel) state = `_execution-level_ — ${escapeCell(executionLevel)}`; + else if (cases.some((item) => deferredQueries.has(item.query))) + state = 'confirmed in deferred-error mode — see below'; + else if (otherRules.has(row.row)) + state = `witnessed; the difference observed is classified as ${[...(otherRules.get(row.row) ?? [])].map(code).join(', ')}`; + else if (cases.length > 0) state = 'exercised; the adapter maps it to the canonical model, so nothing differs'; + else state = 'no corpus witness'; + write(`| ${row.row} | ${escapeCell(row.class)} | ${escapeCell(truncate(row.divergence, 90))} | ${cases.length} | ${state} |`); + } + write(); + + const sections: [string, string, typeof comparisons][] = [ + ['NEW divergences', 'Not yet in Harper’s ledger. Each block proposes a row to add — the spec is not edited here.', news], + ['Divergences matching an existing ledger row', 'Already tracked; cited by row number.', ledgerHits], + ['Reference-parser bugs', 'The reference implementation, not Harper, is wrong here.', referenceBugs], + ['Unclassified', 'Every entry here is a hole in `conformance/classify.ts` and fails the run.', unclassified], + ]; + + for (const [title, blurb, group] of sections) { + write(`## ${title}`); + write(); + write(blurb); + write(); + if (group.length === 0) { + write('_None._'); + write(); + continue; + } + const grouped = new Map(); + for (const comparison of group) { + const key = groupKey(comparison.classification); + const bucket = grouped.get(key); + if (bucket) bucket.push(comparison); + else grouped.set(key, [comparison]); + } + for (const [key, bucket] of grouped) { + const first = bucket[0].classification; + write(`### ${key} — ${bucket.length} case(s)`); + write(); + if (first.verdict === 'ledger') { + const row = ledger.rows.find((candidate) => candidate.row === first.row); + if (row) { + write(`Ledger row ${row.row} (${row.class}): ${row.divergence}`); + write(); + write(`Tracked action: ${row.action}`); + write(); + } + write(first.rationale); + write(); + } else if (first.verdict === 'new') { + write(first.rationale); + write(); + write('**Proposed ledger row**'); + write(); + write('| Divergence | Class | Action |'); + write('|---|---|---|'); + write(`| ${escapeCell(first.proposal.divergence)} | ${escapeCell(first.proposal.class)} | ${escapeCell(first.proposal.action)} |`); + write(); + } else if (first.verdict === 'reference-bug') { + write(first.rationale); + write(); + } + write('| Query | Reference | Harper (adapted) |'); + write('|---|---|---|'); + for (const comparison of bucket) + write( + `| ${queryCell(comparison.case.query)} | ${describeOutcome(comparison.ref)} | ${describeOutcome(comparison.harper)}` + + `${comparison.alsoUninterpretedValues ? ' + uninterpreted values' : ''} |` + ); + write(); + const sample = bucket[0]; + write(`
Worked example: ${queryCell(sample.case.query)}`); + write(); + write('Reference:'); + write(); + write(outcomeDetail(sample.ref)); + write(); + write('Harper, adapted:'); + write(); + write(outcomeDetail(sample.harper)); + write(); + if (sample.differences.length > 0) { + write('Differences:'); + write(); + write('| At | Kind | Reference | Harper |'); + write('|---|---|---|---|'); + for (const difference of sample.differences.slice(0, 12)) + write( + `| ${code(difference.at || '/')} | ${difference.kind} | ${difference.ref === undefined ? '—' : code(stableStringify(difference.ref))} | ${difference.harper === undefined ? '—' : code(stableStringify(difference.harper))} |` + ); + if (sample.differences.length > 12) write(`| … | ${sample.differences.length - 12} more | | |`); + write(); + } + write('
'); + write(); + } + } + + write('## Deferred-error mode (ledger rows 8 and 9)'); + write(); + write('Harper’s production entry point is `new RequestTarget("?" + query)`, which never throws:'); + write('a parse error is deferred into `parseError` for the request pipeline to raise (§6.1'); + write('permits this). These cases show what that path produces where the throwing entry point'); + write('rejects — including row 9, where a reserved call name is rejected *and* still falls'); + write('through into `sort`.'); + write(); + write('| Query | Reference (deferred) | Harper (deferred) | Harper still produced |'); + write('|---|---|---|---|'); + for (const observation of run.deferred) { + const produced = canonicalOf(observation.harper); + write( + `| ${queryCell(observation.case.query)} | ${describeOutcome(observation.ref)} | ${describeOutcome(observation.harper)} | ${produced === undefined ? '—' : code(truncate(stableStringify(produced), 90))} |` + ); + } + write(); + + write('## Every case'); + write(); + write('All ' + comparisons.length + ' corpus cases, in corpus order, with the verdict applied to each.'); + write(); + write('| # | Query | Features | Reference | Harper | Verdict |'); + write('|---:|---|---|---|---|---|'); + comparisons.forEach((comparison, index) => { + const verdict = comparison.classification; + const label = + verdict.verdict === 'agrees' + ? 'agrees' + : verdict.verdict === 'ledger' + ? `ledger row ${verdict.row}` + : verdict.verdict === 'new' + ? `NEW (${verdict.rule})` + : verdict.verdict === 'reference-bug' + ? `reference bug (${verdict.rule})` + : '**UNCLASSIFIED**'; + write( + `| ${index + 1} | ${queryCell(comparison.case.query)} | ${comparison.case.features.map(code).join(' ')} | ${describeOutcome(comparison.ref)} | ` + + `${describeOutcome(comparison.harper)}${comparison.alsoUninterpretedValues ? ' + uninterpreted values' : ''} | ${label} |` + ); + }); + write(); + + return lines.join('\n'); +} + +export type { Difference }; diff --git a/conformance/tagged.ts b/conformance/tagged.ts new file mode 100644 index 0000000..46c229c --- /dev/null +++ b/conformance/tagged.ts @@ -0,0 +1,146 @@ +/** + * Lossless, deterministic tagged encoding for raw host-parser output. + * + * The recorded fixture must round-trip Harper's parse result exactly — including + * shapes JSON has no notion of (URLSearchParams, Date, NaN, `undefined`, arrays + * carrying marker properties such as `select.asArray`) — so that replay feeds the + * adapter the same values a live run would. + * + * Encoding: JSON primitives stay themselves; everything else becomes an object with + * a `$` discriminator. Plain objects are always wrapped, so a recorded object whose + * own key is `$` is never confused with a tag. Object keys are emitted sorted, which + * makes a re-record byte-identical when nothing changed. + */ + +export type Tagged = + | null + | boolean + | number + | string + | { $: 'undefined' } + | { $: 'number'; v: string } + | { $: 'bigint'; v: string } + | { $: 'date'; iso: string } + | { $: 'date'; invalid: true } + | { $: 'error'; name: string; message: string } + | { $: 'function'; name: string } + | { $: 'symbol'; v: string } + | { $: 'usp'; ctor: string; entries: [string, string][]; props?: Record } + | { $: 'array'; items: Tagged[]; props?: Record } + | { $: 'object'; ctor?: string; props: Record }; + +export class TaggedEncodeError extends Error {} + +function sortedProps(source: object, keys: string[], seen: Set): Record { + const props: Record = {}; + for (const key of [...keys].sort()) props[key] = encodeWith((source as Record)[key], seen); + return props; +} + +function encodeWith(value: unknown, seen: Set): Tagged { + if (value === null) return null; + if (value === undefined) return { $: 'undefined' }; + + switch (typeof value) { + case 'boolean': + case 'string': + return value; + case 'number': + // NaN/±Infinity/-0 have no JSON spelling but are real Harper outputs (`limit(x)` → NaN). + return Number.isFinite(value) && !Object.is(value, -0) ? value : { $: 'number', v: String(Object.is(value, -0) ? '-0' : value) }; + case 'bigint': + return { $: 'bigint', v: String(value) }; + case 'symbol': + return { $: 'symbol', v: String(value) }; + case 'function': + return { $: 'function', name: (value as { name?: string }).name ?? '' }; + case 'object': + break; + default: + throw new TaggedEncodeError(`cannot encode ${typeof value}`); + } + + const object = value as object; + if (seen.has(object)) throw new TaggedEncodeError('cannot encode a cyclic structure'); + seen.add(object); + try { + if (object instanceof Date) + return Number.isNaN(object.getTime()) ? { $: 'date', invalid: true } : { $: 'date', iso: object.toISOString() }; + if (object instanceof Error) return { $: 'error', name: object.name, message: object.message }; + if (object instanceof URLSearchParams) { + const own = Object.keys(object); + const encoded: Tagged = { + $: 'usp', + ctor: object.constructor?.name ?? 'URLSearchParams', + entries: [...object.entries()], + }; + if (own.length > 0) (encoded as { props?: Record }).props = sortedProps(object, own, seen); + return encoded; + } + if (Array.isArray(object)) { + const markers = Object.keys(object).filter((key) => !/^(?:0|[1-9]\d*)$/.test(key)); + const encoded: Tagged = { $: 'array', items: object.map((item) => encodeWith(item, seen)) }; + if (markers.length > 0) (encoded as { props?: Record }).props = sortedProps(object, markers, seen); + return encoded; + } + const ctor = object.constructor?.name; + const encoded: { $: 'object'; ctor?: string; props: Record } = { + $: 'object', + props: sortedProps(object, Object.keys(object), seen), + }; + if (ctor && ctor !== 'Object') encoded.ctor = ctor; + return encoded; + } finally { + seen.delete(object); + } +} + +export function encodeTagged(value: unknown): Tagged { + return encodeWith(value, new Set()); +} + +function isTag(value: Tagged): value is Exclude { + return typeof value === 'object' && value !== null; +} + +export function decodeTagged(value: Tagged): unknown { + if (!isTag(value)) return value; + switch (value.$) { + case 'undefined': return undefined; + case 'number': return value.v === '-0' ? -0 : Number(value.v); + case 'bigint': return BigInt(value.v); + case 'symbol': return Symbol(value.v); + case 'function': return Object.defineProperty(() => undefined, 'name', { value: value.name }); + case 'date': return 'invalid' in value ? new Date(NaN) : new Date(value.iso); + case 'error': { + const error = new Error(value.message); + error.name = value.name; + return error; + } + case 'usp': { + const params = new URLSearchParams(); + for (const [key, entry] of value.entries) params.append(key, entry); + // A recorded RequestTarget is a URLSearchParams carrying the parsed query as own + // properties (harper `resources/RequestTarget.ts`); replay preserves both halves. + if (value.props) for (const [key, prop] of Object.entries(value.props)) (params as unknown as Record)[key] = decodeTagged(prop); + return params; + } + case 'array': { + const items: unknown[] = value.items.map(decodeTagged); + if (value.props) for (const [key, prop] of Object.entries(value.props)) (items as unknown as Record)[key] = decodeTagged(prop); + return items; + } + case 'object': { + const object: Record = {}; + for (const [key, prop] of Object.entries(value.props)) object[key] = decodeTagged(prop); + return object; + } + } +} + +/** Constructor name recorded for a tagged object/USP, or undefined for anything else. */ +export function taggedCtor(value: Tagged): string | undefined { + if (!isTag(value)) return undefined; + if (value.$ === 'object' || value.$ === 'usp') return value.ctor; + return undefined; +} diff --git a/conformance/tsconfig.json b/conformance/tsconfig.json new file mode 100644 index 0000000..d0fabb9 --- /dev/null +++ b/conformance/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "allowImportingTsExtensions": true, + "types": ["node"], + "resolveJsonModule": true + }, + "include": ["../conformance/**/*.ts", "../test/conformance/**/*.ts", "../src/**/*.ts"] +} diff --git a/package-lock.json b/package-lock.json index f1f0cf5..f75e49c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,20 @@ "name": "rql", "version": "2.0.0-alpha.0", "devDependencies": { + "@types/node": "24.13.3", "typescript": "^5.8.0" } }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -24,6 +35,13 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 836cd26..f5721b3 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,18 @@ ], "scripts": { "build": "tsc", - "test": "node --experimental-strip-types --test test/v2/parse.test.ts", - "typecheck": "tsc --noEmit" + "test": "npm run test:parser && npm run test:conformance", + "test:parser": "node --experimental-strip-types --test test/v2/parse.test.ts", + "test:conformance": "node --experimental-strip-types --test \"test/conformance/*.test.ts\"", + "typecheck": "tsc --noEmit", + "typecheck:conformance": "tsc --noEmit -p conformance/tsconfig.json", + "conformance": "node scripts/conformance-diff.mjs", + "conformance:record": "node scripts/conformance-diff.mjs --record", + "conformance:check": "node scripts/conformance-diff.mjs --check", + "conformance:refresh-ledger": "node scripts/refresh-ledger.mjs" }, "devDependencies": { + "@types/node": "24.13.3", "typescript": "^5.8.0" } } diff --git a/scripts/conformance-diff.mjs b/scripts/conformance-diff.mjs new file mode 100644 index 0000000..49b5325 --- /dev/null +++ b/scripts/conformance-diff.mjs @@ -0,0 +1,323 @@ +#!/usr/bin/env node +/** + * Differential conformance runner: Harper's REST query parser vs. the RQL 2.0 reference + * parser, over the corpus in `conformance/corpus.ts`. + * + * Two modes: + * + * node scripts/conformance-diff.mjs replay (the default, and what CI runs) + * HARPER_PATH=../harper … --record re-record the Harper fixture, then replay + * + * Replay needs no Harper checkout: it reads the committed fixture of raw Harper output, + * maps it through `conformance/harperAdapter.ts`, and diffs it against a live reference + * parse. Recording is the only step that runs Harper, and it gives each query its own + * short-lived process because Harper's parser holds module-global state. + * + * Flags: + * --record re-record `conformance/fixtures/harper-parse.json` + * --check fail if replay does not reproduce the committed report exactly + * --out write the report somewhere else + * --timeout per-parse wall-clock budget (default 5000) + * --startup-timeout budget for a record worker to load Harper (default 60000) + * --concurrency recording processes in flight (default 4). Safe despite Harper's + * module-global parser state: each process has its own module graph, + * and no two parses ever share one. + * --json also dump the raw comparison data, for debugging the harness + * + * Exit codes: 0 clean · 1 divergences unclassified / fixture stale / report drift · 2 the + * harness itself could not run. + */ +import { fork } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { CORPUS, corpusDigest } from '../conformance/corpus.ts'; +import { buildRun } from '../conformance/compare.ts'; +import { renderReport } from '../conformance/report.ts'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const root = join(scriptDir, '..'); +const FIXTURE = join(root, 'conformance', 'fixtures', 'harper-parse.json'); +const LEDGER = join(root, 'conformance', 'ledger.json'); +const DEFAULT_REPORT = join(root, 'conformance', 'conformance-report.md'); +const FIXTURE_SCHEMA = 1; +const STALE_LEDGER_DAYS = 30; + +const argv = process.argv.slice(2); +const has = (flag) => argv.includes(`--${flag}`); +const option = (name, fallback) => { + const index = argv.indexOf(`--${name}`); + return index === -1 ? fallback : argv[index + 1]; +}; + +const options = { + record: has('record'), + check: has('check'), + out: resolve(option('out', DEFAULT_REPORT)), + timeout: Number(option('timeout', '5000')), + startupTimeout: Number(option('startup-timeout', '60000')), + concurrency: Math.max(1, Number(option('concurrency', '4'))), + json: option('json', undefined), +}; + +const fail = (message, code = 2) => { + console.error(message); + process.exit(code); +}; + +const git = (cwd, args) => { + try { + return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + } catch { + return undefined; + } +}; + +// ── recording: one isolated process per query ─────────────────────────────── + +function recordOne(harperPath, query) { + return new Promise((resolveOutcome) => { + const child = fork(join(scriptDir, 'conformance-record-worker.mjs'), [], { + // A dedicated IPC channel: Harper's module graph writes to stdout/stderr on import, + // and results must not be parsed out of that noise. + stdio: ['ignore', 'ignore', 'pipe', 'ipc'], + env: { ...process.env, HARPER_PATH: harperPath }, + }); + let stderr = ''; + child.stderr?.on('data', (chunk) => { + stderr += chunk; + }); + + let settled = false; + let timer; + const settle = (outcome) => { + if (settled) return; + settled = true; + clearTimeout(timer); + child.kill('SIGKILL'); + resolveOutcome(outcome); + }; + // Loading Harper's module graph takes seconds and has nothing to do with how long the + // parse takes. Timing them together would let a slow or busy machine record a spurious + // timeout INTO the committed fixture, so the two budgets are separate and the parse + // clock only starts once the worker says it is ready. + timer = setTimeout(() => settle({ fatal: `worker did not load Harper within ${options.startupTimeout}ms` }), options.startupTimeout); + + child.on('message', (message) => { + if (message?.type === 'recorded') settle({ strict: message.strict, target: message.target }); + else if (message?.type === 'fatal') settle({ fatal: `${message.name}: ${message.message}` }); + else if (message?.type === 'ready') { + clearTimeout(timer); + timer = setTimeout(() => settle({ timedOut: true }), options.timeout); + child.send({ type: 'record', query }); + } + }); + child.on('error', (error) => settle({ fatal: error.message })); + child.on('exit', (code, signal) => + settle({ fatal: `worker exited (code ${code}, signal ${signal})${stderr ? `: ${stderr.trim().slice(0, 500)}` : ''}` }) + ); + }); +} + +async function record() { + const harperPath = resolve(option('harper', process.env.HARPER_PATH ?? '')); + if (!process.env.HARPER_PATH && !option('harper', undefined)) + fail('--record needs a Harper checkout: set HARPER_PATH=/path/to/harper (or pass --harper ).'); + const searchDist = join(harperPath, 'dist', 'resources', 'search.js'); + if (!existsSync(searchDist)) + fail(`No built Harper parser at ${searchDist}. Run \`npm install && npm run build\` in the Harper checkout first.`); + + let version; + try { + version = JSON.parse(readFileSync(join(harperPath, 'package.json'), 'utf8')).version; + } catch { + version = undefined; + } + + const provenance = { + harper: { + commit: git(harperPath, ['rev-parse', 'HEAD']) ?? 'unknown', + describe: git(harperPath, ['describe', '--tags', '--always']), + dirty: (git(harperPath, ['status', '--porcelain']) ?? '') !== '', + version, + }, + // Recorded here, not rendered from the working tree: the report is committed, so a + // value read from `git rev-parse HEAD` would invalidate it on every later commit. + reference: { + commit: git(root, ['rev-parse', 'HEAD']) ?? 'unknown', + describe: git(root, ['describe', '--tags', '--always']), + dirty: (git(root, ['status', '--porcelain']) ?? '') !== '', + }, + recordedAt: new Date().toISOString(), + corpusDigest: corpusDigest(), + node: process.version, + }; + + const cases = {}; + let done = 0; + let cursor = 0; + const runNext = async () => { + while (cursor < CORPUS.length) { + const item = CORPUS[cursor++]; + const outcome = await recordOne(harperPath, item.query); + if (outcome.fatal) fail(`Recording ${JSON.stringify(item.query)} failed: ${outcome.fatal}`); + cases[item.id] = outcome.timedOut + ? { query: item.query, timedOut: true, timeoutMs: options.timeout } + : { query: item.query, strict: outcome.strict, target: outcome.target }; + done++; + if (done % 25 === 0 || done === CORPUS.length) process.stderr.write(` recorded ${done}/${CORPUS.length}\n`); + } + }; + process.stderr.write(`Recording ${CORPUS.length} cases against Harper at ${harperPath}\n`); + await Promise.all(Array.from({ length: options.concurrency }, runNext)); + + mkdirSync(dirname(FIXTURE), { recursive: true }); + const body = JSON.stringify({ schema: FIXTURE_SCHEMA, provenance, cases }, null, '\t') + '\n'; + // Atomic: a half-written fixture would replay as silent corpus drift. + writeFileSync(`${FIXTURE}.tmp`, body); + renameSync(`${FIXTURE}.tmp`, FIXTURE); + process.stderr.write(`Wrote ${FIXTURE}\n`); +} + +// ── replay: one persistent reference worker, restarted on timeout ─────────── + +class ReferenceRunner { + #child; + #pending; + #nextId = 0; + + async #spawn() { + this.#child = fork(join(scriptDir, 'conformance-ref-worker.mjs'), [], { + stdio: ['ignore', 'ignore', 'inherit', 'ipc'], + }); + // Requests are issued one at a time by the replay loop, so replacing the pending map on + // a restart cannot strand a second in-flight parse. + this.#pending = new Map(); + this.#child.on('message', (message) => { + if (message?.type !== 'result') return; + const entry = this.#pending.get(message.id); + if (!entry) return; + this.#pending.delete(message.id); + clearTimeout(entry.timer); + entry.resolve({ strict: message.strict, deferred: message.deferred }); + }); + await new Promise((ready, reject) => { + this.#child.once('message', (message) => (message?.type === 'ready' ? ready() : reject(new Error('worker did not start')))); + this.#child.once('error', reject); + }); + } + + async start() { + await this.#spawn(); + } + + /** One parse. On a timeout the worker is killed and replaced, so one wedged case + * costs exactly that case rather than the rest of the corpus. */ + parse(query) { + const id = this.#nextId++; + return new Promise((resolveOutcome) => { + const timer = setTimeout(async () => { + this.#pending.delete(id); + this.#child.kill('SIGKILL'); + await this.#spawn(); + resolveOutcome({ + strict: { status: 'timeout', ms: options.timeout }, + deferred: { status: 'timeout', ms: options.timeout }, + }); + }, options.timeout); + this.#pending.set(id, { resolve: resolveOutcome, timer }); + this.#child.send({ type: 'parse', id, query }); + }); + } + + stop() { + this.#child.kill(); + } +} + +async function replay() { + if (!existsSync(FIXTURE)) + fail('No recorded fixture. Run `HARPER_PATH=../harper npm run conformance:record` first.', 1); + const fixture = JSON.parse(readFileSync(FIXTURE, 'utf8')); + if (fixture.schema !== FIXTURE_SCHEMA) + fail(`Fixture schema ${fixture.schema} is not ${FIXTURE_SCHEMA}; re-record with \`npm run conformance:record\`.`, 1); + const digest = corpusDigest(); + if (fixture.provenance?.corpusDigest !== digest) + fail( + `Fixture was recorded for corpus ${fixture.provenance?.corpusDigest}, but the corpus is now ${digest}.\n` + + 'Re-record it: `HARPER_PATH=../harper npm run conformance:record`.', + 1 + ); + const missing = CORPUS.filter((item) => !(item.id in fixture.cases)); + if (missing.length > 0) + fail(`Fixture is missing ${missing.length} case(s), starting with ${JSON.stringify(missing[0].query)}; re-record it.`, 1); + + const ledger = JSON.parse(readFileSync(LEDGER, 'utf8')); + const ledgerAgeDays = Math.floor((Date.now() - Date.parse(ledger.fetchedAt)) / 86_400_000); + if (ledgerAgeDays > STALE_LEDGER_DAYS) + process.stderr.write( + `WARNING: the ledger snapshot is ${ledgerAgeDays} days old (${ledger.source.url}).\n` + + ' Classifications may cite rows that have since changed — run `npm run conformance:refresh-ledger`.\n' + ); + + const runner = new ReferenceRunner(); + await runner.start(); + + // The reference parses run first, sequentially and each under its own timeout; the run is + // then assembled by the same code the tests use. + const referenceOutcomes = new Map(); + try { + for (const item of CORPUS) referenceOutcomes.set(item.id, await runner.parse(item.query)); + } finally { + runner.stop(); + } + + const run = buildRun(fixture, ledger, (item) => referenceOutcomes.get(item.id)); + const { comparisons } = run; + + if (options.json) writeFileSync(resolve(options.json), JSON.stringify(run, null, '\t') + '\n'); + + const report = renderReport(run); + const unclassified = comparisons.filter((comparison) => comparison.classification.verdict === 'unclassified'); + + if (options.check) { + const existing = existsSync(options.out) ? readFileSync(options.out, 'utf8') : ''; + if (existing !== report) { + writeFileSync(`${options.out}.actual`, report); + fail( + `${options.out} is out of date — replay produced a different report.\n` + + `The regenerated report was written to ${options.out}.actual; run \`npm run conformance\` and commit the result.`, + 1 + ); + } + process.stderr.write(`${options.out} is up to date (${comparisons.length} cases).\n`); + } else { + writeFileSync(options.out, report); + process.stderr.write(`Wrote ${options.out} (${comparisons.length} cases).\n`); + } + + const counts = comparisons.reduce((tally, comparison) => { + tally[comparison.classification.verdict] = (tally[comparison.classification.verdict] ?? 0) + 1; + return tally; + }, {}); + process.stderr.write( + `agrees=${counts.agrees ?? 0} ledger=${counts.ledger ?? 0} new=${counts.new ?? 0} ` + + `reference-bug=${counts['reference-bug'] ?? 0} unclassified=${counts.unclassified ?? 0}\n` + ); + + if (unclassified.length > 0) { + process.stderr.write(`\n${unclassified.length} divergence(s) are unclassified. Add a rule in conformance/classify.ts:\n`); + for (const comparison of unclassified.slice(0, 40)) + process.stderr.write( + ` ${JSON.stringify(comparison.case.query)} ref=${comparison.ref.status} harper=${comparison.harper.status}` + + `${comparison.differences.length ? ` diffs=${comparison.differences.map((difference) => difference.at || '/').join(',')}` : ''}\n` + ); + if (unclassified.length > 40) process.stderr.write(` … and ${unclassified.length - 40} more\n`); + process.exit(1); + } +} + +if (options.record) await record(); +await replay(); diff --git a/scripts/conformance-record-worker.mjs b/scripts/conformance-record-worker.mjs new file mode 100644 index 0000000..cfd9774 --- /dev/null +++ b/scripts/conformance-record-worker.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * One-shot worker that records Harper's parse of a single query string. + * + * Harper's parser keeps module-global state (`lastIndex` / `currentQuery` / `queryString` + * in `resources/search.ts`), so two parses must never interleave. Giving each case its own + * short-lived process makes that structural rather than a convention, and it also contains + * a crash or a hang to the one case that caused it. + * + * Two shapes are recorded per query, because Harper has two entry points and they behave + * differently: + * - `strict`: `parseQuery(query)` with no target — how Harper's own unit tests drive it. + * Recorded errors surface as a throw. + * - `target`: `new RequestTarget('?' + query)` — the production path, a URLSearchParams + * that also carries the parsed query, and which defers parse errors into `parseError` + * instead of throwing (ledger rows 1 and 8). + * + * Protocol (Node IPC): parent → { type: 'record', query }, child → { type: 'recorded', ... }. + */ +import { pathToFileURL } from 'node:url'; +import { join } from 'node:path'; +import { encodeTagged } from '../conformance/tagged.ts'; + +const harperPath = process.env.HARPER_PATH; +const describe = (error) => + error instanceof Error ? { name: error.name, message: error.message } : { name: 'Error', message: String(error) }; + +const importFromHarper = (relative) => import(pathToFileURL(join(harperPath, relative)).href); + +async function main() { + const { parseQuery } = await importFromHarper('dist/resources/search.js'); + const { RequestTarget } = await importFromHarper('dist/resources/RequestTarget.js'); + + process.on('message', (message) => { + if (message?.type !== 'record') return; + const query = message.query; + + let strict; + try { + strict = { status: 'ok', raw: encodeTagged(parseQuery(query)) }; + } catch (error) { + strict = { status: 'error', ...describe(error) }; + } + + let target; + try { + target = { status: 'ok', raw: encodeTagged(new RequestTarget(`?${query}`)) }; + } catch (error) { + target = { status: 'error', ...describe(error) }; + } + + // Exit only once the message has actually been flushed to the parent — `process.send` + // is asynchronous, and Harper's module graph leaves handles open (config, sockets), so + // neither a bare `process.exit()` nor waiting for a natural exit is safe. + process.send({ type: 'recorded', strict, target }, () => process.exit(0)); + }); + + process.send({ type: 'ready' }); +} + +main().catch((error) => { + if (process.send) process.send({ type: 'fatal', ...describe(error) }); + process.exit(1); +}); diff --git a/scripts/conformance-ref-worker.mjs b/scripts/conformance-ref-worker.mjs new file mode 100644 index 0000000..1ae81e2 --- /dev/null +++ b/scripts/conformance-ref-worker.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * Persistent worker for the RQL 2.0 reference parser. + * + * The reference parser runs out-of-process so that a pathological case cannot wedge the + * whole corpus run: the parent gives every parse a wall-clock budget and, on a timeout, + * kills and restarts this worker rather than waiting. One long-lived process is enough + * here — unlike the Harper recorder, the reference parser holds no module-global state — + * and it keeps replay fast. + * + * Protocol (Node IPC, so the parser's own stdout/stderr can never corrupt it): + * parent → { type: 'parse', id, query } + * child → { type: 'result', id, strict, deferred } + */ +import { canonicalize } from '../conformance/canonical.ts'; +import { parseQuery } from '../src/index.ts'; + +const describe = (error) => + error instanceof Error ? `${error.name}: ${error.message}` : `Error: ${String(error)}`; + +function strictOutcome(query) { + try { + return { status: 'parsed', canonical: canonicalize(parseQuery(query)) }; + } catch (error) { + return { status: 'rejected', error: describe(error) }; + } +} + +/** §6.1's optional deferred-error mode, for comparison against Harper's (ledger row 8). */ +function deferredOutcome(query) { + try { + const result = parseQuery(query, { deferErrors: true }); + const { parseError, ...rest } = result; + if (parseError) return { status: 'deferred-error', error: describe(parseError), canonical: canonicalize(rest) }; + return { status: 'parsed', canonical: canonicalize(rest) }; + } catch (error) { + return { status: 'rejected', error: describe(error) }; + } +} + +process.on('message', (message) => { + if (message?.type !== 'parse') return; + let strict; + let deferred; + try { + strict = strictOutcome(message.query); + deferred = deferredOutcome(message.query); + } catch (error) { + strict = { status: 'harness-error', error: describe(error) }; + deferred = strict; + } + process.send({ type: 'result', id: message.id, strict, deferred }); +}); + +process.send({ type: 'ready' }); diff --git a/scripts/refresh-ledger.mjs b/scripts/refresh-ledger.mjs new file mode 100755 index 0000000..4e7a4ac --- /dev/null +++ b/scripts/refresh-ledger.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +/** + * Refresh `conformance/ledger.json` from Harper's divergence ledger. + * + * Spec Appendix D deliberately keeps no vendor rows: it links to each implementation's own + * public ledger, which for Harper is issue #2440. The harness needs those rows locally to + * classify a divergence, so it keeps a snapshot — a provenance-stamped CACHE, never a second + * source of truth. The issue stays canonical; the report prints the snapshot's age and warns + * when it is stale. + * + * Usage: node scripts/refresh-ledger.mjs [--issue N] [--repo owner/name] + */ +import { execFileSync } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const args = process.argv.slice(2); +const option = (name, fallback) => { + const index = args.indexOf(`--${name}`); + return index === -1 ? fallback : args[index + 1]; +}; + +const repo = option('repo', 'HarperFast/harper'); +const issue = Number(option('issue', '2440')); + +let raw; +try { + raw = execFileSync('gh', ['issue', 'view', String(issue), '--repo', repo, '--json', 'title,body,url,number'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); +} catch (error) { + console.error(`Could not read ${repo}#${issue} with the gh CLI: ${error.message}`); + console.error('Authenticate with `gh auth login`, or edit conformance/ledger.json by hand from the issue.'); + process.exit(2); +} + +const issueJson = JSON.parse(raw); +const rows = []; +// The ledger is one Markdown table of `| # | Divergence | Class | Action |`. +for (const line of issueJson.body.split('\n')) { + const cells = line.trim().match(/^\|(.+)\|$/); + if (!cells) continue; + // A cell may hold a code span containing `|` (row 14 does), so mask spans before splitting. + const spans = []; + const masked = cells[1].replace(/`[^`]*`/g, (span) => `\u0000${spans.push(span) - 1}\u0000`); + const parts = masked + .split('|') + .map((cell) => cell.replace(/\u0000(\d+)\u0000/g, (_, index) => spans[Number(index)]).trim()); + if (parts.length !== 4) continue; + const row = Number(parts[0]); + if (!Number.isInteger(row)) continue; + rows.push({ row, divergence: parts[1], class: parts[2], action: parts[3] }); +} + +if (rows.length === 0) { + console.error(`No ledger rows found in ${issueJson.url} — has the issue's table format changed?`); + process.exit(2); +} + +const ledger = { + source: { repo, issue: issueJson.number, url: issueJson.url, title: issueJson.title }, + fetchedAt: new Date().toISOString(), + note: 'Snapshot of Harper’s divergence ledger, cached so the harness can classify offline. The issue is canonical; refresh with `npm run conformance:refresh-ledger`.', + rows, +}; + +const target = join(root, 'conformance', 'ledger.json'); +writeFileSync(target, JSON.stringify(ledger, null, '\t') + '\n'); +console.log(`Wrote ${rows.length} ledger rows from ${issueJson.url} to conformance/ledger.json`); diff --git a/test/conformance/adapter.test.ts b/test/conformance/adapter.test.ts new file mode 100644 index 0000000..c8d8907 --- /dev/null +++ b/test/conformance/adapter.test.ts @@ -0,0 +1,316 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { AdapterError, adaptHarperResult } from '../../conformance/harperAdapter.ts'; +import type { ParseResult } from '../../src/types.ts'; + +/** + * Every raw shape below was captured from Harper's built parser (see + * `conformance/fixtures/harper-parse.json` for the recorded originals); the adapter is + * asserted against those shapes rather than against invented ones. + */ +function adapt(raw: unknown): ParseResult { + const outcome = adaptHarperResult(raw); + assert.equal(outcome.status, 'parsed', 'expected a parsed outcome'); + return (outcome as { result: ParseResult }).result; +} + +const query = (body: Record): Record => ({ conditions: [], ...body }); + +describe('harper adapter — fast path (ledger row 1)', () => { + it('maps URLSearchParams entries to verbatim equality conditions', () => { + assert.deepEqual(adapt(new URLSearchParams('a=1&b=2')), { + filter: { + operator: 'and', + terms: [ + { path: ['a'], comparator: 'eq', value: '1' }, + { path: ['b'], comparator: 'eq', value: '2' }, + ], + }, + }); + }); + + it('treats a fast-path name as ONE segment — a literal dot cannot reach this path', () => { + assert.deepEqual(adapt(new URLSearchParams('a%2Eb=1')), { + filter: { operator: 'and', terms: [{ path: ['a.b'], comparator: 'eq', value: '1' }] }, + }); + }); + + it('maps an empty query to an unfiltered result', () => { + assert.deepEqual(adapt(undefined), {}); + assert.deepEqual(adapt(new URLSearchParams('')), {}); + }); +}); + +describe('harper adapter — conditions', () => { + it('resolves Harper comparator aliases to canonical names', () => { + const cases: [string, string, boolean][] = [ + ['equals', 'eq', false], + ['eq', 'eq', false], + ['ne', 'eq', true], + ['not_equal', 'eq', true], + ['notEqual', 'eq', true], + ['sw', 'starts_with', false], + ['ew', 'ends_with', false], + ['ct', 'contains', false], + ['greaterThanEqual', 'ge', false], + ['less_than', 'lt', false], + ]; + for (const [harperName, canonical, negated] of cases) { + const result = adapt(query({ conditions: [{ comparator: harperName, attribute: 'a', value: '1' }] })); + const condition = result.filter?.terms[0] as { comparator: string; negated?: boolean }; + assert.equal(condition.comparator, canonical, harperName); + assert.equal(condition.negated ?? false, negated, harperName); + } + }); + + it('follows HARPER’s vocabulary for `includes`, not Appendix B’s', () => { + // Harper's ALTERNATE_COMPARATOR_NAMES maps `includes` to `in`; the spec maps it to + // `contains`. Mapping it to `contains` here would hide the divergence. + const result = adapt(query({ conditions: [{ comparator: 'includes', attribute: 'a', value: 'x' }] })); + assert.equal((result.filter?.terms[0] as { comparator: string }).comparator, 'in'); + }); + + it('combines an alias’s own negation with the negated flag', () => { + const result = adapt(query({ conditions: [{ comparator: 'ne', attribute: 'a', value: '1', negated: true }] })); + // `not_ne` would be a double negation, so the canonical condition is NOT negated. + assert.deepEqual(result.filter?.terms[0], { path: ['a'], comparator: 'eq', value: '1' }); + }); + + it('passes an unknown comparator through untouched (the open vocabulary)', () => { + const result = adapt(query({ conditions: [{ comparator: 'frobnicate', attribute: 'a', value: 'x' }] })); + assert.equal((result.filter?.terms[0] as { comparator: string }).comparator, 'frobnicate'); + }); + + it('NEGATIVE CONTROL: never re-interprets a value Harper already decoded', () => { + for (const value of ['3', 'true', 'null', '2.5']) { + const result = adapt(query({ conditions: [{ comparator: 'eq', attribute: 'a', value }] })); + assert.equal((result.filter?.terms[0] as { value: unknown }).value, value, `${value} must stay a string`); + } + // …and a typed value Harper produced is passed along as the typed value it is. + const date = new Date('2020-01-01T00:00:00.000Z'); + const typed = adapt(query({ conditions: [{ comparator: 'eq', attribute: 'a', value: date }] })); + assert.equal((typed.filter?.terms[0] as { value: unknown }).value, date); + }); + + it('maps a dotted attribute array to a segment path, and a missing one to []', () => { + const dotted = adapt(query({ conditions: [{ comparator: 'eq', attribute: ['a', 'b'], value: '1' }] })); + assert.deepEqual((dotted.filter?.terms[0] as { path: string[] }).path, ['a', 'b']); + }); + + it('defaults a group operator to `and` and honours `or`', () => { + const both = adapt( + query({ + conditions: [ + { comparator: 'equals', attribute: 'a', value: '1' }, + { comparator: 'equals', attribute: 'b', value: '2' }, + ], + operator: 'or', + }) + ); + assert.equal(both.filter?.operator, 'or'); + assert.equal(adapt(query({ conditions: [{ comparator: 'equals', attribute: 'a', value: '1' }] })).filter?.operator, 'and'); + }); +}); + +describe('harper adapter — element matches', () => { + it('desugars `between` to an element-scoped ge + le (ledger row 4)', () => { + const result = adapt(query({ conditions: [{ comparator: 'between', attribute: 'a', value: ['1', '5'] }] })); + assert.deepEqual(result.filter?.terms[0], { + path: ['a'], + some: { + operator: 'and', + terms: [ + { path: [], comparator: 'ge', value: '1' }, + { path: [], comparator: 'le', value: '5' }, + ], + }, + }); + }); + + it('marks not_between as a negated element match', () => { + const result = adapt(query({ conditions: [{ comparator: 'between', attribute: 'a', value: ['1', '5'], negated: true }] })); + assert.equal((result.filter?.terms[0] as { negated?: boolean }).negated, true); + }); + + it('keeps a `between` whose argument is not a two-element list as a literal condition', () => { + const result = adapt(query({ conditions: [{ comparator: 'between', attribute: 'a', value: '1' }] })); + assert.deepEqual(result.filter?.terms[0], { path: ['a'], comparator: 'between', value: '1' }); + }); + + it('maps chainedConditions to an element match with element-relative paths (row 2)', () => { + const result = adapt( + query({ + conditions: [ + { + comparator: 'ge', + attribute: 'ratings', + value: '3', + operator: 'and', + chainedConditions: [{ comparator: 'le', attribute: null, value: '4' }], + }, + ], + }) + ); + assert.deepEqual(result.filter?.terms[0], { + path: ['ratings'], + some: { + operator: 'and', + terms: [ + { path: [], comparator: 'ge', value: '3' }, + { path: [], comparator: 'le', value: '4' }, + ], + }, + }); + }); + + it('carries a chain’s `or` operator into the element match', () => { + const result = adapt( + query({ + conditions: [ + { + comparator: 'ge', + attribute: 'a', + value: '3', + operator: 'or', + chainedConditions: [{ comparator: 'le', attribute: null, value: '4' }], + }, + ], + }) + ); + assert.equal((result.filter?.terms[0] as { some: { operator: string } }).some.operator, 'or'); + }); + + it('maps `prop[cond&cond]` to an element match on the decoded property path', () => { + const result = adapt( + query({ + conditions: [ + { + name: 'reviews', + operator: 'and', + conditions: [ + { comparator: 'ge', attribute: 'rating', value: '4' }, + { comparator: 'ge', attribute: 'helpful', value: '10' }, + ], + }, + ], + }) + ); + assert.deepEqual(result.filter?.terms[0], { + path: ['reviews'], + some: { + operator: 'and', + terms: [ + { path: ['rating'], comparator: 'ge', value: '4' }, + { path: ['helpful'], comparator: 'ge', value: '10' }, + ], + }, + }); + }); + + it('splits a scoped match’s RAW name on literal dots and decodes each segment (§4.2)', () => { + const dotted = adapt(query({ conditions: [{ name: 'a.b', conditions: [{ comparator: 'equals', attribute: 'c', value: '1' }] }] })); + assert.deepEqual(dotted.filter?.terms[0], { path: ['a', 'b', 'c'], comparator: 'eq', value: '1' }); + const encoded = adapt(query({ conditions: [{ name: 'a%2Eb', conditions: [{ comparator: 'equals', attribute: 'c', value: '1' }] }] })); + assert.deepEqual(encoded.filter?.terms[0], { path: ['a.b', 'c'], comparator: 'eq', value: '1' }); + }); + + it('normalizes a single non-negated inner condition to the concatenated path (§5.3)', () => { + const result = adapt(query({ conditions: [{ name: 'orders', conditions: [{ comparator: 'equals', attribute: 'status', value: 'open' }] }] })); + assert.deepEqual(result.filter?.terms[0], { path: ['orders', 'status'], comparator: 'eq', value: 'open' }); + }); + + it('NEVER flattens a negated inner condition — ∃¬ is not ¬∃ (§5.1.1)', () => { + const result = adapt( + query({ conditions: [{ name: 'tags', conditions: [{ comparator: 'eq', attribute: 'x', value: 'urgent', negated: true }] }] }) + ); + assert.deepEqual(result.filter?.terms[0], { + path: ['tags'], + some: { operator: 'and', terms: [{ path: ['x'], comparator: 'eq', value: 'urgent', negated: true }] }, + }); + }); + + it('reads a nameless nested condition list as a plain group', () => { + const result = adapt( + query({ + conditions: [ + { + conditions: [ + { comparator: 'equals', attribute: 'a', value: '1' }, + { comparator: 'equals', attribute: 'b', value: '2' }, + ], + operator: 'or', + }, + ], + }) + ); + assert.deepEqual(result.filter?.terms[0], { + operator: 'or', + terms: [ + { path: ['a'], comparator: 'eq', value: '1' }, + { path: ['b'], comparator: 'eq', value: '2' }, + ], + }); + }); +}); + +describe('harper adapter — sort, select, limit (ledger row 5)', () => { + it('walks the sort linked list', () => { + const result = adapt(query({ sort: { attribute: 'a', descending: false, next: { attribute: ['b', 'c'], descending: true } } })); + assert.deepEqual(result.sort, [ + { path: ['a'], direction: 'asc' }, + { path: ['b', 'c'], direction: 'desc' }, + ]); + }); + + it('keeps an empty sort attribute as a one-segment empty path', () => { + assert.deepEqual(adapt(query({ sort: { attribute: '', descending: false } })).sort, [{ path: [''], direction: 'asc' }]); + }); + + it('maps the polymorphic select shapes to a projection mode', () => { + assert.deepEqual(adapt(query({ select: 'a' })).select, { mode: 'values', fields: [{ path: ['a'] }] }); + assert.deepEqual(adapt(query({ select: ['a', 'b'] })).select, { + mode: 'records', + fields: [{ path: ['a'] }, { path: ['b'] }], + }); + const tuple = Object.assign(['a', 'b'], { asArray: true }); + assert.deepEqual(adapt(query({ select: tuple })).select, { mode: 'tuples', fields: [{ path: ['a'] }, { path: ['b'] }] }); + }); + + it('maps a nested select, in both the brace and the bracket shape', () => { + const brace = Object.assign(['x', 'y'], { name: 'rel' }); + assert.deepEqual(adapt(query({ select: brace })).select, { + mode: 'records', + fields: [{ path: ['rel'], projection: { mode: 'records', fields: [{ path: ['x'] }, { path: ['y'] }] } }], + }); + assert.deepEqual(adapt(query({ select: { name: 'rel', select: ['x', 'y'], conditions: [] } })).select, { + mode: 'records', + fields: [{ path: ['rel'], projection: { mode: 'records', fields: [{ path: ['x'] }, { path: ['y'] }] } }], + }); + }); + + it('carries limit and offset through, including values Harper failed to validate', () => { + assert.deepEqual(adapt(query({ limit: 5, offset: 2 })), { limit: 5, offset: 2 }); + assert.ok(Number.isNaN(adapt(query({ limit: NaN })).limit)); + assert.equal(adapt(query({ offset: 2, limit: -1 })).limit, -1); + }); +}); + +describe('harper adapter — errors', () => { + it('reports a deferred parseError instead of pretending the query parsed (row 8)', () => { + const outcome = adaptHarperResult( + query({ conditions: [{ comparator: 'equals', attribute: 'a', value: '1' }], parseError: new Error('bad query') }) + ); + assert.equal(outcome.status, 'deferred-error'); + assert.equal((outcome as { message: string }).message, 'bad query'); + assert.equal((outcome as { partial: ParseResult }).partial.filter?.terms.length, 1); + }); + + it('raises AdapterError rather than guessing at a shape it does not know', () => { + assert.throws(() => adaptHarperResult(42), AdapterError); + assert.throws(() => adaptHarperResult(query({ conditions: [{ nonsense: true }] })), AdapterError); + assert.throws(() => adaptHarperResult(query({ conditions: [{ comparator: 'eq', attribute: 7, value: '1' }] })), AdapterError); + assert.throws(() => adaptHarperResult(query({ select: 42 })), AdapterError); + assert.throws(() => adaptHarperResult(query({ limit: 'ten' })), AdapterError); + }); +}); diff --git a/test/conformance/canonical.test.ts b/test/conformance/canonical.test.ts new file mode 100644 index 0000000..e8edcd5 --- /dev/null +++ b/test/conformance/canonical.test.ts @@ -0,0 +1,90 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { canonicalize, diffCanonical, isRejection, stableStringify } from '../../conformance/canonical.ts'; +import type { Json } from '../../conformance/canonical.ts'; + +describe('canonicalize', () => { + it('leaves an absent member absent — absent is not the same as empty', () => { + assert.deepEqual(canonicalize({}), {}); + assert.deepEqual(canonicalize({ sort: [] }), { sort: [] }); + }); + + it('keeps a value’s type, so "3" never reads as 3', () => { + const asString = canonicalize({ filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'eq', value: '3' }] } }); + const asNumber = canonicalize({ filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'eq', value: 3 }] } }); + assert.notDeepEqual(asString, asNumber); + }); + + it('renders a Date, an invalid Date and a non-finite number distinguishably', () => { + const of = (value: unknown): Json => + (canonicalize({ filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'eq', value: value as never }] } }) as never)[ + 'filter' + ]['terms'][0]['value']; + assert.deepEqual(of(new Date('2020-01-01T00:00:00.000Z')), { $date: '2020-01-01T00:00:00.000Z' }); + assert.deepEqual(of(new Date(NaN)), { $date: 'invalid' }); + assert.deepEqual(of(NaN), { $number: 'NaN' }); + }); + + it('distinguishes a negated condition from a plain one', () => { + const plain = canonicalize({ filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'eq', value: '1' }] } }); + const negated = canonicalize({ filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'eq', value: '1', negated: true }] } }); + assert.notDeepEqual(plain, negated); + }); + + it('distinguishes an element match from a plain condition on the same path', () => { + const match = canonicalize({ + filter: { operator: 'and', terms: [{ path: ['a'], some: { operator: 'and', terms: [{ path: [], comparator: 'ge', value: 1 }] } }] }, + }); + const condition = canonicalize({ filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'ge', value: 1 }] } }); + assert.notDeepEqual(match, condition); + }); +}); + +describe('stableStringify', () => { + it('sorts object keys so equal structures produce equal bytes', () => { + assert.equal(stableStringify({ b: 1, a: 2 }), stableStringify({ a: 2, b: 1 })); + assert.equal(stableStringify({ b: 1, a: 2 }), '{"a":2,"b":1}'); + }); + + it('keeps array order, which is meaningful', () => { + assert.notEqual(stableStringify([1, 2]), stableStringify([2, 1])); + }); + + it('indents without changing the value', () => { + assert.equal(JSON.stringify(JSON.parse(stableStringify({ a: [1, { b: 2 }] }, 2))), '{"a":[1,{"b":2}]}'); + }); +}); + +describe('diffCanonical', () => { + it('finds nothing when two results are equal', () => { + assert.deepEqual(diffCanonical({ a: 1, b: [1, 2] }, { b: [1, 2], a: 1 }), []); + }); + + it('names the pointer of a differing leaf', () => { + assert.deepEqual(diffCanonical({ a: { b: 1 } }, { a: { b: 2 } }), [{ at: '/a/b', kind: 'value', ref: 1, harper: 2 }]); + }); + + it('reports a type change rather than descending into it', () => { + assert.deepEqual(diffCanonical({ a: '1' }, { a: 1 }), [{ at: '/a', kind: 'type', ref: '1', harper: 1 }]); + }); + + it('reports members present on only one side', () => { + assert.deepEqual(diffCanonical({ a: 1 }, {}), [{ at: '/a', kind: 'ref-only', ref: 1 }]); + assert.deepEqual(diffCanonical({}, { a: 1 }), [{ at: '/a', kind: 'harper-only', harper: 1 }]); + }); + + it('reports extra and missing array elements by index', () => { + assert.deepEqual(diffCanonical([1], [1, 2]), [{ at: '/1', kind: 'harper-only', harper: 2 }]); + assert.deepEqual(diffCanonical([1, 2], [1]), [{ at: '/1', kind: 'ref-only', ref: 2 }]); + }); +}); + +describe('isRejection', () => { + it('counts both a throw and a deferred error as "not accepted"', () => { + assert.equal(isRejection({ status: 'rejected', error: 'x' }), true); + assert.equal(isRejection({ status: 'deferred-error', error: 'x', canonical: {} }), true); + assert.equal(isRejection({ status: 'parsed', canonical: {} }), false); + assert.equal(isRejection({ status: 'timeout', ms: 1 }), false); + }); +}); diff --git a/test/conformance/classify.test.ts b/test/conformance/classify.test.ts new file mode 100644 index 0000000..65d5b35 --- /dev/null +++ b/test/conformance/classify.test.ts @@ -0,0 +1,144 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import type { Difference, Outcome } from '../../conformance/canonical.ts'; +import type { Case } from '../../conformance/corpus.ts'; +import { type Comparison, classify, partitionDifferences } from '../../conformance/classify.ts'; + +const CASE: Case = { id: 'qtest', query: 'a=1', features: ['form-encoding'] }; + +const comparison = (overrides: Partial): Comparison => ({ + case: CASE, + ref: { status: 'parsed', canonical: {} }, + harper: { status: 'parsed', canonical: {} }, + differences: [], + harperFastPath: false, + ...overrides, +}); + +const valueDifference = (ref: Difference['ref'], harper: Difference['harper']): Difference => ({ + at: '/filter/terms/0/value', + kind: 'type', + ref, + harper, +}); + +const parsed: Outcome = { status: 'parsed', canonical: {} }; +const rejected: Outcome = { status: 'rejected', error: 'SyntaxViolation: nope' }; + +describe('partitionDifferences', () => { + it('removes exactly the differences Harper’s uninterpreted values explain', () => { + const split = partitionDifferences([valueDifference(3, '3'), valueDifference(true, 'true'), valueDifference(null, 'null')]); + assert.deepEqual(split.residual, []); + assert.equal(split.valueMode, true); + }); + + it('keeps a value difference that is NOT just a spelling', () => { + // A different number, not the same number written as a string. + const split = partitionDifferences([valueDifference(3, '4')]); + assert.equal(split.residual.length, 1); + assert.equal(split.valueMode, false); + }); + + it('keeps differences outside a value position', () => { + const split = partitionDifferences([{ at: '/filter/terms/0/comparator', kind: 'value', ref: 'eq', harper: 'equals' }]); + assert.equal(split.residual.length, 1); + assert.equal(split.valueMode, false); + }); + + it('separates a mixed case into an explained part and a residual', () => { + const split = partitionDifferences([ + valueDifference(10, '10'), + { at: '/filter/terms/0/comparator', kind: 'value', ref: 'contains', harper: 'in' }, + ]); + assert.equal(split.residual.length, 1); + assert.equal(split.valueMode, true); + }); +}); + +describe('classify', () => { + it('calls identical results agreement', () => { + assert.deepEqual(classify(comparison({})), { verdict: 'agrees' }); + }); + + it('calls a shared rejection agreement, however each side signalled it', () => { + assert.deepEqual(classify(comparison({ ref: rejected, harper: rejected })), { verdict: 'agrees' }); + assert.deepEqual( + classify(comparison({ ref: rejected, harper: { status: 'deferred-error', error: 'nope', canonical: {} } })), + { verdict: 'agrees' } + ); + }); + + it('classifies a case whose only difference was the value mode', () => { + const verdict = classify(comparison({ differences: [], alsoUninterpretedValues: true })); + assert.equal(verdict.verdict, 'new'); + assert.equal((verdict as { rule: string }).rule, 'value-not-interpreted'); + }); + + it('assigns the fast path to ledger row 1', () => { + const verdict = classify(comparison({ harperFastPath: true, ref: rejected, harper: parsed })); + assert.equal(verdict.verdict, 'ledger'); + assert.equal((verdict as { row: number }).row, 1); + }); + + it('assigns a rejected not(...) to ledger row 13', () => { + const verdict = classify( + comparison({ case: { ...CASE, query: 'not(a=1)', features: ['not-expr'] }, ref: parsed, harper: rejected }) + ); + assert.equal((verdict as { row: number }).row, 13); + }); + + it('assigns a rejected element condition to ledger row 14', () => { + const verdict = classify( + comparison({ case: { ...CASE, query: 'scores[=ge=10]', features: ['elem-cond'] }, ref: parsed, harper: rejected }) + ); + assert.equal((verdict as { row: number }).row, 14); + }); + + it('blames the reference parser for letting a URIError escape', () => { + const verdict = classify(comparison({ ref: { status: 'rejected', error: 'URIError: URI malformed' }, harper: parsed })); + assert.equal(verdict.verdict, 'reference-bug'); + }); + + it('recognizes the `not_` prefix gap from the shape alone, not from the query string', () => { + const verdict = classify( + comparison({ + case: { ...CASE, query: 'anything=not_zzz=1', features: ['not-prefix'] }, + differences: [ + { at: '/filter/terms/0/comparator', kind: 'value', ref: 'zzz', harper: 'not_zzz' }, + { at: '/filter/terms/0/negated', kind: 'ref-only', ref: true }, + ], + }) + ); + assert.equal(verdict.verdict, 'new'); + assert.equal((verdict as { rule: string }).rule, 'harper-not-prefix-only-for-a-fixed-base-set'); + }); + + it('NEGATIVE CONTROL: an unexplained difference is unclassified, never quietly dropped', () => { + const verdict = classify( + comparison({ + case: { ...CASE, query: 'brand.new=1', features: [] }, + differences: [{ at: '/filter/terms/0/path/0', kind: 'value', ref: 'brand', harper: 'purple' }], + }) + ); + assert.deepEqual(verdict, { verdict: 'unclassified' }); + }); + + it('NEGATIVE CONTROL: an unexplained accept/reject split is unclassified', () => { + assert.deepEqual(classify(comparison({ case: { ...CASE, query: 'nothing=matches=this', features: [] }, ref: rejected, harper: parsed })), { + verdict: 'unclassified', + }); + }); + + it('NEGATIVE CONTROL: a rule pinned to witness queries does not fire on another query', () => { + // `harper-missing-out-alias` names `a=out=(1,2)`; the same shape on another query must + // not inherit its verdict. + const verdict = classify( + comparison({ + case: { ...CASE, query: 'z=out=(1,2)', features: ['fiql-alias'] }, + differences: [{ at: '/filter/terms/0/comparator', kind: 'value', ref: 'in', harper: 'out' }], + }) + ); + assert.deepEqual(verdict, { verdict: 'unclassified' }); + }); +}); diff --git a/test/conformance/corpus.test.ts b/test/conformance/corpus.test.ts new file mode 100644 index 0000000..549f53a --- /dev/null +++ b/test/conformance/corpus.test.ts @@ -0,0 +1,65 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { + CORPUS, + EXECUTION_LEVEL_LEDGER_ROWS, + REQUIRED_FEATURES, + corpusDigest, + featureCoverage, + ledgerCoverage, +} from '../../conformance/corpus.ts'; +import type { Ledger } from '../../conformance/report.ts'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const ledger = JSON.parse(readFileSync(join(root, 'conformance', 'ledger.json'), 'utf8')) as Ledger; + +describe('corpus', () => { + it('holds at least the 200 cases the harness is specified to carry', () => { + assert.ok(CORPUS.length >= 200, `corpus has ${CORPUS.length} cases`); + }); + + it('has no duplicate query strings and no colliding ids', () => { + assert.equal(new Set(CORPUS.map((item) => item.query)).size, CORPUS.length); + assert.equal(new Set(CORPUS.map((item) => item.id)).size, CORPUS.length); + }); + + it('gives every required grammar feature at least one case', () => { + const coverage = featureCoverage(); + const uncovered = REQUIRED_FEATURES.filter((feature) => (coverage.get(feature) ?? []).length === 0); + assert.deepEqual(uncovered, [], 'grammar features with no corpus case'); + }); + + it('witnesses every parse-observable row of the divergence ledger', () => { + const witnessed = ledgerCoverage(); + const missing = ledger.rows + .map((row) => row.row) + .filter((row) => !EXECUTION_LEVEL_LEDGER_ROWS[row] && (witnessed.get(row) ?? []).length === 0); + assert.deepEqual(missing, [], 'ledger rows with no witness case'); + }); + + it('does not tag a case with a ledger row the snapshot does not have', () => { + const known = new Set(ledger.rows.map((row) => row.row)); + for (const item of CORPUS) + for (const row of item.ledger ?? []) assert.ok(known.has(row), `${item.query} cites unknown ledger row ${row}`); + }); + + it('produces a digest that changes with the corpus and not otherwise', () => { + assert.equal(corpusDigest(), corpusDigest()); + assert.match(corpusDigest(), /^\d+-[0-9a-f]{8}$/); + assert.notEqual(corpusDigest(), corpusDigest(CORPUS.slice(0, -1))); + assert.notEqual( + corpusDigest(), + corpusDigest([...CORPUS.slice(0, -1), { ...CORPUS[CORPUS.length - 1], id: 'qdeadbeef', query: 'changed=1' }]) + ); + }); + + it('marks only rows that genuinely cannot be witnessed as execution-level', () => { + const witnessed = ledgerCoverage(); + for (const row of Object.keys(EXECUTION_LEVEL_LEDGER_ROWS).map(Number)) + assert.equal((witnessed.get(row) ?? []).length, 0, `row ${row} is marked execution-level but has a witness case`); + }); +}); diff --git a/test/conformance/replay.test.ts b/test/conformance/replay.test.ts new file mode 100644 index 0000000..0f86394 --- /dev/null +++ b/test/conformance/replay.test.ts @@ -0,0 +1,116 @@ +/** + * End-to-end: replay the committed fixture through the same code the runner uses and assert + * that the committed report is exactly what comes out. + * + * This is `npm run conformance:check` as a test, minus the worker isolation — the reference + * parser runs in-process here because a wedged case would fail the test anyway. It is what + * keeps the committed report honest: nobody can hand-edit it, and a change to the adapter, + * the corpus or a classification rule that is not reflected in the report fails here. + */ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { CORPUS, corpusDigest } from '../../conformance/corpus.ts'; +import { canonicalize } from '../../conformance/canonical.ts'; +import type { Outcome } from '../../conformance/canonical.ts'; +import { RULE_IDS } from '../../conformance/classify.ts'; +import { type Fixture, type Ledger, buildRun } from '../../conformance/compare.ts'; +import { renderReport } from '../../conformance/report.ts'; +import { parseQuery } from '../../src/index.ts'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const fixture = JSON.parse(readFileSync(join(root, 'conformance', 'fixtures', 'harper-parse.json'), 'utf8')) as Fixture; +const ledger = JSON.parse(readFileSync(join(root, 'conformance', 'ledger.json'), 'utf8')) as Ledger; +const committedReport = readFileSync(join(root, 'conformance', 'conformance-report.md'), 'utf8'); + +const describeError = (error: unknown): string => + error instanceof Error ? `${error.name}: ${error.message}` : `Error: ${String(error)}`; + +function strict(query: string): Outcome { + try { + return { status: 'parsed', canonical: canonicalize(parseQuery(query)) }; + } catch (error) { + return { status: 'rejected', error: describeError(error) }; + } +} + +function deferred(query: string): Outcome { + try { + const { parseError, ...rest } = parseQuery(query, { deferErrors: true }); + return parseError + ? { status: 'deferred-error', error: describeError(parseError), canonical: canonicalize(rest) } + : { status: 'parsed', canonical: canonicalize(rest) }; + } catch (error) { + return { status: 'rejected', error: describeError(error) }; + } +} + +const run = buildRun(fixture, ledger, (item) => ({ strict: strict(item.query), deferred: deferred(item.query) })); + +describe('fixture integrity', () => { + it('is at the schema version the runner writes', () => { + assert.equal(fixture.schema, 1); + }); + + it('was recorded for exactly this corpus', () => { + assert.equal(fixture.provenance.corpusDigest, corpusDigest()); + for (const item of CORPUS) assert.ok(item.id in fixture.cases, `${item.query} is missing from the fixture`); + assert.equal(Object.keys(fixture.cases).length, CORPUS.length); + }); + + it('records the query alongside each id, so a stale entry is visible', () => { + for (const item of CORPUS) assert.equal(fixture.cases[item.id].query, item.query); + }); + + it('names the Harper and reference revisions it was recorded against', () => { + assert.match(fixture.provenance.harper.commit, /^[0-9a-f]{40}$/); + assert.match(fixture.provenance.reference.commit, /^[0-9a-f]{40}$/); + assert.ok(Date.parse(fixture.provenance.recordedAt) > 0); + }); +}); + +describe('replay', () => { + it('classifies every divergence — nothing is left unclassified', () => { + const unclassified = run.comparisons.filter((comparison) => comparison.classification.verdict === 'unclassified'); + assert.deepEqual( + unclassified.map((comparison) => comparison.case.query), + [] + ); + }); + + it('reproduces the committed report byte for byte', () => { + assert.equal(renderReport(run), committedReport, 'run `npm run conformance` and commit the regenerated report'); + }); + + it('accounts for every corpus case exactly once', () => { + assert.equal(run.comparisons.length, CORPUS.length); + assert.equal(new Set(run.comparisons.map((comparison) => comparison.case.id)).size, CORPUS.length); + }); + + it('leaves no classification rule without a witness', () => { + const fired = new Set( + run.comparisons + .map((comparison) => (comparison.classification as { rule?: string }).rule) + .filter((rule): rule is string => rule !== undefined) + ); + const dead = RULE_IDS.filter((rule) => !fired.has(rule)); + assert.deepEqual(dead, [], 'these rules match nothing — delete them or keep a witness case'); + }); + + it('still finds real agreement — the harness is not reporting everything as a divergence', () => { + const agrees = run.comparisons.filter((comparison) => comparison.classification.verdict === 'agrees'); + assert.ok(agrees.length > CORPUS.length / 4, `${agrees.length} of ${CORPUS.length} cases agree`); + }); + + it('collects deferred-mode evidence for ledger rows 8 and 9', () => { + const queries = new Set(run.deferred.map((observation) => observation.case.query)); + assert.ok(queries.has('group-by(a)'), 'row 9 needs the group-by witness'); + const groupBy = run.deferred.find((observation) => observation.case.query === 'group-by(a)'); + // Row 9: the reserved call name is reported AND still falls through into `sort`. + assert.equal(groupBy?.harper.status, 'deferred-error'); + assert.deepEqual((groupBy?.harper as { canonical: unknown }).canonical, { sort: [{ path: ['a'], direction: 'asc' }] }); + }); +}); diff --git a/test/conformance/tagged.test.ts b/test/conformance/tagged.test.ts new file mode 100644 index 0000000..5d6a5c6 --- /dev/null +++ b/test/conformance/tagged.test.ts @@ -0,0 +1,115 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { TaggedEncodeError, decodeTagged, encodeTagged } from '../../conformance/tagged.ts'; + +/** Encode, decode, encode again — the second encoding must equal the first. */ +function roundTrip(value: unknown): unknown { + const encoded = encodeTagged(value); + const decoded = decodeTagged(encoded); + assert.deepEqual(encodeTagged(decoded), encoded, 'round trip changed the encoding'); + return decoded; +} + +describe('tagged encoding', () => { + it('passes JSON primitives through unchanged', () => { + for (const value of [null, true, false, '', 'x', 0, -1, 2.5]) assert.equal(encodeTagged(value), value); + }); + + it('encodes numbers JSON cannot spell', () => { + assert.deepEqual(encodeTagged(NaN), { $: 'number', v: 'NaN' }); + assert.deepEqual(encodeTagged(Infinity), { $: 'number', v: 'Infinity' }); + assert.deepEqual(encodeTagged(-0), { $: 'number', v: '-0' }); + assert.ok(Number.isNaN(decodeTagged(encodeTagged(NaN)) as number)); + assert.ok(Object.is(decodeTagged(encodeTagged(-0)), -0)); + }); + + it('round-trips undefined, which Harper returns for an empty query', () => { + assert.deepEqual(encodeTagged(undefined), { $: 'undefined' }); + assert.equal(decodeTagged(encodeTagged(undefined)), undefined); + }); + + it('round-trips valid and invalid Dates', () => { + const valid = new Date('2020-01-01T00:00:00.000Z'); + assert.deepEqual(encodeTagged(valid), { $: 'date', iso: '2020-01-01T00:00:00.000Z' }); + assert.deepEqual(roundTrip(valid), valid); + // `a=eq=date:notadate` really does produce one of these. + assert.deepEqual(encodeTagged(new Date(NaN)), { $: 'date', invalid: true }); + assert.ok(Number.isNaN((decodeTagged(encodeTagged(new Date(NaN))) as Date).getTime())); + }); + + it('round-trips a URLSearchParams, preserving repeated names and order', () => { + const params = new URLSearchParams('a=1&b=2&a=3'); + assert.deepEqual(encodeTagged(params), { + $: 'usp', + ctor: 'URLSearchParams', + entries: [ + ['a', '1'], + ['b', '2'], + ['a', '3'], + ], + }); + const decoded = decodeTagged(encodeTagged(params)) as URLSearchParams; + assert.deepEqual([...decoded.entries()], [...params.entries()]); + }); + + it('round-trips a URLSearchParams that also carries own properties (a RequestTarget)', () => { + const target = new URLSearchParams('a=1') as URLSearchParams & { conditions?: unknown; limit?: number }; + target.conditions = [{ attribute: 'a', comparator: 'equals', value: '1' }]; + target.limit = 10; + const decoded = roundTrip(target) as URLSearchParams & { conditions?: unknown; limit?: number }; + assert.deepEqual([...decoded.entries()], [['a', '1']]); + assert.deepEqual(decoded.conditions, [{ attribute: 'a', comparator: 'equals', value: '1' }]); + assert.equal(decoded.limit, 10); + }); + + it('preserves marker properties hung off an array (select.asArray, select.name)', () => { + const select = ['x', 'y'] as string[] & { asArray?: boolean; name?: string }; + select.asArray = true; + select.name = 'rel'; + const encoded = encodeTagged(select) as { $: string; items: unknown[]; props: Record }; + assert.equal(encoded.$, 'array'); + assert.deepEqual(encoded.items, ['x', 'y']); + assert.deepEqual(encoded.props, { asArray: true, name: 'rel' }); + const decoded = roundTrip(select) as string[] & { asArray?: boolean; name?: string }; + assert.deepEqual([...decoded], ['x', 'y']); + assert.equal(decoded.asArray, true); + assert.equal(decoded.name, 'rel'); + }); + + it('records the constructor of a non-plain object', () => { + class Query { + conditions: unknown[] = []; + } + assert.deepEqual(encodeTagged(new Query()), { $: 'object', ctor: 'Query', props: { conditions: { $: 'array', items: [] } } }); + }); + + it('emits object keys in sorted order, so an unchanged re-record is byte-identical', () => { + const one = encodeTagged({ b: 1, a: 2, c: 3 }); + const other = encodeTagged({ c: 3, a: 2, b: 1 }); + assert.equal(JSON.stringify(one), JSON.stringify(other)); + }); + + it('does not mistake a plain object whose own key is "$" for a tag', () => { + const decoded = roundTrip({ $: 'usp', entries: 'not really a URLSearchParams' }) as Record; + assert.deepEqual(decoded, { $: 'usp', entries: 'not really a URLSearchParams' }); + }); + + it('round-trips an Error', () => { + const error = new TypeError('boom'); + const decoded = decodeTagged(encodeTagged(error)) as Error; + assert.equal(decoded.name, 'TypeError'); + assert.equal(decoded.message, 'boom'); + }); + + it('refuses a cyclic structure rather than looping', () => { + const cyclic: Record = {}; + cyclic.self = cyclic; + assert.throws(() => encodeTagged(cyclic), TaggedEncodeError); + }); + + it('allows the same object twice when it is not a cycle', () => { + const shared = { a: 1 }; + assert.deepEqual(roundTrip({ left: shared, right: shared }), { left: { a: 1 }, right: { a: 1 } }); + }); +}); From 32599416d13687d2a1805737f6f61536925ccfa6 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 17:55:24 -0600 Subject: [PATCH 2/9] Harden the conformance runner against a dead reference worker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the pre-push review (Cursor/Grok, round 1): - ReferenceRunner had no `exit`/`error` handler, so a worker that died mid-parse left its promise unsettled and `--check` hung forever instead of failing. The same applied to a replacement that would not start: the rejection escaped a `setTimeout` callback and the parse never resolved. Both now settle every in-flight parse with a harness error, which reaches the classifier, matches no rule, and fails the run with the query named. - `rejected` on one side and `deferred-error` on the other are no longer counted as agreement — the deferring side still produced a partial result. Neither is a shared timeout or adapter gap: an unobserved parse must never read as a pass. - `ledger-6` and `ledger-7` matched on the corpus tag alone, so any other disagreement on those queries would have been attributed to the tolerance row. Both now also require the shape the tolerance produces. - Numeric CLI flags are validated: `--concurrency nope` silently recorded an empty fixture and `--timeout nope` recorded every case as timed out. - A malformed escape in a scoped-match name is an `AdapterError` (adapter gap) rather than an unstructured harness error. - The deferred-mode table carries a fingerprint of the full produced value, so a regression past the 90-character truncation still breaks `conformance:check`. Report totals are unchanged: 190 agree, 33 ledger, 117 new, 9 reference-parser bugs, 0 unclassified. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- conformance/canonical.ts | 15 ++++ conformance/classify.ts | 26 +++++-- conformance/conformance-report.md | 113 +++++++++++++++--------------- conformance/harperAdapter.ts | 9 ++- conformance/report.ts | 12 ++-- pnpm-lock.yaml | 39 +++++++++++ scripts/conformance-diff.mjs | 94 +++++++++++++++++++------ test/conformance/classify.test.ts | 38 ++++++++-- 8 files changed, 256 insertions(+), 90 deletions(-) create mode 100644 pnpm-lock.yaml diff --git a/conformance/canonical.ts b/conformance/canonical.ts index 6d146cb..d639191 100644 --- a/conformance/canonical.ts +++ b/conformance/canonical.ts @@ -102,6 +102,21 @@ export function stableStringify(value: Json, indent = 0): string { return render(value, 0); } +/** + * Short content fingerprint of a canonical value. The report prints truncated JSON for + * readability; the fingerprint is what makes the committed bytes change when a value changes + * past the truncation point, so `--check` cannot miss a regression it did not have room to show. + */ +export function digest(value: Json): string { + const text = stableStringify(value); + let hash = 0x811c9dc5; + for (let index = 0; index < text.length; index++) { + hash ^= text.charCodeAt(index); + hash = Math.imul(hash, 0x01000193) >>> 0; + } + return hash.toString(16).padStart(8, '0'); +} + export type Difference = { /** JSON-pointer-ish location, e.g. `/filter/terms/0/value`. */ at: string; diff --git a/conformance/classify.ts b/conformance/classify.ts index 416a5a3..7241482 100644 --- a/conformance/classify.ts +++ b/conformance/classify.ts @@ -145,7 +145,7 @@ const RULES: readonly Rule[] = [ }, { id: 'ledger-13-no-not-expression', - when: (comparison) => hasFeature(comparison, 'not-expr') && !bothParsed(comparison), + when: (comparison) => hasFeature(comparison, 'not-expr') && harperRejectsOnly(comparison), classify: () => ({ verdict: 'ledger', row: 13, @@ -175,7 +175,9 @@ const RULES: readonly Rule[] = [ }, { id: 'ledger-6-lenient-value-scan', - when: (comparison) => hasLedgerTag(comparison, 6), + // The tag alone would swallow any other disagreement on these queries, so the rule also + // requires the shape the tolerance actually produces: Harper accepts, the reference does not. + when: (comparison) => hasLedgerTag(comparison, 6) && refRejectsOnly(comparison), classify: () => ({ verdict: 'ledger', row: 6, @@ -185,7 +187,7 @@ const RULES: readonly Rule[] = [ }, { id: 'ledger-7-repeated-array-parameter', - when: (comparison) => hasLedgerTag(comparison, 7), + when: (comparison) => hasLedgerTag(comparison, 7) && refRejectsOnly(comparison), classify: () => ({ verdict: 'ledger', row: 7, @@ -551,9 +553,23 @@ const VALUE_MODE_CLASSIFICATION: Extract = { }, }; +/** + * Outcomes that can constitute agreement. A timeout, an adapter gap or a harness error is + * never agreement, even when both sides report it — those mean the harness did not observe + * the parse, which has to surface rather than pass. + */ +const AGREEABLE_STATUSES: ReadonlySet = new Set(['parsed', 'rejected', 'deferred-error']); + export function classify(comparison: Comparison): Classification { - if (isRejection(comparison.ref) && isRejection(comparison.harper)) return { verdict: 'agrees' }; - if (comparison.ref.status === comparison.harper.status && comparison.differences.length === 0 && !comparison.alsoUninterpretedValues) + // Note that agreement requires the same *mode* of rejection, not merely that neither side + // accepted: a `rejected` on one side and a `deferred-error` on the other is a real + // difference (the deferring side still produced a partial result) and must reach the rules. + if ( + comparison.ref.status === comparison.harper.status && + AGREEABLE_STATUSES.has(comparison.ref.status) && + comparison.differences.length === 0 && + !comparison.alsoUninterpretedValues + ) return { verdict: 'agrees' }; for (const rule of RULES) { diff --git a/conformance/conformance-report.md b/conformance/conformance-report.md index ba9f766..caff587 100644 --- a/conformance/conformance-report.md +++ b/conformance/conformance-report.md @@ -2115,61 +2115,64 @@ permits this). These cases show what that path produces where the throwing entry rejects — including row 9, where a reserved call name is rejected *and* still falls through into `sort`. -| Query | Reference (deferred) | Harper (deferred) | Harper still produced | -|---|---|---|---| -| `name==*Jo` | rejected | rejected (deferred) | `{}` | -| `a=eq=unknown:x` | rejected | rejected (deferred) | `{}` | -| `a=1&b=2\|c=3` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `(a=1&b=2\|c=3)` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `(a=in=(1,2)&b=2)` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `ratings=ge=3&=le=4&=ne=5` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["ratin…` | -| `ratings=ge=3&=le=4\|=gt=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["ratin…` | -| `&=le=4` | rejected (deferred) | rejected (deferred) | `{}` | -| `(a=1)&=le=4` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `scores[=ge=10\|=le=2]` | parsed | rejected (deferred) | `{}` | -| `scores[=ge=10&=le=20]` | parsed | rejected (deferred) | `{}` | -| `scores[=ge=10]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["score…` | -| `tags[=not_eq=urgent]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["tags"…` | -| `reviews[=ge=4&author.name=kim]` | parsed | rejected (deferred) | `{}` | -| `not(a=1)` | parsed | rejected (deferred) | `{}` | -| `not(a=1&b=2)` | parsed | rejected (deferred) | `{}` | -| `not(a=1\|b=2)` | parsed | rejected (deferred) | `{}` | -| `not(not(a=1))` | parsed | rejected (deferred) | `{}` | -| `status=open¬(tag=urgent\|tag=blocked)` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `not(scores[=ge=10&=le=20])` | parsed | rejected (deferred) | `{}` | -| `not(reviews[rating=ge=4])` | parsed | rejected (deferred) | `{}` | -| `a=1¬(b=2)` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `[not(a=1)\|b=2]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `not()` | rejected (deferred) | rejected (deferred) | `{}` | -| `not(a=1&b=2\|c=3)` | rejected (deferred) | rejected (deferred) | `{}` | -| `sort()` | parsed | rejected (deferred) | `{}` | -| `sort(a` | rejected (deferred) | rejected (deferred) | `{}` | -| `select(a` | rejected (deferred) | rejected (deferred) | `{"select":{"fields":[],"mode":"records"}}` | -| `select([a,b)` | rejected (deferred) | rejected (deferred) | `{"select":{"fields":[{"path":["a"]}],"mode":"tuples"}}` | -| `limit()` | rejected (deferred) | rejected (deferred) | `{}` | -| `limit(1,2,3)` | rejected (deferred) | rejected (deferred) | `{}` | -| `limit(5,10` | rejected (deferred) | rejected (deferred) | `{"limit":5}` | -| `unknown(1)` | rejected (deferred) | rejected (deferred) | `{}` | -| `lt(price,10)` | rejected (deferred) | rejected (deferred) | `{}` | -| `group-by(a)` | rejected (deferred) | rejected (deferred) | `{"sort":[{"direction":"asc","path":["a"]}]}` | -| `count(a)` | rejected (deferred) | rejected (deferred) | `{}` | -| `distinct(a)` | rejected (deferred) | rejected (deferred) | `{}` | -| `aggregate(a)` | rejected (deferred) | rejected (deferred) | `{}` | -| `(` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `)` | rejected (deferred) | rejected (deferred) | `{}` | -| `[` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `]` | rejected (deferred) | rejected (deferred) | `{}` | -| `(a=1))` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `(a=1]` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | -| `\|` | parsed | rejected (deferred) | `{}` | -| `\|a=1` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `==1` | rejected (deferred) | rejected (deferred) | `{}` | -| `a=1=2` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `a=9lives=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `a<>1` | rejected (deferred) | rejected (deferred) | `{}` | -| `a]b=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | -| `a==%ZZ` | rejected | rejected (deferred) | `{}` | -| `a==%E0%A4%A` | rejected | rejected (deferred) | `{}` | +The `#` column fingerprints the full produced value, so a change past the truncation +point still changes the committed bytes and `conformance:check` still catches it. + +| Query | Reference (deferred) | Harper (deferred) | Harper still produced | # | +|---|---|---|---|---| +| `name==*Jo` | rejected | rejected (deferred) | `{}` | `5465b825` | +| `a=eq=unknown:x` | rejected | rejected (deferred) | `{}` | `5465b825` | +| `a=1&b=2\|c=3` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `37295f93` | +| `(a=1&b=2\|c=3)` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `f8343272` | +| `(a=in=(1,2)&b=2)` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `95b3c221` | +| `ratings=ge=3&=le=4&=ne=5` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["ratin…` | `76fec92f` | +| `ratings=ge=3&=le=4\|=gt=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["ratin…` | `1b82c9a2` | +| `&=le=4` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `(a=1)&=le=4` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `3b8207e8` | +| `scores[=ge=10\|=le=2]` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `scores[=ge=10&=le=20]` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `scores[=ge=10]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["score…` | `fa3b3b48` | +| `tags[=not_eq=urgent]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"elementMatch","path":["tags"…` | `25885a2a` | +| `reviews[=ge=4&author.name=kim]` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `not(a=1)` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `not(a=1&b=2)` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `not(a=1\|b=2)` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `not(not(a=1))` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `status=open¬(tag=urgent\|tag=blocked)` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `e2ec90d5` | +| `not(scores[=ge=10&=le=20])` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `not(reviews[rating=ge=4])` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `a=1¬(b=2)` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `1bf545d5` | +| `[not(a=1)\|b=2]` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `6f4af572` | +| `not()` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `not(a=1&b=2\|c=3)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `sort()` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `sort(a` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `select(a` | rejected (deferred) | rejected (deferred) | `{"select":{"fields":[],"mode":"records"}}` | `664b0b67` | +| `select([a,b)` | rejected (deferred) | rejected (deferred) | `{"select":{"fields":[{"path":["a"]}],"mode":"tuples"}}` | `47fe95e8` | +| `limit()` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `limit(1,2,3)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `limit(5,10` | rejected (deferred) | rejected (deferred) | `{"limit":5}` | `d4bcd7fd` | +| `unknown(1)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `lt(price,10)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `group-by(a)` | rejected (deferred) | rejected (deferred) | `{"sort":[{"direction":"asc","path":["a"]}]}` | `88bc04e3` | +| `count(a)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `distinct(a)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `aggregate(a)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `(` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `c82b0991` | +| `)` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `[` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `c82b0991` | +| `]` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `(a=1))` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `3b8207e8` | +| `(a=1]` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"kind":"group","operator":"and","ter…` | `c82b0991` | +| `\|` | parsed | rejected (deferred) | `{}` | `5465b825` | +| `\|a=1` | parsed | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `1bf545d5` | +| `==1` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `a=1=2` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `1146774a` | +| `a=9lives=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `1bf545d5` | +| `a<>1` | rejected (deferred) | rejected (deferred) | `{}` | `5465b825` | +| `a]b=1` | rejected (deferred) | rejected (deferred) | `{"filter":{"kind":"group","operator":"and","terms":[{"comparator":"eq","kind":"condition"…` | `8c4b0bb0` | +| `a==%ZZ` | rejected | rejected (deferred) | `{}` | `5465b825` | +| `a==%E0%A4%A` | rejected | rejected (deferred) | `{}` | `5465b825` | ## Every case diff --git a/conformance/harperAdapter.ts b/conformance/harperAdapter.ts index f1f1bcb..21e5c95 100644 --- a/conformance/harperAdapter.ts +++ b/conformance/harperAdapter.ts @@ -77,7 +77,14 @@ function decodedPath(attribute: unknown): string[] { */ function rawPath(name: unknown): string[] { if (typeof name !== 'string') throw new AdapterError(`unsupported raw path shape: ${String(name)}`); - return name.split('.').map((segment) => decodeURIComponent(segment)); + return name.split('.').map((segment) => { + try { + return decodeURIComponent(segment); + } catch { + // A malformed escape Harper left in a raw name is an adapter gap, not a harness fault. + throw new AdapterError(`cannot percent-decode path segment ${JSON.stringify(segment)}`); + } + }); } const isConditionShape = (term: Dict): boolean => 'comparator' in term || 'attribute' in term || 'value' in term; diff --git a/conformance/report.ts b/conformance/report.ts index f923811..65df730 100644 --- a/conformance/report.ts +++ b/conformance/report.ts @@ -9,7 +9,7 @@ import type { Case } from './corpus.ts'; import { EXECUTION_LEVEL_LEDGER_ROWS, REQUIRED_FEATURES, featureCoverage, ledgerCoverage } from './corpus.ts'; import type { Difference, Json, Outcome } from './canonical.ts'; -import { stableStringify } from './canonical.ts'; +import { digest, stableStringify } from './canonical.ts'; import type { Classification, Comparison } from './classify.ts'; import type { Ledger, Provenance, RunResult } from './compare.ts'; @@ -304,12 +304,16 @@ export function renderReport(run: RunResult): string { write('rejects — including row 9, where a reserved call name is rejected *and* still falls'); write('through into `sort`.'); write(); - write('| Query | Reference (deferred) | Harper (deferred) | Harper still produced |'); - write('|---|---|---|---|'); + write('The `#` column fingerprints the full produced value, so a change past the truncation'); + write('point still changes the committed bytes and `conformance:check` still catches it.'); + write(); + write('| Query | Reference (deferred) | Harper (deferred) | Harper still produced | # |'); + write('|---|---|---|---|---|'); for (const observation of run.deferred) { const produced = canonicalOf(observation.harper); write( - `| ${queryCell(observation.case.query)} | ${describeOutcome(observation.ref)} | ${describeOutcome(observation.harper)} | ${produced === undefined ? '—' : code(truncate(stableStringify(produced), 90))} |` + `| ${queryCell(observation.case.query)} | ${describeOutcome(observation.ref)} | ${describeOutcome(observation.harper)} | ` + + `${produced === undefined ? '—' : code(truncate(stableStringify(produced), 90))} | ${produced === undefined ? '—' : code(digest(produced))} |` ); } write(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..e9b94bf --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,39 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: 24.13.3 + version: 24.13.3 + typescript: + specifier: ^5.8.0 + version: 5.9.3 + +packages: + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + +snapshots: + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} diff --git a/scripts/conformance-diff.mjs b/scripts/conformance-diff.mjs index 49b5325..222fb5d 100644 --- a/scripts/conformance-diff.mjs +++ b/scripts/conformance-diff.mjs @@ -52,21 +52,29 @@ const option = (name, fallback) => { return index === -1 ? fallback : argv[index + 1]; }; +const fail = (message, code = 2) => { + console.error(message); + process.exit(code); +}; + +/** A numeric flag that is silently NaN would record a whole fixture of spurious timeouts. */ +const positiveIntOption = (name, fallback) => { + const raw = option(name, String(fallback)); + const value = Number(raw); + if (!Number.isInteger(value) || value < 1) fail(`--${name} must be a positive integer; got ${JSON.stringify(raw)}`); + return value; +}; + const options = { record: has('record'), check: has('check'), out: resolve(option('out', DEFAULT_REPORT)), - timeout: Number(option('timeout', '5000')), - startupTimeout: Number(option('startup-timeout', '60000')), - concurrency: Math.max(1, Number(option('concurrency', '4'))), + timeout: positiveIntOption('timeout', 5000), + startupTimeout: positiveIntOption('startup-timeout', 60000), + concurrency: positiveIntOption('concurrency', 4), json: option('json', undefined), }; -const fail = (message, code = 2) => { - console.error(message); - process.exit(code); -}; - const git = (cwd, args) => { try { return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); @@ -185,17 +193,36 @@ async function record() { class ReferenceRunner { #child; - #pending; + #pending = new Map(); #nextId = 0; + /** Set once the worker cannot be replaced; every later parse fails fast instead of hanging. */ + #dead; + + /** + * Settle every in-flight parse with a harness error. A reference outcome the harness could + * not observe must never look like agreement, so it flows on to the classifier, finds no + * rule, and fails the run with the query named. + */ + #failPending(reason) { + const pending = this.#pending; + this.#pending = new Map(); + for (const entry of pending.values()) { + clearTimeout(entry.timer); + const outcome = { status: 'harness-error', error: reason }; + entry.resolve({ strict: outcome, deferred: outcome }); + } + } async #spawn() { - this.#child = fork(join(scriptDir, 'conformance-ref-worker.mjs'), [], { + const child = fork(join(scriptDir, 'conformance-ref-worker.mjs'), [], { stdio: ['ignore', 'ignore', 'inherit', 'ipc'], }); + this.#child = child; // Requests are issued one at a time by the replay loop, so replacing the pending map on // a restart cannot strand a second in-flight parse. this.#pending = new Map(); - this.#child.on('message', (message) => { + + child.on('message', (message) => { if (message?.type !== 'result') return; const entry = this.#pending.get(message.id); if (!entry) return; @@ -203,29 +230,54 @@ class ReferenceRunner { clearTimeout(entry.timer); entry.resolve({ strict: message.strict, deferred: message.deferred }); }); + // Without these, a worker that dies mid-parse (uncaught throw, OOM kill) leaves its + // promise unsettled and the whole run hangs instead of failing. + const onGone = (reason) => { + if (this.#child !== child) return; + this.#dead ??= reason; + this.#failPending(reason); + }; + child.on('exit', (code, signal) => onGone(`the reference worker exited (code ${code}, signal ${signal})`)); + child.on('error', (error) => onGone(`the reference worker failed to run: ${error.message}`)); + await new Promise((ready, reject) => { - this.#child.once('message', (message) => (message?.type === 'ready' ? ready() : reject(new Error('worker did not start')))); - this.#child.once('error', reject); + const timer = setTimeout(() => reject(new Error(`the reference worker did not start within ${options.startupTimeout}ms`)), options.startupTimeout); + const settle = (fn, value) => { + clearTimeout(timer); + fn(value); + }; + child.once('message', (message) => + message?.type === 'ready' ? settle(ready) : settle(reject, new Error('the reference worker sent no ready message')) + ); + child.once('error', (error) => settle(reject, error)); + child.once('exit', (code, signal) => settle(reject, new Error(`the reference worker exited before starting (code ${code}, signal ${signal})`))); }); + this.#dead = undefined; } async start() { await this.#spawn(); } - /** One parse. On a timeout the worker is killed and replaced, so one wedged case - * costs exactly that case rather than the rest of the corpus. */ + /** + * One parse. On a timeout the worker is killed and replaced, so one wedged case costs + * exactly that case rather than the rest of the corpus. A replacement that will not start + * ends the run's reference coverage rather than hanging it. + */ parse(query) { + if (this.#dead) return Promise.resolve({ strict: { status: 'harness-error', error: this.#dead }, deferred: { status: 'harness-error', error: this.#dead } }); const id = this.#nextId++; return new Promise((resolveOutcome) => { const timer = setTimeout(async () => { this.#pending.delete(id); + const timedOut = { status: 'timeout', ms: options.timeout }; this.#child.kill('SIGKILL'); - await this.#spawn(); - resolveOutcome({ - strict: { status: 'timeout', ms: options.timeout }, - deferred: { status: 'timeout', ms: options.timeout }, - }); + try { + await this.#spawn(); + } catch (error) { + this.#dead = `the reference worker could not be restarted: ${error.message}`; + } + resolveOutcome({ strict: timedOut, deferred: timedOut }); }, options.timeout); this.#pending.set(id, { resolve: resolveOutcome, timer }); this.#child.send({ type: 'parse', id, query }); @@ -233,7 +285,7 @@ class ReferenceRunner { } stop() { - this.#child.kill(); + this.#child?.kill(); } } diff --git a/test/conformance/classify.test.ts b/test/conformance/classify.test.ts index 65d5b35..695c16f 100644 --- a/test/conformance/classify.test.ts +++ b/test/conformance/classify.test.ts @@ -61,12 +61,42 @@ describe('classify', () => { assert.deepEqual(classify(comparison({})), { verdict: 'agrees' }); }); - it('calls a shared rejection agreement, however each side signalled it', () => { + it('calls a shared rejection agreement', () => { assert.deepEqual(classify(comparison({ ref: rejected, harper: rejected })), { verdict: 'agrees' }); - assert.deepEqual( - classify(comparison({ ref: rejected, harper: { status: 'deferred-error', error: 'nope', canonical: {} } })), - { verdict: 'agrees' } + }); + + it('does NOT call a throw and a deferred error agreement — the deferring side kept a partial', () => { + const verdict = classify( + comparison({ + case: { ...CASE, query: 'not=a=witness', features: [] }, + ref: rejected, + harper: { status: 'deferred-error', error: 'nope', canonical: { limit: 1 } }, + }) ); + assert.deepEqual(verdict, { verdict: 'unclassified' }); + }); + + it('never calls an unobserved parse agreement, on either side', () => { + const timeout: Outcome = { status: 'timeout', ms: 5000 }; + assert.deepEqual(classify(comparison({ case: { ...CASE, query: 'x=1', features: [] }, ref: timeout, harper: timeout })), { + verdict: 'unclassified', + }); + const gap: Outcome = { status: 'adapter-gap', error: 'unknown shape' }; + assert.deepEqual(classify(comparison({ case: { ...CASE, query: 'y=1', features: [] }, ref: gap, harper: gap })), { + verdict: 'unclassified', + }); + }); + + it('does not attribute an unrelated disagreement to a ledger tag the case happens to carry', () => { + // `ledger-6-lenient-value-scan` fires only on the shape the tolerance produces + // (Harper accepts, the reference does not) — not on any difference at all. + const verdict = classify( + comparison({ + case: { ...CASE, query: 'foo=ba)r', features: ['tolerance'], ledger: [6] }, + differences: [{ at: '/filter/terms/0/path/0', kind: 'value', ref: 'foo', harper: 'bar' }], + }) + ); + assert.deepEqual(verdict, { verdict: 'unclassified' }); }); it('classifies a case whose only difference was the value mode', () => { From 3d880920ecb67651a3d0424275ef61361147e1e1 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:14:42 -0600 Subject: [PATCH 3/9] Make the harness fail closed, and test the crash windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the pre-push review (Codex graded leg, round 3): - An unobserved parse — a timeout, an adapter gap, a harness error — no longer reaches the classification rules. Several rules are pinned to witness queries and matched on the query string alone, so a timed-out reference parse for `limit(x)` was reported as the limit-validation divergence and the run could exit 0 having compared nothing. Such a case is now `unclassified`, which fails the run. - The record worker no longer reports a tagged-encoding failure as a Harper parse rejection: only the parse call may fail that way, and an encoding fault is sent as `fatal`. - The adapter validates known fields instead of guessing: a non-list `conditions`, an operator that is neither `and` nor `or`, and a non-boolean `negated`/`descending`/`asArray` now raise `AdapterError` rather than being read as an empty result or silently defaulted. - The fixture is assembled in corpus order rather than worker-completion order. It differed from corpus order at 156 of 349 positions, so every unchanged re-record produced a huge diff that buried real parser changes. - The committed report is no longer overwritten before the run is validated: an unclassified divergence writes `conformance-report.md.actual` and leaves the last good report alone, and the success path renames a temp file into place. - `ReferenceRunner` moved to `conformance/referenceRunner.ts` with a worker-lifecycle test suite that kills, silences and deletes real workers, so the recovery paths added in the previous commit are covered by tests that actually crash something. The end-to-end replay test never exercised them. - Removed a `pnpm-lock.yaml` that a tool on the recording host generated; this repo uses npm, and a second lockfile does not belong in this change. - Comment cleanup: dropped identifier restatements and reviewer-directed notes, and removed the unused `taggedCtor` export. Verified: 111 parser + 97 conformance tests, `typecheck:conformance` clean, `conformance:check` byte-identical, and two `--record` runs at different concurrency produced byte-identical recorded outcomes. Report totals unchanged at 190/33/117/9/0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- .gitignore | 1 + conformance/README.md | 1 + conformance/canonical.ts | 3 +- conformance/classify.ts | 23 +- conformance/compare.ts | 1 - conformance/conformance-report.md | 4 +- conformance/design.md | 2 + conformance/fixtures/harper-parse.json | 4706 +++++++++++----------- conformance/harperAdapter.ts | 33 +- conformance/referenceRunner.ts | 136 + conformance/report.ts | 1 - conformance/tagged.ts | 7 - pnpm-lock.yaml | 39 - scripts/conformance-diff.mjs | 161 +- scripts/conformance-record-worker.mjs | 27 +- test/conformance/classify.test.ts | 4 +- test/conformance/referenceRunner.test.ts | 140 + 17 files changed, 2738 insertions(+), 2551 deletions(-) create mode 100644 conformance/referenceRunner.ts delete mode 100644 pnpm-lock.yaml create mode 100644 test/conformance/referenceRunner.test.ts diff --git a/.gitignore b/.gitignore index 11ba232..007023e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ downloaded-modules node_modules npm-debug.log conformance/*.actual +conformance/*.tmp diff --git a/conformance/README.md b/conformance/README.md index 5eea832..a6c5225 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -64,6 +64,7 @@ costs that case and not the run. | `canonical.ts` | Deterministic canonical JSON for a `ParseResult`, plus the structural differ the classification rules match on. | | `classify.ts` | The rules. Each maps the *shape* of a disagreement (or a named set of witness queries) to a ledger row, a proposed new row, or a reference-parser bug. | | `compare.ts` | Assembles a run from the fixture plus reference outcomes. Shared by the runner and the tests. | +| `referenceRunner.ts` | Supervises the reference-parser worker: per-parse timeout, restart, and a settled outcome on every failure path. | | `report.ts` | Markdown rendering. Reads only committed data, never the wall clock, so the report is stable. | | `ledger.json` | A provenance-stamped **cache** of harper#2440. The issue stays canonical. | | `fixtures/harper-parse.json` | Recorded raw Harper output, stamped with the Harper commit, the reference commit, the Node version and the corpus digest. | diff --git a/conformance/canonical.ts b/conformance/canonical.ts index d639191..9b1172e 100644 --- a/conformance/canonical.ts +++ b/conformance/canonical.ts @@ -85,7 +85,7 @@ function canonicalTerm(term: Term): Json { return out; } -/** JSON with object keys emitted in sorted order, so equal structures produce equal bytes. */ +/** Keys are emitted sorted, so two equal structures serialize to identical bytes. */ export function stableStringify(value: Json, indent = 0): string { const render = (node: Json, depth: number): string => { const pad = indent ? '\n' + ' '.repeat(indent * (depth + 1)) : ''; @@ -127,7 +127,6 @@ export type Difference = { const typeOf = (node: Json): string => (node === null ? 'null' : Array.isArray(node) ? 'array' : typeof node); -/** Structural difference list between the reference result and the adapted Harper result. */ export function diffCanonical(ref: Json, harper: Json): Difference[] { const differences: Difference[] = []; walk('', ref, harper, differences); diff --git a/conformance/classify.ts b/conformance/classify.ts index 7241482..89c0133 100644 --- a/conformance/classify.ts +++ b/conformance/classify.ts @@ -554,19 +554,26 @@ const VALUE_MODE_CLASSIFICATION: Extract = { }; /** - * Outcomes that can constitute agreement. A timeout, an adapter gap or a harness error is - * never agreement, even when both sides report it — those mean the harness did not observe - * the parse, which has to surface rather than pass. + * The statuses that mean "this parser was actually observed". A timeout, an adapter gap or a + * harness error is none of those: nothing was learned about the query, so such an outcome can + * neither be agreement nor be explained by a rule about how the two parsers differ. */ -const AGREEABLE_STATUSES: ReadonlySet = new Set(['parsed', 'rejected', 'deferred-error']); +const OBSERVED_STATUSES: ReadonlySet = new Set(['parsed', 'rejected', 'deferred-error']); + +const wasObserved = (outcome: Outcome): boolean => OBSERVED_STATUSES.has(outcome.status); export function classify(comparison: Comparison): Classification { - // Note that agreement requires the same *mode* of rejection, not merely that neither side - // accepted: a `rejected` on one side and a `deferred-error` on the other is a real - // difference (the deferring side still produced a partial result) and must reach the rules. + // An unobserved parse must never reach the rules. Several rules are pinned to witness + // queries and would otherwise match on the query string alone, so a timed-out reference + // parse for `limit(x)` would be reported as the limit-validation divergence and the run + // would exit 0 having compared nothing. + if (!wasObserved(comparison.ref) || !wasObserved(comparison.harper)) return { verdict: 'unclassified' }; + + // Agreement requires the same *mode* of rejection, not merely that neither side accepted: + // a `rejected` on one side and a `deferred-error` on the other is a real difference, + // because the deferring side still produced a partial result. if ( comparison.ref.status === comparison.harper.status && - AGREEABLE_STATUSES.has(comparison.ref.status) && comparison.differences.length === 0 && !comparison.alsoUninterpretedValues ) diff --git a/conformance/compare.ts b/conformance/compare.ts index e9da633..1f807e9 100644 --- a/conformance/compare.ts +++ b/conformance/compare.ts @@ -12,7 +12,6 @@ import { decodeTagged, type Tagged } from './tagged.ts'; import { AdapterError, adaptHarperResult } from './harperAdapter.ts'; import { type Classification, type Comparison, classify, partitionDifferences } from './classify.ts'; -/** One case as the fixture stores it. */ export type RecordedCase = { query: string; timedOut?: boolean; diff --git a/conformance/conformance-report.md b/conformance/conformance-report.md index caff587..a718bb6 100644 --- a/conformance/conformance-report.md +++ b/conformance/conformance-report.md @@ -26,8 +26,8 @@ disagreement is classified below; an unclassified disagreement fails the run. |---|---| | Harper commit | `11a1c489168dfb00c6a66472cef14cb3f875286f` (v5.2.2-14-g11a1c4891) | | Harper version | `5.2.2` | -| Reference parser commit | `ab665de98490b53dbcc017857a8e1320d67c704b` (v0.3.3-23-gab665de) **+ uncommitted changes** | -| Fixture recorded | 2026-09-01T23:38:56.709Z | +| Reference parser commit | `32599416d13687d2a1805737f6f61536925ccfa6` (v0.3.3-25-g3259941) **+ uncommitted changes** | +| Fixture recorded | 2026-09-02T00:13:35.509Z | | Recorded on Node | `v26.2.0` | | Corpus digest | `349-6b64d3e9` | | Ledger snapshot | [HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440), 14 rows, fetched 2026-09-01T23:16:18.888Z | diff --git a/conformance/design.md b/conformance/design.md index cb94736..625b6c6 100644 --- a/conformance/design.md +++ b/conformance/design.md @@ -80,6 +80,8 @@ disagreement. - `conformance/canonical.ts` — canonical JSON plus the pointer-addressed differ. - `conformance/classify.ts` — the residual filter and the ordered rule set. - `conformance/compare.ts` — run assembly, shared by the runner and the tests. +- `conformance/referenceRunner.ts` — worker supervision; a separate module so its crash windows + can be tested against deliberately broken workers. - `conformance/report.ts` — Markdown rendering from committed data only. - `scripts/conformance-diff.mjs` — the CLI; `scripts/conformance-record-worker.mjs` and `scripts/conformance-ref-worker.mjs` — the two process boundaries; diff --git a/conformance/fixtures/harper-parse.json b/conformance/fixtures/harper-parse.json index 23a7264..6970fe1 100644 --- a/conformance/fixtures/harper-parse.json +++ b/conformance/fixtures/harper-parse.json @@ -8,28 +8,21 @@ "version": "5.2.2" }, "reference": { - "commit": "ab665de98490b53dbcc017857a8e1320d67c704b", - "describe": "v0.3.3-23-gab665de", + "commit": "32599416d13687d2a1805737f6f61536925ccfa6", + "describe": "v0.3.3-25-g3259941", "dirty": true }, - "recordedAt": "2026-09-01T23:38:56.709Z", + "recordedAt": "2026-09-02T00:13:35.509Z", "corpusDigest": "349-6b64d3e9", "node": "v26.2.0" }, "cases": { - "q5bb13bf6": { - "query": "a=1", + "q811c9dc5": { + "query": "", "strict": { "status": "ok", "raw": { - "$": "usp", - "ctor": "URLSearchParams", - "entries": [ - [ - "a", - "1" - ] - ] + "$": "undefined" } }, "target": { @@ -37,30 +30,32 @@ "raw": { "$": "usp", "ctor": "RequestTarget", - "entries": [ - [ - "a", - "1" - ] - ], + "entries": [], "props": { "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "a=1", + "search": "", "sort": null } } } }, - "q811c9dc5": { - "query": "", + "q5bb13bf6": { + "query": "a=1", "strict": { "status": "ok", "raw": { - "$": "undefined" + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ] + ] } }, "target": { @@ -68,14 +63,19 @@ "raw": { "$": "usp", "ctor": "RequestTarget", - "entries": [], + "entries": [ + [ + "a", + "1" + ] + ], "props": { "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "", + "search": "a=1", "sort": null } } @@ -173,8 +173,8 @@ } } }, - "qe40c292c": { - "query": "a", + "q1024a5c3": { + "query": "a=", "strict": { "status": "ok", "raw": { @@ -205,14 +205,14 @@ "pathname": { "$": "undefined" }, - "search": "a", + "search": "a=", "sort": null } } } }, - "q1024a5c3": { - "query": "a=", + "qe40c292c": { + "query": "a", "strict": { "status": "ok", "raw": { @@ -243,14 +243,14 @@ "pathname": { "$": "undefined" }, - "search": "a=", + "search": "a", "sort": null } } } }, - "q28012870": { - "query": "a=1&", + "q409fc854": { + "query": "a&b", "strict": { "status": "ok", "raw": { @@ -259,7 +259,11 @@ "entries": [ [ "a", - "1" + "" + ], + [ + "b", + "" ] ] } @@ -272,7 +276,11 @@ "entries": [ [ "a", - "1" + "" + ], + [ + "b", + "" ] ], "props": { @@ -281,14 +289,14 @@ "pathname": { "$": "undefined" }, - "search": "a=1&", + "search": "a&b", "sort": null } } } }, - "q94f4e31b": { - "query": "=1", + "q28012870": { + "query": "a=1&", "strict": { "status": "ok", "raw": { @@ -296,7 +304,7 @@ "ctor": "URLSearchParams", "entries": [ [ - "", + "a", "1" ] ] @@ -309,7 +317,7 @@ "ctor": "RequestTarget", "entries": [ [ - "", + "a", "1" ] ], @@ -319,7 +327,7 @@ "pathname": { "$": "undefined" }, - "search": "=1", + "search": "a=1&", "sort": null } } @@ -371,23 +379,14 @@ } } }, - "q409fc854": { - "query": "a&b", + "q230c8c59": { + "query": "&", "strict": { "status": "ok", "raw": { "$": "usp", "ctor": "URLSearchParams", - "entries": [ - [ - "a", - "" - ], - [ - "b", - "" - ] - ] + "entries": [] } }, "target": { @@ -395,36 +394,32 @@ "raw": { "$": "usp", "ctor": "RequestTarget", - "entries": [ - [ - "a", - "" - ], - [ - "b", - "" - ] - ], + "entries": [], "props": { "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "a&b", + "search": "&", "sort": null } } } }, - "q230c8c59": { - "query": "&", + "q94f4e31b": { + "query": "=1", "strict": { "status": "ok", "raw": { "$": "usp", "ctor": "URLSearchParams", - "entries": [] + "entries": [ + [ + "", + "1" + ] + ] } }, "target": { @@ -432,14 +427,19 @@ "raw": { "$": "usp", "ctor": "RequestTarget", - "entries": [], + "entries": [ + [ + "", + "1" + ] + ], "props": { "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "&", + "search": "=1", "sort": null } } @@ -565,8 +565,8 @@ } } }, - "q1f8304f0": { - "query": "a=%20", + "qe93d2c03": { + "query": "a=b%26c", "strict": { "status": "ok", "raw": { @@ -575,7 +575,7 @@ "entries": [ [ "a", - " " + "b&c" ] ] } @@ -588,7 +588,7 @@ "entries": [ [ "a", - " " + "b&c" ] ], "props": { @@ -597,14 +597,14 @@ "pathname": { "$": "undefined" }, - "search": "a=%20", + "search": "a=b%26c", "sort": null } } } }, - "qe93d2c03": { - "query": "a=b%26c", + "qac7f53f7": { + "query": "a%20b=1", "strict": { "status": "ok", "raw": { @@ -612,8 +612,8 @@ "ctor": "URLSearchParams", "entries": [ [ - "a", - "b&c" + "a b", + "1" ] ] } @@ -625,84 +625,24 @@ "ctor": "RequestTarget", "entries": [ [ - "a", - "b&c" - ] - ], - "props": { - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "a=b%26c", - "sort": null - } - } - } - }, - "q5306c41f": { - "query": "name==Jo*", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "name", - "comparator": "starts_with", - "value": "Jo" - } - } - ] - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "name", - "=Jo*" + "a b", + "1" ] ], "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "name", - "comparator": "starts_with", - "value": "Jo" - } - } - ] - }, "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "name==Jo*", + "search": "a%20b=1", "sort": null } } } }, - "qac7f53f7": { - "query": "a%20b=1", + "q1f8304f0": { + "query": "a=%20", "strict": { "status": "ok", "raw": { @@ -710,8 +650,8 @@ "ctor": "URLSearchParams", "entries": [ [ - "a b", - "1" + "a", + " " ] ] } @@ -723,8 +663,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a b", - "1" + "a", + " " ] ], "props": { @@ -733,7 +673,7 @@ "pathname": { "$": "undefined" }, - "search": "a%20b=1", + "search": "a=%20", "sort": null } } @@ -777,8 +717,8 @@ } } }, - "q233cabe9": { - "query": "name=eq=Jo*", + "q5306c41f": { + "query": "name==Jo*", "strict": { "status": "ok", "raw": { @@ -809,7 +749,7 @@ "entries": [ [ "name", - "eq=Jo*" + "=Jo*" ] ], "props": { @@ -831,14 +771,14 @@ "pathname": { "$": "undefined" }, - "search": "name=eq=Jo*", + "search": "name==Jo*", "sort": null } } } }, - "q08fed3ba": { - "query": "name==*", + "q233cabe9": { + "query": "name=eq=Jo*", "strict": { "status": "ok", "raw": { @@ -852,7 +792,7 @@ "props": { "attribute": "name", "comparator": "starts_with", - "value": "" + "value": "Jo" } } ] @@ -869,7 +809,7 @@ "entries": [ [ "name", - "=*" + "eq=Jo*" ] ], "props": { @@ -881,7 +821,7 @@ "props": { "attribute": "name", "comparator": "starts_with", - "value": "" + "value": "Jo" } } ] @@ -891,14 +831,14 @@ "pathname": { "$": "undefined" }, - "search": "name==*", + "search": "name=eq=Jo*", "sort": null } } } }, - "qae4e8a85": { - "query": "name==*Jo*", + "q08fed3ba": { + "query": "name==*", "strict": { "status": "ok", "raw": { @@ -912,7 +852,7 @@ "props": { "attribute": "name", "comparator": "starts_with", - "value": "*Jo" + "value": "" } } ] @@ -929,7 +869,7 @@ "entries": [ [ "name", - "=*Jo*" + "=*" ] ], "props": { @@ -941,7 +881,7 @@ "props": { "attribute": "name", "comparator": "starts_with", - "value": "*Jo" + "value": "" } } ] @@ -951,35 +891,18 @@ "pathname": { "$": "undefined" }, - "search": "name==*Jo*", + "search": "name==*", "sort": null } } } }, - "q7a7ae687": { - "query": "name==12*", + "q586532ad": { + "query": "name==*Jo", "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "name", - "comparator": "starts_with", - "value": "12" - } - } - ] - } - }, - "ctor": "Query" - } + "status": "error", + "name": "Error", + "message": "Unable to parse query, wildcard can only be used at the end of a string at position 9 in 'name==*Jo'" }, "target": { "status": "ok", @@ -989,36 +912,32 @@ "entries": [ [ "name", - "=12*" + "=*Jo" ] ], "props": { "conditions": { "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "name", - "comparator": "starts_with", - "value": "12" - } - } - ] + "items": [] }, "id": null, "isCollection": true, + "parseError": { + "$": "error", + "name": "Error", + "message": "Unable to parse query, wildcard can only be used at the end of a string at position 9 in 'name==*Jo'" + }, "pathname": { "$": "undefined" }, - "search": "name==12*", + "search": "name==*Jo", "sort": null } } } }, - "qed82cd6f": { - "query": "name=starts_with=Jo*", + "qae4e8a85": { + "query": "name==*Jo*", "strict": { "status": "ok", "raw": { @@ -1032,7 +951,7 @@ "props": { "attribute": "name", "comparator": "starts_with", - "value": "Jo*" + "value": "*Jo" } } ] @@ -1049,7 +968,7 @@ "entries": [ [ "name", - "starts_with=Jo*" + "=*Jo*" ] ], "props": { @@ -1061,7 +980,7 @@ "props": { "attribute": "name", "comparator": "starts_with", - "value": "Jo*" + "value": "*Jo" } } ] @@ -1071,53 +990,14 @@ "pathname": { "$": "undefined" }, - "search": "name=starts_with=Jo*", - "sort": null - } - } - } - }, - "q586532ad": { - "query": "name==*Jo", - "strict": { - "status": "error", - "name": "Error", - "message": "Unable to parse query, wildcard can only be used at the end of a string at position 9 in 'name==*Jo'" - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "name", - "=*Jo" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "id": null, - "isCollection": true, - "parseError": { - "$": "error", - "name": "Error", - "message": "Unable to parse query, wildcard can only be used at the end of a string at position 9 in 'name==*Jo'" - }, - "pathname": { - "$": "undefined" - }, - "search": "name==*Jo", + "search": "name==*Jo*", "sort": null } } } }, - "q073d97da": { - "query": "name===Jo*", + "q7a7ae687": { + "query": "name==12*", "strict": { "status": "ok", "raw": { @@ -1130,8 +1010,8 @@ "$": "object", "props": { "attribute": "name", - "comparator": "equals", - "value": "Jo*" + "comparator": "starts_with", + "value": "12" } } ] @@ -1148,7 +1028,7 @@ "entries": [ [ "name", - "==Jo*" + "=12*" ] ], "props": { @@ -1159,8 +1039,8 @@ "$": "object", "props": { "attribute": "name", - "comparator": "equals", - "value": "Jo*" + "comparator": "starts_with", + "value": "12" } } ] @@ -1170,14 +1050,14 @@ "pathname": { "$": "undefined" }, - "search": "name===Jo*", + "search": "name==12*", "sort": null } } } }, - "qe3339e45": { - "query": "a=in=(1,2)", + "qed82cd6f": { + "query": "name=starts_with=Jo*", "strict": { "status": "ok", "raw": { @@ -1189,15 +1069,9 @@ { "$": "object", "props": { - "attribute": "a", - "comparator": "in", - "value": { - "$": "array", - "items": [ - "1", - "2" - ] - } + "attribute": "name", + "comparator": "starts_with", + "value": "Jo*" } } ] @@ -1213,8 +1087,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a", - "in=(1,2)" + "name", + "starts_with=Jo*" ] ], "props": { @@ -1224,15 +1098,9 @@ { "$": "object", "props": { - "attribute": "a", - "comparator": "in", - "value": { - "$": "array", - "items": [ - "1", - "2" - ] - } + "attribute": "name", + "comparator": "starts_with", + "value": "Jo*" } } ] @@ -1242,14 +1110,14 @@ "pathname": { "$": "undefined" }, - "search": "a=in=(1,2)", + "search": "name=starts_with=Jo*", "sort": null } } } }, - "q9973b94c": { - "query": "a=in=()", + "q073d97da": { + "query": "name===Jo*", "strict": { "status": "ok", "raw": { @@ -1261,12 +1129,9 @@ { "$": "object", "props": { - "attribute": "a", - "comparator": "in", - "value": { - "$": "array", - "items": [] - } + "attribute": "name", + "comparator": "equals", + "value": "Jo*" } } ] @@ -1282,8 +1147,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a", - "in=()" + "name", + "==Jo*" ] ], "props": { @@ -1293,12 +1158,9 @@ { "$": "object", "props": { - "attribute": "a", - "comparator": "in", - "value": { - "$": "array", - "items": [] - } + "attribute": "name", + "comparator": "equals", + "value": "Jo*" } } ] @@ -1308,7 +1170,7 @@ "pathname": { "$": "undefined" }, - "search": "a=in=()", + "search": "name===Jo*", "sort": null } } @@ -1434,8 +1296,8 @@ } } }, - "q96686d07": { - "query": "a=in=(1)", + "qe3339e45": { + "query": "a=in=(1,2)", "strict": { "status": "ok", "raw": { @@ -1452,7 +1314,8 @@ "value": { "$": "array", "items": [ - "1" + "1", + "2" ] } } @@ -1471,7 +1334,7 @@ "entries": [ [ "a", - "in=(1)" + "in=(1,2)" ] ], "props": { @@ -1486,7 +1349,8 @@ "value": { "$": "array", "items": [ - "1" + "1", + "2" ] } } @@ -1498,14 +1362,14 @@ "pathname": { "$": "undefined" }, - "search": "a=in=(1)", + "search": "a=in=(1,2)", "sort": null } } } }, - "q0849860f": { - "query": "a=in=(1,2,3,4,5)", + "q96686d07": { + "query": "a=in=(1)", "strict": { "status": "ok", "raw": { @@ -1522,11 +1386,7 @@ "value": { "$": "array", "items": [ - "1", - "2", - "3", - "4", - "5" + "1" ] } } @@ -1545,7 +1405,7 @@ "entries": [ [ "a", - "in=(1,2,3,4,5)" + "in=(1)" ] ], "props": { @@ -1560,11 +1420,7 @@ "value": { "$": "array", "items": [ - "1", - "2", - "3", - "4", - "5" + "1" ] } } @@ -1576,14 +1432,14 @@ "pathname": { "$": "undefined" }, - "search": "a=in=(1,2,3,4,5)", + "search": "a=in=(1)", "sort": null } } } }, - "qf7be7d1a": { - "query": "a=in=(a,b%2Cc)", + "q9973b94c": { + "query": "a=in=()", "strict": { "status": "ok", "raw": { @@ -1599,10 +1455,7 @@ "comparator": "in", "value": { "$": "array", - "items": [ - "a", - "b,c" - ] + "items": [] } } } @@ -1620,7 +1473,7 @@ "entries": [ [ "a", - "in=(a,b,c)" + "in=()" ] ], "props": { @@ -1634,10 +1487,7 @@ "comparator": "in", "value": { "$": "array", - "items": [ - "a", - "b,c" - ] + "items": [] } } } @@ -1648,14 +1498,14 @@ "pathname": { "$": "undefined" }, - "search": "a=in=(a,b%2Cc)", + "search": "a=in=()", "sort": null } } } }, - "q9b80a978": { - "query": "a=in=(number:1,boolean:true)", + "q0849860f": { + "query": "a=in=(1,2,3,4,5)", "strict": { "status": "ok", "raw": { @@ -1672,8 +1522,11 @@ "value": { "$": "array", "items": [ - 1, - true + "1", + "2", + "3", + "4", + "5" ] } } @@ -1692,7 +1545,7 @@ "entries": [ [ "a", - "in=(number:1,boolean:true)" + "in=(1,2,3,4,5)" ] ], "props": { @@ -1707,8 +1560,11 @@ "value": { "$": "array", "items": [ - 1, - true + "1", + "2", + "3", + "4", + "5" ] } } @@ -1720,14 +1576,14 @@ "pathname": { "$": "undefined" }, - "search": "a=in=(number:1,boolean:true)", + "search": "a=in=(1,2,3,4,5)", "sort": null } } } }, - "q377f0f8c": { - "query": "a=out=(1,2)", + "qf7be7d1a": { + "query": "a=in=(a,b%2Cc)", "strict": { "status": "ok", "raw": { @@ -1740,8 +1596,14 @@ "$": "object", "props": { "attribute": "a", - "comparator": "out", - "value": "(1,2)" + "comparator": "in", + "value": { + "$": "array", + "items": [ + "a", + "b,c" + ] + } } } ] @@ -1758,7 +1620,7 @@ "entries": [ [ "a", - "out=(1,2)" + "in=(a,b,c)" ] ], "props": { @@ -1769,8 +1631,14 @@ "$": "object", "props": { "attribute": "a", - "comparator": "out", - "value": "(1,2)" + "comparator": "in", + "value": { + "$": "array", + "items": [ + "a", + "b,c" + ] + } } } ] @@ -1780,7 +1648,7 @@ "pathname": { "$": "undefined" }, - "search": "a=out=(1,2)", + "search": "a=in=(a,b%2Cc)", "sort": null } } @@ -1860,6 +1728,138 @@ } } }, + "q377f0f8c": { + "query": "a=out=(1,2)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "out", + "value": "(1,2)" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "out=(1,2)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "out", + "value": "(1,2)" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=out=(1,2)", + "sort": null + } + } + } + }, + "q9b80a978": { + "query": "a=in=(number:1,boolean:true)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + 1, + true + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(number:1,boolean:true)" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "in", + "value": { + "$": "array", + "items": [ + 1, + true + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(number:1,boolean:true)", + "sort": null + } + } + } + }, "q25715cf0": { "query": "a=eq=(1,2)", "strict": { @@ -2040,8 +2040,8 @@ } } }, - "qd5eb65da": { - "query": "a=between=(1,5,9)", + "q38c1ff5b": { + "query": "a=between=(1,5)", "strict": { "status": "ok", "raw": { @@ -2059,8 +2059,7 @@ "$": "array", "items": [ "1", - "5", - "9" + "5" ] } } @@ -2079,7 +2078,7 @@ "entries": [ [ "a", - "between=(1,5,9)" + "between=(1,5)" ] ], "props": { @@ -2095,8 +2094,7 @@ "$": "array", "items": [ "1", - "5", - "9" + "5" ] } } @@ -2108,7 +2106,7 @@ "pathname": { "$": "undefined" }, - "search": "a=between=(1,5,9)", + "search": "a=between=(1,5)", "sort": null } } @@ -2188,8 +2186,8 @@ } } }, - "q38c1ff5b": { - "query": "a=between=(1,5)", + "qd5eb65da": { + "query": "a=between=(1,5,9)", "strict": { "status": "ok", "raw": { @@ -2207,7 +2205,8 @@ "$": "array", "items": [ "1", - "5" + "5", + "9" ] } } @@ -2226,7 +2225,7 @@ "entries": [ [ "a", - "between=(1,5)" + "between=(1,5,9)" ] ], "props": { @@ -2242,7 +2241,8 @@ "$": "array", "items": [ "1", - "5" + "5", + "9" ] } } @@ -2254,7 +2254,7 @@ "pathname": { "$": "undefined" }, - "search": "a=between=(1,5)", + "search": "a=between=(1,5,9)", "sort": null } } @@ -2484,8 +2484,8 @@ } } }, - "q5a492aa0": { - "query": "a==-5", + "q251f38cb": { + "query": "a==3", "strict": { "status": "ok", "raw": { @@ -2499,7 +2499,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "-5" + "value": "3" } } ] @@ -2516,7 +2516,7 @@ "entries": [ [ "a", - "=-5" + "=3" ] ], "props": { @@ -2528,7 +2528,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "-5" + "value": "3" } } ] @@ -2538,14 +2538,14 @@ "pathname": { "$": "undefined" }, - "search": "a==-5", + "search": "a==3", "sort": null } } } }, - "q251f38cb": { - "query": "a==3", + "q5a492aa0": { + "query": "a==-5", "strict": { "status": "ok", "raw": { @@ -2559,7 +2559,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "3" + "value": "-5" } } ] @@ -2576,7 +2576,7 @@ "entries": [ [ "a", - "=3" + "=-5" ] ], "props": { @@ -2588,7 +2588,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "3" + "value": "-5" } } ] @@ -2598,7 +2598,7 @@ "pathname": { "$": "undefined" }, - "search": "a==3", + "search": "a==-5", "sort": null } } @@ -3024,6 +3024,44 @@ } } }, + "q482500b6": { + "query": "a=null", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "null" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "null" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=null", + "sort": null + } + } + } + }, "q81917910": { "query": "a=eq=number:5", "strict": { @@ -3084,46 +3122,8 @@ } } }, - "q482500b6": { - "query": "a=null", - "strict": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "URLSearchParams", - "entries": [ - [ - "a", - "null" - ] - ] - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "a", - "null" - ] - ], - "props": { - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "a=null", - "sort": null - } - } - } - }, - "q800f614b": { - "query": "a=eq=number:$ff", + "q800f614b": { + "query": "a=eq=number:$ff", "strict": { "status": "ok", "raw": { @@ -3680,8 +3680,8 @@ } } }, - "q2d7831b2": { - "query": "a=eq=date:notadate", + "qb79e442b": { + "query": "a=eq=date:1602872124871", "strict": { "status": "ok", "raw": { @@ -3697,7 +3697,7 @@ "comparator": "eq", "value": { "$": "date", - "invalid": true + "iso": "2020-10-16T18:15:24.871Z" } } } @@ -3715,7 +3715,7 @@ "entries": [ [ "a", - "eq=date:notadate" + "eq=date:1602872124871" ] ], "props": { @@ -3729,7 +3729,7 @@ "comparator": "eq", "value": { "$": "date", - "invalid": true + "iso": "2020-10-16T18:15:24.871Z" } } } @@ -3740,14 +3740,14 @@ "pathname": { "$": "undefined" }, - "search": "a=eq=date:notadate", + "search": "a=eq=date:1602872124871", "sort": null } } } }, - "q0fda2074": { - "query": "a=eq=string:null", + "q2d7831b2": { + "query": "a=eq=date:notadate", "strict": { "status": "ok", "raw": { @@ -3761,7 +3761,10 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "null" + "value": { + "$": "date", + "invalid": true + } } } ] @@ -3778,7 +3781,7 @@ "entries": [ [ "a", - "eq=string:null" + "eq=date:notadate" ] ], "props": { @@ -3790,7 +3793,10 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "null" + "value": { + "$": "date", + "invalid": true + } } } ] @@ -3800,14 +3806,14 @@ "pathname": { "$": "undefined" }, - "search": "a=eq=string:null", + "search": "a=eq=date:notadate", "sort": null } } } }, - "qb79e442b": { - "query": "a=eq=date:1602872124871", + "q0fda2074": { + "query": "a=eq=string:null", "strict": { "status": "ok", "raw": { @@ -3821,10 +3827,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": { - "$": "date", - "iso": "2020-10-16T18:15:24.871Z" - } + "value": "null" } } ] @@ -3841,7 +3844,7 @@ "entries": [ [ "a", - "eq=date:1602872124871" + "eq=string:null" ] ], "props": { @@ -3853,10 +3856,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": { - "$": "date", - "iso": "2020-10-16T18:15:24.871Z" - } + "value": "null" } } ] @@ -3866,7 +3866,7 @@ "pathname": { "$": "undefined" }, - "search": "a=eq=date:1602872124871", + "search": "a=eq=string:null", "sort": null } } @@ -4052,66 +4052,6 @@ } } }, - "q90449a32": { - "query": "a=eq=string:3", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "comparator": "eq", - "value": "3" - } - } - ] - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "a", - "eq=string:3" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "comparator": "eq", - "value": "3" - } - } - ] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "a=eq=string:3", - "sort": null - } - } - } - }, "qf9eaba00": { "query": "a=ne=null", "strict": { @@ -4172,8 +4112,8 @@ } } }, - "q4326264a": { - "query": "a==%2C", + "q90449a32": { + "query": "a=eq=string:3", "strict": { "status": "ok", "raw": { @@ -4187,7 +4127,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "," + "value": "3" } } ] @@ -4204,7 +4144,7 @@ "entries": [ [ "a", - "=," + "eq=string:3" ] ], "props": { @@ -4216,7 +4156,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "," + "value": "3" } } ] @@ -4226,7 +4166,7 @@ "pathname": { "$": "undefined" }, - "search": "a==%2C", + "search": "a=eq=string:3", "sort": null } } @@ -4331,8 +4271,8 @@ } } }, - "q89911502": { - "query": "a==a%2Fb", + "q4326264a": { + "query": "a==%2C", "strict": { "status": "ok", "raw": { @@ -4346,7 +4286,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "a/b" + "value": "," } } ] @@ -4363,7 +4303,7 @@ "entries": [ [ "a", - "=a/b" + "=," ] ], "props": { @@ -4375,7 +4315,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "a/b" + "value": "," } } ] @@ -4385,14 +4325,14 @@ "pathname": { "$": "undefined" }, - "search": "a==a%2Fb", + "search": "a==%2C", "sort": null } } } }, - "q25571d4d": { - "query": "a==%28x%29", + "q30260861": { + "query": "a==%26", "strict": { "status": "ok", "raw": { @@ -4406,7 +4346,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "(x)" + "value": "&" } } ] @@ -4423,7 +4363,7 @@ "entries": [ [ "a", - "=(x)" + "=&" ] ], "props": { @@ -4435,7 +4375,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "(x)" + "value": "&" } } ] @@ -4445,14 +4385,14 @@ "pathname": { "$": "undefined" }, - "search": "a==%28x%29", + "search": "a==%26", "sort": null } } } }, - "q641af072": { - "query": "brand.name=Microsoft", + "q25571d4d": { + "query": "a==%28x%29", "strict": { "status": "ok", "raw": { @@ -4464,15 +4404,9 @@ { "$": "object", "props": { - "attribute": { - "$": "array", - "items": [ - "brand", - "name" - ] - }, - "comparator": "equals", - "value": "Microsoft" + "attribute": "a", + "comparator": "eq", + "value": "(x)" } } ] @@ -4488,8 +4422,8 @@ "ctor": "RequestTarget", "entries": [ [ - "brand.name", - "Microsoft" + "a", + "=(x)" ] ], "props": { @@ -4499,15 +4433,9 @@ { "$": "object", "props": { - "attribute": { - "$": "array", - "items": [ - "brand", - "name" - ] - }, - "comparator": "equals", - "value": "Microsoft" + "attribute": "a", + "comparator": "eq", + "value": "(x)" } } ] @@ -4517,14 +4445,14 @@ "pathname": { "$": "undefined" }, - "search": "brand.name=Microsoft", + "search": "a==%28x%29", "sort": null } } } }, - "q30260861": { - "query": "a==%26", + "q89911502": { + "query": "a==a%2Fb", "strict": { "status": "ok", "raw": { @@ -4538,7 +4466,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "&" + "value": "a/b" } } ] @@ -4555,7 +4483,7 @@ "entries": [ [ "a", - "=&" + "=a/b" ] ], "props": { @@ -4567,7 +4495,7 @@ "props": { "attribute": "a", "comparator": "eq", - "value": "&" + "value": "a/b" } } ] @@ -4577,14 +4505,14 @@ "pathname": { "$": "undefined" }, - "search": "a==%26", + "search": "a==a%2Fb", "sort": null } } } }, - "q7cc95261": { - "query": "a.b.c.d.e==1", + "q641af072": { + "query": "brand.name=Microsoft", "strict": { "status": "ok", "raw": { @@ -4599,15 +4527,12 @@ "attribute": { "$": "array", "items": [ - "a", - "b", - "c", - "d", - "e" + "brand", + "name" ] }, - "comparator": "eq", - "value": "1" + "comparator": "equals", + "value": "Microsoft" } } ] @@ -4623,8 +4548,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a.b.c.d.e", - "=1" + "brand.name", + "Microsoft" ] ], "props": { @@ -4637,15 +4562,12 @@ "attribute": { "$": "array", "items": [ - "a", - "b", - "c", - "d", - "e" + "brand", + "name" ] }, - "comparator": "eq", - "value": "1" + "comparator": "equals", + "value": "Microsoft" } } ] @@ -4655,7 +4577,7 @@ "pathname": { "$": "undefined" }, - "search": "a.b.c.d.e==1", + "search": "brand.name=Microsoft", "sort": null } } @@ -4735,6 +4657,84 @@ } } }, + "q7cc95261": { + "query": "a.b.c.d.e==1", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a.b.c.d.e", + "=1" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": { + "$": "array", + "items": [ + "a", + "b", + "c", + "d", + "e" + ] + }, + "comparator": "eq", + "value": "1" + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a.b.c.d.e==1", + "sort": null + } + } + } + }, "qf192f27a": { "query": "a%2Eb=1", "strict": { @@ -4773,8 +4773,8 @@ } } }, - "q26a398dc": { - "query": "a%2Eb.c==1", + "q0d3613e5": { + "query": "a%2Eb==1", "strict": { "status": "ok", "raw": { @@ -4786,13 +4786,7 @@ { "$": "object", "props": { - "attribute": { - "$": "array", - "items": [ - "a.b", - "c" - ] - }, + "attribute": "a.b", "comparator": "eq", "value": "1" } @@ -4810,7 +4804,7 @@ "ctor": "RequestTarget", "entries": [ [ - "a.b.c", + "a.b", "=1" ] ], @@ -4821,13 +4815,7 @@ { "$": "object", "props": { - "attribute": { - "$": "array", - "items": [ - "a.b", - "c" - ] - }, + "attribute": "a.b", "comparator": "eq", "value": "1" } @@ -4839,14 +4827,14 @@ "pathname": { "$": "undefined" }, - "search": "a%2Eb.c==1", + "search": "a%2Eb==1", "sort": null } } } }, - "q965d8d84": { - "query": "a.b%2Ec==1", + "q26a398dc": { + "query": "a%2Eb.c==1", "strict": { "status": "ok", "raw": { @@ -4861,8 +4849,8 @@ "attribute": { "$": "array", "items": [ - "a", - "b.c" + "a.b", + "c" ] }, "comparator": "eq", @@ -4896,8 +4884,8 @@ "attribute": { "$": "array", "items": [ - "a", - "b.c" + "a.b", + "c" ] }, "comparator": "eq", @@ -4911,14 +4899,14 @@ "pathname": { "$": "undefined" }, - "search": "a.b%2Ec==1", + "search": "a%2Eb.c==1", "sort": null } } } }, - "q674f6d06": { - "query": "%2E==1", + "q965d8d84": { + "query": "a.b%2Ec==1", "strict": { "status": "ok", "raw": { @@ -4930,7 +4918,13 @@ { "$": "object", "props": { - "attribute": ".", + "attribute": { + "$": "array", + "items": [ + "a", + "b.c" + ] + }, "comparator": "eq", "value": "1" } @@ -4948,7 +4942,7 @@ "ctor": "RequestTarget", "entries": [ [ - ".", + "a.b.c", "=1" ] ], @@ -4959,7 +4953,13 @@ { "$": "object", "props": { - "attribute": ".", + "attribute": { + "$": "array", + "items": [ + "a", + "b.c" + ] + }, "comparator": "eq", "value": "1" } @@ -4971,14 +4971,14 @@ "pathname": { "$": "undefined" }, - "search": "%2E==1", + "search": "a.b%2Ec==1", "sort": null } } } }, - "q0d3613e5": { - "query": "a%2Eb==1", + "q674f6d06": { + "query": "%2E==1", "strict": { "status": "ok", "raw": { @@ -4990,7 +4990,7 @@ { "$": "object", "props": { - "attribute": "a.b", + "attribute": ".", "comparator": "eq", "value": "1" } @@ -5008,7 +5008,7 @@ "ctor": "RequestTarget", "entries": [ [ - "a.b", + ".", "=1" ] ], @@ -5019,7 +5019,7 @@ { "$": "object", "props": { - "attribute": "a.b", + "attribute": ".", "comparator": "eq", "value": "1" } @@ -5031,7 +5031,7 @@ "pathname": { "$": "undefined" }, - "search": "a%2Eb==1", + "search": "%2E==1", "sort": null } } @@ -5683,6 +5683,88 @@ } } }, + "qcbff4926": { + "query": "[a=1]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "[a", + "1]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "[a=1]", + "sort": null + } + } + } + }, "q1e32a7c3": { "query": "a=1&(b=2|(c=3&d=4))", "strict": { @@ -5849,8 +5931,8 @@ } } }, - "qcbff4926": { - "query": "[a=1]", + "q3d53b309": { + "query": "[a=1&[b=2|[c=3|d=4]]]", "strict": { "status": "ok", "raw": { @@ -5872,81 +5954,7 @@ "comparator": "equals", "value": "1" } - } - ] - } - }, - "ctor": "Query" - } - ] - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "[a", - "1]" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "comparator": "equals", - "value": "1" - } - } - ] - } - }, - "ctor": "Query" - } - ] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "[a=1]", - "sort": null - } - } - } - }, - "q8a0d7034": { - "query": "((a=1))", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ + }, { "$": "object", "props": { @@ -5956,18 +5964,48 @@ { "$": "object", "props": { - "attribute": "a", + "attribute": "b", "comparator": "equals", - "value": "1" + "value": "2" } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" } ] - } + }, + "operator": "or" }, "ctor": "Query" } ] - } + }, + "operator": "and" }, "ctor": "Query" } @@ -5984,8 +6022,12 @@ "ctor": "RequestTarget", "entries": [ [ - "((a", - "1))" + "[a", + "1" + ], + [ + "[b", + "2|[c=3|d=4]]]" ] ], "props": { @@ -5998,6 +6040,14 @@ "conditions": { "$": "array", "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, { "$": "object", "props": { @@ -6007,18 +6057,48 @@ { "$": "object", "props": { - "attribute": "a", + "attribute": "b", "comparator": "equals", - "value": "1" + "value": "2" } + }, + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + }, + { + "$": "object", + "props": { + "attribute": "d", + "comparator": "equals", + "value": "4" + } + } + ] + }, + "operator": "or" + }, + "ctor": "Query" } ] - } + }, + "operator": "or" }, "ctor": "Query" } ] - } + }, + "operator": "and" }, "ctor": "Query" } @@ -6029,7 +6109,7 @@ "pathname": { "$": "undefined" }, - "search": "((a=1))", + "search": "[a=1&[b=2|[c=3|d=4]]]", "sort": null } } @@ -6201,28 +6281,12 @@ } } }, - "q377f6f88": { - "query": "a=1&b=2|c=3", + "q8a0d7034": { + "query": "((a=1))", "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'a=1&b=2|c=3', Can not mix operators within a condition grouping at position 11" - }, - "target": { "status": "ok", "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "a", - "1" - ], - [ - "b", - "2|c=3" - ] - ], + "$": "object", "props": { "conditions": { "$": "array", @@ -6230,53 +6294,50 @@ { "$": "object", "props": { - "attribute": "a", - "comparator": "equals", - "value": "1" - } - }, - { - "$": "object", - "props": { - "attribute": "b", - "comparator": "equals", - "value": "2" - } - }, - { - "$": "object", - "props": { - "attribute": "c", - "comparator": "equals", - "value": "3" - } + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" } ] - }, - "id": null, - "isCollection": true, - "operator": "and", - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "Can not mix operators within a condition grouping at position 11" - }, - "parseErrorMessage": "Can not mix operators within a condition grouping at position 11", - "pathname": { - "$": "undefined" - }, - "search": "a=1&b=2|c=3", - "sort": null - } + } + }, + "ctor": "Query" } - } - }, - "q3d53b309": { - "query": "[a=1&[b=2|[c=3|d=4]]]", - "strict": { + }, + "target": { "status": "ok", "raw": { - "$": "object", + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "((a", + "1))" + ] + ], "props": { "conditions": { "$": "array", @@ -6287,14 +6348,6 @@ "conditions": { "$": "array", "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "comparator": "equals", - "value": "1" - } - }, { "$": "object", "props": { @@ -6304,56 +6357,110 @@ { "$": "object", "props": { - "attribute": "b", + "attribute": "a", "comparator": "equals", - "value": "2" + "value": "1" } - }, - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "c", - "comparator": "equals", - "value": "3" - } - }, - { - "$": "object", - "props": { - "attribute": "d", - "comparator": "equals", - "value": "4" - } - } - ] - }, - "operator": "or" - }, - "ctor": "Query" } ] - }, - "operator": "or" + } }, "ctor": "Query" } ] - }, - "operator": "and" + } }, "ctor": "Query" } ] - } - }, - "ctor": "Query" + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "((a=1))", + "sort": null + } + } + } + }, + "q377f6f88": { + "query": "a=1&b=2|c=3", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'a=1&b=2|c=3', Can not mix operators within a condition grouping at position 11" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2|c=3" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + }, + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } + } + ] + }, + "id": null, + "isCollection": true, + "operator": "and", + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "Can not mix operators within a condition grouping at position 11" + }, + "parseErrorMessage": "Can not mix operators within a condition grouping at position 11", + "pathname": { + "$": "undefined" + }, + "search": "a=1&b=2|c=3", + "sort": null + } } + } + }, + "qd754d591": { + "query": "(a=1&b=2|c=3)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 13 in '(a=1&b=2|c=3)', Can not mix operators within a condition grouping at position 13" }, "target": { "status": "ok", @@ -6362,12 +6469,12 @@ "ctor": "RequestTarget", "entries": [ [ - "[a", + "(a", "1" ], [ - "[b", - "2|[c=3|d=4]]]" + "b", + "2|c=3)" ] ], "props": { @@ -6391,50 +6498,18 @@ { "$": "object", "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "b", - "comparator": "equals", - "value": "2" - } - }, - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "c", - "comparator": "equals", - "value": "3" - } - }, - { - "$": "object", - "props": { - "attribute": "d", - "comparator": "equals", - "value": "4" - } - } - ] - }, - "operator": "or" - }, - "ctor": "Query" - } - ] - }, - "operator": "or" - }, - "ctor": "Query" + "attribute": "b", + "comparator": "equals", + "value": "2" + } + }, + { + "$": "object", + "props": { + "attribute": "c", + "comparator": "equals", + "value": "3" + } } ] }, @@ -6446,10 +6521,16 @@ }, "id": null, "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "Can not mix operators within a condition grouping at position 13" + }, + "parseErrorMessage": "Can not mix operators within a condition grouping at position 13", "pathname": { "$": "undefined" }, - "search": "[a=1&[b=2|[c=3|d=4]]]", + "search": "(a=1&b=2|c=3)", "sort": null } } @@ -6691,87 +6772,6 @@ } } }, - "qd754d591": { - "query": "(a=1&b=2|c=3)", - "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 13 in '(a=1&b=2|c=3)', Can not mix operators within a condition grouping at position 13" - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "(a", - "1" - ], - [ - "b", - "2|c=3)" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "comparator": "equals", - "value": "1" - } - }, - { - "$": "object", - "props": { - "attribute": "b", - "comparator": "equals", - "value": "2" - } - }, - { - "$": "object", - "props": { - "attribute": "c", - "comparator": "equals", - "value": "3" - } - } - ] - }, - "operator": "and" - }, - "ctor": "Query" - } - ] - }, - "id": null, - "isCollection": true, - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "Can not mix operators within a condition grouping at position 13" - }, - "parseErrorMessage": "Can not mix operators within a condition grouping at position 13", - "pathname": { - "$": "undefined" - }, - "search": "(a=1&b=2|c=3)", - "sort": null - } - } - } - }, "qac905c3b": { "query": "(a=in=(1,2)&b=2)", "strict": { @@ -7151,8 +7151,8 @@ } } }, - "qabc26be9": { - "query": "ratings=ge=3|=le=4", + "q0b1d9bdd": { + "query": "ratings=ge=3&ratings=le=4", "strict": { "status": "ok", "raw": { @@ -7165,26 +7165,21 @@ "$": "object", "props": { "attribute": "ratings", - "chainedConditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": null, - "comparator": "le", - "value": "4" - } - } - ] - }, "comparator": "ge", - "operator": "or", "value": "3" } + }, + { + "$": "object", + "props": { + "attribute": "ratings", + "comparator": "le", + "value": "4" + } } ] - } + }, + "operator": "and" }, "ctor": "Query" } @@ -7197,7 +7192,11 @@ "entries": [ [ "ratings", - "ge=3|=le=4" + "ge=3" + ], + [ + "ratings", + "le=4" ] ], "props": { @@ -7208,39 +7207,34 @@ "$": "object", "props": { "attribute": "ratings", - "chainedConditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": null, - "comparator": "le", - "value": "4" - } - } - ] - }, "comparator": "ge", - "operator": "or", "value": "3" } + }, + { + "$": "object", + "props": { + "attribute": "ratings", + "comparator": "le", + "value": "4" + } } ] }, "id": null, "isCollection": true, + "operator": "and", "pathname": { "$": "undefined" }, - "search": "ratings=ge=3|=le=4", + "search": "ratings=ge=3&ratings=le=4", "sort": null } } } }, - "q0b1d9bdd": { - "query": "ratings=ge=3&ratings=le=4", + "qabc26be9": { + "query": "ratings=ge=3|=le=4", "strict": { "status": "ok", "raw": { @@ -7253,21 +7247,26 @@ "$": "object", "props": { "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, "comparator": "ge", + "operator": "or", "value": "3" } - }, - { - "$": "object", - "props": { - "attribute": "ratings", - "comparator": "le", - "value": "4" - } } ] - }, - "operator": "and" + } }, "ctor": "Query" } @@ -7280,11 +7279,7 @@ "entries": [ [ "ratings", - "ge=3" - ], - [ - "ratings", - "le=4" + "ge=3|=le=4" ] ], "props": { @@ -7295,27 +7290,32 @@ "$": "object", "props": { "attribute": "ratings", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, "comparator": "ge", + "operator": "or", "value": "3" } - }, - { - "$": "object", - "props": { - "attribute": "ratings", - "comparator": "le", - "value": "4" - } } ] }, "id": null, "isCollection": true, - "operator": "and", "pathname": { "$": "undefined" }, - "search": "ratings=ge=3&ratings=le=4", + "search": "ratings=ge=3|=le=4", "sort": null } } @@ -7494,98 +7494,6 @@ } } }, - "q5dd20ac5": { - "query": "a==1&=le=5", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "chainedConditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": null, - "comparator": "le", - "value": "5" - } - } - ] - }, - "comparator": "eq", - "operator": "and", - "value": "1" - } - } - ] - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "a", - "=1" - ], - [ - "", - "le=5" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "chainedConditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": null, - "comparator": "le", - "value": "5" - } - } - ] - }, - "comparator": "eq", - "operator": "and", - "value": "1" - } - } - ] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "a==1&=le=5", - "sort": null - } - } - } - }, "q48dce74c": { "query": "ratings=ge=3&=le=4&b=2", "strict": { @@ -7700,8 +7608,8 @@ } } }, - "qd7cacd58": { - "query": "ratings=ge=3&=5", + "q5dd20ac5": { + "query": "a==1&=le=5", "strict": { "status": "ok", "raw": { @@ -7713,7 +7621,7 @@ { "$": "object", "props": { - "attribute": "ratings", + "attribute": "a", "chainedConditions": { "$": "array", "items": [ @@ -7721,15 +7629,15 @@ "$": "object", "props": { "attribute": null, - "comparator": "ge", + "comparator": "le", "value": "5" } } ] }, - "comparator": "ge", + "comparator": "eq", "operator": "and", - "value": "3" + "value": "1" } } ] @@ -7745,12 +7653,12 @@ "ctor": "RequestTarget", "entries": [ [ - "ratings", - "ge=3" + "a", + "=1" ], [ "", - "5" + "le=5" ] ], "props": { @@ -7760,7 +7668,7 @@ { "$": "object", "props": { - "attribute": "ratings", + "attribute": "a", "chainedConditions": { "$": "array", "items": [ @@ -7768,15 +7676,15 @@ "$": "object", "props": { "attribute": null, - "comparator": "ge", + "comparator": "le", "value": "5" } } ] }, - "comparator": "ge", + "comparator": "eq", "operator": "and", - "value": "3" + "value": "1" } } ] @@ -7786,7 +7694,7 @@ "pathname": { "$": "undefined" }, - "search": "ratings=ge=3&=5", + "search": "a==1&=le=5", "sort": null } } @@ -7884,28 +7792,12 @@ } } }, - "qd25b0a59": { - "query": "ratings=ge=3&=le=4|=gt=1", + "qd7cacd58": { + "query": "ratings=ge=3&=5", "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 24 in 'ratings=ge=3&=le=4|=gt=1', attribute must be specified before equality comparator at position 20" - }, - "target": { "status": "ok", "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "ratings", - "ge=3" - ], - [ - "", - "le=4|=gt=1" - ] - ], + "$": "object", "props": { "conditions": { "$": "array", @@ -7921,50 +7813,38 @@ "$": "object", "props": { "attribute": null, - "comparator": "le", - "value": "4" - } - }, - { - "$": "object", - "props": { - "attribute": null, - "comparator": "gt", - "value": "1" + "comparator": "ge", + "value": "5" } } ] }, "comparator": "ge", - "operator": "or", + "operator": "and", "value": "3" } } ] - }, - "id": null, - "isCollection": true, - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "attribute must be specified before equality comparator at position 20" - }, - "parseErrorMessage": "attribute must be specified before equality comparator at position 20", - "pathname": { - "$": "undefined" - }, - "search": "ratings=ge=3&=le=4|=gt=1", - "sort": null - } + } + }, + "ctor": "Query" } - } - }, - "qbad87f96": { - "query": "a=in=(1,2)&=le=4", - "strict": { + }, + "target": { "status": "ok", "raw": { - "$": "object", + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "ratings", + "ge=3" + ], + [ + "", + "5" + ] + ], "props": { "conditions": { "$": "array", @@ -7972,7 +7852,7 @@ { "$": "object", "props": { - "attribute": "a", + "attribute": "ratings", "chainedConditions": { "$": "array", "items": [ @@ -7980,28 +7860,36 @@ "$": "object", "props": { "attribute": null, - "comparator": "le", - "value": "4" + "comparator": "ge", + "value": "5" } } ] }, - "comparator": "in", + "comparator": "ge", "operator": "and", - "value": { - "$": "array", - "items": [ - "1", - "2" - ] - } + "value": "3" } } ] - } - }, - "ctor": "Query" + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "ratings=ge=3&=5", + "sort": null + } } + } + }, + "qd25b0a59": { + "query": "ratings=ge=3&=le=4|=gt=1", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 24 in 'ratings=ge=3&=le=4|=gt=1', attribute must be specified before equality comparator at position 20" }, "target": { "status": "ok", @@ -8010,12 +7898,12 @@ "ctor": "RequestTarget", "entries": [ [ - "a", - "in=(1,2)" + "ratings", + "ge=3" ], [ "", - "le=4" + "le=4|=gt=1" ] ], "props": { @@ -8025,7 +7913,7 @@ { "$": "object", "props": { - "attribute": "a", + "attribute": "ratings", "chainedConditions": { "$": "array", "items": [ @@ -8036,28 +7924,36 @@ "comparator": "le", "value": "4" } + }, + { + "$": "object", + "props": { + "attribute": null, + "comparator": "gt", + "value": "1" + } } ] }, - "comparator": "in", - "operator": "and", - "value": { - "$": "array", - "items": [ - "1", - "2" - ] - } + "comparator": "ge", + "operator": "or", + "value": "3" } } ] }, "id": null, "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "attribute must be specified before equality comparator at position 20" + }, + "parseErrorMessage": "attribute must be specified before equality comparator at position 20", "pathname": { "$": "undefined" }, - "search": "a=in=(1,2)&=le=4", + "search": "ratings=ge=3&=le=4|=gt=1", "sort": null } } @@ -8181,6 +8077,110 @@ } } }, + "qbad87f96": { + "query": "a=in=(1,2)&=le=4", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "in", + "operator": "and", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "in=(1,2)" + ], + [ + "", + "le=4" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "chainedConditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": null, + "comparator": "le", + "value": "4" + } + } + ] + }, + "comparator": "in", + "operator": "and", + "value": { + "$": "array", + "items": [ + "1", + "2" + ] + } + } + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=in=(1,2)&=le=4", + "sort": null + } + } + } + }, "qbc933def": { "query": "a=between=(1,5)&=ne=3", "strict": { @@ -8831,8 +8831,8 @@ } } }, - "qf97a5a16": { - "query": "a[b=1&c[d=2]]", + "q13b19473": { + "query": "a[b[c=1]]", "strict": { "status": "ok", "raw": { @@ -8847,14 +8847,6 @@ "conditions": { "$": "array", "items": [ - { - "$": "object", - "props": { - "attribute": "b", - "comparator": "equals", - "value": "1" - } - }, { "$": "object", "props": { @@ -8864,21 +8856,20 @@ { "$": "object", "props": { - "attribute": "d", + "attribute": "c", "comparator": "equals", - "value": "2" + "value": "1" } } ] }, - "name": "c" + "name": "b" }, "ctor": "Query" } ] }, - "name": "a", - "operator": "and" + "name": "a" }, "ctor": "Query" } @@ -8895,12 +8886,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a[b", - "1" - ], - [ - "c[d", - "2]]" + "a[b[c", + "1]]" ] ], "props": { @@ -8913,14 +8900,6 @@ "conditions": { "$": "array", "items": [ - { - "$": "object", - "props": { - "attribute": "b", - "comparator": "equals", - "value": "1" - } - }, { "$": "object", "props": { @@ -8930,21 +8909,20 @@ { "$": "object", "props": { - "attribute": "d", + "attribute": "c", "comparator": "equals", - "value": "2" + "value": "1" } } ] }, - "name": "c" + "name": "b" }, "ctor": "Query" } ] }, - "name": "a", - "operator": "and" + "name": "a" }, "ctor": "Query" } @@ -8955,14 +8933,14 @@ "pathname": { "$": "undefined" }, - "search": "a[b=1&c[d=2]]", + "search": "a[b[c=1]]", "sort": null } } } }, - "q13b19473": { - "query": "a[b[c=1]]", + "qf97a5a16": { + "query": "a[b=1&c[d=2]]", "strict": { "status": "ok", "raw": { @@ -8977,6 +8955,14 @@ "conditions": { "$": "array", "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + }, { "$": "object", "props": { @@ -8986,20 +8972,21 @@ { "$": "object", "props": { - "attribute": "c", + "attribute": "d", "comparator": "equals", - "value": "1" + "value": "2" } } ] }, - "name": "b" + "name": "c" }, "ctor": "Query" } ] }, - "name": "a" + "name": "a", + "operator": "and" }, "ctor": "Query" } @@ -9016,8 +9003,12 @@ "ctor": "RequestTarget", "entries": [ [ - "a[b[c", - "1]]" + "a[b", + "1" + ], + [ + "c[d", + "2]]" ] ], "props": { @@ -9030,6 +9021,14 @@ "conditions": { "$": "array", "items": [ + { + "$": "object", + "props": { + "attribute": "b", + "comparator": "equals", + "value": "1" + } + }, { "$": "object", "props": { @@ -9039,20 +9038,21 @@ { "$": "object", "props": { - "attribute": "c", + "attribute": "d", "comparator": "equals", - "value": "1" + "value": "2" } } ] }, - "name": "b" + "name": "c" }, "ctor": "Query" } ] }, - "name": "a" + "name": "a", + "operator": "and" }, "ctor": "Query" } @@ -9063,7 +9063,7 @@ "pathname": { "$": "undefined" }, - "search": "a[b[c=1]]", + "search": "a[b=1&c[d=2]]", "sort": null } } @@ -9509,12 +9509,12 @@ } } }, - "qcaef3596": { - "query": "not(a=1)", + "qfb9f25ea": { + "query": "reviews[=ge=4&author.name=kim]", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'not(a=1)', unknown query function call not at position 8" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'reviews[=ge=4&author.name=kim]', attribute must be specified before equality comparator at position 9" }, "target": { "status": "ok", @@ -9523,8 +9523,12 @@ "ctor": "RequestTarget", "entries": [ [ - "not(a", - "1)" + "reviews[", + "ge=4" + ], + [ + "author.name", + "kim]" ] ], "props": { @@ -9536,25 +9540,25 @@ "isCollection": true, "parseError": { "$": "error", - "name": "SyntaxViolation", - "message": "unknown query function call not at position 8" + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'reviews[=ge=4&author.name=kim]', attribute must be specified before equality comparator at position 9" }, - "parseErrorMessage": "unknown query function call not at position 8", + "parseErrorMessage": "attribute must be specified before equality comparator at position 9", "pathname": { "$": "undefined" }, - "search": "not(a=1)", + "search": "reviews[=ge=4&author.name=kim]", "sort": null } } } }, - "qfb9f25ea": { - "query": "reviews[=ge=4&author.name=kim]", + "qcaef3596": { + "query": "not(a=1)", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'reviews[=ge=4&author.name=kim]', attribute must be specified before equality comparator at position 9" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'not(a=1)', unknown query function call not at position 8" }, "target": { "status": "ok", @@ -9563,12 +9567,8 @@ "ctor": "RequestTarget", "entries": [ [ - "reviews[", - "ge=4" - ], - [ - "author.name", - "kim]" + "not(a", + "1)" ] ], "props": { @@ -9580,14 +9580,14 @@ "isCollection": true, "parseError": { "$": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'chainedConditions') at position 14 in 'reviews[=ge=4&author.name=kim]', attribute must be specified before equality comparator at position 9" + "name": "SyntaxViolation", + "message": "unknown query function call not at position 8" }, - "parseErrorMessage": "attribute must be specified before equality comparator at position 9", + "parseErrorMessage": "unknown query function call not at position 8", "pathname": { "$": "undefined" }, - "search": "reviews[=ge=4&author.name=kim]", + "search": "not(a=1)", "sort": null } } @@ -10667,6 +10667,48 @@ } } }, + "qf2add796": { + "query": "sort()", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in 'sort()', Unknown sort type undefined at position 6" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "sort()", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "Unknown sort type undefined at position 6" + }, + "parseErrorMessage": "Unknown sort type undefined at position 6", + "pathname": { + "$": "undefined" + }, + "search": "sort()", + "sort": { + "$": "undefined" + } + } + } + } + }, "qf35281dd": { "query": "sort(a,)", "strict": { @@ -10736,48 +10778,6 @@ } } }, - "qf2add796": { - "query": "sort()", - "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in 'sort()', Unknown sort type undefined at position 6" - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "sort()", - "" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "id": null, - "isCollection": true, - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "Unknown sort type undefined at position 6" - }, - "parseErrorMessage": "Unknown sort type undefined at position 6", - "pathname": { - "$": "undefined" - }, - "search": "sort()", - "sort": { - "$": "undefined" - } - } - } - } - }, "qbaad7f6e": { "query": "sort(a", "strict": { @@ -10820,50 +10820,6 @@ } } }, - "q3275c249": { - "query": "select(a)", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "select": "a" - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "select(a)", - "" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "select(a)", - "select": "a", - "sort": null - } - } - } - }, "qcb148f59": { "query": "a=1&sort(b)", "strict": { @@ -10941,8 +10897,8 @@ } } }, - "qc612622b": { - "query": "select(a,b)", + "q3275c249": { + "query": "select(a)", "strict": { "status": "ok", "raw": { @@ -10952,13 +10908,7 @@ "$": "array", "items": [] }, - "select": { - "$": "array", - "items": [ - "a", - "b" - ] - } + "select": "a" }, "ctor": "Query" } @@ -10970,7 +10920,7 @@ "ctor": "RequestTarget", "entries": [ [ - "select(a,b)", + "select(a)", "" ] ], @@ -10984,21 +10934,15 @@ "pathname": { "$": "undefined" }, - "search": "select(a,b)", - "select": { - "$": "array", - "items": [ - "a", - "b" - ] - }, + "search": "select(a)", + "select": "a", "sort": null } } } }, - "q536888b1": { - "query": "select(a,)", + "qc612622b": { + "query": "select(a,b)", "strict": { "status": "ok", "raw": { @@ -11011,7 +10955,8 @@ "select": { "$": "array", "items": [ - "a" + "a", + "b" ] } }, @@ -11025,7 +10970,7 @@ "ctor": "RequestTarget", "entries": [ [ - "select(a,)", + "select(a,b)", "" ] ], @@ -11039,11 +10984,12 @@ "pathname": { "$": "undefined" }, - "search": "select(a,)", + "search": "select(a,b)", "select": { "$": "array", "items": [ - "a" + "a", + "b" ] }, "sort": null @@ -11051,8 +10997,8 @@ } } }, - "q54f7d4b7": { - "query": "select(a,b,)", + "q536888b1": { + "query": "select(a,)", "strict": { "status": "ok", "raw": { @@ -11065,9 +11011,7 @@ "select": { "$": "array", "items": [ - "a", - "b", - "" + "a" ] } }, @@ -11081,7 +11025,7 @@ "ctor": "RequestTarget", "entries": [ [ - "select(a,b,)", + "select(a,)", "" ] ], @@ -11095,13 +11039,11 @@ "pathname": { "$": "undefined" }, - "search": "select(a,b,)", + "search": "select(a,)", "select": { "$": "array", "items": [ - "a", - "b", - "" + "a" ] }, "sort": null @@ -11109,8 +11051,8 @@ } } }, - "qb06ba3d0": { - "query": "select([a,b],c)", + "q54f7d4b7": { + "query": "select(a,b,)", "strict": { "status": "ok", "raw": { @@ -11123,14 +11065,9 @@ "select": { "$": "array", "items": [ - { - "$": "array", - "items": [ - "a", - "b" - ] - }, - "c" + "a", + "b", + "" ] } }, @@ -11144,7 +11081,7 @@ "ctor": "RequestTarget", "entries": [ [ - "select([a,b],c)", + "select(a,b,)", "" ] ], @@ -11158,18 +11095,13 @@ "pathname": { "$": "undefined" }, - "search": "select([a,b],c)", + "search": "select(a,b,)", "select": { "$": "array", "items": [ - { - "$": "array", - "items": [ - "a", - "b" - ] - }, - "c" + "a", + "b", + "" ] }, "sort": null @@ -11299,6 +11231,74 @@ } } }, + "qb06ba3d0": { + "query": "select([a,b],c)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "c" + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select([a,b],c)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select([a,b],c)", + "select": { + "$": "array", + "items": [ + { + "$": "array", + "items": [ + "a", + "b" + ] + }, + "c" + ] + }, + "sort": null + } + } + } + }, "q7e542578": { "query": "select(rel{x,y})", "strict": { @@ -11495,8 +11495,8 @@ } } }, - "q2d187d59": { - "query": "select(rel[select(x,y)])", + "q9bcaf069": { + "query": "select(rel{x},rel2{y})", "strict": { "status": "ok", "raw": { @@ -11507,22 +11507,27 @@ "items": [] }, "select": { - "$": "object", - "props": { - "conditions": { + "$": "array", + "items": [ + { "$": "array", - "items": [] + "items": [ + "x" + ], + "props": { + "name": "rel" + } }, - "name": "rel", - "select": { + { "$": "array", "items": [ - "x", "y" - ] + ], + "props": { + "name": "rel2" + } } - }, - "ctor": "Query" + ] } }, "ctor": "Query" @@ -11535,7 +11540,7 @@ "ctor": "RequestTarget", "entries": [ [ - "select(rel[select(x,y)])", + "select(rel{x},rel2{y})", "" ] ], @@ -11549,32 +11554,37 @@ "pathname": { "$": "undefined" }, - "search": "select(rel[select(x,y)])", + "search": "select(rel{x},rel2{y})", "select": { - "$": "object", - "props": { - "conditions": { + "$": "array", + "items": [ + { "$": "array", - "items": [] + "items": [ + "x" + ], + "props": { + "name": "rel" + } }, - "name": "rel", - "select": { + { "$": "array", "items": [ - "x", "y" - ] + ], + "props": { + "name": "rel2" + } } - }, - "ctor": "Query" + ] }, "sort": null } } } }, - "q9bcaf069": { - "query": "select(rel{x},rel2{y})", + "q2d187d59": { + "query": "select(rel[select(x,y)])", "strict": { "status": "ok", "raw": { @@ -11585,27 +11595,22 @@ "items": [] }, "select": { - "$": "array", - "items": [ - { + "$": "object", + "props": { + "conditions": { "$": "array", - "items": [ - "x" - ], - "props": { - "name": "rel" - } + "items": [] }, - { + "name": "rel", + "select": { "$": "array", "items": [ + "x", "y" - ], - "props": { - "name": "rel2" - } + ] } - ] + }, + "ctor": "Query" } }, "ctor": "Query" @@ -11618,7 +11623,7 @@ "ctor": "RequestTarget", "entries": [ [ - "select(rel{x},rel2{y})", + "select(rel[select(x,y)])", "" ] ], @@ -11632,29 +11637,24 @@ "pathname": { "$": "undefined" }, - "search": "select(rel{x},rel2{y})", + "search": "select(rel[select(x,y)])", "select": { - "$": "array", - "items": [ - { + "$": "object", + "props": { + "conditions": { "$": "array", - "items": [ - "x" - ], - "props": { - "name": "rel" - } + "items": [] }, - { + "name": "rel", + "select": { "$": "array", "items": [ + "x", "y" - ], - "props": { - "name": "rel2" - } + ] } - ] + }, + "ctor": "Query" }, "sort": null } @@ -11965,64 +11965,11 @@ "items": [ "a", "b" - ], - "props": { - "asArray": true - } - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "select(a.b)", - "" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "select(a.b)", - "select": { - "$": "array", - "items": [ - "a", - "b" - ], - "props": { - "asArray": true - } - }, - "sort": null - } - } - } - }, - "q285e7865": { - "query": "select(a%2Eb)", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "select": "a.b" + ], + "props": { + "asArray": true + } + } }, "ctor": "Query" } @@ -12048,8 +11995,17 @@ "pathname": { "$": "undefined" }, - "search": "select(a%2Eb)", - "select": "a.b", + "search": "select(a.b)", + "select": { + "$": "array", + "items": [ + "a", + "b" + ], + "props": { + "asArray": true + } + }, "sort": null } } @@ -12123,6 +12079,50 @@ } } }, + "q285e7865": { + "query": "select(a%2Eb)", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "select": "a.b" + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select(a.b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "select(a%2Eb)", + "select": "a.b", + "sort": null + } + } + } + }, "q8e645e42": { "query": "select()", "strict": { @@ -12217,6 +12217,55 @@ } } }, + "q8b202070": { + "query": "select([a,b)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'select([a,b)', expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "select([a,b)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12" + }, + "parseErrorMessage": "expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12", + "pathname": { + "$": "undefined" + }, + "search": "select([a,b)", + "select": { + "$": "array", + "items": [ + "a" + ], + "props": { + "asArray": true + } + }, + "sort": null + } + } + } + }, "q6d485cc8": { "query": "select(a)&sort(b)&limit(2)", "strict": { @@ -12284,55 +12333,6 @@ } } }, - "q8b202070": { - "query": "select([a,b)", - "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'select([a,b)', expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12" - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "select([a,b)", - "" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "id": null, - "isCollection": true, - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12" - }, - "parseErrorMessage": "expected ']', but encountered ')' at position 12, unexpected operator ')' at position 12", - "pathname": { - "$": "undefined" - }, - "search": "select([a,b)", - "select": { - "$": "array", - "items": [ - "a" - ], - "props": { - "asArray": true - } - }, - "sort": null - } - } - } - }, "q70a1675c": { "query": "limit(10)", "strict": { @@ -12377,8 +12377,8 @@ } } }, - "q1c91063f": { - "query": "limit(5,10)", + "qfc3b985f": { + "query": "limit(0)", "strict": { "status": "ok", "raw": { @@ -12388,8 +12388,7 @@ "$": "array", "items": [] }, - "limit": 5, - "offset": 5 + "limit": 0 }, "ctor": "Query" } @@ -12401,7 +12400,7 @@ "ctor": "RequestTarget", "entries": [ [ - "limit(5,10)", + "limit(0)", "" ] ], @@ -12412,19 +12411,18 @@ }, "id": null, "isCollection": true, - "limit": 5, - "offset": 5, + "limit": 0, "pathname": { "$": "undefined" }, - "search": "limit(5,10)", + "search": "limit(0)", "sort": null } } } }, - "qfc3b985f": { - "query": "limit(0)", + "q1c91063f": { + "query": "limit(5,10)", "strict": { "status": "ok", "raw": { @@ -12434,7 +12432,8 @@ "$": "array", "items": [] }, - "limit": 0 + "limit": 5, + "offset": 5 }, "ctor": "Query" } @@ -12446,7 +12445,7 @@ "ctor": "RequestTarget", "entries": [ [ - "limit(0)", + "limit(5,10)", "" ] ], @@ -12457,11 +12456,12 @@ }, "id": null, "isCollection": true, - "limit": 0, + "limit": 5, + "offset": 5, "pathname": { "$": "undefined" }, - "search": "limit(0)", + "search": "limit(5,10)", "sort": null } } @@ -12787,12 +12787,12 @@ } } }, - "qaadb840c": { - "query": "limit(5,10", + "q1a35685f": { + "query": "limit(1,2,3)", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 10 in 'limit(5,10', expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'limit(1,2,3)', limit must have 1 or 2 arguments at position 12" }, "target": { "status": "ok", @@ -12801,7 +12801,7 @@ "ctor": "RequestTarget", "entries": [ [ - "limit(5,10", + "limit(1,2,3)", "" ] ], @@ -12812,28 +12812,27 @@ }, "id": null, "isCollection": true, - "limit": 5, "parseError": { "$": "error", "name": "SyntaxViolation", - "message": "expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10" + "message": "limit must have 1 or 2 arguments at position 12" }, - "parseErrorMessage": "expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10", + "parseErrorMessage": "limit must have 1 or 2 arguments at position 12", "pathname": { "$": "undefined" }, - "search": "limit(5,10", + "search": "limit(1,2,3)", "sort": null } } } }, - "q1a35685f": { - "query": "limit(1,2,3)", + "qaadb840c": { + "query": "limit(5,10", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'limit(1,2,3)', limit must have 1 or 2 arguments at position 12" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 10 in 'limit(5,10', expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10" }, "target": { "status": "ok", @@ -12842,7 +12841,7 @@ "ctor": "RequestTarget", "entries": [ [ - "limit(1,2,3)", + "limit(5,10", "" ] ], @@ -12853,16 +12852,17 @@ }, "id": null, "isCollection": true, + "limit": 5, "parseError": { "$": "error", "name": "SyntaxViolation", - "message": "limit must have 1 or 2 arguments at position 12" + "message": "expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10" }, - "parseErrorMessage": "limit must have 1 or 2 arguments at position 12", + "parseErrorMessage": "expected ')', but encountered end of string} at position 10, no comparison specified before end of string at position 10", "pathname": { "$": "undefined" }, - "search": "limit(1,2,3)", + "search": "limit(5,10", "sort": null } } @@ -12999,7 +12999,47 @@ "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'count(a)', unknown query function call count at position 8" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 8 in 'count(a)', unknown query function call count at position 8" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "count(a)", + "" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unknown query function call count at position 8" + }, + "parseErrorMessage": "unknown query function call count at position 8", + "pathname": { + "$": "undefined" + }, + "search": "count(a)", + "sort": null + } + } + } + }, + "qc2dfd8a9": { + "query": "distinct(a)", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'distinct(a)', unknown query function call distinct at position 11" }, "target": { "status": "ok", @@ -13008,7 +13048,7 @@ "ctor": "RequestTarget", "entries": [ [ - "count(a)", + "distinct(a)", "" ] ], @@ -13022,24 +13062,24 @@ "parseError": { "$": "error", "name": "SyntaxViolation", - "message": "unknown query function call count at position 8" + "message": "unknown query function call distinct at position 11" }, - "parseErrorMessage": "unknown query function call count at position 8", + "parseErrorMessage": "unknown query function call distinct at position 11", "pathname": { "$": "undefined" }, - "search": "count(a)", + "search": "distinct(a)", "sort": null } } } }, - "qc2dfd8a9": { - "query": "distinct(a)", + "qf0e814ee": { + "query": "aggregate(a)", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 11 in 'distinct(a)', unknown query function call distinct at position 11" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'aggregate(a)', unknown query function call aggregate at position 12" }, "target": { "status": "ok", @@ -13048,7 +13088,7 @@ "ctor": "RequestTarget", "entries": [ [ - "distinct(a)", + "aggregate(a)", "" ] ], @@ -13062,13 +13102,13 @@ "parseError": { "$": "error", "name": "SyntaxViolation", - "message": "unknown query function call distinct at position 11" + "message": "unknown query function call aggregate at position 12" }, - "parseErrorMessage": "unknown query function call distinct at position 11", + "parseErrorMessage": "unknown query function call aggregate at position 12", "pathname": { "$": "undefined" }, - "search": "distinct(a)", + "search": "aggregate(a)", "sort": null } } @@ -13122,46 +13162,6 @@ } } }, - "qf0e814ee": { - "query": "aggregate(a)", - "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 12 in 'aggregate(a)', unknown query function call aggregate at position 12" - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "aggregate(a)", - "" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [] - }, - "id": null, - "isCollection": true, - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "unknown query function call aggregate at position 12" - }, - "parseErrorMessage": "unknown query function call aggregate at position 12", - "pathname": { - "$": "undefined" - }, - "search": "aggregate(a)", - "sort": null - } - } - } - }, "q9d259a34": { "query": "limit(1)&limit(2)", "strict": { @@ -13358,8 +13358,8 @@ } } }, - "qfe0e59e0": { - "query": "foo=ba)r", + "qc3934795": { + "query": "select(b)&a=1", "strict": { "status": "ok", "raw": { @@ -13371,13 +13371,14 @@ { "$": "object", "props": { - "attribute": "foo", + "attribute": "a", "comparator": "equals", - "value": "ba)r" + "value": "1" } } ] - } + }, + "select": "b" }, "ctor": "Query" } @@ -13389,8 +13390,12 @@ "ctor": "RequestTarget", "entries": [ [ - "foo", - "ba)r" + "select(b)", + "" + ], + [ + "a", + "1" ] ], "props": { @@ -13400,9 +13405,9 @@ { "$": "object", "props": { - "attribute": "foo", + "attribute": "a", "comparator": "equals", - "value": "ba)r" + "value": "1" } } ] @@ -13412,14 +13417,15 @@ "pathname": { "$": "undefined" }, - "search": "foo=ba)r", + "search": "select(b)&a=1", + "select": "b", "sort": null } } } }, - "qc3934795": { - "query": "select(b)&a=1", + "qfe0e59e0": { + "query": "foo=ba)r", "strict": { "status": "ok", "raw": { @@ -13431,14 +13437,13 @@ { "$": "object", "props": { - "attribute": "a", + "attribute": "foo", "comparator": "equals", - "value": "1" + "value": "ba)r" } } ] - }, - "select": "b" + } }, "ctor": "Query" } @@ -13450,12 +13455,8 @@ "ctor": "RequestTarget", "entries": [ [ - "select(b)", - "" - ], - [ - "a", - "1" + "foo", + "ba)r" ] ], "props": { @@ -13465,9 +13466,9 @@ { "$": "object", "props": { - "attribute": "a", + "attribute": "foo", "comparator": "equals", - "value": "1" + "value": "ba)r" } } ] @@ -13477,8 +13478,7 @@ "pathname": { "$": "undefined" }, - "search": "select(b)&a=1", - "select": "b", + "search": "foo=ba)r", "sort": null } } @@ -13544,8 +13544,8 @@ } } }, - "q0810a835": { - "query": "foo=ba(r", + "q6209be3e": { + "query": "foo==ba(r", "strict": { "status": "ok", "raw": { @@ -13558,7 +13558,7 @@ "$": "object", "props": { "attribute": "foo", - "comparator": "equals", + "comparator": "eq", "value": "ba(r" } } @@ -13576,7 +13576,7 @@ "entries": [ [ "foo", - "ba(r" + "=ba(r" ] ], "props": { @@ -13587,7 +13587,7 @@ "$": "object", "props": { "attribute": "foo", - "comparator": "equals", + "comparator": "eq", "value": "ba(r" } } @@ -13598,14 +13598,14 @@ "pathname": { "$": "undefined" }, - "search": "foo=ba(r", + "search": "foo==ba(r", "sort": null } } } }, - "q6209be3e": { - "query": "foo==ba(r", + "q0810a835": { + "query": "foo=ba(r", "strict": { "status": "ok", "raw": { @@ -13618,7 +13618,7 @@ "$": "object", "props": { "attribute": "foo", - "comparator": "eq", + "comparator": "equals", "value": "ba(r" } } @@ -13636,7 +13636,7 @@ "entries": [ [ "foo", - "=ba(r" + "ba(r" ] ], "props": { @@ -13647,7 +13647,7 @@ "$": "object", "props": { "attribute": "foo", - "comparator": "eq", + "comparator": "equals", "value": "ba(r" } } @@ -13658,7 +13658,7 @@ "pathname": { "$": "undefined" }, - "search": "foo==ba(r", + "search": "foo=ba(r", "sort": null } } @@ -13784,8 +13784,8 @@ } } }, - "qc3a51c1c": { - "query": "itemIds[]=1&itemIds[]=2", + "qbe4b2ad6": { + "query": "itemIds[]=1", "strict": { "status": "ok", "raw": { @@ -13801,18 +13801,9 @@ "comparator": "equals", "value": "1" } - }, - { - "$": "object", - "props": { - "attribute": "itemIds", - "comparator": "equals", - "value": "2" - } } ] - }, - "operator": "and" + } }, "ctor": "Query" } @@ -13826,10 +13817,6 @@ [ "itemIds[]", "1" - ], - [ - "itemIds[]", - "2" ] ], "props": { @@ -13843,31 +13830,22 @@ "comparator": "equals", "value": "1" } - }, - { - "$": "object", - "props": { - "attribute": "itemIds", - "comparator": "equals", - "value": "2" - } } ] }, "id": null, "isCollection": true, - "operator": "and", "pathname": { "$": "undefined" }, - "search": "itemIds[]=1&itemIds[]=2", + "search": "itemIds[]=1", "sort": null } } } }, - "qbe4b2ad6": { - "query": "itemIds[]=1", + "qc3a51c1c": { + "query": "itemIds[]=1&itemIds[]=2", "strict": { "status": "ok", "raw": { @@ -13883,9 +13861,18 @@ "comparator": "equals", "value": "1" } + }, + { + "$": "object", + "props": { + "attribute": "itemIds", + "comparator": "equals", + "value": "2" + } } ] - } + }, + "operator": "and" }, "ctor": "Query" } @@ -13899,6 +13886,10 @@ [ "itemIds[]", "1" + ], + [ + "itemIds[]", + "2" ] ], "props": { @@ -13912,15 +13903,24 @@ "comparator": "equals", "value": "1" } + }, + { + "$": "object", + "props": { + "attribute": "itemIds", + "comparator": "equals", + "value": "2" + } } ] }, "id": null, "isCollection": true, + "operator": "and", "pathname": { "$": "undefined" }, - "search": "itemIds[]=1", + "search": "itemIds[]=1&itemIds[]=2", "sort": null } } @@ -14213,60 +14213,9 @@ "q4ba0bf55": { "query": "((a=1)", "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "a", - "comparator": "equals", - "value": "1" - } - } - ] - } - }, - "ctor": "Query" - } - ] - } - }, - "ctor": "Query" - } - ] - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "((a", - "1)" - ] - ], + "status": "ok", + "raw": { + "$": "object", "props": { "conditions": { "$": "array", @@ -14302,24 +14251,10 @@ "ctor": "Query" } ] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "((a=1)", - "sort": null - } + } + }, + "ctor": "Query" } - } - }, - "q4f98df2a": { - "query": "(a=1))", - "strict": { - "status": "error", - "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in '(a=1))', unexpected token ')' at position 6, unexpected operator ')' at position 6" }, "target": { "status": "ok", @@ -14328,8 +14263,8 @@ "ctor": "RequestTarget", "entries": [ [ - "(a", - "1))" + "((a", + "1)" ] ], "props": { @@ -14345,10 +14280,21 @@ { "$": "object", "props": { - "attribute": "a", - "comparator": "equals", - "value": "1" - } + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" } ] } @@ -14359,16 +14305,10 @@ }, "id": null, "isCollection": true, - "parseError": { - "$": "error", - "name": "SyntaxViolation", - "message": "unexpected token ')' at position 6, unexpected operator ')' at position 6" - }, - "parseErrorMessage": "unexpected token ')' at position 6, unexpected operator ')' at position 6", "pathname": { "$": "undefined" }, - "search": "(a=1))", + "search": "((a=1)", "sort": null } } @@ -14434,6 +14374,66 @@ } } }, + "q4f98df2a": { + "query": "(a=1))", + "strict": { + "status": "error", + "name": "TypeError", + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 6 in '(a=1))', unexpected token ')' at position 6, unexpected operator ')' at position 6" + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "(a", + "1))" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "a", + "comparator": "equals", + "value": "1" + } + } + ] + } + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "parseError": { + "$": "error", + "name": "SyntaxViolation", + "message": "unexpected token ')' at position 6, unexpected operator ')' at position 6" + }, + "parseErrorMessage": "unexpected token ')' at position 6, unexpected operator ')' at position 6", + "pathname": { + "$": "undefined" + }, + "search": "(a=1))", + "sort": null + } + } + } + }, "q3fffffc2": { "query": "[a=1)", "strict": { @@ -14836,12 +14836,12 @@ } } }, - "q6caeea63": { - "query": "a=9lives=1", + "qbdc66099": { + "query": "a=1=2", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 10 in 'a=9lives=1', invalid FIQL operator 9lives at position 9" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 5 in 'a=1=2', invalid FIQL operator 1 at position 4" }, "target": { "status": "ok", @@ -14851,7 +14851,7 @@ "entries": [ [ "a", - "9lives=1" + "1=2" ] ], "props": { @@ -14863,7 +14863,7 @@ "props": { "attribute": "a", "comparator": "equals", - "value": "1" + "value": "2" } } ] @@ -14873,62 +14873,24 @@ "parseError": { "$": "error", "name": "SyntaxViolation", - "message": "invalid FIQL operator 9lives at position 9" - }, - "parseErrorMessage": "invalid FIQL operator 9lives at position 9", - "pathname": { - "$": "undefined" + "message": "invalid FIQL operator 1 at position 4" }, - "search": "a=9lives=1", - "sort": null - } - } - } - }, - "q63a0e982": { - "query": "a=has-dash=1", - "strict": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "URLSearchParams", - "entries": [ - [ - "a", - "has-dash=1" - ] - ] - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "a", - "has-dash=1" - ] - ], - "props": { - "id": null, - "isCollection": true, + "parseErrorMessage": "invalid FIQL operator 1 at position 4", "pathname": { "$": "undefined" }, - "search": "a=has-dash=1", + "search": "a=1=2", "sort": null } } } }, - "qbdc66099": { - "query": "a=1=2", + "q6caeea63": { + "query": "a=9lives=1", "strict": { "status": "error", "name": "TypeError", - "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 5 in 'a=1=2', invalid FIQL operator 1 at position 4" + "message": "Unable to parse query, Cannot read properties of undefined (reading 'parseErrorMessage') at position 10 in 'a=9lives=1', invalid FIQL operator 9lives at position 9" }, "target": { "status": "ok", @@ -14938,7 +14900,7 @@ "entries": [ [ "a", - "1=2" + "9lives=1" ] ], "props": { @@ -14950,7 +14912,7 @@ "props": { "attribute": "a", "comparator": "equals", - "value": "2" + "value": "1" } } ] @@ -14960,20 +14922,20 @@ "parseError": { "$": "error", "name": "SyntaxViolation", - "message": "invalid FIQL operator 1 at position 4" + "message": "invalid FIQL operator 9lives at position 9" }, - "parseErrorMessage": "invalid FIQL operator 1 at position 4", + "parseErrorMessage": "invalid FIQL operator 9lives at position 9", "pathname": { "$": "undefined" }, - "search": "a=1=2", + "search": "a=9lives=1", "sort": null } } } }, - "q10b998b0": { - "query": "a,b=1", + "q63a0e982": { + "query": "a=has-dash=1", "strict": { "status": "ok", "raw": { @@ -14981,8 +14943,8 @@ "ctor": "URLSearchParams", "entries": [ [ - "a,b", - "1" + "a", + "has-dash=1" ] ] } @@ -14994,8 +14956,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a,b", - "1" + "a", + "has-dash=1" ] ], "props": { @@ -15004,7 +14966,7 @@ "pathname": { "$": "undefined" }, - "search": "a,b=1", + "search": "a=has-dash=1", "sort": null } } @@ -15048,8 +15010,8 @@ } } }, - "q3452cf54": { - "query": "{a}", + "q10b998b0": { + "query": "a,b=1", "strict": { "status": "ok", "raw": { @@ -15057,8 +15019,8 @@ "ctor": "URLSearchParams", "entries": [ [ - "{a}", - "" + "a,b", + "1" ] ] } @@ -15070,8 +15032,8 @@ "ctor": "RequestTarget", "entries": [ [ - "{a}", - "" + "a,b", + "1" ] ], "props": { @@ -15080,14 +15042,14 @@ "pathname": { "$": "undefined" }, - "search": "{a}", + "search": "a,b=1", "sort": null } } } }, - "q0bd21456": { - "query": "a=1&b", + "q3452cf54": { + "query": "{a}", "strict": { "status": "ok", "raw": { @@ -15095,11 +15057,7 @@ "ctor": "URLSearchParams", "entries": [ [ - "a", - "1" - ], - [ - "b", + "{a}", "" ] ] @@ -15112,11 +15070,7 @@ "ctor": "RequestTarget", "entries": [ [ - "a", - "1" - ], - [ - "b", + "{a}", "" ] ], @@ -15126,7 +15080,7 @@ "pathname": { "$": "undefined" }, - "search": "a=1&b", + "search": "{a}", "sort": null } } @@ -15170,8 +15124,8 @@ } } }, - "q4e1d7405": { - "query": "a=1&&&b=2", + "q0bd21456": { + "query": "a=1&b", "strict": { "status": "ok", "raw": { @@ -15184,7 +15138,7 @@ ], [ "b", - "2" + "" ] ] } @@ -15201,7 +15155,7 @@ ], [ "b", - "2" + "" ] ], "props": { @@ -15210,25 +15164,29 @@ "pathname": { "$": "undefined" }, - "search": "a=1&&&b=2", + "search": "a=1&b", "sort": null } } } }, - "q6e823c79": { - "query": "a!==", + "q4e1d7405": { + "query": "a=1&&&b=2", "strict": { "status": "ok", "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [] - } - }, - "ctor": "Query" + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1" + ], + [ + "b", + "2" + ] + ] } }, "target": { @@ -15238,21 +15196,21 @@ "ctor": "RequestTarget", "entries": [ [ - "a!", - "=" + "a", + "1" + ], + [ + "b", + "2" ] ], "props": { - "conditions": { - "$": "array", - "items": [] - }, "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "a!==", + "search": "a=1&&&b=2", "sort": null } } @@ -15318,8 +15276,8 @@ } } }, - "q67b38d71": { - "query": "a>=", + "q6e823c79": { + "query": "a!==", "strict": { "status": "ok", "raw": { @@ -15340,8 +15298,8 @@ "ctor": "RequestTarget", "entries": [ [ - "a>", - "" + "a!", + "=" ] ], "props": { @@ -15354,25 +15312,25 @@ "pathname": { "$": "undefined" }, - "search": "a>=", + "search": "a!==", "sort": null } } } }, - "q12a5c004": { - "query": "a=1#b=2", + "q67b38d71": { + "query": "a>=", "strict": { "status": "ok", "raw": { - "$": "usp", - "ctor": "URLSearchParams", - "entries": [ - [ - "a", - "1#b=2" - ] - ] + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [] + } + }, + "ctor": "Query" } }, "target": { @@ -15382,17 +15340,21 @@ "ctor": "RequestTarget", "entries": [ [ - "a", - "1#b=2" + "a>", + "" ] ], "props": { + "conditions": { + "$": "array", + "items": [] + }, "id": null, "isCollection": true, "pathname": { "$": "undefined" }, - "search": "a=1#b=2", + "search": "a>=", "sort": null } } @@ -15438,6 +15400,44 @@ } } }, + "q12a5c004": { + "query": "a=1#b=2", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "a", + "1#b=2" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a", + "1#b=2" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a=1#b=2", + "sort": null + } + } + } + }, "q8f5d2232": { "query": "a=1&limit(2)&", "strict": { @@ -16331,7 +16331,163 @@ "pathname": { "$": "undefined" }, - "search": "a=in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49)", + "search": "a=in=(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49)", + "sort": null + } + } + } + }, + "q5d587d3e": { + "query": "a[b[c[d[e=1]]]]", + "strict": { + "status": "ok", + "raw": { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "e", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "d" + }, + "ctor": "Query" + } + ] + }, + "name": "c" + }, + "ctor": "Query" + } + ] + }, + "name": "b" + }, + "ctor": "Query" + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + } + }, + "ctor": "Query" + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "a[b[c[d[e", + "1]]]]" + ] + ], + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "conditions": { + "$": "array", + "items": [ + { + "$": "object", + "props": { + "attribute": "e", + "comparator": "equals", + "value": "1" + } + } + ] + }, + "name": "d" + }, + "ctor": "Query" + } + ] + }, + "name": "c" + }, + "ctor": "Query" + } + ] + }, + "name": "b" + }, + "ctor": "Query" + } + ] + }, + "name": "a" + }, + "ctor": "Query" + } + ] + }, + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "a[b[c[d[e=1]]]]", "sort": null } } @@ -16733,162 +16889,6 @@ } } }, - "q5d587d3e": { - "query": "a[b[c[d[e=1]]]]", - "strict": { - "status": "ok", - "raw": { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "e", - "comparator": "equals", - "value": "1" - } - } - ] - }, - "name": "d" - }, - "ctor": "Query" - } - ] - }, - "name": "c" - }, - "ctor": "Query" - } - ] - }, - "name": "b" - }, - "ctor": "Query" - } - ] - }, - "name": "a" - }, - "ctor": "Query" - } - ] - } - }, - "ctor": "Query" - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "a[b[c[d[e", - "1]]]]" - ] - ], - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "conditions": { - "$": "array", - "items": [ - { - "$": "object", - "props": { - "attribute": "e", - "comparator": "equals", - "value": "1" - } - } - ] - }, - "name": "d" - }, - "ctor": "Query" - } - ] - }, - "name": "c" - }, - "ctor": "Query" - } - ] - }, - "name": "b" - }, - "ctor": "Query" - } - ] - }, - "name": "a" - }, - "ctor": "Query" - } - ] - }, - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "a[b[c[d[e=1]]]]", - "sort": null - } - } - } - }, "qe5c09edb": { "query": "price=abc", "strict": { @@ -16927,44 +16927,6 @@ } } }, - "qcf2043b5": { - "query": "price=true", - "strict": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "URLSearchParams", - "entries": [ - [ - "price", - "true" - ] - ] - } - }, - "target": { - "status": "ok", - "raw": { - "$": "usp", - "ctor": "RequestTarget", - "entries": [ - [ - "price", - "true" - ] - ], - "props": { - "id": null, - "isCollection": true, - "pathname": { - "$": "undefined" - }, - "search": "price=true", - "sort": null - } - } - } - }, "q7898e334": { "query": "price=null", "strict": { @@ -17003,8 +16965,8 @@ } } }, - "q5b72eb57": { - "query": "price=x%20y", + "qcf2043b5": { + "query": "price=true", "strict": { "status": "ok", "raw": { @@ -17013,7 +16975,7 @@ "entries": [ [ "price", - "x y" + "true" ] ] } @@ -17026,7 +16988,7 @@ "entries": [ [ "price", - "x y" + "true" ] ], "props": { @@ -17035,14 +16997,14 @@ "pathname": { "$": "undefined" }, - "search": "price=x%20y", + "search": "price=true", "sort": null } } } }, - "q1489ef57": { - "query": "price==5", + "qc666eccc": { + "query": "price=2.5", "strict": { "status": "ok", "raw": { @@ -17055,8 +17017,8 @@ "$": "object", "props": { "attribute": "price", - "comparator": "eq", - "value": "5" + "comparator": "equals", + "value": "2.5" } } ] @@ -17073,7 +17035,7 @@ "entries": [ [ "price", - "=5" + "2.5" ] ], "props": { @@ -17084,8 +17046,8 @@ "$": "object", "props": { "attribute": "price", - "comparator": "eq", - "value": "5" + "comparator": "equals", + "value": "2.5" } } ] @@ -17095,14 +17057,52 @@ "pathname": { "$": "undefined" }, - "search": "price==5", + "search": "price=2.5", + "sort": null + } + } + } + }, + "q5b72eb57": { + "query": "price=x%20y", + "strict": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "URLSearchParams", + "entries": [ + [ + "price", + "x y" + ] + ] + } + }, + "target": { + "status": "ok", + "raw": { + "$": "usp", + "ctor": "RequestTarget", + "entries": [ + [ + "price", + "x y" + ] + ], + "props": { + "id": null, + "isCollection": true, + "pathname": { + "$": "undefined" + }, + "search": "price=x%20y", "sort": null } } } }, - "qc666eccc": { - "query": "price=2.5", + "q1489ef57": { + "query": "price==5", "strict": { "status": "ok", "raw": { @@ -17115,8 +17115,8 @@ "$": "object", "props": { "attribute": "price", - "comparator": "equals", - "value": "2.5" + "comparator": "eq", + "value": "5" } } ] @@ -17133,7 +17133,7 @@ "entries": [ [ "price", - "2.5" + "=5" ] ], "props": { @@ -17144,8 +17144,8 @@ "$": "object", "props": { "attribute": "price", - "comparator": "equals", - "value": "2.5" + "comparator": "eq", + "value": "5" } } ] @@ -17155,14 +17155,14 @@ "pathname": { "$": "undefined" }, - "search": "price=2.5", + "search": "price==5", "sort": null } } } }, - "q2e986eb5": { - "query": "price==null", + "qeddac7a8": { + "query": "price==abc", "strict": { "status": "ok", "raw": { @@ -17176,7 +17176,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": null + "value": "abc" } } ] @@ -17193,7 +17193,7 @@ "entries": [ [ "price", - "=null" + "=abc" ] ], "props": { @@ -17205,7 +17205,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": null + "value": "abc" } } ] @@ -17215,14 +17215,14 @@ "pathname": { "$": "undefined" }, - "search": "price==null", + "search": "price==abc", "sort": null } } } }, - "qeddac7a8": { - "query": "price==abc", + "q2e986eb5": { + "query": "price==null", "strict": { "status": "ok", "raw": { @@ -17236,7 +17236,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": "abc" + "value": null } } ] @@ -17253,7 +17253,7 @@ "entries": [ [ "price", - "=abc" + "=null" ] ], "props": { @@ -17265,7 +17265,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": "abc" + "value": null } } ] @@ -17275,7 +17275,7 @@ "pathname": { "$": "undefined" }, - "search": "price==abc", + "search": "price==null", "sort": null } } @@ -17341,8 +17341,8 @@ } } }, - "qc870f494": { - "query": "price==x%20y", + "qf5c9433f": { + "query": "price==2.5", "strict": { "status": "ok", "raw": { @@ -17356,7 +17356,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": "x y" + "value": "2.5" } } ] @@ -17373,7 +17373,7 @@ "entries": [ [ "price", - "=x y" + "=2.5" ] ], "props": { @@ -17385,7 +17385,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": "x y" + "value": "2.5" } } ] @@ -17395,14 +17395,14 @@ "pathname": { "$": "undefined" }, - "search": "price==x%20y", + "search": "price==2.5", "sort": null } } } }, - "qf5c9433f": { - "query": "price==2.5", + "qc870f494": { + "query": "price==x%20y", "strict": { "status": "ok", "raw": { @@ -17416,7 +17416,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": "2.5" + "value": "x y" } } ] @@ -17433,7 +17433,7 @@ "entries": [ [ "price", - "=2.5" + "=x y" ] ], "props": { @@ -17445,7 +17445,7 @@ "props": { "attribute": "price", "comparator": "eq", - "value": "2.5" + "value": "x y" } } ] @@ -17455,7 +17455,7 @@ "pathname": { "$": "undefined" }, - "search": "price==2.5", + "search": "price==x%20y", "sort": null } } @@ -17521,8 +17521,8 @@ } } }, - "qf555dbf2": { - "query": "price===null", + "q0387a731": { + "query": "price===abc", "strict": { "status": "ok", "raw": { @@ -17536,7 +17536,7 @@ "props": { "attribute": "price", "comparator": "equals", - "value": "null" + "value": "abc" } } ] @@ -17553,7 +17553,7 @@ "entries": [ [ "price", - "==null" + "==abc" ] ], "props": { @@ -17565,7 +17565,7 @@ "props": { "attribute": "price", "comparator": "equals", - "value": "null" + "value": "abc" } } ] @@ -17575,14 +17575,14 @@ "pathname": { "$": "undefined" }, - "search": "price===null", + "search": "price===abc", "sort": null } } } }, - "q8351b6f3": { - "query": "price===true", + "qf555dbf2": { + "query": "price===null", "strict": { "status": "ok", "raw": { @@ -17596,7 +17596,7 @@ "props": { "attribute": "price", "comparator": "equals", - "value": "true" + "value": "null" } } ] @@ -17613,7 +17613,7 @@ "entries": [ [ "price", - "==true" + "==null" ] ], "props": { @@ -17625,7 +17625,7 @@ "props": { "attribute": "price", "comparator": "equals", - "value": "true" + "value": "null" } } ] @@ -17635,14 +17635,14 @@ "pathname": { "$": "undefined" }, - "search": "price===true", + "search": "price===null", "sort": null } } } }, - "q0387a731": { - "query": "price===abc", + "q8351b6f3": { + "query": "price===true", "strict": { "status": "ok", "raw": { @@ -17656,7 +17656,7 @@ "props": { "attribute": "price", "comparator": "equals", - "value": "abc" + "value": "true" } } ] @@ -17673,7 +17673,7 @@ "entries": [ [ "price", - "==abc" + "==true" ] ], "props": { @@ -17685,7 +17685,7 @@ "props": { "attribute": "price", "comparator": "equals", - "value": "abc" + "value": "true" } } ] @@ -17695,7 +17695,7 @@ "pathname": { "$": "undefined" }, - "search": "price===abc", + "search": "price===true", "sort": null } } @@ -18241,8 +18241,8 @@ } } }, - "qd23eee5e": { - "query": "price!==null", + "qd6b3aa45": { + "query": "price!==abc", "strict": { "status": "ok", "raw": { @@ -18256,7 +18256,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "null" + "value": "abc" } } ] @@ -18273,7 +18273,7 @@ "entries": [ [ "price!", - "=null" + "=abc" ] ], "props": { @@ -18285,7 +18285,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "null" + "value": "abc" } } ] @@ -18295,14 +18295,14 @@ "pathname": { "$": "undefined" }, - "search": "price!==null", + "search": "price!==abc", "sort": null } } } }, - "qf6f90b47": { - "query": "price!==true", + "qd23eee5e": { + "query": "price!==null", "strict": { "status": "ok", "raw": { @@ -18316,7 +18316,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "true" + "value": "null" } } ] @@ -18333,7 +18333,7 @@ "entries": [ [ "price!", - "=true" + "=null" ] ], "props": { @@ -18345,7 +18345,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "true" + "value": "null" } } ] @@ -18355,14 +18355,14 @@ "pathname": { "$": "undefined" }, - "search": "price!==true", + "search": "price!==null", "sort": null } } } }, - "qbf0c45de": { - "query": "price!==2.5", + "qf6f90b47": { + "query": "price!==true", "strict": { "status": "ok", "raw": { @@ -18376,7 +18376,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "2.5" + "value": "true" } } ] @@ -18393,7 +18393,7 @@ "entries": [ [ "price!", - "=2.5" + "=true" ] ], "props": { @@ -18405,7 +18405,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "2.5" + "value": "true" } } ] @@ -18415,14 +18415,14 @@ "pathname": { "$": "undefined" }, - "search": "price!==2.5", + "search": "price!==true", "sort": null } } } }, - "q19fc39ed": { - "query": "price!==x%20y", + "qbf0c45de": { + "query": "price!==2.5", "strict": { "status": "ok", "raw": { @@ -18436,7 +18436,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "x y" + "value": "2.5" } } ] @@ -18453,7 +18453,7 @@ "entries": [ [ "price!", - "=x y" + "=2.5" ] ], "props": { @@ -18465,7 +18465,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "x y" + "value": "2.5" } } ] @@ -18475,14 +18475,14 @@ "pathname": { "$": "undefined" }, - "search": "price!==x%20y", + "search": "price!==2.5", "sort": null } } } }, - "qd6b3aa45": { - "query": "price!==abc", + "q19fc39ed": { + "query": "price!==x%20y", "strict": { "status": "ok", "raw": { @@ -18496,7 +18496,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "abc" + "value": "x y" } } ] @@ -18513,7 +18513,7 @@ "entries": [ [ "price!", - "=abc" + "=x y" ] ], "props": { @@ -18525,7 +18525,7 @@ "props": { "attribute": "price", "comparator": "not_equal", - "value": "abc" + "value": "x y" } } ] @@ -18535,14 +18535,14 @@ "pathname": { "$": "undefined" }, - "search": "price!==abc", + "search": "price!==x%20y", "sort": null } } } }, - "qd425c346": { - "query": "priceabc", + "q77138ceb": { + "query": "price>5", "strict": { "status": "ok", "raw": { @@ -19276,7 +19276,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "abc" + "value": "5" } } ] @@ -19292,7 +19292,7 @@ "ctor": "RequestTarget", "entries": [ [ - "price>abc", + "price>5", "" ] ], @@ -19305,7 +19305,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "abc" + "value": "5" } } ] @@ -19315,14 +19315,14 @@ "pathname": { "$": "undefined" }, - "search": "price>abc", + "search": "price>5", "sort": null } } } }, - "q6e8fd8bc": { - "query": "price>true", + "q25a6e434": { + "query": "price>abc", "strict": { "status": "ok", "raw": { @@ -19336,7 +19336,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "true" + "value": "abc" } } ] @@ -19352,7 +19352,7 @@ "ctor": "RequestTarget", "entries": [ [ - "price>true", + "price>abc", "" ] ], @@ -19365,7 +19365,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "true" + "value": "abc" } } ] @@ -19375,14 +19375,14 @@ "pathname": { "$": "undefined" }, - "search": "price>true", + "search": "price>abc", "sort": null } } } }, - "q77138ceb": { - "query": "price>5", + "q0ed17279": { + "query": "price>null", "strict": { "status": "ok", "raw": { @@ -19396,7 +19396,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "5" + "value": null } } ] @@ -19412,7 +19412,7 @@ "ctor": "RequestTarget", "entries": [ [ - "price>5", + "price>null", "" ] ], @@ -19425,7 +19425,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "5" + "value": null } } ] @@ -19435,14 +19435,14 @@ "pathname": { "$": "undefined" }, - "search": "price>5", + "search": "price>null", "sort": null } } } }, - "q27c49b9b": { - "query": "price>2.5", + "q6e8fd8bc": { + "query": "price>true", "strict": { "status": "ok", "raw": { @@ -19456,7 +19456,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "2.5" + "value": "true" } } ] @@ -19472,7 +19472,7 @@ "ctor": "RequestTarget", "entries": [ [ - "price>2.5", + "price>true", "" ] ], @@ -19485,7 +19485,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": "2.5" + "value": "true" } } ] @@ -19495,14 +19495,14 @@ "pathname": { "$": "undefined" }, - "search": "price>2.5", + "search": "price>true", "sort": null } } } }, - "q0ed17279": { - "query": "price>null", + "q27c49b9b": { + "query": "price>2.5", "strict": { "status": "ok", "raw": { @@ -19516,7 +19516,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": null + "value": "2.5" } } ] @@ -19532,7 +19532,7 @@ "ctor": "RequestTarget", "entries": [ [ - "price>null", + "price>2.5", "" ] ], @@ -19545,7 +19545,7 @@ "props": { "attribute": "price", "comparator": "gt", - "value": null + "value": "2.5" } } ] @@ -19555,7 +19555,7 @@ "pathname": { "$": "undefined" }, - "search": "price>null", + "search": "price>2.5", "sort": null } } @@ -19621,8 +19621,8 @@ } } }, - "q0d90140d": { - "query": "price>=abc", + "q41b32092": { + "query": "price>=5", "strict": { "status": "ok", "raw": { @@ -19636,7 +19636,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": "abc" + "value": "5" } } ] @@ -19653,7 +19653,7 @@ "entries": [ [ "price>", - "abc" + "5" ] ], "props": { @@ -19665,7 +19665,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": "abc" + "value": "5" } } ] @@ -19675,14 +19675,14 @@ "pathname": { "$": "undefined" }, - "search": "price>=abc", + "search": "price>=5", "sort": null } } } }, - "q41b32092": { - "query": "price>=5", + "q0d90140d": { + "query": "price>=abc", "strict": { "status": "ok", "raw": { @@ -19696,7 +19696,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": "5" + "value": "abc" } } ] @@ -19713,7 +19713,7 @@ "entries": [ [ "price>", - "5" + "abc" ] ], "props": { @@ -19725,7 +19725,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": "5" + "value": "abc" } } ] @@ -19735,14 +19735,14 @@ "pathname": { "$": "undefined" }, - "search": "price>=5", + "search": "price>=abc", "sort": null } } } }, - "qc05992bf": { - "query": "price>=true", + "q7cdecfa6": { + "query": "price>=null", "strict": { "status": "ok", "raw": { @@ -19756,7 +19756,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": "true" + "value": null } } ] @@ -19773,7 +19773,7 @@ "entries": [ [ "price>", - "true" + "null" ] ], "props": { @@ -19785,7 +19785,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": "true" + "value": null } } ] @@ -19795,14 +19795,14 @@ "pathname": { "$": "undefined" }, - "search": "price>=true", + "search": "price>=null", "sort": null } } } }, - "q7cdecfa6": { - "query": "price>=null", + "qc05992bf": { + "query": "price>=true", "strict": { "status": "ok", "raw": { @@ -19816,7 +19816,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": null + "value": "true" } } ] @@ -19833,7 +19833,7 @@ "entries": [ [ "price>", - "null" + "true" ] ], "props": { @@ -19845,7 +19845,7 @@ "props": { "attribute": "price", "comparator": "ge", - "value": null + "value": "true" } } ] @@ -19855,7 +19855,7 @@ "pathname": { "$": "undefined" }, - "search": "price>=null", + "search": "price>=true", "sort": null } } @@ -20341,8 +20341,8 @@ } } }, - "qf8a42b32": { - "query": "price=ne=10", + "q1e9bfe51": { + "query": "price=starts_with=10", "strict": { "status": "ok", "raw": { @@ -20355,7 +20355,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "ne", + "comparator": "starts_with", "value": "10" } } @@ -20373,7 +20373,7 @@ "entries": [ [ "price", - "ne=10" + "starts_with=10" ] ], "props": { @@ -20384,7 +20384,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "ne", + "comparator": "starts_with", "value": "10" } } @@ -20395,14 +20395,14 @@ "pathname": { "$": "undefined" }, - "search": "price=ne=10", + "search": "price=starts_with=10", "sort": null } } } }, - "q1e9bfe51": { - "query": "price=starts_with=10", + "q6e29a4da": { + "query": "price=ends_with=10", "strict": { "status": "ok", "raw": { @@ -20415,7 +20415,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "starts_with", + "comparator": "ends_with", "value": "10" } } @@ -20433,7 +20433,7 @@ "entries": [ [ "price", - "starts_with=10" + "ends_with=10" ] ], "props": { @@ -20444,7 +20444,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "starts_with", + "comparator": "ends_with", "value": "10" } } @@ -20455,14 +20455,14 @@ "pathname": { "$": "undefined" }, - "search": "price=starts_with=10", + "search": "price=ends_with=10", "sort": null } } } }, - "q9a1e46ea": { - "query": "price=equals=10", + "qf8a42b32": { + "query": "price=ne=10", "strict": { "status": "ok", "raw": { @@ -20475,7 +20475,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "equals", + "comparator": "ne", "value": "10" } } @@ -20493,7 +20493,7 @@ "entries": [ [ "price", - "equals=10" + "ne=10" ] ], "props": { @@ -20504,7 +20504,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "equals", + "comparator": "ne", "value": "10" } } @@ -20515,14 +20515,14 @@ "pathname": { "$": "undefined" }, - "search": "price=equals=10", + "search": "price=ne=10", "sort": null } } } }, - "q6e29a4da": { - "query": "price=ends_with=10", + "q9a1e46ea": { + "query": "price=equals=10", "strict": { "status": "ok", "raw": { @@ -20535,7 +20535,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "ends_with", + "comparator": "equals", "value": "10" } } @@ -20553,7 +20553,7 @@ "entries": [ [ "price", - "ends_with=10" + "equals=10" ] ], "props": { @@ -20564,7 +20564,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "ends_with", + "comparator": "equals", "value": "10" } } @@ -20575,7 +20575,7 @@ "pathname": { "$": "undefined" }, - "search": "price=ends_with=10", + "search": "price=equals=10", "sort": null } } @@ -20821,8 +20821,8 @@ } } }, - "qd66bc7b2": { - "query": "price=includes=10", + "q5802bcf1": { + "query": "price=ew=10", "strict": { "status": "ok", "raw": { @@ -20835,7 +20835,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "includes", + "comparator": "ew", "value": "10" } } @@ -20853,7 +20853,7 @@ "entries": [ [ "price", - "includes=10" + "ew=10" ] ], "props": { @@ -20864,7 +20864,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "includes", + "comparator": "ew", "value": "10" } } @@ -20875,7 +20875,7 @@ "pathname": { "$": "undefined" }, - "search": "price=includes=10", + "search": "price=ew=10", "sort": null } } @@ -20941,8 +20941,8 @@ } } }, - "q5802bcf1": { - "query": "price=ew=10", + "qd66bc7b2": { + "query": "price=includes=10", "strict": { "status": "ok", "raw": { @@ -20955,7 +20955,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "ew", + "comparator": "includes", "value": "10" } } @@ -20973,7 +20973,7 @@ "entries": [ [ "price", - "ew=10" + "includes=10" ] ], "props": { @@ -20984,7 +20984,7 @@ "$": "object", "props": { "attribute": "price", - "comparator": "ew", + "comparator": "includes", "value": "10" } } @@ -20995,7 +20995,7 @@ "pathname": { "$": "undefined" }, - "search": "price=ew=10", + "search": "price=includes=10", "sort": null } } @@ -21617,8 +21617,8 @@ } } }, - "q9487c806": { - "query": "labels=not_ct=urgent", + "q5c41a384": { + "query": "labels=not_contains=urgent", "strict": { "status": "ok", "raw": { @@ -21631,7 +21631,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "ct", + "comparator": "contains", "negated": true, "value": "urgent" } @@ -21650,7 +21650,7 @@ "entries": [ [ "labels", - "not_ct=urgent" + "not_contains=urgent" ] ], "props": { @@ -21661,7 +21661,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "ct", + "comparator": "contains", "negated": true, "value": "urgent" } @@ -21673,14 +21673,14 @@ "pathname": { "$": "undefined" }, - "search": "labels=not_ct=urgent", + "search": "labels=not_contains=urgent", "sort": null } } } }, - "q5c41a384": { - "query": "labels=not_contains=urgent", + "q9487c806": { + "query": "labels=not_ct=urgent", "strict": { "status": "ok", "raw": { @@ -21693,7 +21693,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "contains", + "comparator": "ct", "negated": true, "value": "urgent" } @@ -21712,7 +21712,7 @@ "entries": [ [ "labels", - "not_contains=urgent" + "not_ct=urgent" ] ], "props": { @@ -21723,7 +21723,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "contains", + "comparator": "ct", "negated": true, "value": "urgent" } @@ -21735,7 +21735,7 @@ "pathname": { "$": "undefined" }, - "search": "labels=not_contains=urgent", + "search": "labels=not_ct=urgent", "sort": null } } @@ -21987,8 +21987,8 @@ } } }, - "qe72db062": { - "query": "labels=not_gt=urgent", + "qcd061832": { + "query": "labels=not_le=urgent", "strict": { "status": "ok", "raw": { @@ -22001,7 +22001,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "not_gt", + "comparator": "not_le", "value": "urgent" } } @@ -22019,7 +22019,7 @@ "entries": [ [ "labels", - "not_gt=urgent" + "not_le=urgent" ] ], "props": { @@ -22030,7 +22030,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "not_gt", + "comparator": "not_le", "value": "urgent" } } @@ -22041,14 +22041,14 @@ "pathname": { "$": "undefined" }, - "search": "labels=not_gt=urgent", + "search": "labels=not_le=urgent", "sort": null } } } }, - "qcd061832": { - "query": "labels=not_le=urgent", + "qe72db062": { + "query": "labels=not_gt=urgent", "strict": { "status": "ok", "raw": { @@ -22061,7 +22061,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "not_le", + "comparator": "not_gt", "value": "urgent" } } @@ -22079,7 +22079,7 @@ "entries": [ [ "labels", - "not_le=urgent" + "not_gt=urgent" ] ], "props": { @@ -22090,7 +22090,7 @@ "$": "object", "props": { "attribute": "labels", - "comparator": "not_le", + "comparator": "not_gt", "value": "urgent" } } @@ -22101,7 +22101,7 @@ "pathname": { "$": "undefined" }, - "search": "labels=not_le=urgent", + "search": "labels=not_gt=urgent", "sort": null } } diff --git a/conformance/harperAdapter.ts b/conformance/harperAdapter.ts index 21e5c95..2e2801c 100644 --- a/conformance/harperAdapter.ts +++ b/conformance/harperAdapter.ts @@ -19,7 +19,9 @@ * surprising way, that is recorded in the report as an observation, not folded in here. * 5. **Total, except for genuinely unknown shapes.** Anything the adapter cannot place * raises `AdapterError`, which the report surfaces as an adapter gap rather than - * silently dropping. + * silently dropping. A known field carrying an unknown shape or value counts: guessing + * at it would let the harness compare a fabricated result, and possibly report agreement, + * the day Harper changes one of these structures. */ import type { Condition, ElementMatch, Field, Group, ParseResult, Projection, SortKey, Value } from '../src/types.ts'; @@ -35,6 +37,19 @@ type Dict = Record; const isDict = (value: unknown): value is Dict => typeof value === 'object' && value !== null; +/** Harper writes `operator` only as 'and' or 'or', and leaves it unset for a single term. */ +function groupOperator(raw: unknown, where: string): 'and' | 'or' { + if (raw === undefined || raw === null || raw === 'and') return 'and'; + if (raw === 'or') return 'or'; + throw new AdapterError(`unsupported ${where} operator ${JSON.stringify(raw)}`); +} + +function optionalBoolean(raw: unknown, where: string): boolean { + if (raw === undefined || raw === null) return false; + if (typeof raw !== 'boolean') throw new AdapterError(`${where} is not a boolean: ${JSON.stringify(raw)}`); + return raw; +} + /** Harper comparator name → canonical comparator plus whether the alias itself negates. */ const COMPARATOR_ALIASES: Readonly> = { // Symbolic-operator outputs (`SYMBOL_OPERATORS` in resources/search.ts). @@ -114,7 +129,7 @@ function relativeLeg(raw: Dict, scopePath: string[]): Condition { function adaptCondition(raw: Dict): Condition | ElementMatch { const rawComparator = raw.comparator === undefined || raw.comparator === null ? 'equals' : String(raw.comparator); const path = decodedPath(raw.attribute); - const negatedByFlag = raw.negated === true; + const negatedByFlag = optionalBoolean(raw.negated, 'condition.negated'); if (rawComparator === 'between') { const bounds = raw.value; @@ -155,7 +170,9 @@ function adaptCondition(raw: Dict): Condition | ElementMatch { */ function withChain(raw: Dict, head: Condition | ElementMatch, path: string[]): Condition | ElementMatch { const chained = raw.chainedConditions; - if (!Array.isArray(chained) || chained.length === 0) return head; + if (chained === undefined || chained === null) return head; + if (!Array.isArray(chained)) throw new AdapterError(`chainedConditions is not a list: ${String(chained)}`); + if (chained.length === 0) return head; const terms: (Condition | Group | ElementMatch)[] = []; if ('some' in head) terms.push(...head.some.terms); @@ -165,7 +182,7 @@ function withChain(raw: Dict, head: Condition | ElementMatch, path: string[]): C terms.push(relativeLeg(leg, path)); } - const match: ElementMatch = { path, some: { operator: raw.operator === 'or' ? 'or' : 'and', terms } }; + const match: ElementMatch = { path, some: { operator: groupOperator(raw.operator, 'chain'), terms } }; if ('some' in head && head.negated) match.negated = true; return normalizeElementMatch(match); } @@ -184,7 +201,7 @@ function adaptTerm(raw: unknown): Condition | Group | ElementMatch { function adaptGroup(raw: Dict): Group { const conditions = raw.conditions as unknown[]; - return { operator: raw.operator === 'or' ? 'or' : 'and', terms: conditions.map(adaptTerm) }; + return { operator: groupOperator(raw.operator, 'group'), terms: conditions.map(adaptTerm) }; } /** Harper's sort is a linked list of `{attribute, descending, next}` (ledger row 5). */ @@ -195,7 +212,7 @@ function adaptSort(raw: unknown): SortKey[] { if (!isDict(node)) throw new AdapterError(`unsupported sort node: ${String(node)}`); keys.push({ path: node.attribute === '' ? [''] : decodedPath(node.attribute), - direction: node.descending === true ? 'desc' : 'asc', + direction: optionalBoolean(node.descending, 'sort.descending') ? 'desc' : 'asc', }); node = node.next; } @@ -229,7 +246,7 @@ function adaptSelect(raw: unknown): Projection { if (Array.isArray(raw)) { const named = (raw as unknown as Dict).name; if (named !== undefined) return { mode: 'records', fields: [adaptField(raw)] }; - if ((raw as unknown as Dict).asArray === true) return { mode: 'tuples', fields: raw.map(adaptField) }; + if (optionalBoolean((raw as unknown as Dict).asArray, 'select.asArray')) return { mode: 'tuples', fields: raw.map(adaptField) }; return { mode: 'records', fields: raw.map(adaptField) }; } if (isDict(raw) && raw.select !== undefined) return { mode: 'records', fields: [adaptField(raw)] }; @@ -244,6 +261,8 @@ function adaptNumber(raw: unknown, field: string): number { function adaptQueryBody(raw: Dict): ParseResult { const result: ParseResult = {}; const conditions = raw.conditions; + if (conditions !== undefined && conditions !== null && !Array.isArray(conditions)) + throw new AdapterError(`conditions is not a list: ${Object.prototype.toString.call(conditions)}`); if (Array.isArray(conditions) && conditions.length > 0) result.filter = adaptGroup(raw); if (raw.sort !== undefined && raw.sort !== null) result.sort = adaptSort(raw.sort); if (raw.select !== undefined) result.select = adaptSelect(raw.select); diff --git a/conformance/referenceRunner.ts b/conformance/referenceRunner.ts new file mode 100644 index 0000000..b1cdfe1 --- /dev/null +++ b/conformance/referenceRunner.ts @@ -0,0 +1,136 @@ +/** + * Supervises the out-of-process reference parser. + * + * The parser itself holds no global state, so one long-lived worker is enough; what this + * class exists for is the failure modes. A parse that never comes back must not hang the + * corpus, and a worker that dies must not leave its promise unsettled — in both cases the + * run has to end with a verdict rather than with a wedged process. Every path here settles + * the request: a timeout as `timeout`, anything else as `harness-error`, both of which + * `classify` refuses to treat as agreement. + * + * It lives in its own module so those paths can be tested against a deliberately broken + * worker; a crash window cannot be verified by a test that never crashes. + */ + +import { type ChildProcess, fork } from 'node:child_process'; +import type { Outcome } from './canonical.ts'; + +export type ReferenceOutcomes = { strict: Outcome; deferred: Outcome }; + +export type ReferenceRunnerOptions = { + /** Path to the worker script to fork. */ + workerPath: string; + /** Wall-clock budget for one parse. */ + timeoutMs: number; + /** Budget for a worker to load and report itself ready. */ + startupTimeoutMs: number; + /** Where a worker's stderr goes. Inherited by default so real failures are visible. */ + stderr?: 'inherit' | 'ignore'; +}; + +type Pending = { resolve: (outcomes: ReferenceOutcomes) => void; timer: NodeJS.Timeout }; + +const both = (outcome: Outcome): ReferenceOutcomes => ({ strict: outcome, deferred: outcome }); + +export class ReferenceRunner { + readonly #options: ReferenceRunnerOptions; + #child: ChildProcess | undefined; + #pending = new Map(); + #nextId = 0; + /** Set once the worker cannot be replaced; every later parse then fails fast. */ + #dead: string | undefined; + + constructor(options: ReferenceRunnerOptions) { + this.#options = options; + } + + /** Why the runner stopped working, or undefined while it is healthy. */ + get deadReason(): string | undefined { + return this.#dead; + } + + #settleAll(reason: string): void { + const pending = this.#pending; + this.#pending = new Map(); + for (const entry of pending.values()) { + clearTimeout(entry.timer); + entry.resolve(both({ status: 'harness-error', error: reason })); + } + } + + async #spawn(): Promise { + const child = fork(this.#options.workerPath, [], { stdio: ['ignore', 'ignore', this.#options.stderr ?? 'inherit', 'ipc'] }); + this.#child = child; + this.#pending = new Map(); + + child.on('message', (message: { type?: string; id?: number; strict?: Outcome; deferred?: Outcome }) => { + if (message?.type !== 'result' || message.id === undefined) return; + const entry = this.#pending.get(message.id); + if (!entry) return; + this.#pending.delete(message.id); + clearTimeout(entry.timer); + entry.resolve({ strict: message.strict as Outcome, deferred: message.deferred as Outcome }); + }); + + const onGone = (reason: string): void => { + if (this.#child !== child) return; + this.#dead ??= reason; + this.#settleAll(reason); + }; + child.on('exit', (code, signal) => onGone(`the reference worker exited (code ${code}, signal ${signal})`)); + child.on('error', (error) => onGone(`the reference worker failed to run: ${error.message}`)); + + await new Promise((ready, reject) => { + const timer = setTimeout( + () => reject(new Error(`the reference worker did not start within ${this.#options.startupTimeoutMs}ms`)), + this.#options.startupTimeoutMs + ); + const settle = (finish: () => void): void => { + clearTimeout(timer); + finish(); + }; + child.once('message', (message: { type?: string }) => + settle(() => (message?.type === 'ready' ? ready() : reject(new Error('the reference worker sent no ready message')))) + ); + child.once('error', (error) => settle(() => reject(error))); + child.once('exit', (code, signal) => + settle(() => reject(new Error(`the reference worker exited before starting (code ${code}, signal ${signal})`))) + ); + }); + this.#dead = undefined; + } + + async start(): Promise { + await this.#spawn(); + } + + /** + * Parse one query. On a timeout the worker is killed and replaced, so a wedged case costs + * that case and not the rest of the corpus; a replacement that will not start ends the + * run's reference coverage instead of hanging it. + */ + parse(query: string): Promise { + if (this.#dead) return Promise.resolve(both({ status: 'harness-error', error: this.#dead })); + const child = this.#child; + if (!child) return Promise.resolve(both({ status: 'harness-error', error: 'the reference worker was never started' })); + + const id = this.#nextId++; + return new Promise((resolve) => { + const timer = setTimeout(() => { + this.#pending.delete(id); + child.kill('SIGKILL'); + this.#spawn() + .catch((error: Error) => { + this.#dead = `the reference worker could not be restarted: ${error.message}`; + }) + .finally(() => resolve(both({ status: 'timeout', ms: this.#options.timeoutMs }))); + }, this.#options.timeoutMs); + this.#pending.set(id, { resolve, timer }); + child.send({ type: 'parse', id, query }); + }); + } + + stop(): void { + this.#child?.kill(); + } +} diff --git a/conformance/report.ts b/conformance/report.ts index 65df730..ac967db 100644 --- a/conformance/report.ts +++ b/conformance/report.ts @@ -34,7 +34,6 @@ function describeOutcome(outcome: Outcome): string { } } -/** The canonical result an outcome carries, if it produced one. */ export function canonicalOf(outcome: Outcome): Json | undefined { return outcome.status === 'parsed' || outcome.status === 'deferred-error' ? outcome.canonical : undefined; } diff --git a/conformance/tagged.ts b/conformance/tagged.ts index 46c229c..297a663 100644 --- a/conformance/tagged.ts +++ b/conformance/tagged.ts @@ -137,10 +137,3 @@ export function decodeTagged(value: Tagged): unknown { } } } - -/** Constructor name recorded for a tagged object/USP, or undefined for anything else. */ -export function taggedCtor(value: Tagged): string | undefined { - if (!isTag(value)) return undefined; - if (value.$ === 'object' || value.$ === 'usp') return value.ctor; - return undefined; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index e9b94bf..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,39 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@types/node': - specifier: 24.13.3 - version: 24.13.3 - typescript: - specifier: ^5.8.0 - version: 5.9.3 - -packages: - - '@types/node@24.13.3': - resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - -snapshots: - - '@types/node@24.13.3': - dependencies: - undici-types: 7.18.2 - - typescript@5.9.3: {} - - undici-types@7.18.2: {} diff --git a/scripts/conformance-diff.mjs b/scripts/conformance-diff.mjs index 222fb5d..943e9c5 100644 --- a/scripts/conformance-diff.mjs +++ b/scripts/conformance-diff.mjs @@ -35,6 +35,7 @@ import { fileURLToPath } from 'node:url'; import { CORPUS, corpusDigest } from '../conformance/corpus.ts'; import { buildRun } from '../conformance/compare.ts'; +import { ReferenceRunner } from '../conformance/referenceRunner.ts'; import { renderReport } from '../conformance/report.ts'; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -163,15 +164,16 @@ async function record() { node: process.version, }; - const cases = {}; + const recorded = new Array(CORPUS.length); let done = 0; let cursor = 0; const runNext = async () => { while (cursor < CORPUS.length) { - const item = CORPUS[cursor++]; + const index = cursor++; + const item = CORPUS[index]; const outcome = await recordOne(harperPath, item.query); if (outcome.fatal) fail(`Recording ${JSON.stringify(item.query)} failed: ${outcome.fatal}`); - cases[item.id] = outcome.timedOut + recorded[index] = outcome.timedOut ? { query: item.query, timedOut: true, timeoutMs: options.timeout } : { query: item.query, strict: outcome.strict, target: outcome.target }; done++; @@ -181,6 +183,14 @@ async function record() { process.stderr.write(`Recording ${CORPUS.length} cases against Harper at ${harperPath}\n`); await Promise.all(Array.from({ length: options.concurrency }, runNext)); + // Assembled in corpus order, not in the order the workers happened to finish: a fixture + // keyed by completion order re-diffs at hundreds of positions on every unchanged re-record + // and buries the parser change someone is actually looking for. + const cases = {}; + CORPUS.forEach((item, index) => { + cases[item.id] = recorded[index]; + }); + mkdirSync(dirname(FIXTURE), { recursive: true }); const body = JSON.stringify({ schema: FIXTURE_SCHEMA, provenance, cases }, null, '\t') + '\n'; // Atomic: a half-written fixture would replay as silent corpus drift. @@ -189,106 +199,6 @@ async function record() { process.stderr.write(`Wrote ${FIXTURE}\n`); } -// ── replay: one persistent reference worker, restarted on timeout ─────────── - -class ReferenceRunner { - #child; - #pending = new Map(); - #nextId = 0; - /** Set once the worker cannot be replaced; every later parse fails fast instead of hanging. */ - #dead; - - /** - * Settle every in-flight parse with a harness error. A reference outcome the harness could - * not observe must never look like agreement, so it flows on to the classifier, finds no - * rule, and fails the run with the query named. - */ - #failPending(reason) { - const pending = this.#pending; - this.#pending = new Map(); - for (const entry of pending.values()) { - clearTimeout(entry.timer); - const outcome = { status: 'harness-error', error: reason }; - entry.resolve({ strict: outcome, deferred: outcome }); - } - } - - async #spawn() { - const child = fork(join(scriptDir, 'conformance-ref-worker.mjs'), [], { - stdio: ['ignore', 'ignore', 'inherit', 'ipc'], - }); - this.#child = child; - // Requests are issued one at a time by the replay loop, so replacing the pending map on - // a restart cannot strand a second in-flight parse. - this.#pending = new Map(); - - child.on('message', (message) => { - if (message?.type !== 'result') return; - const entry = this.#pending.get(message.id); - if (!entry) return; - this.#pending.delete(message.id); - clearTimeout(entry.timer); - entry.resolve({ strict: message.strict, deferred: message.deferred }); - }); - // Without these, a worker that dies mid-parse (uncaught throw, OOM kill) leaves its - // promise unsettled and the whole run hangs instead of failing. - const onGone = (reason) => { - if (this.#child !== child) return; - this.#dead ??= reason; - this.#failPending(reason); - }; - child.on('exit', (code, signal) => onGone(`the reference worker exited (code ${code}, signal ${signal})`)); - child.on('error', (error) => onGone(`the reference worker failed to run: ${error.message}`)); - - await new Promise((ready, reject) => { - const timer = setTimeout(() => reject(new Error(`the reference worker did not start within ${options.startupTimeout}ms`)), options.startupTimeout); - const settle = (fn, value) => { - clearTimeout(timer); - fn(value); - }; - child.once('message', (message) => - message?.type === 'ready' ? settle(ready) : settle(reject, new Error('the reference worker sent no ready message')) - ); - child.once('error', (error) => settle(reject, error)); - child.once('exit', (code, signal) => settle(reject, new Error(`the reference worker exited before starting (code ${code}, signal ${signal})`))); - }); - this.#dead = undefined; - } - - async start() { - await this.#spawn(); - } - - /** - * One parse. On a timeout the worker is killed and replaced, so one wedged case costs - * exactly that case rather than the rest of the corpus. A replacement that will not start - * ends the run's reference coverage rather than hanging it. - */ - parse(query) { - if (this.#dead) return Promise.resolve({ strict: { status: 'harness-error', error: this.#dead }, deferred: { status: 'harness-error', error: this.#dead } }); - const id = this.#nextId++; - return new Promise((resolveOutcome) => { - const timer = setTimeout(async () => { - this.#pending.delete(id); - const timedOut = { status: 'timeout', ms: options.timeout }; - this.#child.kill('SIGKILL'); - try { - await this.#spawn(); - } catch (error) { - this.#dead = `the reference worker could not be restarted: ${error.message}`; - } - resolveOutcome({ strict: timedOut, deferred: timedOut }); - }, options.timeout); - this.#pending.set(id, { resolve: resolveOutcome, timer }); - this.#child.send({ type: 'parse', id, query }); - }); - } - - stop() { - this.#child?.kill(); - } -} - async function replay() { if (!existsSync(FIXTURE)) fail('No recorded fixture. Run `HARPER_PATH=../harper npm run conformance:record` first.', 1); @@ -314,7 +224,11 @@ async function replay() { ' Classifications may cite rows that have since changed — run `npm run conformance:refresh-ledger`.\n' ); - const runner = new ReferenceRunner(); + const runner = new ReferenceRunner({ + workerPath: join(scriptDir, 'conformance-ref-worker.mjs'), + timeoutMs: options.timeout, + startupTimeoutMs: options.startupTimeout, + }); await runner.start(); // The reference parses run first, sequentially and each under its own timeout; the run is @@ -334,22 +248,6 @@ async function replay() { const report = renderReport(run); const unclassified = comparisons.filter((comparison) => comparison.classification.verdict === 'unclassified'); - if (options.check) { - const existing = existsSync(options.out) ? readFileSync(options.out, 'utf8') : ''; - if (existing !== report) { - writeFileSync(`${options.out}.actual`, report); - fail( - `${options.out} is out of date — replay produced a different report.\n` + - `The regenerated report was written to ${options.out}.actual; run \`npm run conformance\` and commit the result.`, - 1 - ); - } - process.stderr.write(`${options.out} is up to date (${comparisons.length} cases).\n`); - } else { - writeFileSync(options.out, report); - process.stderr.write(`Wrote ${options.out} (${comparisons.length} cases).\n`); - } - const counts = comparisons.reduce((tally, comparison) => { tally[comparison.classification.verdict] = (tally[comparison.classification.verdict] ?? 0) + 1; return tally; @@ -359,7 +257,10 @@ async function replay() { `reference-bug=${counts['reference-bug'] ?? 0} unclassified=${counts.unclassified ?? 0}\n` ); + // The run is validated BEFORE the committed report is touched. A dead reference worker or + // an unclassified divergence must not first clobber the last good report on its way out. if (unclassified.length > 0) { + writeFileSync(`${options.out}.actual`, report); process.stderr.write(`\n${unclassified.length} divergence(s) are unclassified. Add a rule in conformance/classify.ts:\n`); for (const comparison of unclassified.slice(0, 40)) process.stderr.write( @@ -367,8 +268,26 @@ async function replay() { `${comparison.differences.length ? ` diffs=${comparison.differences.map((difference) => difference.at || '/').join(',')}` : ''}\n` ); if (unclassified.length > 40) process.stderr.write(` … and ${unclassified.length - 40} more\n`); - process.exit(1); + fail(`${options.out} was left unchanged; the run that could not be classified is in ${options.out}.actual.`, 1); } + + if (options.check) { + const existing = existsSync(options.out) ? readFileSync(options.out, 'utf8') : ''; + if (existing !== report) { + writeFileSync(`${options.out}.actual`, report); + fail( + `${options.out} is out of date — replay produced a different report.\n` + + `The regenerated report was written to ${options.out}.actual; run \`npm run conformance\` and commit the result.`, + 1 + ); + } + process.stderr.write(`${options.out} is up to date (${comparisons.length} cases).\n`); + return; + } + + writeFileSync(`${options.out}.tmp`, report); + renameSync(`${options.out}.tmp`, options.out); + process.stderr.write(`Wrote ${options.out} (${comparisons.length} cases).\n`); } if (options.record) await record(); diff --git a/scripts/conformance-record-worker.mjs b/scripts/conformance-record-worker.mjs index cfd9774..f022df0 100644 --- a/scripts/conformance-record-worker.mjs +++ b/scripts/conformance-record-worker.mjs @@ -31,22 +31,33 @@ async function main() { const { parseQuery } = await importFromHarper('dist/resources/search.js'); const { RequestTarget } = await importFromHarper('dist/resources/RequestTarget.js'); + /** + * Run one entry point and tag its result. Only the PARSE may fail as a rejection — + * an encoding failure is a harness fault, and recording it as `status: 'error'` would + * put a fake Harper rejection in the fixture for a rule to classify as a divergence. + */ + const capture = (parse) => { + let result; + try { + result = parse(); + } catch (error) { + return { status: 'error', ...describe(error) }; + } + return { status: 'ok', raw: encodeTagged(result) }; + }; + process.on('message', (message) => { if (message?.type !== 'record') return; const query = message.query; let strict; - try { - strict = { status: 'ok', raw: encodeTagged(parseQuery(query)) }; - } catch (error) { - strict = { status: 'error', ...describe(error) }; - } - let target; try { - target = { status: 'ok', raw: encodeTagged(new RequestTarget(`?${query}`)) }; + strict = capture(() => parseQuery(query)); + target = capture(() => new RequestTarget(`?${query}`)); } catch (error) { - target = { status: 'error', ...describe(error) }; + process.send({ type: 'fatal', ...describe(error) }, () => process.exit(1)); + return; } // Exit only once the message has actually been flushed to the parent — `process.send` diff --git a/test/conformance/classify.test.ts b/test/conformance/classify.test.ts index 695c16f..822ef47 100644 --- a/test/conformance/classify.test.ts +++ b/test/conformance/classify.test.ts @@ -88,8 +88,8 @@ describe('classify', () => { }); it('does not attribute an unrelated disagreement to a ledger tag the case happens to carry', () => { - // `ledger-6-lenient-value-scan` fires only on the shape the tolerance produces - // (Harper accepts, the reference does not) — not on any difference at all. + // The tolerance rule requires Harper to accept where the reference rejects, not merely + // that the case carries the tag. const verdict = classify( comparison({ case: { ...CASE, query: 'foo=ba)r', features: ['tolerance'], ledger: [6] }, diff --git a/test/conformance/referenceRunner.test.ts b/test/conformance/referenceRunner.test.ts new file mode 100644 index 0000000..711efad --- /dev/null +++ b/test/conformance/referenceRunner.test.ts @@ -0,0 +1,140 @@ +/** + * Worker-lifecycle tests for the replay supervisor. + * + * These run real subprocesses, including deliberately broken ones, because the paths worth + * testing here are the crash windows: a worker that dies mid-parse, one that never answers, + * one that never starts, and one that cannot be replaced. Each must end in a settled outcome + * the classifier refuses to call agreement — never in a promise nobody resolves. + */ +import { after, before, describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { ReferenceRunner } from '../../conformance/referenceRunner.ts'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const REAL_WORKER = join(root, 'scripts', 'conformance-ref-worker.mjs'); + +let stubDir: string; +const stub = (name: string, body: string): string => { + const path = join(stubDir, name); + writeFileSync(path, body); + return path; +}; + +before(() => { + stubDir = mkdtempSync(join(tmpdir(), 'rql-ref-runner-')); +}); +after(() => { + rmSync(stubDir, { recursive: true, force: true }); +}); + +/** Fail loudly rather than letting the suite hang if a path stops settling. */ +function withDeadline(promise: Promise, ms: number, what: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => { + const timer = setTimeout(() => reject(new Error(`${what} never settled within ${ms}ms`)), ms); + timer.unref(); + }), + ]); +} + +// Worker stderr is silenced: several of these workers are supposed to fail, and their stack +// traces in the test output read like the suite itself broke. +const options = { timeoutMs: 1500, startupTimeoutMs: 5000, stderr: 'ignore' } as const; + +describe('ReferenceRunner — the happy path', () => { + it('parses through the real worker in both error modes', async () => { + const runner = new ReferenceRunner({ workerPath: REAL_WORKER, ...options }); + await runner.start(); + try { + const parsed = await withDeadline(runner.parse('a=1'), 10_000, 'a valid parse'); + assert.equal(parsed.strict.status, 'parsed'); + const rejected = await withDeadline(runner.parse('a=1&b=2|c=3'), 10_000, 'an invalid parse'); + assert.equal(rejected.strict.status, 'rejected'); + // §6.1's deferred mode keeps a partial result rather than throwing. + assert.equal(rejected.deferred.status, 'deferred-error'); + assert.equal(runner.deadReason, undefined); + } finally { + runner.stop(); + } + }); +}); + +describe('ReferenceRunner — failure paths', () => { + it('settles an in-flight parse when the worker dies instead of answering', async () => { + const path = stub( + 'dies-mid-parse.mjs', + `process.on('message', () => process.exit(3));\nprocess.send({ type: 'ready' });\n` + ); + const runner = new ReferenceRunner({ workerPath: path, ...options }); + await runner.start(); + const outcomes = await withDeadline(runner.parse('a=1'), 10_000, 'a parse whose worker died'); + assert.equal(outcomes.strict.status, 'harness-error'); + assert.match((outcomes.strict as { error: string }).error, /exited \(code 3/); + assert.ok(runner.deadReason, 'the runner should record why it stopped working'); + }); + + it('fails fast on every later parse once the worker is gone, rather than hanging', async () => { + const path = stub('dies-again.mjs', `process.on('message', () => process.exit(4));\nprocess.send({ type: 'ready' });\n`); + const runner = new ReferenceRunner({ workerPath: path, ...options }); + await runner.start(); + await withDeadline(runner.parse('a=1'), 10_000, 'the first parse'); + const second = await withDeadline(runner.parse('b=2'), 5_000, 'the parse after the worker died'); + assert.equal(second.strict.status, 'harness-error'); + }); + + it('times out a worker that never answers, and stays usable afterwards', async () => { + const path = stub('never-answers.mjs', `process.on('message', () => {});\nprocess.send({ type: 'ready' });\n`); + const runner = new ReferenceRunner({ workerPath: path, timeoutMs: 300, startupTimeoutMs: 5000, stderr: 'ignore' }); + await runner.start(); + try { + const first = await withDeadline(runner.parse('a=1'), 10_000, 'a parse against a silent worker'); + assert.deepEqual(first.strict, { status: 'timeout', ms: 300 }); + assert.equal(runner.deadReason, undefined, 'a replaced worker leaves the runner healthy'); + // The replacement is a fresh process, so the next case is timed independently. + const second = await withDeadline(runner.parse('b=2'), 10_000, 'the parse after a timeout'); + assert.deepEqual(second.strict, { status: 'timeout', ms: 300 }); + } finally { + runner.stop(); + } + }); + + it('reports a timeout AND marks itself dead when the replacement cannot start', async () => { + // Answers nothing, and deletes itself so the restart has no script to fork. + const path = stub( + 'vanishes.mjs', + `import { rmSync } from 'node:fs';\nprocess.on('message', () => rmSync(new URL(import.meta.url)));\nprocess.send({ type: 'ready' });\n` + ); + const runner = new ReferenceRunner({ workerPath: path, timeoutMs: 300, startupTimeoutMs: 1000, stderr: 'ignore' }); + await runner.start(); + const first = await withDeadline(runner.parse('a=1'), 10_000, 'a parse whose replacement cannot start'); + assert.equal(first.strict.status, 'timeout'); + assert.match(runner.deadReason ?? '', /could not be restarted/); + const second = await withDeadline(runner.parse('b=2'), 5_000, 'the parse after a failed restart'); + assert.equal(second.strict.status, 'harness-error'); + }); + + it('rejects start() for a worker that exits before reporting ready', async () => { + const path = stub('exits-at-once.mjs', `process.exit(7);\n`); + const runner = new ReferenceRunner({ workerPath: path, ...options }); + await assert.rejects(withDeadline(runner.start(), 10_000, 'start against an exiting worker'), /exited before starting/); + }); + + it('rejects start() for a worker that never reports ready', async () => { + const path = stub('never-ready.mjs', `setTimeout(() => {}, 60_000);\n`); + const runner = new ReferenceRunner({ workerPath: path, timeoutMs: 300, startupTimeoutMs: 400, stderr: 'ignore' }); + await assert.rejects(withDeadline(runner.start(), 10_000, 'start against a silent worker'), /did not start within 400ms/); + runner.stop(); + }); + + it('does not hang when asked to parse before it was started', async () => { + const runner = new ReferenceRunner({ workerPath: REAL_WORKER, ...options }); + const outcomes = await withDeadline(runner.parse('a=1'), 5_000, 'a parse before start()'); + assert.equal(outcomes.strict.status, 'harness-error'); + }); +}); From da6d661958c400e461c5bf915605bf4e2569402e Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:15:12 -0600 Subject: [PATCH 4/9] Add the negative controls the review asked for Two regressions the fail-closed changes prevent now have tests that would catch them coming back: a pinned-witness query (`limit(x)`) stays unclassified when either parse was not observed, and a known adapter field carrying an unknown shape or value raises AdapterError rather than being silently defaulted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- test/conformance/adapter.test.ts | 11 +++++++++++ test/conformance/classify.test.ts | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/test/conformance/adapter.test.ts b/test/conformance/adapter.test.ts index c8d8907..5348414 100644 --- a/test/conformance/adapter.test.ts +++ b/test/conformance/adapter.test.ts @@ -306,6 +306,17 @@ describe('harper adapter — errors', () => { assert.equal((outcome as { partial: ParseResult }).partial.filter?.terms.length, 1); }); + it('raises AdapterError for a KNOWN field carrying an unknown shape or value', () => { + // Guessing here would let the harness compare a fabricated result — and possibly report + // agreement — the first time Harper changes one of these structures. + assert.throws(() => adaptHarperResult({ conditions: { nope: true } }), AdapterError); + assert.throws(() => adaptHarperResult(query({ conditions: [{ comparator: 'eq', attribute: 'a', value: '1' }], operator: 'xor' })), AdapterError); + assert.throws(() => adaptHarperResult(query({ conditions: [{ comparator: 'eq', attribute: 'a', value: '1', negated: 'yes' }] })), AdapterError); + assert.throws(() => adaptHarperResult(query({ conditions: [{ comparator: 'eq', attribute: 'a', value: '1', chainedConditions: 'nope' }] })), AdapterError); + assert.throws(() => adaptHarperResult(query({ sort: { attribute: 'a', descending: 'yes' } })), AdapterError); + assert.throws(() => adaptHarperResult(query({ select: Object.assign(['a'], { asArray: 'yes' }) })), AdapterError); + }); + it('raises AdapterError rather than guessing at a shape it does not know', () => { assert.throws(() => adaptHarperResult(42), AdapterError); assert.throws(() => adaptHarperResult(query({ conditions: [{ nonsense: true }] })), AdapterError); diff --git a/test/conformance/classify.test.ts b/test/conformance/classify.test.ts index 822ef47..454832c 100644 --- a/test/conformance/classify.test.ts +++ b/test/conformance/classify.test.ts @@ -76,6 +76,21 @@ describe('classify', () => { assert.deepEqual(verdict, { verdict: 'unclassified' }); }); + it('NEGATIVE CONTROL: a pinned witness query is NOT classified when a parse was not observed', () => { + // `harper-limit-arguments-unvalidated` names `limit(x)` and matches on the query alone. + // Were an unobserved outcome allowed through, a timed-out reference parse would be + // reported as that divergence and the run would pass having compared nothing. + for (const unobserved of [ + { status: 'timeout', ms: 5000 } as Outcome, + { status: 'harness-error', error: 'the reference worker exited (code 1, signal null)' } as Outcome, + { status: 'adapter-gap', error: 'unsupported select shape' } as Outcome, + ]) { + const item = { ...CASE, query: 'limit(x)', features: ['limit', 'adversarial'] }; + assert.deepEqual(classify(comparison({ case: item, ref: unobserved, harper: parsed })), { verdict: 'unclassified' }); + assert.deepEqual(classify(comparison({ case: item, ref: parsed, harper: unobserved })), { verdict: 'unclassified' }); + } + }); + it('never calls an unobserved parse agreement, on either side', () => { const timeout: Outcome = { status: 'timeout', ms: 5000 }; assert.deepEqual(classify(comparison({ case: { ...CASE, query: 'x=1', features: [] }, ref: timeout, harper: timeout })), { From 1594d9aeb55004084c8e34c6447190a9fcea391f Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:18:05 -0600 Subject: [PATCH 5/9] Say which reference-parser revision the report's reference side came from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The provenance row read "Reference parser commit", which implied the reference results below it were produced by that commit. They are not: Harper's side is the recorded fixture and is exactly the commit named, but the reference side is parsed live at replay time by whatever is in `src/`. The row is now labelled as the recording-time stamp and the report says which side is which, and how the byte-equality check keeps the two honest. Dirtiness for that row is also measured over `src/` alone — an edit to the harness said nothing about which parser produced the reference results, but it still stamped the fixture "+ uncommitted changes". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- conformance/conformance-report.md | 10 ++++++++-- conformance/fixtures/harper-parse.json | 8 ++++---- conformance/report.ts | 8 +++++++- scripts/conformance-diff.mjs | 4 +++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/conformance/conformance-report.md b/conformance/conformance-report.md index a718bb6..b9e8f4a 100644 --- a/conformance/conformance-report.md +++ b/conformance/conformance-report.md @@ -26,8 +26,8 @@ disagreement is classified below; an unclassified disagreement fails the run. |---|---| | Harper commit | `11a1c489168dfb00c6a66472cef14cb3f875286f` (v5.2.2-14-g11a1c4891) | | Harper version | `5.2.2` | -| Reference parser commit | `32599416d13687d2a1805737f6f61536925ccfa6` (v0.3.3-25-g3259941) **+ uncommitted changes** | -| Fixture recorded | 2026-09-02T00:13:35.509Z | +| Reference parser at recording | `da6d661958c400e461c5bf915605bf4e2569402e` (v0.3.3-27-gda6d661) | +| Fixture recorded | 2026-09-02T00:17:34.368Z | | Recorded on Node | `v26.2.0` | | Corpus digest | `349-6b64d3e9` | | Ledger snapshot | [HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440), 14 rows, fetched 2026-09-01T23:16:18.888Z | @@ -36,6 +36,12 @@ disagreement is classified below; an unclassified disagreement fails the run. The ledger snapshot is a cache. [HarperFast/harper#2440](https://github.com/HarperFast/harper/issues/2440) stays canonical — refresh with `npm run conformance:refresh-ledger` before trusting a classification. +Harper’s side of every comparison below is the recorded fixture, so it is exactly the +commit named above. The reference side is parsed live at replay time by the parser in the +working tree, which the row above dates only as of the recording: change `src/` and +`npm run conformance:check` fails until the report is regenerated, and +`npm run conformance:record` re-stamps the row. + ## Totals | Outcome | Cases | diff --git a/conformance/fixtures/harper-parse.json b/conformance/fixtures/harper-parse.json index 6970fe1..f95bacc 100644 --- a/conformance/fixtures/harper-parse.json +++ b/conformance/fixtures/harper-parse.json @@ -8,11 +8,11 @@ "version": "5.2.2" }, "reference": { - "commit": "32599416d13687d2a1805737f6f61536925ccfa6", - "describe": "v0.3.3-25-g3259941", - "dirty": true + "commit": "da6d661958c400e461c5bf915605bf4e2569402e", + "describe": "v0.3.3-27-gda6d661", + "dirty": false }, - "recordedAt": "2026-09-02T00:13:35.509Z", + "recordedAt": "2026-09-02T00:17:34.368Z", "corpusDigest": "349-6b64d3e9", "node": "v26.2.0" }, diff --git a/conformance/report.ts b/conformance/report.ts index ac967db..5d87a79 100644 --- a/conformance/report.ts +++ b/conformance/report.ts @@ -110,7 +110,7 @@ export function renderReport(run: RunResult): string { write(`| Harper commit | ${code(provenance.harper.commit)}${provenance.harper.describe ? ` (${escapeCell(provenance.harper.describe)})` : ''}${provenance.harper.dirty ? ' **+ uncommitted changes**' : ''} |`); write(`| Harper version | ${provenance.harper.version ? code(provenance.harper.version) : '_unknown_'} |`); write( - `| Reference parser commit | ${code(provenance.reference.commit)}${provenance.reference.describe ? ` (${escapeCell(provenance.reference.describe)})` : ''}${provenance.reference.dirty ? ' **+ uncommitted changes**' : ''} |` + `| Reference parser at recording | ${code(provenance.reference.commit)}${provenance.reference.describe ? ` (${escapeCell(provenance.reference.describe)})` : ''}${provenance.reference.dirty ? ' **+ uncommitted changes**' : ''} |` ); write(`| Fixture recorded | ${provenance.recordedAt} |`); write(`| Recorded on Node | ${code(provenance.node)} |`); @@ -124,6 +124,12 @@ export function renderReport(run: RunResult): string { write('The ledger snapshot is a cache. [' + escapeCell(`${ledger.source.repo}#${ledger.source.issue}`) + '](' + ledger.source.url + ') stays canonical —'); write('refresh with `npm run conformance:refresh-ledger` before trusting a classification.'); write(); + write('Harper’s side of every comparison below is the recorded fixture, so it is exactly the'); + write('commit named above. The reference side is parsed live at replay time by the parser in the'); + write('working tree, which the row above dates only as of the recording: change `src/` and'); + write('`npm run conformance:check` fails until the report is regenerated, and'); + write('`npm run conformance:record` re-stamps the row.'); + write(); write('## Totals'); write(); diff --git a/scripts/conformance-diff.mjs b/scripts/conformance-diff.mjs index 943e9c5..d91082a 100644 --- a/scripts/conformance-diff.mjs +++ b/scripts/conformance-diff.mjs @@ -154,10 +154,12 @@ async function record() { }, // Recorded here, not rendered from the working tree: the report is committed, so a // value read from `git rev-parse HEAD` would invalidate it on every later commit. + // Dirtiness is measured over `src/` alone — an edit to the harness itself says nothing + // about which parser the reference side of the report came from. reference: { commit: git(root, ['rev-parse', 'HEAD']) ?? 'unknown', describe: git(root, ['describe', '--tags', '--always']), - dirty: (git(root, ['status', '--porcelain']) ?? '') !== '', + dirty: (git(root, ['status', '--porcelain', '--', 'src']) ?? '') !== '', }, recordedAt: new Date().toISOString(), corpusDigest: corpusDigest(), From 434028580cef0770fe5c9fa067c8bd60c240ce45 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:28:48 -0600 Subject: [PATCH 6/9] Reject a parse-result member the adapter has never seen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last finding from the validation round: the adapter read the members it knows and ignored everything else, so a result member Harper grows later would be dropped without a word — and dropping one silently is how the harness would come to report agreement on a query whose meaning had changed. Known members are now an explicit set, split between the ones that carry query semantics and the RequestTarget plumbing (`id`, `isCollection`, `pathname`, `search`) that deliberately does not. Both lists were built from what the recorded fixture actually contains; anything outside them raises AdapterError, which the report surfaces as an adapter gap. Also trimmed the narration out of `referenceRunner.ts` — the rationale for its failure paths belongs in `conformance/design.md`, which has it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- conformance/harperAdapter.ts | 20 ++++++++++++++++++++ conformance/referenceRunner.ts | 22 +++++++--------------- test/conformance/adapter.test.ts | 25 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/conformance/harperAdapter.ts b/conformance/harperAdapter.ts index 2e2801c..e375fbf 100644 --- a/conformance/harperAdapter.ts +++ b/conformance/harperAdapter.ts @@ -258,7 +258,27 @@ function adaptNumber(raw: unknown, field: string): number { return raw; } +/** + * Result members the adapter understands. `conditions`/`operator` describe the filter; + * `sort`/`select`/`limit`/`offset` shape the results; `parseError`/`parseErrorMessage` carry a + * deferred rejection; `name` marks a scoped match. The rest are RequestTarget plumbing that + * carries no query semantics — the request path and id it was constructed from. + * + * Anything outside both sets is a member Harper grew after this adapter was written, and + * dropping it silently could let the harness report agreement on a query whose meaning changed. + */ +const KNOWN_RESULT_MEMBERS: ReadonlySet = new Set([ + 'conditions', 'operator', 'name', 'sort', 'select', 'limit', 'offset', 'parseError', 'parseErrorMessage', +]); +const IGNORED_HOST_MEMBERS: ReadonlySet = new Set(['id', 'isCollection', 'pathname', 'search']); + +function assertKnownMembers(raw: Dict): void { + const unknown = Object.keys(raw).filter((key) => !KNOWN_RESULT_MEMBERS.has(key) && !IGNORED_HOST_MEMBERS.has(key)); + if (unknown.length > 0) throw new AdapterError(`unknown parse-result member(s): ${unknown.sort().join(', ')}`); +} + function adaptQueryBody(raw: Dict): ParseResult { + assertKnownMembers(raw); const result: ParseResult = {}; const conditions = raw.conditions; if (conditions !== undefined && conditions !== null && !Array.isArray(conditions)) diff --git a/conformance/referenceRunner.ts b/conformance/referenceRunner.ts index b1cdfe1..67d7d34 100644 --- a/conformance/referenceRunner.ts +++ b/conformance/referenceRunner.ts @@ -1,15 +1,10 @@ /** * Supervises the out-of-process reference parser. * - * The parser itself holds no global state, so one long-lived worker is enough; what this - * class exists for is the failure modes. A parse that never comes back must not hang the - * corpus, and a worker that dies must not leave its promise unsettled — in both cases the - * run has to end with a verdict rather than with a wedged process. Every path here settles - * the request: a timeout as `timeout`, anything else as `harness-error`, both of which - * `classify` refuses to treat as agreement. - * - * It lives in its own module so those paths can be tested against a deliberately broken - * worker; a crash window cannot be verified by a test that never crashes. + * Every path settles the request it was given: a parse that never returns becomes `timeout`, + * and a worker that dies, will not start, or cannot be replaced becomes `harness-error` — + * neither of which `classify` will treat as agreement. A promise left unsettled here would + * hang the whole corpus instead. */ import { type ChildProcess, fork } from 'node:child_process'; @@ -40,15 +35,16 @@ export class ReferenceRunner { /** Set once the worker cannot be replaced; every later parse then fails fast. */ #dead: string | undefined; + constructor(options: ReferenceRunnerOptions) { this.#options = options; } - /** Why the runner stopped working, or undefined while it is healthy. */ get deadReason(): string | undefined { return this.#dead; } + /** Hand every in-flight parse the same failure, rather than leaving it pending. */ #settleAll(reason: string): void { const pending = this.#pending; this.#pending = new Map(); @@ -104,11 +100,7 @@ export class ReferenceRunner { await this.#spawn(); } - /** - * Parse one query. On a timeout the worker is killed and replaced, so a wedged case costs - * that case and not the rest of the corpus; a replacement that will not start ends the - * run's reference coverage instead of hanging it. - */ + /** On a timeout the worker is killed and replaced, so a wedged case costs only that case. */ parse(query: string): Promise { if (this.#dead) return Promise.resolve(both({ status: 'harness-error', error: this.#dead })); const child = this.#child; diff --git a/test/conformance/adapter.test.ts b/test/conformance/adapter.test.ts index 5348414..b3a1ef3 100644 --- a/test/conformance/adapter.test.ts +++ b/test/conformance/adapter.test.ts @@ -317,6 +317,31 @@ describe('harper adapter — errors', () => { assert.throws(() => adaptHarperResult(query({ select: Object.assign(['a'], { asArray: 'yes' }) })), AdapterError); }); + it('raises AdapterError for a result member it has never seen', () => { + // Silently dropping a member Harper grew later could let the harness report agreement on + // a query whose meaning changed. + assert.throws(() => adaptHarperResult(query({ distinct: ['a'] })), /unknown parse-result member/); + const target = new URLSearchParams('a=1') as URLSearchParams & Record; + target.conditions = []; + target.groupBy = ['a']; + assert.throws(() => adaptHarperResult(target), /unknown parse-result member/); + }); + + it('ignores the RequestTarget plumbing that carries no query semantics', () => { + const target = new URLSearchParams('a=1') as URLSearchParams & Record; + Object.assign(target, { + id: null, + isCollection: true, + pathname: undefined, + search: 'a=1', + sort: null, + conditions: [{ comparator: 'equals', attribute: 'a', value: '1' }], + }); + assert.deepEqual(adapt(target), { + filter: { operator: 'and', terms: [{ path: ['a'], comparator: 'eq', value: '1' }] }, + }); + }); + it('raises AdapterError rather than guessing at a shape it does not know', () => { assert.throws(() => adaptHarperResult(42), AdapterError); assert.throws(() => adaptHarperResult(query({ conditions: [{ nonsense: true }] })), AdapterError); From 6de4105a19eaeef6c2994eaa1bb0297615b583f7 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:35:57 -0600 Subject: [PATCH 7/9] Close the startup-failure leak, a test race, and a misleading diagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the validation round (Cursor/Grok): - A worker that missed its startup budget was rejected but not killed, so the process was orphaned and a late `ready` could revive a worker the caller had already recorded as dead — on a restart that meant every later case reported a harness error while a live worker sat there. Every startup failure now kills the child on its way out. - The "replacement cannot start" test had the worker delete its own script inside the parse handler, racing the 300ms timeout: under a loaded test runner the restart wins, finds the file, and the assertion fails. The parent now removes the script after startup, so the condition is set before the parse begins. - A run that could not be classified told the operator to add a rule in `classify.ts` even when the reason was that a parser was never observed, which no rule can ever cover. Those two causes are now reported separately. Also completes the previous commit: the fast path skipped the unknown-member check, so a new result member on a query Harper never parses would still have been dropped silently. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- conformance/classify.ts | 4 +++- conformance/harperAdapter.ts | 5 ++++- conformance/referenceRunner.ts | 24 +++++++++++++-------- scripts/conformance-diff.mjs | 27 +++++++++++++++++++----- test/conformance/adapter.test.ts | 4 ++++ test/conformance/referenceRunner.test.ts | 10 ++++----- 6 files changed, 53 insertions(+), 21 deletions(-) diff --git a/conformance/classify.ts b/conformance/classify.ts index 89c0133..bc59975 100644 --- a/conformance/classify.ts +++ b/conformance/classify.ts @@ -560,7 +560,9 @@ const VALUE_MODE_CLASSIFICATION: Extract = { */ const OBSERVED_STATUSES: ReadonlySet = new Set(['parsed', 'rejected', 'deferred-error']); -const wasObserved = (outcome: Outcome): boolean => OBSERVED_STATUSES.has(outcome.status); +/** Whether a parser was actually observed for this outcome (as opposed to timing out, or + * failing inside the harness). */ +export const wasObserved = (outcome: Outcome): boolean => OBSERVED_STATUSES.has(outcome.status); export function classify(comparison: Comparison): Classification { // An unobserved parse must never reach the rules. Several rules are pinned to witness diff --git a/conformance/harperAdapter.ts b/conformance/harperAdapter.ts index e375fbf..f227a7d 100644 --- a/conformance/harperAdapter.ts +++ b/conformance/harperAdapter.ts @@ -311,7 +311,10 @@ export function adaptHarperResult(raw: unknown): HarperOutcome { // A RequestTarget is a URLSearchParams that ALSO carries the parsed query when the // parsing path ran (the dual shape of ledger row 1). const carrier = raw as unknown as Dict; - if (carrier.conditions === undefined) return finish(adaptSearchParams(raw), carrier); + if (carrier.conditions === undefined) { + assertKnownMembers(carrier); + return finish(adaptSearchParams(raw), carrier); + } return finish(adaptQueryBody(carrier), carrier); } if (!isDict(raw)) throw new AdapterError(`unsupported parse result: ${String(raw)}`); diff --git a/conformance/referenceRunner.ts b/conformance/referenceRunner.ts index 67d7d34..f620654 100644 --- a/conformance/referenceRunner.ts +++ b/conformance/referenceRunner.ts @@ -77,20 +77,26 @@ export class ReferenceRunner { child.on('error', (error) => onGone(`the reference worker failed to run: ${error.message}`)); await new Promise((ready, reject) => { + // Every failure kills the child. A worker that merely missed its startup budget is + // still running, and leaving it alive orphans a process AND lets a late `ready` + // revive a worker the caller has already written off as dead. + const fail = (error: Error): void => { + clearTimeout(timer); + child.kill('SIGKILL'); + reject(error); + }; const timer = setTimeout( - () => reject(new Error(`the reference worker did not start within ${this.#options.startupTimeoutMs}ms`)), + () => fail(new Error(`the reference worker did not start within ${this.#options.startupTimeoutMs}ms`)), this.#options.startupTimeoutMs ); - const settle = (finish: () => void): void => { + child.once('message', (message: { type?: string }) => { + if (message?.type !== 'ready') return fail(new Error('the reference worker sent no ready message')); clearTimeout(timer); - finish(); - }; - child.once('message', (message: { type?: string }) => - settle(() => (message?.type === 'ready' ? ready() : reject(new Error('the reference worker sent no ready message')))) - ); - child.once('error', (error) => settle(() => reject(error))); + ready(); + }); + child.once('error', (error) => fail(error)); child.once('exit', (code, signal) => - settle(() => reject(new Error(`the reference worker exited before starting (code ${code}, signal ${signal})`))) + fail(new Error(`the reference worker exited before starting (code ${code}, signal ${signal})`)) ); }); this.#dead = undefined; diff --git a/scripts/conformance-diff.mjs b/scripts/conformance-diff.mjs index d91082a..c440778 100644 --- a/scripts/conformance-diff.mjs +++ b/scripts/conformance-diff.mjs @@ -35,6 +35,7 @@ import { fileURLToPath } from 'node:url'; import { CORPUS, corpusDigest } from '../conformance/corpus.ts'; import { buildRun } from '../conformance/compare.ts'; +import { wasObserved } from '../conformance/classify.ts'; import { ReferenceRunner } from '../conformance/referenceRunner.ts'; import { renderReport } from '../conformance/report.ts'; @@ -263,13 +264,29 @@ async function replay() { // an unclassified divergence must not first clobber the last good report on its way out. if (unclassified.length > 0) { writeFileSync(`${options.out}.actual`, report); - process.stderr.write(`\n${unclassified.length} divergence(s) are unclassified. Add a rule in conformance/classify.ts:\n`); - for (const comparison of unclassified.slice(0, 40)) + // Two very different causes land in the same bucket, and telling an operator to write a + // classification rule for a case where a parser was never observed sends them the wrong way. + const unobserved = unclassified.filter((c) => !wasObserved(c.ref) || !wasObserved(c.harper)); + const undiagnosed = unclassified.filter((c) => wasObserved(c.ref) && wasObserved(c.harper)); + const list = (group) => { + for (const comparison of group.slice(0, 20)) + process.stderr.write( + ` ${JSON.stringify(comparison.case.query)} ref=${comparison.ref.status} harper=${comparison.harper.status}` + + `${comparison.differences.length ? ` diffs=${comparison.differences.map((difference) => difference.at || '/').join(',')}` : ''}\n` + ); + if (group.length > 20) process.stderr.write(` … and ${group.length - 20} more\n`); + }; + if (unobserved.length > 0) { process.stderr.write( - ` ${JSON.stringify(comparison.case.query)} ref=${comparison.ref.status} harper=${comparison.harper.status}` + - `${comparison.differences.length ? ` diffs=${comparison.differences.map((difference) => difference.at || '/').join(',')}` : ''}\n` + `\n${unobserved.length} case(s) were never compared — a parser timed out, the adapter could not map the\n` + + `result, or the harness itself failed. No classification rule can cover these; fix the run:\n` ); - if (unclassified.length > 40) process.stderr.write(` … and ${unclassified.length - 40} more\n`); + list(unobserved); + } + if (undiagnosed.length > 0) { + process.stderr.write(`\n${undiagnosed.length} divergence(s) are unclassified. Add a rule in conformance/classify.ts:\n`); + list(undiagnosed); + } fail(`${options.out} was left unchanged; the run that could not be classified is in ${options.out}.actual.`, 1); } diff --git a/test/conformance/adapter.test.ts b/test/conformance/adapter.test.ts index b3a1ef3..4fe4487 100644 --- a/test/conformance/adapter.test.ts +++ b/test/conformance/adapter.test.ts @@ -325,6 +325,10 @@ describe('harper adapter — errors', () => { target.conditions = []; target.groupBy = ['a']; assert.throws(() => adaptHarperResult(target), /unknown parse-result member/); + // …including on the fast path, where there are no conditions to walk at all. + const fastPath = new URLSearchParams('a=1') as URLSearchParams & Record; + fastPath.groupBy = ['a']; + assert.throws(() => adaptHarperResult(fastPath), /unknown parse-result member/); }); it('ignores the RequestTarget plumbing that carries no query semantics', () => { diff --git a/test/conformance/referenceRunner.test.ts b/test/conformance/referenceRunner.test.ts index 711efad..437cc7a 100644 --- a/test/conformance/referenceRunner.test.ts +++ b/test/conformance/referenceRunner.test.ts @@ -105,13 +105,13 @@ describe('ReferenceRunner — failure paths', () => { }); it('reports a timeout AND marks itself dead when the replacement cannot start', async () => { - // Answers nothing, and deletes itself so the restart has no script to fork. - const path = stub( - 'vanishes.mjs', - `import { rmSync } from 'node:fs';\nprocess.on('message', () => rmSync(new URL(import.meta.url)));\nprocess.send({ type: 'ready' });\n` - ); + const path = stub('goes-missing.mjs', `process.on('message', () => {});\nprocess.send({ type: 'ready' });\n`); const runner = new ReferenceRunner({ workerPath: path, timeoutMs: 300, startupTimeoutMs: 1000, stderr: 'ignore' }); await runner.start(); + // Removed from the PARENT, after the worker is up: having the child delete its own + // script inside the parse handler races the timeout, and under a loaded test runner the + // restart can win and find the file still there. + rmSync(path); const first = await withDeadline(runner.parse('a=1'), 10_000, 'a parse whose replacement cannot start'); assert.equal(first.strict.status, 'timeout'); assert.match(runner.deadReason ?? '', /could not be restarted/); From 3865728146d221bd3e3f644b92310adcab6dd1c2 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:40:42 -0600 Subject: [PATCH 8/9] Enforce the reference runner's single-flight invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline version documented that only one parse is ever in flight; the extraction dropped the note and nothing enforced it. Replacing a timed-out worker discards the pending set with it, so a second concurrent parse would be resolved from its own timer as a spurious timeout and would replace the worker again — silently, and precisely for whoever tries to speed the corpus up by not awaiting each parse. It now throws instead, so that attempt fails immediately and says where the parallelism belongs: one runner per concurrent parse. The invariant is recorded in the design note rather than narrated in the module. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- conformance/design.md | 4 +++- conformance/referenceRunner.ts | 18 +++++++++++++----- test/conformance/referenceRunner.test.ts | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/conformance/design.md b/conformance/design.md index 625b6c6..628b159 100644 --- a/conformance/design.md +++ b/conformance/design.md @@ -81,7 +81,9 @@ disagreement. - `conformance/classify.ts` — the residual filter and the ordered rule set. - `conformance/compare.ts` — run assembly, shared by the runner and the tests. - `conformance/referenceRunner.ts` — worker supervision; a separate module so its crash windows - can be tested against deliberately broken workers. + can be tested against deliberately broken workers. It is single-flight by construction: + replacing a timed-out worker discards the pending set, so a second concurrent parse would + resolve as a spurious timeout. Parallelism belongs one level up, in more runners. - `conformance/report.ts` — Markdown rendering from committed data only. - `scripts/conformance-diff.mjs` — the CLI; `scripts/conformance-record-worker.mjs` and `scripts/conformance-ref-worker.mjs` — the two process boundaries; diff --git a/conformance/referenceRunner.ts b/conformance/referenceRunner.ts index f620654..6c9351f 100644 --- a/conformance/referenceRunner.ts +++ b/conformance/referenceRunner.ts @@ -1,10 +1,9 @@ /** * Supervises the out-of-process reference parser. * - * Every path settles the request it was given: a parse that never returns becomes `timeout`, - * and a worker that dies, will not start, or cannot be replaced becomes `harness-error` — - * neither of which `classify` will treat as agreement. A promise left unsettled here would - * hang the whole corpus instead. + * Two invariants: every request settles — a parse that never returns becomes `timeout`, a + * worker that dies or cannot be replaced becomes `harness-error`, and neither counts as + * agreement — and only one parse may be in flight at a time (see `parse`). */ import { type ChildProcess, fork } from 'node:child_process'; @@ -106,8 +105,17 @@ export class ReferenceRunner { await this.#spawn(); } - /** On a timeout the worker is killed and replaced, so a wedged case costs only that case. */ + /** + * On a timeout the worker is killed and replaced, so a wedged case costs only that case. + * + * SINGLE-FLIGHT. Replacing the worker discards the pending set with it, so a second + * concurrent parse would be resolved from its own timer as a spurious timeout and would + * replace the worker again. That is enforced rather than documented, because the natural + * way to speed the corpus up is to stop awaiting each parse — run several runners instead. + */ parse(query: string): Promise { + if (this.#pending.size > 0) + throw new Error('ReferenceRunner.parse is single-flight; use one runner per concurrent parse'); if (this.#dead) return Promise.resolve(both({ status: 'harness-error', error: this.#dead })); const child = this.#child; if (!child) return Promise.resolve(both({ status: 'harness-error', error: 'the reference worker was never started' })); diff --git a/test/conformance/referenceRunner.test.ts b/test/conformance/referenceRunner.test.ts index 437cc7a..3fdd1fc 100644 --- a/test/conformance/referenceRunner.test.ts +++ b/test/conformance/referenceRunner.test.ts @@ -132,6 +132,21 @@ describe('ReferenceRunner — failure paths', () => { runner.stop(); }); + it('refuses a second concurrent parse rather than resolving one of them spuriously', async () => { + const path = stub('slow.mjs', `process.on('message', (m) => setTimeout(() => process.send({ type: 'result', id: m.id, strict: { status: 'parsed', canonical: {} }, deferred: { status: 'parsed', canonical: {} } }), 50));\nprocess.send({ type: 'ready' });\n`); + const runner = new ReferenceRunner({ workerPath: path, ...options }); + await runner.start(); + try { + const inFlight = runner.parse('a=1'); + assert.throws(() => runner.parse('b=2'), /single-flight/); + assert.equal((await withDeadline(inFlight, 10_000, 'the first parse')).strict.status, 'parsed'); + // …and the guard lifts once nothing is pending. + assert.equal((await withDeadline(runner.parse('c=3'), 10_000, 'a later parse')).strict.status, 'parsed'); + } finally { + runner.stop(); + } + }); + it('does not hang when asked to parse before it was started', async () => { const runner = new ReferenceRunner({ workerPath: REAL_WORKER, ...options }); const outcomes = await withDeadline(runner.parse('a=1'), 5_000, 'a parse before start()'); From 97e7b8b528fcc52ac4480f6b0458f0c44348d2d2 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 1 Sep 2026 18:43:25 -0600 Subject: [PATCH 9/9] Hold the single-flight guard across the restart, not just the wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard was derived from the pending map, which a timed-out parse empties before it kills and replaces the worker — so it lapsed for exactly the window it existed to protect. It is now an explicit flag held until the returned promise settles, restart included, and the test observes that window with a worker that is deliberately slow to report ready. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015EVCBmd9GozJihPeck1CvM --- conformance/referenceRunner.ts | 14 +++++++++++--- test/conformance/referenceRunner.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/conformance/referenceRunner.ts b/conformance/referenceRunner.ts index 6c9351f..7a8fca9 100644 --- a/conformance/referenceRunner.ts +++ b/conformance/referenceRunner.ts @@ -33,6 +33,8 @@ export class ReferenceRunner { #nextId = 0; /** Set once the worker cannot be replaced; every later parse then fails fast. */ #dead: string | undefined; + /** Held for the whole of a parse, restart included — see `parse`. */ + #busy = false; constructor(options: ReferenceRunnerOptions) { @@ -114,14 +116,17 @@ export class ReferenceRunner { * way to speed the corpus up is to stop awaiting each parse — run several runners instead. */ parse(query: string): Promise { - if (this.#pending.size > 0) - throw new Error('ReferenceRunner.parse is single-flight; use one runner per concurrent parse'); + // The flag is held until the returned promise settles, not merely while the request is + // pending: a timed-out parse spends its last stretch restarting the worker with nothing + // pending, and that window is exactly when a second caller would corrupt the state. + if (this.#busy) throw new Error('ReferenceRunner.parse is single-flight; use one runner per concurrent parse'); if (this.#dead) return Promise.resolve(both({ status: 'harness-error', error: this.#dead })); const child = this.#child; if (!child) return Promise.resolve(both({ status: 'harness-error', error: 'the reference worker was never started' })); + this.#busy = true; const id = this.#nextId++; - return new Promise((resolve) => { + const done = new Promise((resolve) => { const timer = setTimeout(() => { this.#pending.delete(id); child.kill('SIGKILL'); @@ -134,6 +139,9 @@ export class ReferenceRunner { this.#pending.set(id, { resolve, timer }); child.send({ type: 'parse', id, query }); }); + return done.finally(() => { + this.#busy = false; + }); } stop(): void { diff --git a/test/conformance/referenceRunner.test.ts b/test/conformance/referenceRunner.test.ts index 3fdd1fc..5889a1a 100644 --- a/test/conformance/referenceRunner.test.ts +++ b/test/conformance/referenceRunner.test.ts @@ -147,6 +147,28 @@ describe('ReferenceRunner — failure paths', () => { } }); + it('holds the single-flight guard across the restart a timeout triggers', async () => { + // Slow to report ready, so the restart window is wide enough to observe: the pending + // entry is dropped the moment the timer fires, and the guard must still hold until the + // replacement is up and the original parse has settled. + const path = stub( + 'slow-to-start.mjs', + `process.on('message', () => {});\nsetTimeout(() => process.send({ type: 'ready' }), 400);\n` + ); + const runner = new ReferenceRunner({ workerPath: path, timeoutMs: 200, startupTimeoutMs: 5000, stderr: 'ignore' }); + await runner.start(); + try { + const timingOut = runner.parse('a=1'); + await new Promise((wake) => setTimeout(wake, 350)); + assert.throws(() => runner.parse('b=2'), /single-flight/); + assert.equal((await withDeadline(timingOut, 10_000, 'the timing-out parse')).strict.status, 'timeout'); + // …and it lifts once that parse has settled. + assert.doesNotThrow(() => void runner.parse('c=3').catch(() => undefined)); + } finally { + runner.stop(); + } + }); + it('does not hang when asked to parse before it was started', async () => { const runner = new ReferenceRunner({ workerPath: REAL_WORKER, ...options }); const outcomes = await withDeadline(runner.parse('a=1'), 5_000, 'a parse before start()');