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 +73 -1
- package/README.md +20 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +26 -1
- package/dist/args.js.map +1 -1
- package/dist/collections.d.ts +1 -1
- package/dist/collections.js +1 -1
- package/dist/format/github.d.ts +1 -1
- package/dist/format/github.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +30 -2
- package/dist/key.d.ts.map +1 -1
- package/dist/key.js +58 -0
- package/dist/key.js.map +1 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +55 -2
- package/dist/parse.js.map +1 -1
- package/dist/types.d.ts +66 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -10
- package/SPEC.md +0 -386
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.
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
Coverage is bounded by what actually exercises the proxy. A query no test issues is not observed,
|
|
159
|
+
and absence of observation is not evidence that an index is unused. That limit is inherent; the
|
|
160
|
+
narrower ones that are not — the Firebase Web SDK's transport, and snapshot listeners — are
|
|
161
|
+
[named in the spec](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
|
|
175
|
-
npm run verify-package # builds, packs, installs
|
|
178
|
+
npm test # builds, then runs both suites against dist/
|
|
179
|
+
npm run verify-package # builds, packs, installs each tarball, exercises the bins and the APIs
|
|
176
180
|
```
|
|
177
181
|
|
|
178
182
|
Both scripts build first as an explicit step rather than through a `pre` hook, because npm skips
|
|
179
183
|
`pre`/`post` scripts entirely under `ignore-scripts=true` — a setting many developers turn on. With
|
|
180
184
|
a hook, that configuration silently tests whatever `dist/` happened to be lying around.
|
|
181
185
|
|
|
186
|
+
The packages version independently and release from separate tags: `v0.2.0` publishes the linter,
|
|
187
|
+
`record-v0.2.0` publishes the recorder.
|
|
188
|
+
|
|
182
189
|
## License
|
|
183
190
|
|
|
184
191
|
Apache-2.0
|
package/dist/args.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type Command = LintCommand | {
|
|
|
18
18
|
kind: 'version';
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
|
-
* Parsed in-tree rather than with a dependency (SPEC §
|
|
21
|
+
* Parsed in-tree rather than with a dependency (SPEC §8). Supports `--flag value` and
|
|
22
22
|
* `--flag=value`, and `--` to end option parsing.
|
|
23
23
|
*/
|
|
24
24
|
export declare function parseArgs(argv: readonly string[]): Command;
|
package/dist/args.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEvD,6EAA6E;AAC7E,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,gBAAgB;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEvD,6EAA6E;AAC7E,qBAAa,UAAW,SAAQ,KAAK;IACnC,SAAkB,IAAI,gBAAgB;CACvC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAoB3E;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA2F1D;AA+DD,wBAAgB,KAAK,IAAI,MAAM,CA6B9B"}
|
package/dist/args.js
CHANGED
|
@@ -7,7 +7,17 @@ export class UsageError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
const FORMATS = ['text', 'json', 'github'];
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Verbs that belong to the family but not to this package.
|
|
11
|
+
*
|
|
12
|
+
* `indexwright` carries no runtime dependency in any version (SPEC §8), so capture — which needs a
|
|
13
|
+
* proxy and a wire decoder — ships separately. Only verbs that exist are listed: pointing at a
|
|
14
|
+
* package that does not implement one yet would be a worse answer than not knowing.
|
|
15
|
+
*/
|
|
16
|
+
const ELSEWHERE = {
|
|
17
|
+
record: { home: '@indexwright/record', bin: 'indexwright-record' },
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parsed in-tree rather than with a dependency (SPEC §8). Supports `--flag value` and
|
|
11
21
|
* `--flag=value`, and `--` to end option parsing.
|
|
12
22
|
*/
|
|
13
23
|
export function parseArgs(argv) {
|
|
@@ -19,6 +29,18 @@ export function parseArgs(argv) {
|
|
|
19
29
|
return { kind: 'version' };
|
|
20
30
|
const [command, ...rest] = argv;
|
|
21
31
|
if (command !== 'lint') {
|
|
32
|
+
// SPEC §3: the cost of splitting the family into two packages is a second package to
|
|
33
|
+
// discover, so a verb that lives in the other one has to say where it went. Reporting it as
|
|
34
|
+
// an unknown command would read as "indexwright cannot do this".
|
|
35
|
+
// An own-property lookup, not `in`: `ELSEWHERE` inherits from `Object.prototype`, so `in`
|
|
36
|
+
// also answers yes for `constructor` and `toString` and the message would then name the
|
|
37
|
+
// package as "undefined" instead of reporting an unknown command.
|
|
38
|
+
const elsewhere = Object.hasOwn(ELSEWHERE, command ?? '') ? ELSEWHERE[command] : undefined;
|
|
39
|
+
if (elsewhere !== undefined) {
|
|
40
|
+
const { home, bin } = elsewhere;
|
|
41
|
+
throw new UsageError(`"${command}" is not part of indexwright; it ships as ${home}. ` +
|
|
42
|
+
`Install it with "npm install --save-dev ${home}" and run "${bin}".`);
|
|
43
|
+
}
|
|
22
44
|
throw new UsageError(`unknown command "${command}"; the only command is "lint"`);
|
|
23
45
|
}
|
|
24
46
|
const files = [];
|
|
@@ -165,6 +187,9 @@ export function usage() {
|
|
|
165
187
|
' 0 completed; warnings may have been emitted',
|
|
166
188
|
' 1 warning count exceeded --max-warnings',
|
|
167
189
|
' 2 usage error, unreadable file, or malformed input',
|
|
190
|
+
'',
|
|
191
|
+
'Capturing the queries a test suite issues is a separate package, @indexwright/record,',
|
|
192
|
+
'so that this one keeps no runtime dependencies.',
|
|
168
193
|
].join('\n');
|
|
169
194
|
}
|
|
170
195
|
//# sourceMappingURL=args.js.map
|
package/dist/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,6EAA6E;AAC7E,MAAM,OAAO,UAAW,SAAQ,KAAK;IACjB,IAAI,GAAG,YAAY,CAAC;CACvC;AAcD,MAAM,OAAO,GAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
|
package/dist/collections.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §
|
|
1
|
+
/** Small ordering and grouping helpers. Determinism is a stated requirement (SPEC §8). */
|
|
2
2
|
export declare function compareStrings(a: string, b: string): number;
|
|
3
3
|
/** Group while remembering first-seen order, so callers can sort explicitly rather than by luck. */
|
|
4
4
|
export declare function groupBy<T>(items: Iterable<T>, keyOf: (item: T) => string): Map<string, T[]>;
|
package/dist/collections.js
CHANGED
package/dist/format/github.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface GithubOutput {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Findings are annotations without a line number: locating a finding inside the JSON would need a
|
|
10
|
-
* position-tracking parser, which v0.1.0 does not carry (SPEC §
|
|
10
|
+
* position-tracking parser, which v0.1.0 does not carry (SPEC §5).
|
|
11
11
|
*/
|
|
12
12
|
export declare function formatGithub(result: LintResult): GithubOutput;
|
|
13
13
|
//# sourceMappingURL=github.d.ts.map
|
package/dist/format/github.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { oneLine } from './inline.js';
|
|
2
2
|
/**
|
|
3
3
|
* Findings are annotations without a line number: locating a finding inside the JSON would need a
|
|
4
|
-
* position-tracking parser, which v0.1.0 does not carry (SPEC §
|
|
4
|
+
* position-tracking parser, which v0.1.0 does not carry (SPEC §5).
|
|
5
5
|
*/
|
|
6
6
|
export function formatGithub(result) {
|
|
7
7
|
const commands = [
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The JavaScript API.
|
|
3
3
|
*
|
|
4
|
-
* Provisional before 1.0 (SPEC §
|
|
4
|
+
* Provisional before 1.0 (SPEC §10): only the `json` output shape carries the compatibility promise.
|
|
5
5
|
* This exists so the rules can be run without spawning a process — from a test, a codemod, or a
|
|
6
6
|
* bespoke reporter.
|
|
7
7
|
*/
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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 §
|
|
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,
|
|
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:
|
|
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,
|
|
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":"
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"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"}
|
package/dist/parse.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
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
|
-
|
|
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":"
|
|
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 §
|
|
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?:
|
|
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 §
|
|
120
|
+
/** A single warning. Never an error: see SPEC §8. */
|
|
58
121
|
export interface Finding {
|
|
59
122
|
rule: RuleId;
|
|
60
123
|
file: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,4FAA4F;IAC5F,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,
|
|
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 §
|
|
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;
|
|
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.
|
|
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.
|