@chenglou/freerange 0.0.2 → 0.0.4

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
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.4 - 2026-07-31
6
+
7
+ - Allow `Date.now()` to represent dates before the Unix epoch.
8
+ - Support numeric phantom types (#6).
9
+ - Only recognize `Math`, `Number`, `Infinity`, parser functions, and browser globals when they resolve to TypeScript's standard libraries.
10
+
11
+ ## 0.0.3 - 2026-07-28
12
+
13
+ - Analyze arrow functions and function expressions assigned directly to top-level `const` names.
14
+ - Preserve type narrowing through `&&` and `||` expressions.
15
+ - Analyze each `Math.random()` call as a fresh number from zero up to, but not including, one.
16
+ - Avoid quadratic work when tracking values through deep chains of same-file function calls (#3).
17
+ - Consolidate analysis limits and practical refactoring examples in the README.
18
+
5
19
  ## 0.0.2 - 2026-07-21
6
20
 
7
21
  - Add node support (#2)
package/README.md CHANGED
@@ -117,45 +117,249 @@ Every plain `number` parameter, including a number field in a fixed-shape object
117
117
 
118
118
  ## Writing Analyzable TypeScript
119
119
 
120
- Freerange supports a subset of TS:
121
- - Named, synchronous top-level functions in a file; Freerange follows calls between functions in the same file
120
+ Freerange deliberately analyzes a restricted part of TypeScript. When code leaves that scope, `fr --audit` says so instead of guessing what the code does. Freerange currently supports:
121
+
122
+ - Named, synchronous top-level functions. Both `function size(...) {}` and direct `const size = (...) => ...` declarations work. Freerange follows calls between functions in the same file
122
123
  - Numbers, booleans, strings, nullable values, plain objects, tagged unions, dense arrays, and fixed tuples
123
124
  - `if`/`else`, ternaries, non-fallthrough `switch`, `&&`, `||`, `!`, `??`, `for`, `while`, and `for...of` loops
124
125
  - Arithmetic, comparisons, object field and array reads, selected `Math` operations, and `Number.isInteger`, `Number.isFinite`, and `Number.isNaN`
125
126
 
126
127
  Freerange could theoretically support a much larger subset of TS, and did before its public release. Those patterns often made numeric inference and proofs much harder and slower, however, and some questions are undecidable in general. Now that AI agents write code, we strongly recommend asking agents to refactor important calculations into shapes that Freerange analyzes well, guided by `fr --audit`. Code that is easy to analyze tends to resemble functional programming: immutable data, explicit inputs and outputs, and clean, direct control flow.
127
128
 
128
- - **Put important calculations in small named functions.** A React component, callback, or async function can call a plain synchronous helper. Keep any helper functions that Freerange needs to inspect in the same file.
129
- ```tsx
130
- export function fittedImageHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {
131
- const width = Math.max(1, imageWidth)
132
- const height = Math.max(1, imageHeight)
133
- return (frameWidth * height) / width
134
- }
129
+ Use precise TypeScript types. Avoid `any`, casts, and suppression comments, and parse external data before passing it to a numeric helper. A file containing `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, or `eval` is rejected because its declared types cannot be trusted.
130
+
131
+ Before changing an input rule, decide what the application should do. If `columnCount` must be a positive integer, either require that with leading `console.assert` calls or normalize it with `Math.max(1, Math.floor(columnCount))`. Only normalize when the application wants that runtime behavior. Audit code: `[encode-input-rule]`.
132
+
133
+ ### Numeric Analysis
134
+
135
+ For each `number`, Freerange remembers its lowest and highest possible values, whether it is an integer, whether it may be `NaN` or infinite, and at most one exact value that has been ruled out. It reasons about JavaScript floating-point numbers rather than ideal real numbers and does not use a general-purpose theorem prover.
136
+
137
+ Freerange also supports numeric phantom types, e.g. `type Pixels = number & {readonly __brand: 'pixels'}`.
138
+
139
+ #### No common-subexpression elimination
140
+
141
+ Freerange does not replace repeated calculations with one stored result, even when their source code is identical. This function checks one subtraction, then divides by a newly evaluated subtraction:
142
+
143
+ ```ts
144
+ export function progressBar(value: number, start: number, end: number): number {
145
+ if (end - start === 0) return 0
146
+ return (value - start) / (end - start)
147
+ }
148
+ ```
149
+
150
+ Calculate the subtraction once when the check and division must use the same result:
151
+
152
+ ```ts
153
+ export function progressBar(value: number, start: number, end: number): number {
154
+ const span = end - start
155
+ if (span === 0) return 0
156
+ return (value - start) / span
157
+ }
158
+ ```
159
+
160
+ Freerange recognizes aliases, repeated reads of the same immutable field or array position, and the same argument passed to multiple parameters. A newly evaluated calculation or function call is a new value. Audit code: `[guard-derived-value]`.
161
+
162
+ #### Ranges use inclusive endpoints
163
+
164
+ Freerange stores every range using its lowest and highest included values. JavaScript numbers are discrete, so the neighboring representable number can often express a strict endpoint: `Math.random()` is stored as `0..0.9999999999999999` and reported as `at least 0 and less than 1`. This needs no rewrite.
165
+
166
+ #### Branches merge into one continuous range
167
+
168
+ Freerange combines both branch results into one continuous range. Here `width` becomes `240..480`, which includes `300` even though neither branch returns it:
169
+
170
+ ```ts
171
+ export function previewRatio(compact: boolean): number {
172
+ const width = compact ? 240 : 480
173
+ return 100 / (width - 300)
174
+ }
175
+ ```
176
+
177
+ Keep a calculation inside each branch when it depends on the separate alternatives:
178
+
179
+ ```ts
180
+ export function previewRatio(compact: boolean): number {
181
+ if (compact) return 100 / (240 - 300)
182
+ return 100 / (480 - 300)
183
+ }
184
+ ```
185
+
186
+ A broad but safe range may need no rewrite.
187
+
188
+ #### A number remembers at most one excluded value
189
+
190
+ After `code !== 240` and `code !== 300`, Freerange may remember only the later exclusion. When an operation depends on a particular exclusion, check the value that the operation uses:
191
+
192
+ ```ts
193
+ const divisor = code - 240
194
+ if (divisor === 0) return 0
195
+ return 100 / divisor
196
+ ```
197
+
198
+ Freerange does not retain arbitrary sets such as "every number except 240 and 300."
199
+
200
+ #### No transitive reasoning between comparisons
201
+
202
+ Freerange does not combine `left <= middle` and `middle <= right` to prove `left <= right`:
203
+
204
+ ```ts
205
+ if (left > middle) throw new Error('out of order')
206
+ if (middle > right) throw new Error('out of order')
207
+ console.assert(left <= right) // unproven
208
+ ```
209
+
210
+ When one value is built from another, keep the relationship visible in that calculation:
211
+
212
+ ```ts
213
+ const gap = Math.max(0, requestedGap)
214
+ const left = navRight + gap
215
+ console.assert(navRight <= left)
216
+ ```
217
+
218
+ If the values arrive independently, Freerange may leave the relationship unproven.
219
+
220
+ #### No algebraic inversion of conditions
221
+
222
+ Freerange narrows the direct operands of a comparison but does not rearrange `width * 2 > 10` to derive `width > 5`. Check the value used by the later operation:
223
+
224
+ ```ts
225
+ export function previewScale(width: number): number {
226
+ if (width <= 5) return 1
227
+ return 100 / width
228
+ }
229
+ ```
230
+
231
+ #### No algebraic normalization
232
+
233
+ Freerange does not rewrite expressions using associativity, commutativity, or distributivity. Those rules do not always preserve JavaScript floating-point results:
234
+
235
+ ```ts
236
+ const amount = 9_007_199_254_740_992
237
+ const first = 3 + amount + 2 // 9007199254740998
238
+ const second = 1 + amount + 4 // 9007199254740996
239
+ ```
240
+
241
+ Choose the operation order whose floating-point behavior the application wants. For example, `frameWidth / (imageWidth / imageHeight)` introduces a ratio that can round to zero; `(frameWidth * imageHeight) / imageWidth` avoids that particular problem, although the two expressions can still round differently. Audit code: `[use-direct-operands]`.
242
+
243
+ #### Numeric truthiness is unsupported
244
+
245
+ Freerange does not guess what a number used as a condition means. Write `width === 0` instead of relying on a truthy or falsy number such as `width || 1`. Audit code: `[write-explicit-condition]`.
246
+
247
+ ### Functions
248
+
249
+ Put important calculations in named synchronous functions. A React component, callback, or async function can call a plain helper:
250
+
251
+ ```tsx
252
+ export function fittedImageHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {
253
+ return (frameWidth * Math.max(1, imageHeight)) / Math.max(1, imageWidth)
254
+ }
255
+
256
+ function ImageCard(props: {frameWidth: number; imageWidth: number; imageHeight: number}) {
257
+ const height = fittedImageHeight(props.frameWidth, props.imageWidth, props.imageHeight)
258
+ return <img style={{height}} />
259
+ }
260
+ ```
261
+
262
+ Literal default parameters and omitted optional parameters work in supported same-file calls. Object and calculated defaults do not. Passing more arguments than the implementation declares is unsupported.
135
263
 
136
- function ImageCard(props: {frameWidth: number; imageWidth: number; imageHeight: number}) {
137
- const height = fittedImageHeight(props.frameWidth, props.imageWidth, props.imageHeight)
138
- return <img style={{height}} />
264
+ #### A function return does not include how the value was calculated
265
+
266
+ Freerange evaluates supported same-file helpers using what the caller knows. It keeps what the helper may return, including numeric ranges and object fields, but not an equation such as "this result is exactly `end - start`":
267
+
268
+ ```ts
269
+ function span(start: number, end: number): number {
270
+ return end - start
271
+ }
272
+
273
+ export function progressBar(value: number, start: number, end: number): number {
274
+ return (value - start) / span(start, end)
275
+ }
276
+ ```
277
+
278
+ When the caller needs that exact calculation, calculate and check it in the caller, then pass the result to a helper. The same limitation applies to booleans: `true` from `isValidIndex(values, index)` does not tell the caller which checks made it true, so write those checks where they protect the array read.
279
+
280
+ #### Imported function bodies are not analyzed
281
+
282
+ Freerange does not follow imported functions. Keep a numeric rule that needs analysis in a supported helper with explicit inputs:
283
+
284
+ ```ts
285
+ export function labelWidthFromMeasurement(measuredWidth: number): number {
286
+ return Math.max(120, measuredWidth + 32)
287
+ }
288
+ ```
289
+
290
+ An unsupported caller can pass `measureText(text)` into this helper, allowing Freerange to verify the rule but not the imported measurement. Imported constants work when their initializer resolves to a numeric literal such as `export const GAP = 24`. Runtime import cycles are outside Freerange's scope: an imported module must finish initializing before analyzed code reads its values. Type-only import cycles are fine.
291
+
292
+ #### No higher-order function analysis
293
+
294
+ Freerange does not analyze callbacks passed to higher-order functions such as `reduce`, `map`, or `filter`. For a simple scalar aggregation, write the loop directly:
295
+
296
+ ```ts
297
+ export function totalWidth(widths: number[]): number {
298
+ let total = 0
299
+ for (let index = 0; index < widths.length; index += 1) {
300
+ total += widths[index]!
139
301
  }
140
- ```
302
+ return total
303
+ }
304
+ ```
305
+
306
+ This is not a general replacement for `map` or `filter`: object and array writes remain unsupported, and callback arguments, effects, and result allocation may matter. Audit code: `[use-loop-for-aggregation]`.
307
+
308
+ ### Objects, Arrays, and Changing State
309
+
310
+ Freerange reads plain objects, fixed tuples, dense arrays, and tagged unions declared in the project through at most eight nested levels. Give each union case a tag, use an exhaustive non-fallthrough `switch`, and keep deeply nested or unclassifiable data outside important numeric helpers.
311
+
312
+ Freerange assumes that property reads are stable and perform no work during one analyzed synchronous call. A getter or Proxy that changes its answer or performs work is outside the scope.
313
+
314
+ #### No object and array writes
315
+
316
+ Freerange allows local variables to be reassigned but does not track writes through an object or array. Return a new value when the application does not require mutation or stable object identity:
317
+
318
+ ```ts
319
+ export function moveRight(point: {x: number; y: number}, distance: number): {x: number; y: number} {
320
+ return {x: point.x + distance, y: point.y}
321
+ }
322
+ ```
323
+
324
+ Object spread is also unsupported because JavaScript copies only an object's own enumerable properties, which may not match the fields declared by its TypeScript type. List the fields explicitly. Rebuilding an object is not equivalent to mutation when other code observes its identity or the mutation.
325
+
326
+ #### Reads of changing state are not referentially transparent
327
+
328
+ Referential transparency means that evaluating the same expression again is equivalent to reusing its previous result. Freerange does not make that assumption for a clock, viewport, scroll position, or mutable module binding. Store one read when the check and later use should observe the same value:
141
329
 
142
- - **Name a calculation before checking it.** If the divisor is `oldMax - oldMin`, write `const oldSpan = oldMax - oldMin`, check `oldSpan === 0`, and divide by `oldSpan`. Checking `oldMin === oldMax` does not tell Freerange about the separately calculated `oldSpan`. Audit code: `[guard-derived-value]`.
330
+ ```ts
331
+ export function viewportScale(): number {
332
+ const viewportWidth = window.innerWidth
333
+ if (viewportWidth === 0) return 0
334
+ return 100 / viewportWidth
335
+ }
336
+ ```
143
337
 
144
- - **Decide how invalid inputs should be handled before using them.** If `columnCount` must be a positive integer, either require that with leading `console.assert` calls or normalize it with `Math.max(1, Math.floor(columnCount))`. Only normalize when the application actually wants that runtime behavior. Audit code: `[encode-input-rule]`.
338
+ Keep separate reads when two observations are intentional, such as two clock reads used to measure elapsed time.
145
339
 
146
- - **Choose the order of arithmetic deliberately.** Formulas that are equivalent on paper can round, overflow, or underflow differently with JavaScript numbers. For example, `frameWidth / (imageWidth / imageHeight)` introduces a ratio that can round to zero; `(frameWidth * imageHeight) / imageWidth` avoids that particular problem. The two expressions can still round differently. Audit code: `[use-direct-operands]`.
340
+ #### Array reads require dense arrays and valid indexes
147
341
 
148
- - **Decide what a missing array element means.** Use `values[index] ?? fallback` only when the application really wants a fallback. Otherwise, prove that `index` is an integer from zero through `values.length - 1` before using `values[index]!`. A bounds check cannot detect a hole in a sparse array, so Freerange expects arrays to be dense. Audit codes: `[handle-missing-element]`, `[guard-array-index]`.
342
+ Use `values[index] ?? fallback` only when the application wants a fallback. Otherwise, prove that `index` is an integer from zero through `values.length - 1` before using `values[index]!`. A bounds check cannot detect a hole in a sparse array, so Freerange expects arrays to be dense. Audit codes: `[handle-missing-element]`, `[guard-array-index]`.
343
+
344
+ ### Loops
149
345
 
150
- - **Write the condition and loop directly.** Prefer `width === 0` over using a number as a condition, such as `width || 1`. Use a regular loop for a simple dense-array calculation when callback arguments, callback effects, and a newly allocated result array do not matter. Audit codes: `[write-explicit-condition]`, `[use-loop-for-aggregation]`.
346
+ #### Loops find stable ranges, not exact formulas
151
347
 
152
- - **Write object copies explicitly.** Use `{width: layout.width, height: layout.height}` instead of `{...layout}`. Use dense arrays and fixed-length tuples, and do not modify objects or arrays after creating them. Rebuilding an object is not equivalent to mutation when other code observes its identity or the mutation.
348
+ Freerange checks a loop until the possible values at the start of an iteration stop changing. It does not simulate the exact runtime iteration count or derive a formula for the final value:
153
349
 
154
- - **Give each union case a tag and switch on it.** Use a tagged union when different cases carry different fields. Make the `switch` exhaustive and do not use fallthrough.
350
+ ```ts
351
+ export function fixedTotal(): number {
352
+ let total = 0
353
+ for (let index = 0; index < 3; index += 1) {
354
+ total += 2
355
+ }
356
+ return total
357
+ }
358
+ ```
155
359
 
156
- - **Check and use the same local value.** When a value comes from module, class, or reactive state, store it in a local first. For example, write `const currentScale = scale; if (currentScale !== null) return currentScale` instead of checking one read of `scale` and returning another.
360
+ Freerange knows that the result is a nonnegative integer but does not derive that it is exactly `6`. Ordinary counting loops usually settle after two or three checks. If a range still changes after 16 checks, Freerange stops analyzing that path. Write a formula directly when it is the intended implementation, but do not replace repeated floating-point arithmetic with multiplication unless the different rounding behavior is acceptable.
157
361
 
158
- - **Use precise TypeScript types.** Avoid `any`, casts, and suppression comments. Parse external data before passing it to a numeric helper, give the helper typed parameters, and pass only the fields it uses. A file containing `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, or `eval` is rejected because its declared types cannot be trusted.
362
+ Code outside this scope may make a result less precise or stop analysis. Freerange does not publish a stronger guarantee by pretending that unsupported code was understood. If an unsupported pattern is important and cannot be reasonably refactored, please file an issue.
159
363
 
160
364
  ## Recommended TypeScript Config
161
365
 
@@ -186,55 +390,17 @@ Freerange uses a few terms consistently:
186
390
  - `partially supported`: Freerange can analyze some, but not all, of the function.
187
391
  - `skipped`: some top-level statements in the modules weren't analyzed.
188
392
 
189
- A caller requirement is not automatically a bug. For example, `requires: columns >= 1` means the function is safe under that condition; it does not mean Freerange found a caller passing zero. Freerange checks supported same-file calls, but it is not a repository-wide call-site verifier. Imported calls and unsupported callers may remain unchecked.
190
-
191
- An `ensures` line assumes its `requires` and `assumes`. A requirement may be a real API rule, or it may expose a relationship Freerange cannot currently prove. An assumption may identify a real input boundary or an analysis limitation. Decide what the program should do before changing code to remove either one.
192
-
193
- Always read the coverage line. No findings does not mean an unsupported file is safe. A derived guarantee becoming weaker, for example `at least 54` becoming `at least 0`, appears in the audit rather than the shorter findings output.
194
-
195
- ## Analysis Scope
196
-
197
- Freerange deliberately analyzes a restricted part of TypeScript. When code leaves that scope, `fr --audit` says so instead of guessing what the code does. If an unsupported pattern is important in production code and cannot be reasonably refactored, please file an issue. We're open to expanding the scope when the added complexity proves worthwhile.
198
-
199
- ### Numbers
200
-
201
- Freerange's numeric analysis is designed for layouts and other everyday application code. For each TypeScript `number`, Freerange remembers its lowest and highest possible values, whether it is an integer, whether it may be `NaN` or infinite, and at most one exact value that has been ruled out. For example, after `value !== 0`, Freerange remembers that `value` cannot be `0`.
393
+ ### Caller Requirements
202
394
 
203
- Freerange does not keep gaps or arbitrary sets of possible numbers. If one branch produces `1..2` and another produces `10..11`, Freerange keeps the combined range `1..11`. A later check that rules out a different exact value may replace the value remembered from an earlier check.
395
+ Every plain `number` parameter must be finite and not `NaN`. The same rule applies to numeric fields in fixed-shape object parameters, even when the function does not read them. Numeric literal types such as `1 | 2` already satisfy the rule. Nullable numbers, arrays, tuples, and tagged unions use more specific `assumes` lines instead. A supported literal default can satisfy the requirement when a caller omits an argument.
204
396
 
205
- Freerange recognizes repeated uses of the same stored value, including local aliases, stable property and array reads, lengths, and the same argument passed to multiple parameters of a function in the same file. For example, `const span = right - left; span - span` is exactly `0` unless `span` is infinite or `NaN`. Two separately evaluated expressions are not assumed to produce the same value just because their source code looks alike. Store the result in a local when the equality matters.
397
+ Division and array reads can create additional requirements. Freerange tries to express them using the function's parameters so that supported same-file callers can prove them, pass them to their own callers, or report a definitely invalid argument. If a condition cannot be expressed that way, `fr --audit` prints a local `assumes` line instead.
206
398
 
207
- Freerange does not search for arbitrary relationships between different values or use a general-purpose theorem prover. Those approaches made earlier versions less predictable without proving much more real code. Freerange also reasons about JavaScript floating-point numbers rather than ideal real numbers. Real-number algebra would produce false guarantees because JavaScript arithmetic rounds and can overflow or underflow.
208
-
209
- ### Function calls
210
-
211
- Freerange analyzes supported function calls in the same file using what it knows at each call. It does not analyze imported functions. It reads an imported constant only when the constant resolves to a numeric literal such as `export const GAP = 24`. Literal default parameters work in supported calls; object and calculated defaults do not. Passing more arguments than the implementation declares is also unsupported.
212
-
213
- Freerange does not guess what an unknown function does, when a callback runs, which exceptions are caught, how another reference might change an object, or how a framework schedules work.
214
-
215
- ### Static assertions
216
-
217
- Inside a `console.assert`, Freerange can prove several common UI calculations through named values: `Math.min` and `Math.max`, adding or subtracting a nonnegative value, multiplying both sides of a comparison by the same nonnegative value, `index % columnCount < columnCount` when `columnCount` is positive, and fields read from a newly created object. Freerange does not chain arbitrary comparisons: `left <= middle` and `middle <= right` do not by themselves prove `left <= right`.
218
-
219
- ### Loops
220
-
221
- Freerange checks a loop repeatedly until the possible values at the start of an iteration stop changing. It does not simulate every runtime iteration or try to derive a formula for the final value. Ordinary counting loops usually settle after two or three checks. If the possible values still change after 16 checks, Freerange stops analyzing that path.
222
-
223
- ### Objects and arrays
224
-
225
- Freerange reads plain objects, tuples, arrays, and tagged unions declared in your project through at most eight nested levels. A deeper property becomes unknown. A function is unsupported when one of its parameter types cannot be represented within this scope.
226
-
227
- Freerange assumes that reading a property returns the same value and performs no work during one analyzed function call. A getter or Proxy that changes its answer or performs work is outside the scope. Property writes, including assignments that invoke setters, are unsupported. Object spread is also unsupported because JavaScript copies only an object's own enumerable properties, which may not match the fields declared by its TypeScript type.
228
-
229
- ### Caller requirements
230
-
231
- Every plain `number` parameter must be finite and not `NaN`. The same rule applies to numeric fields in fixed-shape object parameters, even when the function does not read them. Numeric literal types such as `1 | 2` already satisfy the rule. Nullable numbers, arrays, tuples, and tagged unions use more specific `assumes` lines instead. When a caller omits an argument with a supported literal default, the default can satisfy the requirement automatically.
232
-
233
- Calls to supported functions in the same file either prove these requirements, require their own callers to satisfy them, or report a definitely invalid argument. After a call proves that an argument is finite, later uses of that same stored value can reuse the result. Writing `console.assert(Number.isFinite(value))` at the start of a function is allowed but normally redundant. `Number.isInteger(value)` is stronger and replaces the finite requirement.
399
+ A caller requirement is not automatically a bug. For example, `requires: columns >= 1` means the function is safe under that condition; it does not mean Freerange found a caller passing zero. Freerange checks supported same-file calls, but it is not a repository-wide call-site verifier. Imported calls and unsupported callers may remain unchecked.
234
400
 
235
- Division and array reads can create additional requirements. Freerange tries to express each requirement using the function's parameters so that callers can be checked. Later operations can reuse the requirement only when they use the same stored value in the same branch. Assigning a new value or repeating the calculation starts over. Freerange traces each intermediate value at most once; if it cannot express the condition using the parameters, `fr --audit` prints a local `assumes` condition instead.
401
+ An `ensures` line assumes its `requires` and `assumes`. A requirement may be a real API rule, or it may expose a relationship Freerange cannot currently prove. An assumption may identify a real input boundary or an analysis limitation. Decide what the program should do before changing code to remove either one.
236
402
 
237
- Code outside this scope may make a result less precise or stop analysis. Freerange does not publish a stronger guarantee by pretending that unsupported code was understood.
403
+ Always read the coverage line. No findings does not mean an unsupported file is safe. A derived guarantee becoming weaker, for example `at least 54` becoming `at least 0`, appears in the audit rather than the shorter findings output.
238
404
 
239
405
  ## Development
240
406