indexwright 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/README.md +20 -13
- package/SPEC.md +328 -31
- package/dist/args.d.ts +1 -1
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +26 -1
- package/dist/args.js.map +1 -1
- package/dist/collections.d.ts +1 -1
- package/dist/collections.js +1 -1
- package/dist/format/github.d.ts +1 -1
- package/dist/format/github.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/package.json +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,30 @@ All notable changes to this project are documented here. The format follows
|
|
|
5
5
|
versioning. Pre-1.0, rule additions and message changes may land in minor releases; the `json`
|
|
6
6
|
output shape is the stable contract.
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.2.0] — 2026-08-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- A query corpus format, specified in [SPEC.md](SPEC.md) §7 and versioned by its own
|
|
13
|
+
`corpusVersion` rather than by any package's release number. It is the contract between capture
|
|
14
|
+
and the coverage check, and the first indexwright artefact meant to be committed and reviewed in
|
|
15
|
+
a diff alongside `firestore.indexes.json`.
|
|
16
|
+
- `@indexwright/record`, a separate package that writes one. See
|
|
17
|
+
[its changelog](packages/record/CHANGELOG.md).
|
|
18
|
+
- `indexwright record` now says that the verb ships in `@indexwright/record` and how to run it,
|
|
19
|
+
instead of reporting an unknown command. The cost of splitting the family into two packages is a
|
|
20
|
+
second package to discover, and "unknown command" reads as "indexwright cannot do this".
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- SPEC §3 no longer justifies the package split with a gRPC stack for capture. Capture needs none;
|
|
25
|
+
the split rests on v0.3's replay, which needs a Firestore client. Sections 7–10 shifted to 8–11
|
|
26
|
+
when the corpus format was inserted as §7.
|
|
27
|
+
|
|
28
|
+
Nothing about the linter's rules, output, or exit codes changed in this release. `indexwright`
|
|
29
|
+
still declares no runtime dependencies, and now so does `@indexwright/record`.
|
|
30
|
+
|
|
31
|
+
## [0.1.1] — 2026-08-08
|
|
9
32
|
|
|
10
33
|
No change to the published files: this release exists to exercise the publishing path itself.
|
|
11
34
|
|
|
@@ -30,5 +53,6 @@ First release. Static analysis of `firestore.indexes.json`; no network access, n
|
|
|
30
53
|
`__name__` to the same resource.
|
|
31
54
|
- A provisional JavaScript API, so the rules can run without spawning a process.
|
|
32
55
|
|
|
56
|
+
[0.2.0]: https://github.com/uny/indexwright/releases/tag/v0.2.0
|
|
33
57
|
[0.1.1]: https://github.com/uny/indexwright/releases/tag/v0.1.1
|
|
34
58
|
[0.1.0]: https://github.com/uny/indexwright/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -143,21 +143,22 @@ console.log(result.summary.byRule);
|
|
|
143
143
|
|
|
144
144
|
## Scope
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
connection.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
`indexwright lint` is static analysis of declaration files only: no network access, no credentials,
|
|
147
|
+
no Firestore connection. Answering *is this index needed* takes the queries, which is a separate
|
|
148
|
+
package:
|
|
149
|
+
|
|
150
|
+
- **v0.2 — query capture, shipped.** [`@indexwright/record`](packages/record) runs a test suite
|
|
151
|
+
with `FIRESTORE_EMULATOR_HOST` pointed at a pass-through proxy and records the `StructuredQuery`
|
|
152
|
+
shapes it observes as a corpus. This is language- and framework-independent because it operates
|
|
153
|
+
on the wire protocol rather than on source code.
|
|
154
154
|
- **v0.3 — coverage check.** Replay a captured corpus against a throwaway database with the
|
|
155
155
|
candidate index set applied, and report queries that fail with `FAILED_PRECONDITION`. The oracle
|
|
156
156
|
is Firestore itself; indexwright does not reimplement the undocumented matching rule.
|
|
157
157
|
|
|
158
|
-
Coverage
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
Coverage is bounded by what actually exercises the proxy. A query no test issues is not observed,
|
|
159
|
+
and absence of observation is not evidence that an index is unused. That limit is inherent; the
|
|
160
|
+
narrower ones that are not — the Firebase Web SDK's transport, and snapshot listeners — are
|
|
161
|
+
[named in the spec](SPEC.md) and counted in the corpus rather than passed over.
|
|
161
162
|
|
|
162
163
|
## Toward 1.0
|
|
163
164
|
|
|
@@ -169,16 +170,22 @@ Until then the version stays below 1.0 and the rules stay provisional.
|
|
|
169
170
|
|
|
170
171
|
## Development
|
|
171
172
|
|
|
173
|
+
This is an npm workspace holding two packages: `indexwright` at the root and
|
|
174
|
+
[`@indexwright/record`](packages/record) under `packages/`. Both scripts cover both.
|
|
175
|
+
|
|
172
176
|
```bash
|
|
173
177
|
npm install
|
|
174
|
-
npm test # builds, then runs
|
|
175
|
-
npm run verify-package # builds, packs, installs
|
|
178
|
+
npm test # builds, then runs both suites against dist/
|
|
179
|
+
npm run verify-package # builds, packs, installs each tarball, exercises the bins and the APIs
|
|
176
180
|
```
|
|
177
181
|
|
|
178
182
|
Both scripts build first as an explicit step rather than through a `pre` hook, because npm skips
|
|
179
183
|
`pre`/`post` scripts entirely under `ignore-scripts=true` — a setting many developers turn on. With
|
|
180
184
|
a hook, that configuration silently tests whatever `dist/` happened to be lying around.
|
|
181
185
|
|
|
186
|
+
The packages version independently and release from separate tags: `v0.2.0` publishes the linter,
|
|
187
|
+
`record-v0.2.0` publishes the recorder.
|
|
188
|
+
|
|
182
189
|
## License
|
|
183
190
|
|
|
184
191
|
Apache-2.0
|
package/SPEC.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Linter and query-coverage checker for Firestore composite indexes.
|
|
4
4
|
|
|
5
|
-
Status: **v0.
|
|
5
|
+
Status: **v0.2.0 (draft)**. Pre-1.0: rules and CLI surface may change between minor versions.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -61,8 +61,11 @@ Firestore connection.
|
|
|
61
61
|
- **v0.2 — query capture.** The Firestore emulator speaks the public Firestore v1 gRPC API in
|
|
62
62
|
plaintext on a local port. An intercepting proxy can decode `RunQuery` requests and record the
|
|
63
63
|
observed `StructuredQuery` shapes, yielding a query corpus harvested from execution rather than
|
|
64
|
-
hand-written. This is language
|
|
65
|
-
protocol rather than on source code
|
|
64
|
+
hand-written. This is independent of language and framework, because it operates on the wire
|
|
65
|
+
protocol rather than on source code — but not of transport: the emulator also serves the Firebase
|
|
66
|
+
Web SDK over WebChannel rather than gRPC, and a suite driven from a browser does not pass through
|
|
67
|
+
a gRPC proxy at all. v0.2 captures the gRPC transport, which is what server-side SDKs use. The
|
|
68
|
+
corpus format is specified in §7.
|
|
66
69
|
- **v0.3 — coverage check.** Replay a captured corpus against a throwaway Firestore database that
|
|
67
70
|
has the candidate index set applied, and report queries that fail with `FAILED_PRECONDITION`.
|
|
68
71
|
The oracle is Firestore itself; indexwright does not reimplement the undocumented matching rule.
|
|
@@ -71,29 +74,43 @@ The v0.2/v0.3 split is deliberate: capture is cheap and offline, while the cover
|
|
|
71
74
|
delegated to the platform. Reimplementing index matching would risk emitting false
|
|
72
75
|
`FAILED_PRECONDITION` verdicts and blocking development on a rule that is not published.
|
|
73
76
|
|
|
74
|
-
**Packaging of v0.2/v0.3.**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
**Packaging of v0.2/v0.3.** `record` and `check` run inside an adopter's project, so whatever they
|
|
78
|
+
depend on lands in an adopter's tree, and the build-time carve-out of §8 does not reach them. They
|
|
79
|
+
ship as a separate package, `@indexwright/record`. **`indexwright` itself acquires no runtime
|
|
80
|
+
dependency, in any version.**
|
|
81
|
+
|
|
82
|
+
The cost being separated is `check`'s, not `record`'s. Replay executes captured shapes against a
|
|
83
|
+
real database and reads the status Firestore answers with, which means a Firestore client — the
|
|
84
|
+
thing §8 exists to keep out of a linter that runs on every push, in every CI job, in projects that
|
|
85
|
+
may never touch Firestore from a server. `check` runs where a project is already talking to
|
|
86
|
+
Firestore server-side and has already resolved that client transitively. The dependency is added
|
|
87
|
+
where it is very likely already resolved, and is absent where it would be new.
|
|
88
|
+
|
|
89
|
+
Capture turned out not to need one. An earlier draft of this section justified the split with a
|
|
90
|
+
gRPC stack for `record` too — `@grpc/grpc-js` and protobuf definitions for the Firestore v1 API, on
|
|
91
|
+
the grounds that hand-writing a decoder for a wire format owned by someone else is not a cost worth
|
|
92
|
+
paying. Neither half held. `@grpc/grpc-js` implements clients and servers for services known at
|
|
93
|
+
build time; a transparent proxy is `node:http2` and raw frames, and grpc-js has no part in it. And
|
|
94
|
+
§7 fixes a closed operator vocabulary and requires an unrecognised enum value to be counted rather
|
|
95
|
+
than named, so the enum table has to exist in-tree whichever library reads the bytes — what a
|
|
96
|
+
protobuf runtime would add on top of it is varint and length-delimited parsing over field numbers
|
|
97
|
+
that a released `.proto` cannot renumber. `@indexwright/record` v0.2 therefore declares no runtime
|
|
98
|
+
dependency either, and takes one when `check` arrives.
|
|
99
|
+
|
|
100
|
+
It does not depend on `indexwright` yet. Capture reads no index declarations, so the index model
|
|
101
|
+
and the `json` contract are `check`'s needs rather than `record`'s. Declaring the dependency early
|
|
102
|
+
would put a package in an adopter's tree that nothing imports.
|
|
90
103
|
|
|
91
104
|
The cost is a second package to discover. `indexwright record` in an installation that has only the
|
|
92
105
|
linter must say where the verb lives, not report an unknown command.
|
|
93
106
|
|
|
94
107
|
**Known limit of v0.2/v0.3:** coverage is bounded by what actually exercises the proxy. A query that
|
|
95
108
|
no test issues is not observed, and absence of observation is not evidence that an index is unused.
|
|
96
|
-
|
|
109
|
+
That much is inherent. Two further gaps are not, and are implementation gaps v0.2 ships with: a
|
|
110
|
+
suite driven through the Firebase Web SDK does not reach a gRPC proxy at all, and a query issued as
|
|
111
|
+
a snapshot listener travels by `Listen` rather than `RunQuery` and is counted rather than recorded
|
|
112
|
+
(§7). Both narrow what a corpus covers without narrowing what it appears to cover, which is why §7
|
|
113
|
+
counts them out loud.
|
|
97
114
|
|
|
98
115
|
## 4. CLI
|
|
99
116
|
|
|
@@ -319,7 +336,276 @@ omitted, and `errors` is `[]` on a clean run.
|
|
|
319
336
|
**`github`** — GitHub Actions workflow commands (`::warning file=…::`) plus a Markdown summary
|
|
320
337
|
suitable for `$GITHUB_STEP_SUMMARY`.
|
|
321
338
|
|
|
322
|
-
## 7.
|
|
339
|
+
## 7. Query corpus (v0.2)
|
|
340
|
+
|
|
341
|
+
`record` writes a **query corpus**: the set of distinct query shapes observed on the emulator's wire
|
|
342
|
+
during a run, conventionally `firestore.queries.json`. The corpus is the contract between capture
|
|
343
|
+
(v0.2) and the coverage check (v0.3), and it is meant to be committed alongside
|
|
344
|
+
`firestore.indexes.json` and reviewed in a diff like any other declaration.
|
|
345
|
+
|
|
346
|
+
A query enters the corpus when its request is observed, whatever the server answers next. One that
|
|
347
|
+
failed still describes something the application issues — and a query that failed *because an index
|
|
348
|
+
was missing* is precisely the case v0.3 exists to find, so waiting for a successful status would
|
|
349
|
+
drop the most interesting entries in the file.
|
|
350
|
+
|
|
351
|
+
`record` writes the corpus whole, replacing whatever was there, and writes it atomically so that an
|
|
352
|
+
interrupted run leaves the previous file intact rather than a truncated one. Replacing rather than
|
|
353
|
+
merging is the honest reading: a corpus is evidence of what one run exercised, and a run that
|
|
354
|
+
executed three tests did not exercise the other two hundred. The cost is that a filtered or aborted
|
|
355
|
+
run produces a diff that deletes query shapes which still exist — which is a visible deletion a
|
|
356
|
+
reviewer can question, where a merge would have produced a file that silently outlives the tests
|
|
357
|
+
that justified it.
|
|
358
|
+
|
|
359
|
+
### What a shape is
|
|
360
|
+
|
|
361
|
+
A shape is the part of a `StructuredQuery` that determines which index can serve it: the collection,
|
|
362
|
+
the query scope, the filter tree, and the sort order. Everything else is discarded.
|
|
363
|
+
|
|
364
|
+
**Values are not recorded.** A query's values are customer data, and the corpus is a file that
|
|
365
|
+
persists in a repository. They are also unnecessary: index selection is a function of field paths,
|
|
366
|
+
operators, and directions, not of what is compared against. Discarding them removes the part of a
|
|
367
|
+
captured query most likely to carry a secret, and the only part that changes between runs of the
|
|
368
|
+
same test.
|
|
369
|
+
|
|
370
|
+
It does not make the corpus non-sensitive, and nothing here should be read as saying so. Field paths
|
|
371
|
+
are recorded verbatim, and a field path is not always static: `members.alice@example.com` is an
|
|
372
|
+
ordinary way to query a map, and it is at once a real identifier and a corpus entry that will never
|
|
373
|
+
recur. Collection ids carry schema and sometimes tenancy. A corpus describes an application's data
|
|
374
|
+
model, and it earns the access controls of the repository it is committed to — it does not become
|
|
375
|
+
publishable by having had its values stripped.
|
|
376
|
+
|
|
377
|
+
**Project and database are not recorded.** They are properties of the environment the capture ran
|
|
378
|
+
in, not of the query, and recording them would make a corpus captured against one emulator instance
|
|
379
|
+
look different from the same corpus captured against another.
|
|
380
|
+
|
|
381
|
+
**`limit`, `offset`, cursors, and `select` are not recorded.** None of them changes which index
|
|
382
|
+
serves the query; a projection is served by the index the underlying query already needs.
|
|
383
|
+
|
|
384
|
+
**Occurrence counts are not recorded.** A corpus is a set, not a histogram. A count changes on every
|
|
385
|
+
run without changing anything about what must be indexed, which would make the file churn in every
|
|
386
|
+
diff and train reviewers to skim it. `record` reports counts on stderr, where they are useful for
|
|
387
|
+
triage and where they do not have to be committed.
|
|
388
|
+
|
|
389
|
+
### Vocabulary
|
|
390
|
+
|
|
391
|
+
The corpus reuses the declaration vocabulary of §5, so that a corpus entry and an index key can be
|
|
392
|
+
read against each other with one translation, which the table below states in full: an
|
|
393
|
+
`ARRAY_CONTAINS` filter is served by a field declared `arrayConfig: CONTAINS`.
|
|
394
|
+
|
|
395
|
+
| Corpus | Source on the wire | Aligns with |
|
|
396
|
+
|:--|:--|:--|
|
|
397
|
+
| `collectionGroup` | `CollectionSelector.collection_id` | index `collectionGroup` |
|
|
398
|
+
| `queryScope` — `COLLECTION` \| `COLLECTION_GROUP` | `all_descendants` | index `queryScope` |
|
|
399
|
+
| `direction` — `ASCENDING` \| `DESCENDING` | `Order.direction` | field `order` |
|
|
400
|
+
| `op` — `ARRAY_CONTAINS` | `FieldFilter.Operator` | field `arrayConfig: CONTAINS` |
|
|
401
|
+
|
|
402
|
+
Operators are written with their protobuf enum names: `LESS_THAN`, `LESS_THAN_OR_EQUAL`,
|
|
403
|
+
`GREATER_THAN`, `GREATER_THAN_OR_EQUAL`, `EQUAL`, `NOT_EQUAL`, `ARRAY_CONTAINS`, `IN`,
|
|
404
|
+
`ARRAY_CONTAINS_ANY`, `NOT_IN` for a field filter; `IS_NAN`, `IS_NULL`, `IS_NOT_NAN`, `IS_NOT_NULL`
|
|
405
|
+
for a unary filter; `AND` and `OR` for a composite. Enum names rather than SDK spellings, for the
|
|
406
|
+
same reason §5 writes `ASCENDING` rather than `asc`: the file speaks Firestore's own vocabulary, so
|
|
407
|
+
an entry can be read against a wire capture without a lookup table in between. The list is closed —
|
|
408
|
+
an operator outside it is not written into the corpus under a name of indexwright's invention, and
|
|
409
|
+
protobuf would hand it over as a bare number anyway. It is counted instead, as below.
|
|
410
|
+
|
|
411
|
+
**A published default is applied; an unpublished one is not guessed at.** Every one of these enums
|
|
412
|
+
has a zero value, and the two cases are not alike. `Order.direction` is documented to default to
|
|
413
|
+
`ASCENDING`, so an `Order` that arrives as `DIRECTION_UNSPECIFIED` is recorded as `ASCENDING` —
|
|
414
|
+
that is Firestore's own statement of what the value means, not indexwright's guess, and skipping the
|
|
415
|
+
query instead would drop a real index requirement on the strength of a field the client simply left
|
|
416
|
+
at its default. `OPERATOR_UNSPECIFIED`, an operator added after this specification was written, and
|
|
417
|
+
any other value the vocabulary above cannot name have no such published meaning: the query is
|
|
418
|
+
skipped and counted rather than guessed at. `StructuredQuery.from` is a repeated field, and the same
|
|
419
|
+
line applies — an entry holds exactly one `collectionGroup`, so a query that does not carry exactly
|
|
420
|
+
one selector with a non-empty `collection_id` is skipped, because the corpus has no way to say what
|
|
421
|
+
it means rather than because its meaning is unknown.
|
|
422
|
+
|
|
423
|
+
### Canonical query key
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
<collectionGroup>::<queryScope>::<where>::<orderBy>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`<where>` serialises the filter tree: a field or unary filter as `<fieldPath>:<op>`, a composite as
|
|
430
|
+
`<op>(<child>|<child>|…)`. The root is always a composite — `StructuredQuery.where` may be a bare
|
|
431
|
+
field or unary filter, and is then wrapped in an `AND` — so a query with one filter serialises as
|
|
432
|
+
`AND(status:EQUAL)` and a query with none as `AND()`. A root that is already a composite keeps its
|
|
433
|
+
own operator: a top-level `OR` is `OR(…)`, never `AND(OR(…))`. `<orderBy>` is
|
|
434
|
+
`<fieldPath>:<direction>` joined by `|`, and is empty when the query declares no sort order. A
|
|
435
|
+
two-filter, two-sort query keys as:
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
orders::COLLECTION::AND(amount:GREATER_THAN|status:EQUAL)::amount:DESCENDING|createdAt:ASCENDING
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`<collectionGroup>` and `<fieldPath>` are escaped before they are joined: a backslash becomes `\\`,
|
|
442
|
+
each of `:`, `|`, `(`, `)` is prefixed with one, and every character below `U+0020` becomes `\uXXXX`.
|
|
443
|
+
Enum names need no escaping but are subject to the same rule, so that decoding is uniform. The
|
|
444
|
+
control-character clause is not about ambiguity but about §6's requirement that a value read off an
|
|
445
|
+
untrusted input cannot forge a line of output: a key holding a raw newline would end the line it was
|
|
446
|
+
printed on.
|
|
447
|
+
|
|
448
|
+
Escaping is what makes the key injective, and the key is what entries are de-duplicated on. A field
|
|
449
|
+
path may hold any of these characters — Firestore quotes a path in backticks precisely so that it
|
|
450
|
+
can — and unescaped, one path spelled `` `a:EQUAL|b` `` serialises identically to two filters on `a`
|
|
451
|
+
and `b`. Two distinct queries would then share an entry and only one of them would ever be replayed,
|
|
452
|
+
which is the failure this section closes on: not a gap in coverage but a gap that looks like
|
|
453
|
+
coverage.
|
|
454
|
+
|
|
455
|
+
Both `AND` and `OR` are commutative, so a composite's children are sorted by their own serialised
|
|
456
|
+
form, compared by Unicode code point, before joining. This makes the key independent of the order
|
|
457
|
+
the filters were written in, which is what allows two spellings of one query to collapse into one
|
|
458
|
+
corpus entry. A composite child carrying the same operator as its parent is flattened into it, since
|
|
459
|
+
`AND(a|AND(b|c))` and `AND(a|b|c)` are one query; nesting under a *different* operator is meaningful
|
|
460
|
+
and is kept. Sort order is *not* commutative and is preserved as sent.
|
|
461
|
+
|
|
462
|
+
Repeated children are kept rather than de-duplicated, for the reason §5 compares multisets:
|
|
463
|
+
`tier == "a" OR tier == "b"` is a two-disjunct query whose shape is `OR(tier:EQUAL|tier:EQUAL)`, and
|
|
464
|
+
collapsing it to one disjunct would describe a query that was never issued.
|
|
465
|
+
|
|
466
|
+
### File shape
|
|
467
|
+
|
|
468
|
+
```jsonc
|
|
469
|
+
{
|
|
470
|
+
"corpusVersion": 1,
|
|
471
|
+
"queries": [
|
|
472
|
+
{
|
|
473
|
+
"key": "orders::COLLECTION::AND(status:EQUAL)::createdAt:DESCENDING",
|
|
474
|
+
"collectionGroup": "orders",
|
|
475
|
+
"queryScope": "COLLECTION",
|
|
476
|
+
"where": {
|
|
477
|
+
"op": "AND",
|
|
478
|
+
"filters": [
|
|
479
|
+
{ "fieldPath": "status", "op": "EQUAL" }
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
"orderBy": [
|
|
483
|
+
{ "fieldPath": "createdAt", "direction": "DESCENDING" }
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
],
|
|
487
|
+
"skipped": ["aggregation-query"]
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
A node carrying `filters` is a composite; a node carrying `fieldPath` is a leaf; no node carries
|
|
492
|
+
both, and none carries neither. Every field is always present, as in §6: `where` is a composite even
|
|
493
|
+
when the query filtered nothing, `orderBy` is `[]` rather than omitted, and `skipped` is `[]` on a
|
|
494
|
+
run that discarded nothing. `queries` is sorted by `key` and `skipped` holds the distinct reasons
|
|
495
|
+
observed, sorted ascending — a set, so that it is as diff-stable as the rest of the file. Both sorts
|
|
496
|
+
compare by Unicode code point. Counts for each reason go to stderr.
|
|
497
|
+
|
|
498
|
+
The stored `where` is the normalised tree the key was computed from, not the tree as it arrived:
|
|
499
|
+
children sorted, same-operator composites flattened, exactly as *Canonical query key* describes. Two
|
|
500
|
+
recorders that observe the same query in different spellings therefore write the same bytes, which
|
|
501
|
+
they would not if the key were normalised and the tree left as sent — the entry that happened to
|
|
502
|
+
win de-duplication would decide the file.
|
|
503
|
+
|
|
504
|
+
`key` is derived from the four fields beneath it and carries nothing they do not: it is there so
|
|
505
|
+
that entries can be sorted, de-duplicated, and cited in a review without re-serialising the tree. It
|
|
506
|
+
is unique within `queries`. A reader handed an entry that breaks any of this — a `key` disagreeing
|
|
507
|
+
with its own tree, a node that is both leaf and composite, a member the format does not define —
|
|
508
|
+
refuses the file rather than repairing it, because the readings it would be choosing between are
|
|
509
|
+
different queries and picking one is how a corpus comes to describe coverage it never had.
|
|
510
|
+
|
|
511
|
+
`corpusVersion` is an integer that names the format, not the tool: it changes only when a corpus
|
|
512
|
+
written by one version can no longer be read correctly by another, and it does not move when
|
|
513
|
+
`@indexwright/record` is released. A reader handed a `corpusVersion` it does not know refuses the
|
|
514
|
+
file and says so. It does not fall back to reading what it recognises, which would silently mis-read
|
|
515
|
+
exactly the change the integer exists to announce.
|
|
516
|
+
|
|
517
|
+
### Implicit fields are not materialised
|
|
518
|
+
|
|
519
|
+
Firestore appends the document key to every query's sort order, and promotes an inequality field
|
|
520
|
+
into it. `record` neither adds those fields nor removes them: the corpus records the sort order as
|
|
521
|
+
sent.
|
|
522
|
+
|
|
523
|
+
What is sent is not the same for every client. The Node server SDK serialises the orders the
|
|
524
|
+
application wrote and leaves the rest to the server; the Firebase Web SDK normalises first, so a
|
|
525
|
+
`where('age', '>', 18)` reaches the wire already carrying `age ASCENDING, __name__ ASCENDING`. One
|
|
526
|
+
application query therefore has two legitimate corpus shapes depending on which SDK issued it, and
|
|
527
|
+
a corpus is comparable across runs of one project rather than across projects. Recording what was
|
|
528
|
+
sent is what keeps that difference visible; normalising in either direction would hide it behind a
|
|
529
|
+
rule this specification would have had to invent.
|
|
530
|
+
|
|
531
|
+
This is deliberate. §5 has to define the implicit `__name__` direction because a linter comparing
|
|
532
|
+
two declarations has no other way to tell whether they name the same resource. A corpus has no such
|
|
533
|
+
need: the v0.3 oracle is Firestore itself, which applies the real rule. Materialising a guessed
|
|
534
|
+
`__name__` into the corpus would put an unpublished behaviour into a durable file and make every
|
|
535
|
+
entry wrong if the guess were wrong. When a human reads a corpus entry against an index key, §5's
|
|
536
|
+
definition is the one that applies.
|
|
537
|
+
|
|
538
|
+
### Replay without values
|
|
539
|
+
|
|
540
|
+
v0.3 must turn a corpus entry back into a query it can execute, and the entry has no values to put
|
|
541
|
+
back. It synthesises them: a unary filter needs none, `IN`, `NOT_IN`, and `ARRAY_CONTAINS_ANY` need
|
|
542
|
+
a one-element array, and everything else needs a single scalar. "Everything else" is a finite list
|
|
543
|
+
rather than a default, because the vocabulary above enumerates the operators a corpus may hold and
|
|
544
|
+
anything outside it was skipped at capture as `unsupported-shape`.
|
|
545
|
+
|
|
546
|
+
Two shapes are not free-form and cannot be synthesised from the same rule. A `__name__` filter takes
|
|
547
|
+
a document reference under the collection being queried, not a scalar, because Firestore validates
|
|
548
|
+
the operand's type against the document key before it selects an index. And an entry whose root
|
|
549
|
+
composite has no children replays with `where` omitted altogether, not as an empty `AND`: a wire
|
|
550
|
+
`CompositeFilter` must carry at least one filter. Both would otherwise fail with `INVALID_ARGUMENT`,
|
|
551
|
+
and an `INVALID_ARGUMENT` is not a `FAILED_PRECONDITION`. v0.3 reports the latter and never the
|
|
552
|
+
former: a replay that comes back invalid is either a synthesis this section got wrong or a query
|
|
553
|
+
that was already invalid when it was captured — the corpus admits those, since it records what was
|
|
554
|
+
sent rather than what succeeded — and neither is a statement about the index set. Both are reported
|
|
555
|
+
as un-replayable entries, which is a defect in the tooling or the test that issued them.
|
|
556
|
+
|
|
557
|
+
This rests on the claim above — that index selection does not depend on the compared value or its
|
|
558
|
+
type. The claim is consistent with how the field is indexed rather than the value, but it is not
|
|
559
|
+
published, and it is the one assumption in v0.3 that a synthesised replay could get wrong. If it is
|
|
560
|
+
false, replay reports `FAILED_PRECONDITION` where a real query would have succeeded, which is a
|
|
561
|
+
false positive of exactly the kind §2 forbids acting on. v0.3 must test the claim before it reports.
|
|
562
|
+
|
|
563
|
+
### What is not captured
|
|
564
|
+
|
|
565
|
+
`record` captures `RunQuery`. Everything else the proxy sees, it counts under one of the reasons
|
|
566
|
+
below and records nothing:
|
|
567
|
+
|
|
568
|
+
- **`Listen`** — a snapshot listener carries its query in `Target.QueryTarget.structured_query` and
|
|
569
|
+
issues no `RunQuery` at all. Its index requirements are exactly those of the query it holds, so
|
|
570
|
+
this is the one omission that costs coverage rather than preventing a misreport: a suite whose
|
|
571
|
+
only exercise of a collection is `onSnapshot` yields a corpus with no entry for it. Counted as
|
|
572
|
+
`listen-query`, and named in §3 as a gap rather than an inherent limit. Capturing it is the first
|
|
573
|
+
extension worth making.
|
|
574
|
+
- **`PartitionQuery`** — carries a `StructuredQuery` the same way, but as a bulk-read entry point
|
|
575
|
+
rather than an application query. Counted as `partition-query`.
|
|
576
|
+
- **`RunAggregationQuery`** — `count()`, `sum()`, and `average()` carry a `StructuredQuery` and have
|
|
577
|
+
their own index requirements, which are not necessarily those of the underlying query. Recording
|
|
578
|
+
the inner query would misreport them, so v0.2 counts them as `aggregation-query`. Capturing them
|
|
579
|
+
properly is a v0.3-or-later extension.
|
|
580
|
+
- **`find_nearest`** — vector search is counted as `vector-query` for the same reason: it is served
|
|
581
|
+
by a `vectorConfig` index whose matching rule this specification does not yet model.
|
|
582
|
+
- **A shape the vocabulary cannot express** — an unrecognised or unspecified enum value, or a `from`
|
|
583
|
+
that does not name exactly one collection. Counted as `unsupported-shape`.
|
|
584
|
+
- **A query-bearing RPC this specification does not model** — `ExecutePipeline` carries a
|
|
585
|
+
`StructuredPipeline` rather than a `StructuredQuery`, and pipelines are outside v0.2. Counted as
|
|
586
|
+
`unsupported-rpc`, so that `skipped: []` keeps meaning *nothing was declined* rather than *nothing
|
|
587
|
+
the proxy happened to recognise was declined*. The same reason covers any other method on the
|
|
588
|
+
Firestore service that is neither captured above nor on the list of methods known to carry no
|
|
589
|
+
query at all: a method added after this was written is counted, not assumed harmless.
|
|
590
|
+
- **A message compressed with an encoding `record` cannot undo** — gRPC marks compression per
|
|
591
|
+
message and names the codec in `grpc-encoding`. `gzip` and `deflate` are undone and the message
|
|
592
|
+
read; anything else is counted as `unsupported-encoding`. A client that negotiates a codec this
|
|
593
|
+
package does not implement would otherwise have every query it sent vanish without trace.
|
|
594
|
+
- **Bytes that do not parse** — a truncated frame, a body that ends mid-message, or a payload that
|
|
595
|
+
is not the message the method declares. Counted as `undecodable-message`. This is the one reason
|
|
596
|
+
that indicates a defect rather than a boundary: it should not occur against a conforming client,
|
|
597
|
+
and a corpus carrying it is reporting that something read the wire wrongly.
|
|
598
|
+
|
|
599
|
+
`skipped` draws from that closed vocabulary and nothing else. No text decoded from the wire is ever
|
|
600
|
+
interpolated into a reason: the corpus is committed, and a free-text field fed by intercepted
|
|
601
|
+
traffic is a second way for data to reach the repository after values have been stripped from the
|
|
602
|
+
first.
|
|
603
|
+
|
|
604
|
+
They are reported rather than dropped silently. §3's known limit — that coverage is bounded by what
|
|
605
|
+
exercises the proxy — is about queries no test issues; a query that *was* issued and then discarded
|
|
606
|
+
without trace would be a different and worse failure, because it would look like coverage.
|
|
607
|
+
|
|
608
|
+
## 8. Design principles
|
|
323
609
|
|
|
324
610
|
**Warn, do not fail.** The rules encode heuristics whose false-positive rates are, at v0.1.0,
|
|
325
611
|
unmeasured. Shipping them as blocking checks would teach users to suppress the tool. Enforcement is
|
|
@@ -335,10 +621,14 @@ an aspiration of the project: it holds in every version, and a test asserts it.
|
|
|
335
621
|
|
|
336
622
|
The principle is about what lands in an adopter's tree, so build- and test-time tooling that never
|
|
337
623
|
ships is out of its scope. It is not a claim that no part of indexwright may depend on anything.
|
|
338
|
-
Where a verb needs a library it cannot reasonably write — the
|
|
339
|
-
that verb ships as its own package instead of as a dependency of the linter. What the principle
|
|
624
|
+
Where a verb needs a library it cannot reasonably write — the Firestore client behind `check` (§3)
|
|
625
|
+
— that verb ships as its own package instead of as a dependency of the linter. What the principle
|
|
340
626
|
forbids is making every adopter of `lint` pay for it.
|
|
341
627
|
|
|
628
|
+
`@indexwright/record` holds the line further than the principle requires: at v0.2 it declares no
|
|
629
|
+
runtime dependency either, because capture turned out to need none (§3). That is not a promise it
|
|
630
|
+
makes for every version, and `check` will end it.
|
|
631
|
+
|
|
342
632
|
**No network, no credentials, in `lint`.** Static analysis must be runnable in any environment,
|
|
343
633
|
including a sandboxed CI step with no cloud access. Network use is confined to the planned
|
|
344
634
|
`record`/`check` verbs, which are separate commands in a separate package (§3).
|
|
@@ -350,7 +640,7 @@ claim, which is the point.
|
|
|
350
640
|
**Deterministic output.** Findings are emitted in a stable sort order (file, rule, key), so that
|
|
351
641
|
output can be diffed across runs.
|
|
352
642
|
|
|
353
|
-
##
|
|
643
|
+
## 9. Testing
|
|
354
644
|
|
|
355
645
|
Rules are tested against small hand-written fixtures that isolate one condition each, with explicit
|
|
356
646
|
positive and negative cases.
|
|
@@ -360,7 +650,7 @@ count over live data fails whenever that data legitimately changes, which trains
|
|
|
360
650
|
edit the test rather than read it. Fixtures encode the invariant; real files are for manual
|
|
361
651
|
exploration only.
|
|
362
652
|
|
|
363
|
-
##
|
|
653
|
+
## 10. Compatibility
|
|
364
654
|
|
|
365
655
|
- Node.js ≥ 22, ESM.
|
|
366
656
|
- Input schema follows the Firebase CLI's `firestore.indexes.json`. Unknown keys are preserved and
|
|
@@ -370,12 +660,19 @@ exploration only.
|
|
|
370
660
|
- The package also exports a JavaScript API, so the rules can be run without spawning a process.
|
|
371
661
|
That API is **provisional**: it is not part of the stable contract before 1.0 and may change in
|
|
372
662
|
any minor release. Only the `json` output shape carries the compatibility promise.
|
|
373
|
-
- indexwright is published as a family: `indexwright`, the linter,
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
663
|
+
- indexwright is published as a family: `indexwright`, the linter, and — from v0.2 —
|
|
664
|
+
`@indexwright/record`, capture and coverage. Both carry no runtime dependencies at v0.2; `check`
|
|
665
|
+
will give the second one a Firestore client (§3). They version independently, and their version
|
|
666
|
+
numbers are not held in step: `@indexwright/record` 0.2.0 and `indexwright` 0.2.0 coincide only
|
|
667
|
+
because the family reached v0.2 together. When `check` lands and declares a range of
|
|
668
|
+
`indexwright` whose `json` contract it reads, that range is what ties them, not the numbers.
|
|
669
|
+
- The query corpus (§7) is the second stable contract, and the only one that crosses a package
|
|
670
|
+
boundary as a file. It is versioned by its own `corpusVersion` rather than by either package's
|
|
671
|
+
release number, so a writer and a reader agree on the integer and not on each other's versions.
|
|
672
|
+
Everything else in `@indexwright/record` is provisional before 1.0 on the same terms as the
|
|
673
|
+
JavaScript API above.
|
|
674
|
+
|
|
675
|
+
## 11. Toward 1.0
|
|
379
676
|
|
|
380
677
|
1.0 requires, at minimum:
|
|
381
678
|
|
package/dist/args.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type Command = LintCommand | {
|
|
|
18
18
|
kind: 'version';
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* Parsed in-tree rather than with a dependency (SPEC §
|
|
21
|
+
* Parsed in-tree rather than with a dependency (SPEC §8). Supports `--flag value` and
|
|
22
22
|
* `--flag=value`, and `--` to end option parsing.
|
|
23
23
|
*/
|
|
24
24
|
export declare function parseArgs(argv: readonly string[]): Command;
|
package/dist/args.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEvD,6EAA6E;AAC7E,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,gBAAgB;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEvD,6EAA6E;AAC7E,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,gBAAgB;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAoB3E;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA2F1D;AA+DD,wBAAgB,KAAK,IAAI,MAAM,CA6B9B"}
|
package/dist/args.js
CHANGED
|
@@ -7,7 +7,17 @@ export class UsageError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
const FORMATS = ['text', 'json', 'github'];
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Verbs that belong to the family but not to this package.
|
|
11
|
+
*
|
|
12
|
+
* `indexwright` carries no runtime dependency in any version (SPEC §8), so capture — which needs a
|
|
13
|
+
* proxy and a wire decoder — ships separately. Only verbs that exist are listed: pointing at a
|
|
14
|
+
* package that does not implement one yet would be a worse answer than not knowing.
|
|
15
|
+
*/
|
|
16
|
+
const ELSEWHERE = {
|
|
17
|
+
record: { home: '@indexwright/record', bin: 'indexwright-record' },
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parsed in-tree rather than with a dependency (SPEC §8). Supports `--flag value` and
|
|
11
21
|
* `--flag=value`, and `--` to end option parsing.
|
|
12
22
|
*/
|
|
13
23
|
export function parseArgs(argv) {
|
|
@@ -19,6 +29,18 @@ export function parseArgs(argv) {
|
|
|
19
29
|
return { kind: 'version' };
|
|
20
30
|
const [command, ...rest] = argv;
|
|
21
31
|
if (command !== 'lint') {
|
|
32
|
+
// SPEC §3: the cost of splitting the family into two packages is a second package to
|
|
33
|
+
// discover, so a verb that lives in the other one has to say where it went. Reporting it as
|
|
34
|
+
// an unknown command would read as "indexwright cannot do this".
|
|
35
|
+
// An own-property lookup, not `in`: `ELSEWHERE` inherits from `Object.prototype`, so `in`
|
|
36
|
+
// also answers yes for `constructor` and `toString` and the message would then name the
|
|
37
|
+
// package as "undefined" instead of reporting an unknown command.
|
|
38
|
+
const elsewhere = Object.hasOwn(ELSEWHERE, command ?? '') ? ELSEWHERE[command] : undefined;
|
|
39
|
+
if (elsewhere !== undefined) {
|
|
40
|
+
const { home, bin } = elsewhere;
|
|
41
|
+
throw new UsageError(`"${command}" is not part of indexwright; it ships as ${home}. ` +
|
|
42
|
+
`Install it with "npm install --save-dev ${home}" and run "${bin}".`);
|
|
43
|
+
}
|
|
22
44
|
throw new UsageError(`unknown command "${command}"; the only command is "lint"`);
|
|
23
45
|
}
|
|
24
46
|
const files = [];
|
|
@@ -165,6 +187,9 @@ export function usage() {
|
|
|
165
187
|
' 0 completed; warnings may have been emitted',
|
|
166
188
|
' 1 warning count exceeded --max-warnings',
|
|
167
189
|
' 2 usage error, unreadable file, or malformed input',
|
|
190
|
+
'',
|
|
191
|
+
'Capturing the queries a test suite issues is a separate package, @indexwright/record,',
|
|
192
|
+
'so that this one keeps no runtime dependencies.',
|
|
168
193
|
].join('\n');
|
|
169
194
|
}
|
|
170
195
|
//# sourceMappingURL=args.js.map
|
package/dist/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,6EAA6E;AAC7E,MAAM,OAAO,UAAW,SAAQ,KAAK;IACjB,IAAI,GAAG,YAAY,CAAC;CACvC;AAcD,MAAM,OAAO,GAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
|
package/dist/collections.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §
|
|
1
|
+
/** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §8). */
|
|
2
2
|
export declare function compareStrings(a: string, b: string): number;
|
|
3
3
|
/** Group while remembering first-seen order, so callers can sort explicitly rather than by luck. */
|
|
4
4
|
export declare function groupBy<T>(items: Iterable<T>, keyOf: (item: T) => string): Map<string, T[]>;
|
package/dist/collections.js
CHANGED
package/dist/format/github.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface GithubOutput {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Findings are annotations without a line number: locating a finding inside the JSON would need a
|
|
10
|
-
* position-tracking parser, which v0.1.0 does not carry (SPEC §
|
|
10
|
+
* position-tracking parser, which v0.1.0 does not carry (SPEC §5).
|
|
11
11
|
*/
|
|
12
12
|
export declare function formatGithub(result: LintResult): GithubOutput;
|
|
13
13
|
//# sourceMappingURL=github.d.ts.map
|
package/dist/format/github.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { oneLine } from './inline.js';
|
|
2
2
|
/**
|
|
3
3
|
* Findings are annotations without a line number: locating a finding inside the JSON would need a
|
|
4
|
-
* position-tracking parser, which v0.1.0 does not carry (SPEC §
|
|
4
|
+
* position-tracking parser, which v0.1.0 does not carry (SPEC §5).
|
|
5
5
|
*/
|
|
6
6
|
export function formatGithub(result) {
|
|
7
7
|
const commands = [
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The JavaScript API.
|
|
3
3
|
*
|
|
4
|
-
* Provisional before 1.0 (SPEC §
|
|
4
|
+
* Provisional before 1.0 (SPEC §10): only the `json` output shape carries the compatibility promise.
|
|
5
5
|
* This exists so the rules can be run without spawning a process — from a test, a codemod, or a
|
|
6
6
|
* bespoke reporter.
|
|
7
7
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The JavaScript API.
|
|
3
3
|
*
|
|
4
|
-
* Provisional before 1.0 (SPEC §
|
|
4
|
+
* Provisional before 1.0 (SPEC §10): only the `json` output shape carries the compatibility promise.
|
|
5
5
|
* This exists so the rules can be run without spawning a process — from a test, a codemod, or a
|
|
6
6
|
* bespoke reporter.
|
|
7
7
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The input types mirror `firestore.indexes.json` loosely on purpose: unknown keys are carried
|
|
5
5
|
* through rather than rejected, so a field added by a future Firebase release does not break
|
|
6
|
-
* linting (SPEC §
|
|
6
|
+
* linting (SPEC §10).
|
|
7
7
|
*/
|
|
8
8
|
/** One entry of an index's `fields` array. */
|
|
9
9
|
export interface IndexField {
|
|
@@ -54,7 +54,7 @@ export interface AnalysedIndex {
|
|
|
54
54
|
}
|
|
55
55
|
export declare const RULE_IDS: readonly ["scope-mismatch", "field-order-variant", "explicit-name-field", "quota-headroom"];
|
|
56
56
|
export type RuleId = (typeof RULE_IDS)[number];
|
|
57
|
-
/** A single warning. Never an error: see SPEC §
|
|
57
|
+
/** A single warning. Never an error: see SPEC §8. */
|
|
58
58
|
export interface Finding {
|
|
59
59
|
rule: RuleId;
|
|
60
60
|
file: string;
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The input types mirror `firestore.indexes.json` loosely on purpose: unknown keys are carried
|
|
5
5
|
* through rather than rejected, so a field added by a future Firebase release does not break
|
|
6
|
-
* linting (SPEC §
|
|
6
|
+
* linting (SPEC §10).
|
|
7
7
|
*/
|
|
8
8
|
export const RULE_IDS = [
|
|
9
9
|
'scope-mismatch',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indexwright",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Linter for Firestore composite index declarations (firestore.indexes.json)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"firestore",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"license": "Apache-2.0",
|
|
20
20
|
"author": "Yuki Nagai",
|
|
21
21
|
"type": "module",
|
|
22
|
+
"workspaces": [
|
|
23
|
+
"packages/*"
|
|
24
|
+
],
|
|
22
25
|
"bin": {
|
|
23
26
|
"indexwright": "dist/cli.js"
|
|
24
27
|
},
|
|
@@ -41,10 +44,10 @@
|
|
|
41
44
|
"node": ">=22"
|
|
42
45
|
},
|
|
43
46
|
"scripts": {
|
|
44
|
-
"build": "tsc --build --force",
|
|
45
|
-
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "npm run build && node --test \"test/*.test.js\"",
|
|
47
|
-
"verify-package": "npm run build && node scripts/verify-package.mjs",
|
|
47
|
+
"build": "tsc --build --force tsconfig.json packages/record/tsconfig.json",
|
|
48
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p packages/record/tsconfig.json",
|
|
49
|
+
"test": "npm run build && node --test \"test/*.test.js\" \"packages/*/test/*.test.js\"",
|
|
50
|
+
"verify-package": "npm run build && node scripts/verify-package.mjs && node scripts/verify-record-package.mjs",
|
|
48
51
|
"prepublishOnly": "npm run build"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|