indexwright 0.1.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 +22 -0
- package/LICENSE +201 -0
- package/README.md +184 -0
- package/SPEC.md +386 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +170 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +96 -0
- package/dist/cli.js.map +1 -0
- package/dist/collections.d.ts +6 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +25 -0
- package/dist/collections.js.map +1 -0
- package/dist/format/github.d.ts +13 -0
- package/dist/format/github.d.ts.map +1 -0
- package/dist/format/github.js +63 -0
- package/dist/format/github.js.map +1 -0
- package/dist/format/inline.d.ts +11 -0
- package/dist/format/inline.d.ts.map +1 -0
- package/dist/format/inline.js +15 -0
- package/dist/format/inline.js.map +1 -0
- package/dist/format/json.d.ts +7 -0
- package/dist/format/json.d.ts.map +1 -0
- package/dist/format/json.js +25 -0
- package/dist/format/json.js.map +1 -0
- package/dist/format/text.d.ts +3 -0
- package/dist/format/text.d.ts.map +1 -0
- package/dist/format/text.js +65 -0
- package/dist/format/text.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/key.d.ts +35 -0
- package/dist/key.d.ts.map +1 -0
- package/dist/key.js +76 -0
- package/dist/key.js.map +1 -0
- package/dist/lint.d.ts +22 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +110 -0
- package/dist/lint.js.map +1 -0
- package/dist/parse.d.ts +11 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +88 -0
- package/dist/parse.js.map +1 -0
- package/dist/rules/explicit-name-field.d.ts +13 -0
- package/dist/rules/explicit-name-field.d.ts.map +1 -0
- package/dist/rules/explicit-name-field.js +35 -0
- package/dist/rules/explicit-name-field.js.map +1 -0
- package/dist/rules/field-order-variant.d.ts +10 -0
- package/dist/rules/field-order-variant.d.ts.map +1 -0
- package/dist/rules/field-order-variant.js +60 -0
- package/dist/rules/field-order-variant.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +25 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quota-headroom.d.ts +10 -0
- package/dist/rules/quota-headroom.d.ts.map +1 -0
- package/dist/rules/quota-headroom.js +40 -0
- package/dist/rules/quota-headroom.js.map +1 -0
- package/dist/rules/scope-mismatch.d.ts +10 -0
- package/dist/rules/scope-mismatch.d.ts.map +1 -0
- package/dist/rules/scope-mismatch.js +45 -0
- package/dist/rules/scope-mismatch.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +54 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# indexwright — Specification
|
|
2
|
+
|
|
3
|
+
Linter and query-coverage checker for Firestore composite indexes.
|
|
4
|
+
|
|
5
|
+
Status: **v0.1.0 (draft)**. Pre-1.0: rules and CLI surface may change between minor versions.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Why this exists
|
|
10
|
+
|
|
11
|
+
Firestore composite indexes are declared as data (`firestore.indexes.json`) and applied as
|
|
12
|
+
whole-state. Three properties of the platform make that declaration hard to get right, and none of
|
|
13
|
+
them are addressed by existing tooling.
|
|
14
|
+
|
|
15
|
+
**The emulator does not enforce composite indexes.** The Firestore emulator "does not track
|
|
16
|
+
composite indexes and instead executes any valid query." A query that will fail in production with
|
|
17
|
+
`FAILED_PRECONDITION` passes locally. Strict indexing in the emulator is not a planned feature.
|
|
18
|
+
|
|
19
|
+
**Firestore exposes no per-index usage metrics.** Index entry reads are billed but do not appear in
|
|
20
|
+
the usage dashboard, and there is no index-level metric. You cannot ask the platform which of your
|
|
21
|
+
indexes are actually used, so you cannot safely identify unused ones.
|
|
22
|
+
|
|
23
|
+
**The index-matching rule is undocumented.** Google's documentation states that field ordering must
|
|
24
|
+
be specified, but does not define how field order maps to query shape — in particular whether the
|
|
25
|
+
relative order of equality and `array-contains` fields before the range fields affects matching.
|
|
26
|
+
This means index changes cannot be validated against documentation; only empirically.
|
|
27
|
+
|
|
28
|
+
The asymmetry is stark: security rules have an official test harness
|
|
29
|
+
(`@firebase/rules-unit-testing`) and can be verified in the emulator. **Composite indexes have no
|
|
30
|
+
equivalent.** A survey of GitHub and npm found no linter for `firestore.indexes.json`; the only
|
|
31
|
+
adjacent projects are three abandoned index *generators* (0–6 stars, last pushed 2021–2025).
|
|
32
|
+
|
|
33
|
+
Consequently, index declarations drift into states that are detectable by inspection but that
|
|
34
|
+
nothing inspects: inconsistent `queryScope` within a collection, near-duplicate indexes that differ
|
|
35
|
+
only in field order, artifacts of round-tripping through live exports, and silent approach to the
|
|
36
|
+
per-database index quota.
|
|
37
|
+
|
|
38
|
+
## 2. What this is not
|
|
39
|
+
|
|
40
|
+
**indexwright cannot determine whether an index is needed.** That question requires the queries,
|
|
41
|
+
which live in application code, not in the index declaration. No rule in this specification asserts
|
|
42
|
+
that an index is unnecessary, and no output should be read as authorising a deletion.
|
|
43
|
+
|
|
44
|
+
Deleting a composite index is not symmetric with adding one: an addition does not disturb existing
|
|
45
|
+
queries while it backfills, but a deletion breaks its queries the moment it takes effect, and
|
|
46
|
+
restoring it requires a fresh backfill. **A linter that only sees the declaration must never be the
|
|
47
|
+
basis for a deletion.**
|
|
48
|
+
|
|
49
|
+
What indexwright asserts is narrower and safer: *these declarations are structurally inconsistent
|
|
50
|
+
with each other, and a human should look.*
|
|
51
|
+
|
|
52
|
+
## 3. Scope
|
|
53
|
+
|
|
54
|
+
### v0.1.0
|
|
55
|
+
|
|
56
|
+
Static analysis of index declaration files. Four rules (§5). No network access, no credentials, no
|
|
57
|
+
Firestore connection.
|
|
58
|
+
|
|
59
|
+
### Planned
|
|
60
|
+
|
|
61
|
+
- **v0.2 — query capture.** The Firestore emulator speaks the public Firestore v1 gRPC API in
|
|
62
|
+
plaintext on a local port. An intercepting proxy can decode `RunQuery` requests and record the
|
|
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.
|
|
66
|
+
- **v0.3 — coverage check.** Replay a captured corpus against a throwaway Firestore database that
|
|
67
|
+
has the candidate index set applied, and report queries that fail with `FAILED_PRECONDITION`.
|
|
68
|
+
The oracle is Firestore itself; indexwright does not reimplement the undocumented matching rule.
|
|
69
|
+
|
|
70
|
+
The v0.2/v0.3 split is deliberate: capture is cheap and offline, while the coverage decision is
|
|
71
|
+
delegated to the platform. Reimplementing index matching would risk emitting false
|
|
72
|
+
`FAILED_PRECONDITION` verdicts and blocking development on a rule that is not published.
|
|
73
|
+
|
|
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.
|
|
90
|
+
|
|
91
|
+
The cost is a second package to discover. `indexwright record` in an installation that has only the
|
|
92
|
+
linter must say where the verb lives, not report an unknown command.
|
|
93
|
+
|
|
94
|
+
**Known limit of v0.2/v0.3:** coverage is bounded by what actually exercises the proxy. A query that
|
|
95
|
+
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.
|
|
97
|
+
|
|
98
|
+
## 4. CLI
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
indexwright lint <file...> [options]
|
|
102
|
+
|
|
103
|
+
Options:
|
|
104
|
+
--format <fmt> text (default) | json | github
|
|
105
|
+
--max-warnings <n> exit 1 if warnings exceed n (default: unlimited → always exit 0)
|
|
106
|
+
--rule <id> run only the given rule; repeatable
|
|
107
|
+
--disable <id> skip the given rule; repeatable
|
|
108
|
+
--quota <n> per-database composite index limit for R4 (default: 1000)
|
|
109
|
+
--quota-threshold <p> warn above this fraction of the limit (default: 0.8)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Exit codes
|
|
113
|
+
|
|
114
|
+
| Code | Meaning |
|
|
115
|
+
|-----:|:--------|
|
|
116
|
+
| 0 | Completed. Warnings may have been emitted (default policy). |
|
|
117
|
+
| 1 | Warning count exceeded `--max-warnings`. |
|
|
118
|
+
| 2 | Usage error, unreadable file, or malformed input. |
|
|
119
|
+
|
|
120
|
+
**The default is exit 0 even with findings.** A linter whose rules have unmeasured false-positive
|
|
121
|
+
rates must not gate a pipeline by default. Adopters opt into enforcement with `--max-warnings`
|
|
122
|
+
once they have measured their own noise level.
|
|
123
|
+
|
|
124
|
+
### Input
|
|
125
|
+
|
|
126
|
+
Any file in the `firestore.indexes.json` shape:
|
|
127
|
+
|
|
128
|
+
```jsonc
|
|
129
|
+
{
|
|
130
|
+
"indexes": [
|
|
131
|
+
{
|
|
132
|
+
"collectionGroup": "…",
|
|
133
|
+
"queryScope": "COLLECTION" | "COLLECTION_GROUP",
|
|
134
|
+
"fields": [ { "fieldPath": "…", "order": "ASCENDING" | "DESCENDING" }
|
|
135
|
+
| { "fieldPath": "…", "arrayConfig": "CONTAINS" }
|
|
136
|
+
| { "fieldPath": "…", "vectorConfig": { … } } ],
|
|
137
|
+
"density": "…" // optional, passed through
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"fieldOverrides": [ … ] // parsed, not analysed in v0.1.0
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Multiple files may be passed; each is analysed independently. Rules are not applied across files.
|
|
145
|
+
|
|
146
|
+
### Validation
|
|
147
|
+
|
|
148
|
+
A file is **malformed** when it is not valid JSON, when the top level is not an object, when
|
|
149
|
+
`indexes` is absent or is not an array, when an index lacks `collectionGroup`, `queryScope`, or a
|
|
150
|
+
non-empty `fields`, or when a field lacks `fieldPath` or declares none — or more than one — of
|
|
151
|
+
`order`, `arrayConfig`, and `vectorConfig`.
|
|
152
|
+
|
|
153
|
+
Nothing beyond that is refused. The *values* of `queryScope` and `order` are not checked against an
|
|
154
|
+
enumeration, unknown keys anywhere are ignored, and a declaration that repeats a `fieldPath` within
|
|
155
|
+
one index — which does occur in live exports — is carried through rather than rejected. Refusing to
|
|
156
|
+
analyse a file is the harshest outcome available to a tool that otherwise only warns, and it costs
|
|
157
|
+
the reader every other index in the file, so it is reserved for input that cannot be read as an
|
|
158
|
+
index declaration at all.
|
|
159
|
+
|
|
160
|
+
Malformed input does not abort the run. Each file is read and analysed on its own: files that parse
|
|
161
|
+
are linted and their findings reported, files that do not are reported as errors, and the process
|
|
162
|
+
exits 2 once every file has been handled. Reporting only the first bad file would hide findings in
|
|
163
|
+
the files that were fine.
|
|
164
|
+
|
|
165
|
+
## 5. Rules
|
|
166
|
+
|
|
167
|
+
Every rule emits **warnings**, never errors. Each finding carries: rule id, file, a canonical index
|
|
168
|
+
key, and a one-line reason. A finding that concerns the file as a whole rather than any particular
|
|
169
|
+
index carries a **null** key.
|
|
170
|
+
|
|
171
|
+
A rule whose subject is a *set* of indexes emits one grouped finding for that set, not one finding
|
|
172
|
+
per member. The finding's `key` is the lexicographically smallest member key and `related` holds the
|
|
173
|
+
remaining member keys, sorted ascending and de-duplicated. This keeps the warning count proportional
|
|
174
|
+
to the number of problems rather than to the number of indexes, which matters because
|
|
175
|
+
`--max-warnings` is counted in findings.
|
|
176
|
+
|
|
177
|
+
### Canonical index key
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
<collectionGroup>::<queryScope>::<fieldPath>:<direction>|<fieldPath>:<direction>|…
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
where `direction` is `ASCENDING`, `DESCENDING`, `CONTAINS`, or — for a `vectorConfig` field —
|
|
184
|
+
`VECTOR(<dimension>)`, written `VECTOR(?)` when no dimension is declared. A trailing implicit
|
|
185
|
+
`__name__` entry is stripped before forming the key, so that declarations that differ only in
|
|
186
|
+
whether the document key is written explicitly resolve to the same resource.
|
|
187
|
+
|
|
188
|
+
### The implicit `__name__` direction
|
|
189
|
+
|
|
190
|
+
Firestore appends the document key to every composite index, and its direction is not declared, so
|
|
191
|
+
stripping it requires knowing which direction it would have had. indexwright defines that direction
|
|
192
|
+
as **the `order` of the last preceding field that carries an `order`, or `ASCENDING` when no
|
|
193
|
+
preceding field carries one** — the latter arising when the index ends with an `arrayConfig` field.
|
|
194
|
+
|
|
195
|
+
This is not published behaviour. It is read off live exports, which render the key explicitly:
|
|
196
|
+
`[type ASC, createdAt DESC, __name__ DESC]`, `[public ASC, startAt ASC, __name__ ASC]`, and
|
|
197
|
+
`[isRecommended ASC, tags CONTAINS, __name__ ASC]` are all shapes an export produces.
|
|
198
|
+
|
|
199
|
+
The definition is used only to decide whether a written `__name__` is redundant, and the decision is
|
|
200
|
+
deliberately one-sided: a trailing `__name__` is stripped **only** when its direction equals the
|
|
201
|
+
value above, and one that differs — `[totalNbUses DESC, __name__ ASC]` occurs in real exports — is
|
|
202
|
+
treated as meaningful and kept. A wrong definition can therefore fail to merge two spellings of one
|
|
203
|
+
index, but can never merge two indexes that are actually distinct.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### R1 · `scope-mismatch`
|
|
208
|
+
|
|
209
|
+
**Detects.** Within one `collectionGroup`, more than one distinct `queryScope` is declared. One
|
|
210
|
+
finding is emitted per affected `collectionGroup`, reporting how many indexes each scope holds.
|
|
211
|
+
|
|
212
|
+
The scope with the fewest indexes is named as the minority, and the finding's keys are that scope's
|
|
213
|
+
indexes. When no scope holds a strict minority — an even split — the finding says so instead of
|
|
214
|
+
naming one, and reports the keys of the lexicographically first scope. It still fires: the detection
|
|
215
|
+
condition is that the scopes disagree, and an even split is a disagreement. Suppressing it would
|
|
216
|
+
narrow the rule to a shape the rule does not claim.
|
|
217
|
+
|
|
218
|
+
**Rationale.** A `COLLECTION`-scoped index does not serve a collection-group query, and vice versa.
|
|
219
|
+
When every other index on a collection uses one scope and a newly added one uses the other, the new
|
|
220
|
+
declaration is frequently a mistake that will surface only as a production `FAILED_PRECONDITION`.
|
|
221
|
+
|
|
222
|
+
**False positives.** Legitimate when an application queries the same collection both as a single
|
|
223
|
+
collection and as a collection group. This is why the rule warns rather than fails.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### R2 · `field-order-variant`
|
|
228
|
+
|
|
229
|
+
**Detects.** Two or more indexes that share `collectionGroup`, `queryScope`, and the same *set* of
|
|
230
|
+
`fieldPath:direction` pairs, but declare them in different orders.
|
|
231
|
+
|
|
232
|
+
"Same set" means the same *multiset*: a `fieldPath` that repeats within one index is unusual but not
|
|
233
|
+
rejected (§4), and comparing multisets keeps the grouping well defined when it does. The comparison
|
|
234
|
+
runs on the canonicalised field list, so an index that writes `__name__` explicitly groups with one
|
|
235
|
+
that does not. Two indexes that are byte-identical do not constitute different orders and do not
|
|
236
|
+
fire this rule.
|
|
237
|
+
|
|
238
|
+
**Rationale.** Firestore treats a different field order as a different index. Each variant consumes
|
|
239
|
+
write amplification, storage, and quota independently. Variants proliferate easily: a query change
|
|
240
|
+
that reorders fields adds a new index without removing the old one, and round-tripping through a
|
|
241
|
+
live export can reintroduce an ordering that was intended to be replaced.
|
|
242
|
+
|
|
243
|
+
**False positives.** Legitimate when distinct queries genuinely require distinct orderings — for
|
|
244
|
+
example, two queries that order by the same two fields in opposite directions. The finding asks for
|
|
245
|
+
that justification to be recorded, not for one variant to be removed.
|
|
246
|
+
|
|
247
|
+
**Output.** All members of the variant group are listed together, so the reader can judge the set
|
|
248
|
+
rather than one member at a time.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### R3 · `explicit-name-field`
|
|
253
|
+
|
|
254
|
+
**Detects.** An index whose **last** `fields` entry is an explicit `__name__` carrying the implicit
|
|
255
|
+
default direction (§5, *The implicit `__name__` direction*). A `__name__` written anywhere other
|
|
256
|
+
than last, or written last with a direction other than the default, is not flagged.
|
|
257
|
+
|
|
258
|
+
**Rationale.** Firestore appends the document key implicitly; live exports render it explicitly.
|
|
259
|
+
An explicit `__name__` in a hand-maintained declaration is therefore a signature of a value that
|
|
260
|
+
round-tripped through an export rather than being authored directly. It does not change the
|
|
261
|
+
resource identity, but it makes the file inconsistent and can mask genuine duplicates from
|
|
262
|
+
naive text comparison.
|
|
263
|
+
|
|
264
|
+
**False positives.** Explicit `__name__` with a direction that differs from the implicit default is
|
|
265
|
+
meaningful and should not be flagged. The rule only fires on a trailing `__name__` matching the
|
|
266
|
+
implicit default.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### R4 · `quota-headroom`
|
|
271
|
+
|
|
272
|
+
**Detects.** The number of composite indexes in the file exceeds `--quota-threshold` of `--quota`.
|
|
273
|
+
|
|
274
|
+
**Rationale.** Composite indexes are capped per database. The limit is reached gradually and
|
|
275
|
+
silently; the first symptom is a failed index creation at deploy time, which is a poor moment to
|
|
276
|
+
discover it. Reporting headroom continuously makes the trend visible.
|
|
277
|
+
|
|
278
|
+
The comparison is a strict `count > quota × threshold`, so the defaults (1000, 0.8) fire at 801.
|
|
279
|
+
|
|
280
|
+
**False positives.** None in principle; the threshold is configurable because appropriate headroom
|
|
281
|
+
depends on the rate of index growth.
|
|
282
|
+
|
|
283
|
+
**Output.** The finding is about the file, not about any one index, so its `key` is `null` and its
|
|
284
|
+
`related` is empty. One finding per file at most.
|
|
285
|
+
|
|
286
|
+
## 6. Output formats
|
|
287
|
+
|
|
288
|
+
**`text`** — human-readable, grouped by rule, intended for a terminal.
|
|
289
|
+
|
|
290
|
+
**`json`** — a stable machine-readable shape:
|
|
291
|
+
|
|
292
|
+
```jsonc
|
|
293
|
+
{
|
|
294
|
+
"version": "0.1.0",
|
|
295
|
+
"files": ["…"],
|
|
296
|
+
"summary": { "warnings": 0, "errors": 0, "byRule": { "scope-mismatch": 0, … } },
|
|
297
|
+
"findings": [
|
|
298
|
+
{
|
|
299
|
+
"rule": "scope-mismatch",
|
|
300
|
+
"file": "…",
|
|
301
|
+
"key": "…", // null for a finding about the file as a whole
|
|
302
|
+
"message": "…",
|
|
303
|
+
"related": ["…"] // other keys in the same finding group; [] when there are none
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
"errors": [
|
|
307
|
+
{ "file": "…", "message": "…" } // files that could not be read or parsed
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`files` lists every file the run was given, whether or not it parsed, sorted by path. `byRule` holds
|
|
313
|
+
one entry for every rule that ran after `--rule` and `--disable` were applied, including rules that
|
|
314
|
+
found nothing; a rule that did not run is absent rather than zero. `findings` is sorted by file,
|
|
315
|
+
then by rule in the order of §5, then by key with a null key sorting first. `related` is sorted
|
|
316
|
+
ascending. `errors` is sorted by file. Every field is always present: `related` is `[]` rather than
|
|
317
|
+
omitted, and `errors` is `[]` on a clean run.
|
|
318
|
+
|
|
319
|
+
**`github`** — GitHub Actions workflow commands (`::warning file=…::`) plus a Markdown summary
|
|
320
|
+
suitable for `$GITHUB_STEP_SUMMARY`.
|
|
321
|
+
|
|
322
|
+
## 7. Design principles
|
|
323
|
+
|
|
324
|
+
**Warn, do not fail.** The rules encode heuristics whose false-positive rates are, at v0.1.0,
|
|
325
|
+
unmeasured. Shipping them as blocking checks would teach users to suppress the tool. Enforcement is
|
|
326
|
+
opt-in and per-adopter.
|
|
327
|
+
|
|
328
|
+
**Never authorise a deletion.** See §2. No output phrasing may suggest that an index is unused or
|
|
329
|
+
safe to remove.
|
|
330
|
+
|
|
331
|
+
**No dependencies in `indexwright`.** A linter that pulls a dependency tree into a build pipeline
|
|
332
|
+
undermines its own purpose. Argument parsing and formatting are implemented in-tree, and the
|
|
333
|
+
published package declares no runtime dependencies. This is a property of the package rather than
|
|
334
|
+
an aspiration of the project: it holds in every version, and a test asserts it.
|
|
335
|
+
|
|
336
|
+
The principle is about what lands in an adopter's tree, so build- and test-time tooling that never
|
|
337
|
+
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
|
|
340
|
+
forbids is making every adopter of `lint` pay for it.
|
|
341
|
+
|
|
342
|
+
**No network, no credentials, in `lint`.** Static analysis must be runnable in any environment,
|
|
343
|
+
including a sandboxed CI step with no cloud access. Network use is confined to the planned
|
|
344
|
+
`record`/`check` verbs, which are separate commands in a separate package (§3).
|
|
345
|
+
|
|
346
|
+
**Delegate undocumented semantics to the platform.** Where Firestore's behaviour is not published —
|
|
347
|
+
principally index matching — indexwright measures rather than models. This bounds what the tool can
|
|
348
|
+
claim, which is the point.
|
|
349
|
+
|
|
350
|
+
**Deterministic output.** Findings are emitted in a stable sort order (file, rule, key), so that
|
|
351
|
+
output can be diffed across runs.
|
|
352
|
+
|
|
353
|
+
## 8. Testing
|
|
354
|
+
|
|
355
|
+
Rules are tested against small hand-written fixtures that isolate one condition each, with explicit
|
|
356
|
+
positive and negative cases.
|
|
357
|
+
|
|
358
|
+
**Assertions are never written against a real project's index file.** A test that asserts a finding
|
|
359
|
+
count over live data fails whenever that data legitimately changes, which trains maintainers to
|
|
360
|
+
edit the test rather than read it. Fixtures encode the invariant; real files are for manual
|
|
361
|
+
exploration only.
|
|
362
|
+
|
|
363
|
+
## 9. Compatibility
|
|
364
|
+
|
|
365
|
+
- Node.js ≥ 22, ESM.
|
|
366
|
+
- Input schema follows the Firebase CLI's `firestore.indexes.json`. Unknown keys are preserved and
|
|
367
|
+
ignored rather than rejected, so that a newer field does not break linting.
|
|
368
|
+
- Semantic versioning. Pre-1.0, rule additions and message changes may land in minor releases;
|
|
369
|
+
the `json` output shape is the stable contract and changes only in major releases after 1.0.
|
|
370
|
+
- The package also exports a JavaScript API, so the rules can be run without spawning a process.
|
|
371
|
+
That API is **provisional**: it is not part of the stable contract before 1.0 and may change in
|
|
372
|
+
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
|
|
379
|
+
|
|
380
|
+
1.0 requires, at minimum:
|
|
381
|
+
|
|
382
|
+
- Measured false-positive rates for R1 and R2 across more than one real project.
|
|
383
|
+
- At least one adopter outside the project of origin.
|
|
384
|
+
- A decision, informed by that data, on whether any rule should default to failing.
|
|
385
|
+
|
|
386
|
+
Until then the version stays below 1.0 and the README states plainly that the rules are provisional.
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { OutputFormat, RuleId } from './types.js';
|
|
2
|
+
/** Anything the user could have typed differently. Mapped to exit code 2. */
|
|
3
|
+
export declare class UsageError extends Error {
|
|
4
|
+
readonly name = "UsageError";
|
|
5
|
+
}
|
|
6
|
+
export interface LintCommand {
|
|
7
|
+
kind: 'lint';
|
|
8
|
+
files: string[];
|
|
9
|
+
format: OutputFormat;
|
|
10
|
+
maxWarnings: number;
|
|
11
|
+
rules: RuleId[];
|
|
12
|
+
quota: number;
|
|
13
|
+
quotaThreshold: number;
|
|
14
|
+
}
|
|
15
|
+
export type Command = LintCommand | {
|
|
16
|
+
kind: 'help';
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'version';
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Parsed in-tree rather than with a dependency (SPEC §7). Supports `--flag value` and
|
|
22
|
+
* `--flag=value`, and `--` to end option parsing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseArgs(argv: readonly string[]): Command;
|
|
25
|
+
export declare function usage(): string;
|
|
26
|
+
//# sourceMappingURL=args.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { DEFAULT_QUOTA, DEFAULT_QUOTA_THRESHOLD } from './lint.js';
|
|
2
|
+
import { isRuleId, rules } from './rules/index.js';
|
|
3
|
+
import { RULE_IDS } from './types.js';
|
|
4
|
+
/** Anything the user could have typed differently. Mapped to exit code 2. */
|
|
5
|
+
export class UsageError extends Error {
|
|
6
|
+
name = 'UsageError';
|
|
7
|
+
}
|
|
8
|
+
const FORMATS = ['text', 'json', 'github'];
|
|
9
|
+
/**
|
|
10
|
+
* Parsed in-tree rather than with a dependency (SPEC §7). Supports `--flag value` and
|
|
11
|
+
* `--flag=value`, and `--` to end option parsing.
|
|
12
|
+
*/
|
|
13
|
+
export function parseArgs(argv) {
|
|
14
|
+
if (argv.length === 0)
|
|
15
|
+
throw new UsageError('no command given');
|
|
16
|
+
if (argv.includes('--help') || argv.includes('-h'))
|
|
17
|
+
return { kind: 'help' };
|
|
18
|
+
if (argv.includes('--version'))
|
|
19
|
+
return { kind: 'version' };
|
|
20
|
+
const [command, ...rest] = argv;
|
|
21
|
+
if (command !== 'lint') {
|
|
22
|
+
throw new UsageError(`unknown command "${command}"; the only command is "lint"`);
|
|
23
|
+
}
|
|
24
|
+
const files = [];
|
|
25
|
+
let format = 'text';
|
|
26
|
+
let maxWarnings = Number.POSITIVE_INFINITY;
|
|
27
|
+
let quota = DEFAULT_QUOTA;
|
|
28
|
+
let quotaThreshold = DEFAULT_QUOTA_THRESHOLD;
|
|
29
|
+
const selected = [];
|
|
30
|
+
const disabled = [];
|
|
31
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
32
|
+
const argument = rest[i];
|
|
33
|
+
if (argument === '--') {
|
|
34
|
+
files.push(...rest.slice(i + 1));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (!argument.startsWith('--')) {
|
|
38
|
+
files.push(argument);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const equals = argument.indexOf('=');
|
|
42
|
+
const name = equals === -1 ? argument : argument.slice(0, equals);
|
|
43
|
+
const inlineValue = equals === -1 ? null : argument.slice(equals + 1);
|
|
44
|
+
const takeValue = () => {
|
|
45
|
+
if (inlineValue !== null)
|
|
46
|
+
return inlineValue;
|
|
47
|
+
const next = rest[i + 1];
|
|
48
|
+
if (next === undefined)
|
|
49
|
+
throw new UsageError(`${name} needs a value`);
|
|
50
|
+
i += 1;
|
|
51
|
+
return next;
|
|
52
|
+
};
|
|
53
|
+
switch (name) {
|
|
54
|
+
case '--format':
|
|
55
|
+
format = parseFormat(takeValue());
|
|
56
|
+
break;
|
|
57
|
+
case '--max-warnings':
|
|
58
|
+
maxWarnings = parseCount(takeValue(), name);
|
|
59
|
+
break;
|
|
60
|
+
case '--rule':
|
|
61
|
+
selected.push(parseRuleId(takeValue(), name));
|
|
62
|
+
break;
|
|
63
|
+
case '--disable':
|
|
64
|
+
disabled.push(parseRuleId(takeValue(), name));
|
|
65
|
+
break;
|
|
66
|
+
case '--quota':
|
|
67
|
+
quota = parsePositiveInteger(takeValue(), name);
|
|
68
|
+
break;
|
|
69
|
+
case '--quota-threshold':
|
|
70
|
+
quotaThreshold = parseFraction(takeValue(), name);
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new UsageError(`unknown option "${name}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (files.length === 0)
|
|
77
|
+
throw new UsageError('no input files given');
|
|
78
|
+
return {
|
|
79
|
+
kind: 'lint',
|
|
80
|
+
// De-duplicated so a shell glob that repeats a path does not double every finding.
|
|
81
|
+
files: [...new Set(files)],
|
|
82
|
+
format,
|
|
83
|
+
maxWarnings,
|
|
84
|
+
rules: resolveRules(selected, disabled),
|
|
85
|
+
quota,
|
|
86
|
+
quotaThreshold,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function resolveRules(selected, disabled) {
|
|
90
|
+
const base = selected.length > 0 ? new Set(selected) : new Set(RULE_IDS);
|
|
91
|
+
for (const id of disabled)
|
|
92
|
+
base.delete(id);
|
|
93
|
+
if (base.size === 0) {
|
|
94
|
+
throw new UsageError('--rule and --disable leave no rules to run');
|
|
95
|
+
}
|
|
96
|
+
return RULE_IDS.filter((id) => base.has(id));
|
|
97
|
+
}
|
|
98
|
+
function parseFormat(value) {
|
|
99
|
+
if (!FORMATS.includes(value)) {
|
|
100
|
+
throw new UsageError(`unknown format "${value}"; expected one of ${FORMATS.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
/** A typo must not silently produce a clean run, so an unknown rule id is a usage error. */
|
|
105
|
+
function parseRuleId(value, option) {
|
|
106
|
+
if (!isRuleId(value)) {
|
|
107
|
+
throw new UsageError(`unknown rule "${value}" for ${option}; expected one of ${RULE_IDS.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
/** A plain decimal numeral, with or without a fractional part: `5`, `0.8`, `.8`, `-1`. */
|
|
112
|
+
const DECIMAL = /^-?(\d+(\.\d*)?|\.\d+)$/;
|
|
113
|
+
/**
|
|
114
|
+
* `Number` reads `""` as 0, ignores surrounding whitespace, and reads `"0x10"` as 16. A CI step
|
|
115
|
+
* that writes `--max-warnings=$LIMIT` with `LIMIT` unset would therefore turn "unlimited" into
|
|
116
|
+
* "zero tolerance" silently, so an option value has to be a numeral and nothing else. `NaN` fails
|
|
117
|
+
* every caller's range check, which reports it as the usage error it is.
|
|
118
|
+
*/
|
|
119
|
+
function toNumber(value) {
|
|
120
|
+
return DECIMAL.test(value) ? Number(value) : Number.NaN;
|
|
121
|
+
}
|
|
122
|
+
function parseCount(value, option) {
|
|
123
|
+
const parsed = toNumber(value);
|
|
124
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
125
|
+
throw new UsageError(`${option} needs a non-negative integer, got "${value}"`);
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
}
|
|
129
|
+
function parsePositiveInteger(value, option) {
|
|
130
|
+
const parsed = toNumber(value);
|
|
131
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
132
|
+
throw new UsageError(`${option} needs a positive integer, got "${value}"`);
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
function parseFraction(value, option) {
|
|
137
|
+
const parsed = toNumber(value);
|
|
138
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) {
|
|
139
|
+
throw new UsageError(`${option} needs a number in (0, 1], got "${value}"`);
|
|
140
|
+
}
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
export function usage() {
|
|
144
|
+
const ruleLines = rules.map((rule) => ` ${rule.id.padEnd(21)}${rule.description}`);
|
|
145
|
+
return [
|
|
146
|
+
'indexwright lint <file...> [options]',
|
|
147
|
+
'',
|
|
148
|
+
'Lints Firestore composite index declarations. Every rule emits warnings, never errors.',
|
|
149
|
+
'No finding indicates that an index is unused or safe to delete.',
|
|
150
|
+
'',
|
|
151
|
+
'Options:',
|
|
152
|
+
' --format <fmt> text (default) | json | github',
|
|
153
|
+
' --max-warnings <n> exit 1 if warnings exceed n (default: unlimited)',
|
|
154
|
+
' --rule <id> run only the given rule; repeatable',
|
|
155
|
+
' --disable <id> skip the given rule; repeatable',
|
|
156
|
+
` --quota <n> per-database composite index limit (default: ${DEFAULT_QUOTA})`,
|
|
157
|
+
` --quota-threshold <p> warn above this fraction of the limit (default: ${DEFAULT_QUOTA_THRESHOLD})`,
|
|
158
|
+
' -h, --help show this message',
|
|
159
|
+
' --version show the version',
|
|
160
|
+
'',
|
|
161
|
+
'Rules:',
|
|
162
|
+
...ruleLines,
|
|
163
|
+
'',
|
|
164
|
+
'Exit codes:',
|
|
165
|
+
' 0 completed; warnings may have been emitted',
|
|
166
|
+
' 1 warning count exceeded --max-warnings',
|
|
167
|
+
' 2 usage error, unreadable file, or malformed input',
|
|
168
|
+
].join('\n');
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=args.js.map
|
package/dist/args.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface Streams {
|
|
3
|
+
out(text: string): void;
|
|
4
|
+
err(text: string): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Exit codes (SPEC §4): 0 completed, 1 warnings over the budget, 2 usage error or unusable input.
|
|
8
|
+
* A file that could not be analysed is a 2 regardless of `--max-warnings`, because the run did not
|
|
9
|
+
* cover what it was asked to cover.
|
|
10
|
+
*/
|
|
11
|
+
export declare function run(argv: readonly string[], streams: Streams): number;
|
|
12
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAWA,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CA2CrE"}
|