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 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.1.1] — unreleased
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
- v0.1.0 is static analysis of declaration files only: no network access, no credentials, no Firestore
147
- connection.
148
-
149
- Planned, and deliberately split:
150
-
151
- - **v0.2 query capture.** The emulator speaks the Firestore v1 gRPC API in plaintext on a local
152
- port, so an intercepting proxy can record observed `StructuredQuery` shapes. This is language- and
153
- framework-independent because it operates on the wire protocol.
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 will be bounded by what actually exercises the proxy. A query no test issues is not
159
- observed, and absence of observation is not evidence that an index is unused. That limit is
160
- inherent.
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 the suite against dist/
175
- npm run verify-package # builds, packs, installs the tarball, exercises the bin and the API
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.1.0 (draft)**. Pre-1.0: rules and CLI surface may change between minor versions.
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- and framework-independent, because it operates on the wire
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.** Capture needs a gRPC stack `@grpc/grpc-js` and protobuf definitions
75
- for the Firestore v1 API and hand-writing a decoder for a wire format owned by someone else is
76
- not a cost worth paying. That collides with §7: `record` and `check` run inside an adopter's
77
- project, so their dependencies land in an adopter's tree, and the build-time carve-out does not
78
- reach them.
79
-
80
- They therefore ship as a separate package, `@indexwright/record`, which depends on `indexwright`
81
- for the index model and the `json` contract. **`indexwright` itself acquires no runtime dependency,
82
- in any version.**
83
-
84
- The split is not a workaround; it puts each cost where it is cheapest. `lint` runs on every push,
85
- in every CI job, in projects that may never touch Firestore from a server — that is where a
86
- transitive dependency tree is least welcome. `record` runs against a local emulator, in a project
87
- that is already talking to Firestore server-side and therefore already resolves `@grpc/grpc-js`
88
- transitively through `@google-cloud/firestore`. The dependency is added where it is very likely
89
- already resolved, and is absent where it would be new.
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
- This limit is inherent, not an implementation gap.
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. Design principles
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 gRPC stack behind `record` (§3)
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
- ## 8. Testing
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
- ## 9. Compatibility
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, which carries no runtime
374
- dependencies, and — from v0.2 — `@indexwright/record`, capture and coverage, which depends on the
375
- linter and on a gRPC stack (§3). They version independently; `@indexwright/record` declares the
376
- range of `indexwright` whose `json` contract it reads.
377
-
378
- ## 10. Toward 1.0
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 §7). Supports `--flag value` and
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;
@@ -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;AAI3E;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6E1D;AA+DD,wBAAgB,KAAK,IAAI,MAAM,CA0B9B"}
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
- * Parsed in-tree rather than with a dependency (SPEC §7). Supports `--flag value` and
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":"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"}
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;AAOpE;;;;;;GAMG;AACH,MAAM,SAAS,GAA2C;IACxD,MAAM,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE,GAAG,EAAE,oBAAoB,EAAE;CACnE,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAuB;IAC/C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,kBAAkB,CAAC,CAAC;IAChE,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC5E,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAE3D,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,qFAAqF;QACrF,4FAA4F;QAC5F,iEAAiE;QACjE,0FAA0F;QAC1F,wFAAwF;QACxF,kEAAkE;QAClE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,OAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrG,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC;YAChC,MAAM,IAAI,UAAU,CAClB,IAAI,OAAO,6CAA6C,IAAI,IAAI;gBAC9D,2CAA2C,IAAI,cAAc,GAAG,IAAI,CACvE,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,UAAU,CAAC,oBAAoB,OAAO,+BAA+B,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,MAAM,GAAiB,MAAM,CAAC;IAClC,IAAI,WAAW,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC3C,IAAI,KAAK,GAAG,aAAa,CAAC;IAC1B,IAAI,cAAc,GAAG,uBAAuB,CAAC;IAC7C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;QAEnC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAClE,MAAM,WAAW,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACtE,MAAM,SAAS,GAAG,GAAW,EAAE;YAC7B,IAAI,WAAW,KAAK,IAAI;gBAAE,OAAO,WAAW,CAAC;YAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACzB,IAAI,IAAI,KAAK,SAAS;gBAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,gBAAgB,CAAC,CAAC;YACtE,CAAC,IAAI,CAAC,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QAEF,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,UAAU;gBACb,MAAM,GAAG,WAAW,CAAC,SAAS,EAAE,CAAC,CAAC;gBAClC,MAAM;YACR,KAAK,gBAAgB;gBACnB,WAAW,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC;gBAC5C,MAAM;YACR,KAAK,QAAQ;gBACX,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;gBAC9C,MAAM;YACR,KAAK,WAAW;gBACd,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC;gBAC9C,MAAM;YACR,KAAK,SAAS;gBACZ,KAAK,GAAG,oBAAoB,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC;gBAChD,MAAM;YACR,KAAK,mBAAmB;gBACtB,cAAc,GAAG,aAAa,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,CAAC;gBAClD,MAAM;YACR;gBACE,MAAM,IAAI,UAAU,CAAC,mBAAmB,IAAI,GAAG,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,sBAAsB,CAAC,CAAC;IAErE,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,mFAAmF;QACnF,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1B,MAAM;QACN,WAAW;QACX,KAAK,EAAE,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC;QACvC,KAAK;QACL,cAAc;KACf,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,QAA2B,EAAE,QAA2B;IAC5E,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAS,QAAQ,CAAC,CAAC;IACjF,KAAK,MAAM,EAAE,IAAI,QAAQ;QAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,4CAA4C,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAqB,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,UAAU,CAAC,mBAAmB,KAAK,sBAAsB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,KAAqB,CAAC;AAC/B,CAAC;AAED,4FAA4F;AAC5F,SAAS,WAAW,CAAC,KAAa,EAAE,MAAc;IAChD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,UAAU,CAAC,iBAAiB,KAAK,SAAS,MAAM,qBAAqB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,0FAA0F;AAC1F,MAAM,OAAO,GAAG,yBAAyB,CAAC;AAE1C;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;AAC1D,CAAC;AAED,SAAS,UAAU,CAAC,KAAa,EAAE,MAAc;IAC/C,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,UAAU,CAAC,GAAG,MAAM,uCAAuC,KAAK,GAAG,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa,EAAE,MAAc;IACzD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,UAAU,CAAC,GAAG,MAAM,mCAAmC,KAAK,GAAG,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,KAAa,EAAE,MAAc;IAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,UAAU,CAAC,GAAG,MAAM,mCAAmC,KAAK,GAAG,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,KAAK;IACnB,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACpF,OAAO;QACL,sCAAsC;QACtC,EAAE;QACF,wFAAwF;QACxF,iEAAiE;QACjE,EAAE;QACF,UAAU;QACV,wDAAwD;QACxD,0EAA0E;QAC1E,6DAA6D;QAC7D,yDAAyD;QACzD,wEAAwE,aAAa,GAAG;QACxF,2EAA2E,uBAAuB,GAAG;QACrG,2CAA2C;QAC3C,0CAA0C;QAC1C,EAAE;QACF,QAAQ;QACR,GAAG,SAAS;QACZ,EAAE;QACF,aAAa;QACb,gDAAgD;QAChD,4CAA4C;QAC5C,uDAAuD;QACvD,EAAE;QACF,uFAAuF;QACvF,iDAAiD;KAClD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
@@ -1,4 +1,4 @@
1
- /** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §7). */
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[]>;
@@ -1,4 +1,4 @@
1
- /** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §7). */
1
+ /** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §8). */
2
2
  export function compareStrings(a, b) {
3
3
  if (a < b)
4
4
  return -1;
@@ -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 §4).
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
@@ -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).
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 §9): only the `json` output shape carries the compatibility promise.
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 §9): only the `json` output shape carries the compatibility promise.
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 §9).
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 §7. */
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 §9).
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.1.1",
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": {