indexwright 0.1.1 → 0.3.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,77 @@ 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.3.0] — 2026-09-21
9
+
10
+ The other half of the file. `fieldOverrides` — single-field index configuration — was typed
11
+ `unknown[]` and read by nothing; it is now validated to the same depth as `indexes` and reduced to
12
+ a canonical form, so that `@indexwright/record` 0.7.0 can reconcile a declaration against a live
13
+ field listing on the one key the linter will report on. No rule reads it yet, and nothing about the
14
+ linter's output or exit codes changes. The package also moves inside the repository, to
15
+ `packages/indexwright`, without moving anything about what is published.
16
+
17
+ ### Added
18
+
19
+ - **`fieldOverrides` is validated and has a canonical form** (issues #53 and #54). The file's other
20
+ half was typed `unknown[]` and read by nothing. It is now held to the same depth as `indexes` —
21
+ an entry needs `collectionGroup`, `fieldPath`, and an `indexes` array (empty for an exemption),
22
+ each index exactly one of `order`, `arrayConfig`, and `vectorConfig` (and a `queryScope`, read as
23
+ `COLLECTION` when omitted, as the Firebase CLI does), and `ttl` a boolean when present — and `analyseOverrides` reduces each to an `AnalysedOverride` keyed as
24
+ `<collectionGroup>::<fieldPath>::<queryScope>:<direction>|…`, sorted, with entries alike in scope and
25
+ direction collapsed, as the set it is. No rule reads it yet; the form exists so that `@indexwright/record` can reconcile a
26
+ declaration against a live field listing on the same key the linter will report on, rather than
27
+ on a second model. A file whose `fieldOverrides` cannot be read that way is now malformed, where
28
+ it previously passed unexamined. `fieldDirection` accepts any object carrying the three configs,
29
+ which every `IndexField` still is.
30
+
31
+ ### Changed
32
+
33
+ - `IndexDocument.fieldOverrides` is typed `FieldOverride[]`, no longer `unknown[]`. A consumer
34
+ that built one by hand with entries of another shape stops compiling.
35
+ - The package now lives at `packages/indexwright` rather than at the repository root, so that
36
+ `@indexwright/record` can depend on it from the working tree instead of from a published release.
37
+ Nothing about the published package moves: same name, same bin, same `exports`, same
38
+ dependency-free guarantee.
39
+ - `SPEC.md` is no longer shipped inside the tarball. It specifies both packages, so it belongs to
40
+ the repository rather than to either one, and `files` cannot reach above a package directory. The
41
+ alternative — copying it in at pack time — runs through `prepublishOnly`, which npm skips entirely
42
+ under `ignore-scripts=true`; that would make the verified tarball and the published one differ in
43
+ exactly the way `verify-package` exists to catch. The spec is linked from the README and is in the
44
+ repository.
45
+
46
+ ### Notes
47
+
48
+ - **The override key carries no marker telling it from an index key.** `posts::tags::COLLECTION:…`
49
+ and `posts::COLLECTION::tags:…` are the same shape read by different rules, and a reader that
50
+ does not know which listing a key came from cannot tell them apart by the string alone. Decided
51
+ and left as it is for this release: no rule reports an override key yet and the `json` output
52
+ does not carry one, so adding a marker later would change nothing that is a contract today, while
53
+ adding one now would fix a spelling nothing has needed.
54
+
55
+ ## [0.2.0] — 2026-08-10
56
+
57
+ ### Added
58
+
59
+ - A query corpus format, specified in [SPEC.md](https://github.com/uny/indexwright/blob/main/SPEC.md) §7 and versioned by its own
60
+ `corpusVersion` rather than by any package's release number. It is the contract between capture
61
+ and the coverage check, and the first indexwright artefact meant to be committed and reviewed in
62
+ a diff alongside `firestore.indexes.json`.
63
+ - `@indexwright/record`, a separate package that writes one. See
64
+ [its changelog](https://github.com/uny/indexwright/blob/main/packages/record/CHANGELOG.md).
65
+ - `indexwright record` now says that the verb ships in `@indexwright/record` and how to run it,
66
+ instead of reporting an unknown command. The cost of splitting the family into two packages is a
67
+ second package to discover, and "unknown command" reads as "indexwright cannot do this".
68
+
69
+ ### Changed
70
+
71
+ - SPEC §3 no longer justifies the package split with a gRPC stack for capture. Capture needs none;
72
+ the split rests on v0.3's replay, which needs a Firestore client. Sections 7–10 shifted to 8–11
73
+ when the corpus format was inserted as §7.
74
+
75
+ Nothing about the linter's rules, output, or exit codes changed in this release. `indexwright`
76
+ still declares no runtime dependencies, and now so does `@indexwright/record`.
77
+
78
+ ## [0.1.1] — 2026-08-08
9
79
 
10
80
  No change to the published files: this release exists to exercise the publishing path itself.
11
81
 
@@ -30,5 +100,7 @@ First release. Static analysis of `firestore.indexes.json`; no network access, n
30
100
  `__name__` to the same resource.
31
101
  - A provisional JavaScript API, so the rules can run without spawning a process.
32
102
 
103
+ [0.3.0]: https://github.com/uny/indexwright/releases/tag/v0.3.0
104
+ [0.2.0]: https://github.com/uny/indexwright/releases/tag/v0.2.0
33
105
  [0.1.1]: https://github.com/uny/indexwright/releases/tag/v0.1.1
34
106
  [0.1.0]: https://github.com/uny/indexwright/releases/tag/v0.1.0
package/README.md CHANGED
@@ -88,7 +88,7 @@ measured your own noise level.
88
88
  | `quota-headroom` | The declared index count is close to the per-database limit. |
89
89
 
90
90
  Each rule's rationale, its known false positives, and the exact detection condition are in
91
- [SPEC.md](SPEC.md) §5.
91
+ [SPEC.md](https://github.com/uny/indexwright/blob/main/SPEC.md) §5.
92
92
 
93
93
  ### A note on `__name__`
94
94
 
@@ -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.
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:
148
149
 
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.
150
+ - **v0.2 — query capture, shipped.** [`@indexwright/record`](https://github.com/uny/indexwright/tree/main/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](https://github.com/uny/indexwright/blob/main/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 under `packages/`: `indexwright` and
174
+ [`@indexwright/record`](https://github.com/uny/indexwright/tree/main/packages/record). 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/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":"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"}
@@ -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,18 +1,18 @@
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
  */
8
8
  export { formatGithub, type GithubOutput } from './format/github.js';
9
9
  export { formatJson } from './format/json.js';
10
10
  export { formatText } from './format/text.js';
11
- export { analyse, canonicalFields, fieldDirection, implicitNameDirection, indexKey, NAME_FIELD, } from './key.js';
11
+ export { analyse, analyseOverrides, canonicalFields, canonicalSingleFieldIndexes, fieldDirection, formatSingleFieldIndex, implicitNameDirection, indexKey, NAME_FIELD, overrideKey, type IndexConfig, } from './key.js';
12
12
  export { DEFAULT_QUOTA, DEFAULT_QUOTA_THRESHOLD, lintDocuments, lintFiles, lintTexts, type DocumentInput, type LintOptions, type TextInput, } from './lint.js';
13
13
  export { MalformedInputError, parseDocument, validateDocument } from './parse.js';
14
14
  export { getRule, isRuleId, rules } from './rules/index.js';
15
15
  export { RULE_IDS } from './types.js';
16
- export type { AnalysedIndex, CanonicalField, CompositeIndex, Finding, IndexDocument, IndexField, LintError, LintResult, LintSummary, OutputFormat, Rule, RuleContext, RuleId, RuleOptions, } from './types.js';
16
+ export type { AnalysedIndex, AnalysedOverride, CanonicalField, CanonicalSingleFieldIndex, CompositeIndex, FieldOverride, Finding, IndexDocument, IndexField, LintError, LintResult, LintSummary, OutputFormat, Rule, RuleContext, RuleId, RuleOptions, SingleFieldIndex, } from './types.js';
17
17
  export { VERSION } from './version.js';
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,QAAQ,EACR,UAAU,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,SAAS,EACT,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,OAAO,EACP,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,EACV,WAAW,EACX,YAAY,EACZ,IAAI,EACJ,WAAW,EACX,MAAM,EACN,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,eAAe,EACf,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,UAAU,EACV,WAAW,EACX,KAAK,WAAW,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,SAAS,EACT,SAAS,EACT,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EACzB,cAAc,EACd,aAAa,EACb,OAAO,EACP,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,EACV,WAAW,EACX,YAAY,EACZ,IAAI,EACJ,WAAW,EACX,MAAM,EACN,WAAW,EACX,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
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
  */
8
8
  export { formatGithub } from './format/github.js';
9
9
  export { formatJson } from './format/json.js';
10
10
  export { formatText } from './format/text.js';
11
- export { analyse, canonicalFields, fieldDirection, implicitNameDirection, indexKey, NAME_FIELD, } from './key.js';
11
+ export { analyse, analyseOverrides, canonicalFields, canonicalSingleFieldIndexes, fieldDirection, formatSingleFieldIndex, implicitNameDirection, indexKey, NAME_FIELD, overrideKey, } from './key.js';
12
12
  export { DEFAULT_QUOTA, DEFAULT_QUOTA_THRESHOLD, lintDocuments, lintFiles, lintTexts, } from './lint.js';
13
13
  export { MalformedInputError, parseDocument, validateDocument } from './parse.js';
14
14
  export { getRule, isRuleId, rules } from './rules/index.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,eAAe,EACf,cAAc,EACd,qBAAqB,EACrB,QAAQ,EACR,UAAU,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,SAAS,EACT,SAAS,GAIV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAiBtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAqB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,OAAO,EACP,gBAAgB,EAChB,eAAe,EACf,2BAA2B,EAC3B,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,QAAQ,EACR,UAAU,EACV,WAAW,GAEZ,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,SAAS,EACT,SAAS,GAIV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAqBtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
package/dist/key.d.ts CHANGED
@@ -1,12 +1,14 @@
1
- import type { AnalysedIndex, CanonicalField, IndexDocument, IndexField } from './types.js';
1
+ import type { AnalysedIndex, AnalysedOverride, CanonicalField, CanonicalSingleFieldIndex, IndexDocument, IndexField, SingleFieldIndex } from './types.js';
2
2
  export declare const NAME_FIELD = "__name__";
3
+ /** The three configs a field can carry, which a composite index's field and a single-field index share. */
4
+ export type IndexConfig = Pick<IndexField, 'order' | 'arrayConfig' | 'vectorConfig'>;
3
5
  /**
4
6
  * The direction a field contributes to the canonical key.
5
7
  *
6
8
  * `parse.ts` guarantees exactly one of `order`, `arrayConfig`, and `vectorConfig` is present, so
7
9
  * the fallback is unreachable for validated input.
8
10
  */
9
- export declare function fieldDirection(field: IndexField): string;
11
+ export declare function fieldDirection(field: IndexConfig): string;
10
12
  /**
11
13
  * The direction Firestore would give the document key it appends to this field list.
12
14
  *
@@ -32,4 +34,30 @@ export declare function formatField(field: CanonicalField): string;
32
34
  export declare function indexKey(collectionGroup: string, queryScope: string, fields: readonly CanonicalField[]): string;
33
35
  /** Precompute the canonical form once so every rule shares one interpretation of the document. */
34
36
  export declare function analyse(document: IndexDocument): AnalysedIndex[];
37
+ export declare function formatSingleFieldIndex(index: CanonicalSingleFieldIndex): string;
38
+ /**
39
+ * The canonical key of a field override (SPEC §5, *Canonical override key*).
40
+ *
41
+ * The same three-part shape as `indexKey`, with the field path where the query scope sits and the
42
+ * scope inside each entry — a single-field index names one field, so what varies per entry is the
43
+ * scope. `indexes` must already be in canonical order; the key is a rendering, not a sort.
44
+ */
45
+ export declare function overrideKey(collectionGroup: string, fieldPath: string, indexes: readonly CanonicalSingleFieldIndex[]): string;
46
+ /**
47
+ * Reduce an override's declared set to canonical form.
48
+ *
49
+ * A composite index's fields are a sequence and their order is part of the key; an override's
50
+ * indexes are a set, and Firestore holds at most one single-field index per (scope, direction) of
51
+ * a field. So the entries are sorted, and entries that agree on scope and direction are collapsed
52
+ * to one: two spellings of one configuration reach the same key, while two configurations that
53
+ * differ in any member cannot. The collapse reads only what the key reads — two entries alike in
54
+ * scope and direction but differing in a key the form does not see (`density`, `unique`) collapse
55
+ * too, and `source.indexes` keeps both for a consumer that refuses what the key cannot express.
56
+ */
57
+ export declare function canonicalSingleFieldIndexes(indexes: readonly SingleFieldIndex[]): CanonicalSingleFieldIndex[];
58
+ /**
59
+ * The counterpart of `analyse` for the document's other half. An absent `fieldOverrides` is an
60
+ * empty set, which is what a hand-written file without the key means.
61
+ */
62
+ export declare function analyseOverrides(document: IndexDocument): AnalysedOverride[];
35
63
  //# sourceMappingURL=key.d.ts.map
package/dist/key.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAkB,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3G,eAAO,MAAM,UAAU,aAAa,CAAC;AAErC;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAQxD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAM3E;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,+FAA+F;IAC/F,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,eAAe,CAS9E;AAMD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEzD;AAED,wBAAgB,QAAQ,CACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC,MAAM,CAER;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa,EAAE,CAEhE"}
1
+ {"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EAGzB,aAAa,EACb,UAAU,EACV,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,eAAO,MAAM,UAAU,aAAa,CAAC;AAErC,2GAA2G;AAC3G,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,aAAa,GAAG,cAAc,CAAC,CAAC;AAErF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAQzD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAM3E;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,+FAA+F;IAC/F,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,eAAe,CAS9E;AAMD,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEzD;AAED,wBAAgB,QAAQ,CACtB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,cAAc,EAAE,GAChC,MAAM,CAER;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa,EAAE,CAEhE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,yBAAyB,GAAG,MAAM,CAE/E;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,yBAAyB,EAAE,GAC5C,MAAM,CAER;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,yBAAyB,EAAE,CAe7B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,GAAG,gBAAgB,EAAE,CAI5E"}
package/dist/key.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { compareStrings } from './collections.js';
1
2
  export const NAME_FIELD = '__name__';
2
3
  /**
3
4
  * The direction a field contributes to the canonical key.
@@ -61,6 +62,63 @@ export function indexKey(collectionGroup, queryScope, fields) {
61
62
  export function analyse(document) {
62
63
  return document.indexes.map((source, position) => analyseIndex(source, position));
63
64
  }
65
+ export function formatSingleFieldIndex(index) {
66
+ return `${index.queryScope}:${index.direction}`;
67
+ }
68
+ /**
69
+ * The canonical key of a field override (SPEC §5, *Canonical override key*).
70
+ *
71
+ * The same three-part shape as `indexKey`, with the field path where the query scope sits and the
72
+ * scope inside each entry — a single-field index names one field, so what varies per entry is the
73
+ * scope. `indexes` must already be in canonical order; the key is a rendering, not a sort.
74
+ */
75
+ export function overrideKey(collectionGroup, fieldPath, indexes) {
76
+ return `${collectionGroup}::${fieldPath}::${indexes.map(formatSingleFieldIndex).join('|')}`;
77
+ }
78
+ /**
79
+ * Reduce an override's declared set to canonical form.
80
+ *
81
+ * A composite index's fields are a sequence and their order is part of the key; an override's
82
+ * indexes are a set, and Firestore holds at most one single-field index per (scope, direction) of
83
+ * a field. So the entries are sorted, and entries that agree on scope and direction are collapsed
84
+ * to one: two spellings of one configuration reach the same key, while two configurations that
85
+ * differ in any member cannot. The collapse reads only what the key reads — two entries alike in
86
+ * scope and direction but differing in a key the form does not see (`density`, `unique`) collapse
87
+ * too, and `source.indexes` keeps both for a consumer that refuses what the key cannot express.
88
+ */
89
+ export function canonicalSingleFieldIndexes(indexes) {
90
+ const seen = new Set();
91
+ const canonical = [];
92
+ for (const index of indexes) {
93
+ const entry = { queryScope: index.queryScope, direction: fieldDirection(index) };
94
+ // Not the rendered `scope:direction`, which is not injective — nothing forbids `:` in either
95
+ // part. `JSON.stringify` over the pair is, without an assumption about the vocabulary.
96
+ const identity = JSON.stringify([entry.queryScope, entry.direction]);
97
+ if (seen.has(identity))
98
+ continue;
99
+ seen.add(identity);
100
+ canonical.push(entry);
101
+ }
102
+ return canonical.sort((a, b) => compareStrings(a.queryScope, b.queryScope) || compareStrings(a.direction, b.direction));
103
+ }
104
+ /**
105
+ * The counterpart of `analyse` for the document's other half. An absent `fieldOverrides` is an
106
+ * empty set, which is what a hand-written file without the key means.
107
+ */
108
+ export function analyseOverrides(document) {
109
+ return (document.fieldOverrides ?? []).map((source, position) => analyseOverride(source, position));
110
+ }
111
+ function analyseOverride(source, position) {
112
+ const indexes = canonicalSingleFieldIndexes(source.indexes);
113
+ return {
114
+ source,
115
+ position,
116
+ collectionGroup: source.collectionGroup,
117
+ fieldPath: source.fieldPath,
118
+ indexes,
119
+ key: overrideKey(source.collectionGroup, source.fieldPath, indexes),
120
+ };
121
+ }
64
122
  function analyseIndex(source, position) {
65
123
  const { fields, redundantNameDirection } = canonicalFields(source.fields);
66
124
  return {
package/dist/key.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,aAAa,EAAc,MAAM,YAAY,CAAC;AAE5E;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAkB,IAAI,yBAAyB;CAChD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAUzD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAa5D"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EAGd,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAkB,IAAI,yBAAyB;CAChD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAUzD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CA4B5D"}
package/dist/parse.js CHANGED
@@ -29,7 +29,19 @@ export function validateDocument(raw) {
29
29
  throw new MalformedInputError('"indexes" must be an array');
30
30
  }
31
31
  const validated = indexes.map((entry, i) => validateIndex(entry, `indexes[${i}]`));
32
- return { ...raw, indexes: validated };
32
+ // Optional, because a hand-written file usually has none and the Firebase CLI omits the key when
33
+ // an export has none. Present, it is validated to the same depth as `indexes`: an override this
34
+ // tool cannot read is one it would otherwise pass through *as* read, and the consumer that
35
+ // reconciles it against a live listing would then vouch for a field it never examined (#53).
36
+ const overrides = raw['fieldOverrides'];
37
+ if (overrides === undefined) {
38
+ return { ...raw, indexes: validated };
39
+ }
40
+ if (!Array.isArray(overrides)) {
41
+ throw new MalformedInputError('"fieldOverrides" must be an array');
42
+ }
43
+ const validatedOverrides = overrides.map((entry, i) => validateOverride(entry, `fieldOverrides[${i}]`));
44
+ return { ...raw, indexes: validated, fieldOverrides: validatedOverrides };
33
45
  }
34
46
  function validateIndex(raw, path) {
35
47
  if (!isObject(raw)) {
@@ -55,6 +67,48 @@ function validateField(raw, path) {
55
67
  throw new MalformedInputError(`${path}: must be an object`);
56
68
  }
57
69
  const fieldPath = requireString(raw['fieldPath'], `${path}: "fieldPath"`);
70
+ validateConfig(raw, path);
71
+ return { ...raw, fieldPath };
72
+ }
73
+ function validateOverride(raw, path) {
74
+ if (!isObject(raw)) {
75
+ throw new MalformedInputError(`${path}: must be an object`);
76
+ }
77
+ const collectionGroup = requireString(raw['collectionGroup'], `${path}: "collectionGroup"`);
78
+ const fieldPath = requireString(raw['fieldPath'], `${path}: "fieldPath"`);
79
+ // Empty is not the error it is for a composite index: an override declaring no indexes is an
80
+ // exemption, which is a configuration Firestore holds and an export writes out.
81
+ const indexes = raw['indexes'];
82
+ if (!Array.isArray(indexes)) {
83
+ throw new MalformedInputError(`${path}: "indexes" must be an array`);
84
+ }
85
+ const validatedIndexes = indexes.map((entry, i) => validateSingleFieldIndex(entry, `${path}.indexes[${i}]`));
86
+ // Checked for type only. Whether the field has a TTL policy is not something a canonical form
87
+ // reads, but a value that is not a boolean is not a TTL declaration the Firebase CLI would write.
88
+ if (raw['ttl'] !== undefined && typeof raw['ttl'] !== 'boolean') {
89
+ throw new MalformedInputError(`${path}: "ttl" must be a boolean`);
90
+ }
91
+ return { ...raw, collectionGroup, fieldPath, indexes: validatedIndexes };
92
+ }
93
+ function validateSingleFieldIndex(raw, path) {
94
+ if (!isObject(raw)) {
95
+ throw new MalformedInputError(`${path}: must be an object`);
96
+ }
97
+ // Absent, the scope is `COLLECTION`: the Firebase CLI's validator checks `queryScope` only when
98
+ // it is present and its own exports always write it, so an omission is a hand-written file, and
99
+ // `COLLECTION` is the default the CLI itself supplies when it upgrades an old-format composite
100
+ // index. A present value is still held to a string.
101
+ const queryScope = raw['queryScope'] === undefined
102
+ ? 'COLLECTION'
103
+ : requireString(raw['queryScope'], `${path}: "queryScope"`);
104
+ validateConfig(raw, path);
105
+ return { ...raw, queryScope };
106
+ }
107
+ /**
108
+ * The one-of check a composite index's field and a single-field index share: exactly one of
109
+ * `order`, `arrayConfig`, and `vectorConfig`, each of the right type.
110
+ */
111
+ function validateConfig(raw, path) {
58
112
  const configured = ['order', 'arrayConfig', 'vectorConfig'].filter((name) => raw[name] !== undefined);
59
113
  if (configured.length === 0) {
60
114
  throw new MalformedInputError(`${path}: needs one of "order", "arrayConfig", or "vectorConfig"`);
@@ -71,7 +125,6 @@ function validateField(raw, path) {
71
125
  if (raw['vectorConfig'] !== undefined && !isObject(raw['vectorConfig'])) {
72
126
  throw new MalformedInputError(`${path}: "vectorConfig" must be an object`);
73
127
  }
74
- return { ...raw, fieldPath };
75
128
  }
76
129
  function requireString(value, label) {
77
130
  if (typeof value !== 'string') {
package/dist/parse.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC1B,IAAI,GAAG,qBAAqB,CAAC;CAChD;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,gGAAgG;QAChG,MAAM,MAAM,GAAI,KAAe,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACpE,MAAM,IAAI,mBAAmB,CAAC,iBAAiB,MAAM,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAAC,qCAAqC,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,mBAAmB,CAAC,mBAAmB,CAAC,CAAC;IACrD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,mBAAmB,CAAC,4BAA4B,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IAEnF,iGAAiG;IACjG,gGAAgG;IAChG,2FAA2F;IAC3F,6FAA6F;IAC7F,MAAM,SAAS,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACxC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,SAAS,EAAmB,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,mBAAmB,CAAC,mCAAmC,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,kBAAkB,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CACpD,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAChD,CAAC;IACF,OAAO,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAmB,CAAC;AAC7F,CAAC;AAED,SAAS,aAAa,CAAC,GAAY,EAAE,IAAY;IAC/C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,eAAe,GAAG,aAAa,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC5F,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,GAAG,IAAI,gBAAgB,CAAC,CAAC;IAE7E,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,8BAA8B,CAAC,CAAC;IACvE,CAAC;IAED,gGAAgG;IAChG,+FAA+F;IAC/F,sEAAsE;IACtE,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IAE/F,OAAO,EAAE,GAAG,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,eAAe,EAAoB,CAAC;AAC5F,CAAC;AAED,SAAS,aAAa,CAAC,GAAY,EAAE,IAAY;IAC/C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,GAAG,IAAI,eAAe,CAAC,CAAC;IAC1E,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,OAAO,EAAE,GAAG,GAAG,EAAE,SAAS,EAAgB,CAAC;AAC7C,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAY,EAAE,IAAY;IAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC9D,CAAC;IACD,MAAM,eAAe,GAAG,aAAa,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC5F,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,GAAG,IAAI,eAAe,CAAC,CAAC;IAE1E,6FAA6F;IAC7F,gFAAgF;IAChF,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,8BAA8B,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAChD,wBAAwB,CAAC,KAAK,EAAE,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,CACzD,CAAC;IAEF,8FAA8F;IAC9F,kGAAkG;IAClG,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,SAAS,EAAE,CAAC;QAChE,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,2BAA2B,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,EAAE,GAAG,GAAG,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAmB,CAAC;AAC5F,CAAC;AAED,SAAS,wBAAwB,CAAC,GAAY,EAAE,IAAY;IAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,qBAAqB,CAAC,CAAC;IAC9D,CAAC;IACD,gGAAgG;IAChG,gGAAgG;IAChG,+FAA+F;IAC/F,oDAAoD;IACpD,MAAM,UAAU,GACd,GAAG,CAAC,YAAY,CAAC,KAAK,SAAS;QAC7B,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,GAAG,IAAI,gBAAgB,CAAC,CAAC;IAChE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,OAAO,EAAE,GAAG,GAAG,EAAE,UAAU,EAAsB,CAAC;AACpD,CAAC;AAED;;;GAGG;AACH,SAAS,cAAc,CAAC,GAA4B,EAAE,IAAY;IAChE,MAAM,UAAU,GAAI,CAAC,OAAO,EAAE,aAAa,EAAE,cAAc,CAAW,CAAC,MAAM,CAC3E,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,CAClC,CAAC;IACF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,mBAAmB,CAC3B,GAAG,IAAI,0DAA0D,CAClE,CAAC;IACJ,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,mBAAmB,CAC3B,GAAG,IAAI,cAAc,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,uBAAuB,CAChG,CAAC;IACJ,CAAC;IAED,2FAA2F;IAC3F,+DAA+D;IAC/D,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,SAAS;QAAE,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,WAAW,CAAC,CAAC;IAChF,IAAI,GAAG,CAAC,aAAa,CAAC,KAAK,SAAS;QAAE,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,GAAG,IAAI,iBAAiB,CAAC,CAAC;IAClG,IAAI,GAAG,CAAC,cAAc,CAAC,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,mBAAmB,CAAC,GAAG,IAAI,oCAAoC,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,KAAc,EAAE,KAAa;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,mBAAmB,CAC3B,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,KAAK,aAAa,CAAC,CAAC,CAAC,GAAG,KAAK,mBAAmB,CAC1E,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,mBAAmB,CAAC,GAAG,KAAK,oBAAoB,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
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 {
@@ -23,10 +23,46 @@ export interface CompositeIndex {
23
23
  fields: IndexField[];
24
24
  [key: string]: unknown;
25
25
  }
26
+ /**
27
+ * One entry of a field override's `indexes` array: a single-field index.
28
+ *
29
+ * The same three configs as `IndexField`, minus `fieldPath` — the override names the field once,
30
+ * above — plus the `queryScope` a composite index carries at the top level. A single-field index is
31
+ * one field, so the scope is the only thing that varies from entry to entry besides the direction.
32
+ */
33
+ export interface SingleFieldIndex {
34
+ /**
35
+ * `COLLECTION` or `COLLECTION_GROUP` in practice; not validated against an enumeration. `parse.ts`
36
+ * fills in `COLLECTION` when the declaration omits it, as the Firebase CLI does.
37
+ */
38
+ queryScope: string;
39
+ order?: string;
40
+ arrayConfig?: string;
41
+ vectorConfig?: Record<string, unknown>;
42
+ [key: string]: unknown;
43
+ }
44
+ /**
45
+ * One entry of the document's `fieldOverrides` array.
46
+ *
47
+ * Firestore indexes every field of every document by default; an override replaces that default
48
+ * for one field of one collection group with the set declared in `indexes`. The set is the *whole*
49
+ * configuration the field ends up with, not a delta — an export materialises the defaults an
50
+ * override keeps — and an empty set is an exemption: the field is not indexed at all.
51
+ *
52
+ * `ttl` is carried and not analysed. It decides when a document is deleted, not which queries are
53
+ * served, so nothing that reads a canonical form has a use for it.
54
+ */
55
+ export interface FieldOverride {
56
+ collectionGroup: string;
57
+ fieldPath: string;
58
+ indexes: SingleFieldIndex[];
59
+ ttl?: boolean;
60
+ [key: string]: unknown;
61
+ }
26
62
  /** A parsed and validated `firestore.indexes.json`. */
27
63
  export interface IndexDocument {
28
64
  indexes: CompositeIndex[];
29
- fieldOverrides?: unknown[];
65
+ fieldOverrides?: FieldOverride[];
30
66
  [key: string]: unknown;
31
67
  }
32
68
  /** A field reduced to the two things the canonical key is built from. */
@@ -52,9 +88,36 @@ export interface AnalysedIndex {
52
88
  */
53
89
  readonly redundantNameDirection: string | null;
54
90
  }
91
+ /** A single-field index reduced to the two things its canonical form is built from. */
92
+ export interface CanonicalSingleFieldIndex {
93
+ queryScope: string;
94
+ /** `ASCENDING`, `DESCENDING`, `CONTAINS`, or `VECTOR(<dimension>)`. */
95
+ direction: string;
96
+ }
97
+ /**
98
+ * A field override with its canonical form precomputed, the counterpart of `AnalysedIndex`.
99
+ *
100
+ * Built by `analyseOverrides` and shared with `@indexwright/record`, which reconciles it against a
101
+ * live field listing; a rule reading `fieldOverrides` reads this rather than the source (issues #53
102
+ * and #54 ask for one model, not two).
103
+ */
104
+ export interface AnalysedOverride {
105
+ /** The declaration as written, including any keys indexwright does not understand. */
106
+ readonly source: FieldOverride;
107
+ /** Position within the document's `fieldOverrides` array, for messages that need to point at one. */
108
+ readonly position: number;
109
+ readonly collectionGroup: string;
110
+ readonly fieldPath: string;
111
+ /**
112
+ * The declared set, in canonical order: sorted by query scope, then direction, with entries alike
113
+ * in both collapsed to one (`source.indexes` keeps them all). Empty for an exemption.
114
+ */
115
+ readonly indexes: readonly CanonicalSingleFieldIndex[];
116
+ readonly key: string;
117
+ }
55
118
  export declare const RULE_IDS: readonly ["scope-mismatch", "field-order-variant", "explicit-name-field", "quota-headroom"];
56
119
  export type RuleId = (typeof RULE_IDS)[number];
57
- /** A single warning. Never an error: see SPEC §7. */
120
+ /** A single warning. Never an error: see SPEC §8. */
58
121
  export interface Finding {
59
122
  rule: RuleId;
60
123
  file: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsDH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,gBAAgB;IAChB,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;CACR,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyHH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,gBAAgB;IAChB,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;CACR,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indexwright",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Linter for Firestore composite index declarations (firestore.indexes.json)",
5
5
  "keywords": [
6
6
  "firestore",
@@ -10,11 +10,12 @@
10
10
  "linter",
11
11
  "static-analysis"
12
12
  ],
13
- "homepage": "https://github.com/uny/indexwright#readme",
13
+ "homepage": "https://github.com/uny/indexwright/tree/main/packages/indexwright#readme",
14
14
  "bugs": "https://github.com/uny/indexwright/issues",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "git+https://github.com/uny/indexwright.git"
17
+ "url": "git+https://github.com/uny/indexwright.git",
18
+ "directory": "packages/indexwright"
18
19
  },
19
20
  "license": "Apache-2.0",
20
21
  "author": "Yuki Nagai",
@@ -33,22 +34,18 @@
33
34
  "files": [
34
35
  "dist",
35
36
  "README.md",
36
- "SPEC.md",
37
37
  "CHANGELOG.md",
38
38
  "LICENSE"
39
39
  ],
40
40
  "engines": {
41
41
  "node": ">=22"
42
42
  },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
43
46
  "scripts": {
44
47
  "build": "tsc --build --force",
45
48
  "typecheck": "tsc --noEmit",
46
- "test": "npm run build && node --test \"test/*.test.js\"",
47
- "verify-package": "npm run build && node scripts/verify-package.mjs",
48
49
  "prepublishOnly": "npm run build"
49
- },
50
- "devDependencies": {
51
- "@types/node": "^22.10.2",
52
- "typescript": "^5.7.2"
53
50
  }
54
51
  }
package/SPEC.md DELETED
@@ -1,386 +0,0 @@
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.