{"_id":"@chenglou/freerange","_rev":"5-2624cd1c0a1bf2875eaf41fa7d6a1156","name":"@chenglou/freerange","dist-tags":{"latest":"0.0.5"},"versions":{"0.0.1":{"name":"@chenglou/freerange","version":"0.0.1","license":"MIT","_id":"@chenglou/freerange@0.0.1","maintainers":[{"name":"chenglou","email":"chenglou92@gmail.com"}],"homepage":"https://github.com/chenglou/freerange#readme","bugs":{"url":"https://github.com/chenglou/freerange/issues"},"bin":{"fr":"fr.ts"},"dist":{"shasum":"f5b625136f72a785937e250843c7d09519e5aebb","tarball":"https://registry.npmjs.org/@chenglou/freerange/-/freerange-0.0.1.tgz","fileCount":36,"integrity":"sha512-RCdvTZX66Dp5roRrld+2GH4tJV+uyo21nEsF/lxwDBjzDFagG9CnJ7go5Qim2ZDHTC40lQWNF1AprDxTDQTxfg==","signatures":[{"sig":"MEUCIC5Q/wNIKSx3TW3y8R+rAdSkMAIxvAVCh4RSvJPC1HG+AiEAv3vk4dZTEMDs2oZRg5QYZTf8w7e4vZN6mWgLQL1LSFg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":559988},"type":"module","gitHead":"4c4522f8a2b7a925953bcbcfe174a6fba33ccbaa","scripts":{"fr":"bun fr.ts","demo":"HOST=${HOST:-127.0.0.1}; PORT=${PORT:-3000}; bun ./demo/index.html --host=$HOST:$PORT","check":"bunx @typescript/native && oxlint --type-aware && bun knip && bun test --parallel=4"},"_npmUser":{"name":"chenglou","email":"chenglou92@gmail.com"},"repository":{"url":"git+https://github.com/chenglou/freerange.git","type":"git"},"_npmVersion":"10.9.2","description":"Static numeric range analysis for TypeScript","directories":{},"_nodeVersion":"23.10.0","dependencies":{"typescript":"^6.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^6.14.2","oxlint":"latest","@types/bun":"^1.3.14","oxlint-tsgolint":"latest","@typescript/native":"npm:typescript@^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/freerange_0.0.1_1784593366455_0.810609407616049","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@chenglou/freerange","version":"0.0.2","license":"MIT","_id":"@chenglou/freerange@0.0.2","maintainers":[{"name":"chenglou","email":"chenglou92@gmail.com"}],"homepage":"https://github.com/chenglou/freerange#readme","bugs":{"url":"https://github.com/chenglou/freerange/issues"},"bin":{"fr":"dist/fr.js"},"dist":{"shasum":"8dba9516ff5e0f2f344d9e53f4bb3a116bbdfab5","tarball":"https://registry.npmjs.org/@chenglou/freerange/-/freerange-0.0.2.tgz","fileCount":37,"integrity":"sha512-xUsvKo4YyjuDqIqoOyxdXPPcd58GElU1Je/BX7gCG/DNtXAnRPq6QqivvwsycB0EFvYNsnWAXk7tOYfUzTnR1g==","signatures":[{"sig":"MEYCIQCKYeMpVxobiBqTKgXoZpfRADSL2FjSFCCnPSGr8Fwa0wIhAIeX/HHJCaT6LrhcdFc0HFCUpSEL2z5GVMNIPMPTtXTw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":888900},"type":"module","gitHead":"4e2124280748b15e43d62c810c10e5c5261da6d7","scripts":{"fr":"bun fr.ts","demo":"HOST=${HOST:-127.0.0.1}; PORT=${PORT:-3000}; bun ./demo/index.html --host=$HOST:$PORT","build":"bun build fr.ts --target=node --packages=external --banner=\"#!/usr/bin/env node\" --outfile=dist/fr.js","check":"bunx @typescript/native && oxlint --type-aware && bun knip && bun test --parallel=4","prepack":"bun run build"},"_npmUser":{"name":"chenglou","email":"chenglou92@gmail.com"},"repository":{"url":"git+https://github.com/chenglou/freerange.git","type":"git"},"_npmVersion":"10.9.2","description":"Static numeric range analysis for TypeScript","directories":{},"_nodeVersion":"23.10.0","dependencies":{"typescript":"^6.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^6.14.2","oxlint":"latest","@types/bun":"^1.3.14","oxlint-tsgolint":"latest","@typescript/native":"npm:typescript@^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/freerange_0.0.2_1784636665897_0.8107368568080242","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@chenglou/freerange","version":"0.0.3","license":"MIT","_id":"@chenglou/freerange@0.0.3","maintainers":[{"name":"chenglou","email":"chenglou92@gmail.com"}],"homepage":"https://github.com/chenglou/freerange#readme","bugs":{"url":"https://github.com/chenglou/freerange/issues"},"bin":{"fr":"dist/fr.js"},"dist":{"shasum":"ea75f88ed0032ee4f8393fd74a2b4ed0f969b221","tarball":"https://registry.npmjs.org/@chenglou/freerange/-/freerange-0.0.3.tgz","fileCount":39,"integrity":"sha512-564fVz+2a4vqdWyQq7+DQM1scZKTmET/wIvjxDvzVXQ7mSi1D3CB0C3PAO8F5H/NAD4Pkyx4VRtHfkaZVY3bEQ==","signatures":[{"sig":"MEYCIQCkDZ1Jbu7b78JjKK41B43QgwLSKKBm+u4joRXHMISBCAIhAORE1X34niOtYiaQY5E+bYgYbqrMZ5fDj5nCdgEhVlA8","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":907116},"type":"module","gitHead":"a1ae2a9bfeecee37db33d0dcb6d453e4fce21915","scripts":{"fr":"bun fr.ts","demo":"HOST=${HOST:-127.0.0.1}; PORT=${PORT:-3000}; bun ./demo/index.html --host=$HOST:$PORT","build":"bun build fr.ts --target=node --packages=external --banner=\"#!/usr/bin/env node\" --outfile=dist/fr.js","check":"bunx @typescript/native && oxlint --type-aware && bun knip && bun test --parallel=4","prepack":"bun run build"},"_npmUser":{"name":"chenglou","email":"chenglou92@gmail.com"},"repository":{"url":"git+https://github.com/chenglou/freerange.git","type":"git"},"_npmVersion":"10.9.2","description":"Static numeric range analysis for TypeScript","directories":{},"_nodeVersion":"23.10.0","dependencies":{"typescript":"^6.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^6.14.2","oxlint":"latest","@types/bun":"^1.3.14","oxlint-tsgolint":"latest","@typescript/native":"npm:typescript@^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/freerange_0.0.3_1785282675386_0.8182897000144602","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@chenglou/freerange","version":"0.0.4","license":"MIT","_id":"@chenglou/freerange@0.0.4","maintainers":[{"name":"chenglou","email":"chenglou92@gmail.com"}],"homepage":"https://github.com/chenglou/freerange#readme","bugs":{"url":"https://github.com/chenglou/freerange/issues"},"bin":{"fr":"dist/fr.js"},"dist":{"shasum":"830dbb1beee350900cf53d2afe13d6ffbb373a93","tarball":"https://registry.npmjs.org/@chenglou/freerange/-/freerange-0.0.4.tgz","fileCount":40,"integrity":"sha512-eq6K7izgp9uo3EQZ43NCY19hj7ZCFwk0+vY+xkeKv3O9BtdfNEmz5VnxDvFOs+wLrwd8rGYr/R6SHqrYy9hNbg==","signatures":[{"sig":"MEUCIFzQVuLzDhx8blrG2bFp3J3ajamOWkhtX1RXEyKeeokcAiEA9zS9JYCEpfXt1z8+5YjuJWg1hJeMiJanDwvjapluC3I=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":905523},"type":"module","gitHead":"76cb0d7ec6669b0c42e565792e196f6e09d7ff70","scripts":{"fr":"bun fr.ts","demo":"HOST=${HOST:-127.0.0.1}; PORT=${PORT:-3000}; bun ./demo/index.html --host=$HOST:$PORT","build":"bun build fr.ts --target=node --packages=external --banner=\"#!/usr/bin/env node\" --outfile=dist/fr.js","check":"bunx @typescript/native && oxlint --type-aware && bun knip && bun test --parallel=4","prepack":"bun run build"},"_npmUser":{"name":"chenglou","email":"chenglou92@gmail.com"},"repository":{"url":"git+https://github.com/chenglou/freerange.git","type":"git"},"_npmVersion":"10.9.2","description":"Static numeric range analysis for TypeScript","directories":{},"_nodeVersion":"23.10.0","dependencies":{"typescript":"^6.0.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"knip":"^6.14.2","oxlint":"latest","@types/bun":"^1.3.14","oxlint-tsgolint":"latest","@typescript/native":"npm:typescript@^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/freerange_0.0.4_1785548943068_0.9920817501933761","host":"s3://npm-registry-packages-npm-production"}},"0.0.5":{"name":"@chenglou/freerange","version":"0.0.5","description":"Static numeric range analysis for TypeScript","license":"MIT","repository":{"type":"git","url":"git+https://github.com/chenglou/freerange.git"},"type":"module","bin":{"fr":"dist/fr.js"},"publishConfig":{"access":"public"},"scripts":{"build":"bun build fr.ts --target=node --packages=external --banner=\"#!/usr/bin/env node\" --outfile=dist/fr.js","check":"bunx @typescript/native && oxlint --type-aware && bun knip && bun test --parallel=4","demo":"HOST=${HOST:-127.0.0.1}; PORT=${PORT:-3000}; bun ./demo/index.html --host=$HOST:$PORT","fr":"bun fr.ts","prepack":"bun run build"},"dependencies":{"typescript":"^6.0.2"},"devDependencies":{"@types/bun":"^1.3.14","@typescript/native":"npm:typescript@^7.0.2","knip":"^6.14.2","oxlint":"latest","oxlint-tsgolint":"latest"},"_id":"@chenglou/freerange@0.0.5","gitHead":"16832d661b3c36a68bbb38ea48986cf6eb085ffd","bugs":{"url":"https://github.com/chenglou/freerange/issues"},"homepage":"https://github.com/chenglou/freerange#readme","_nodeVersion":"23.10.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-PrgYQX0eoi1EXiyBlZfVUpkQHS0kpKOW03x33FuRbkavum4MifkPkXsgv/SUQzXEr7L1kVa1lhjL/BsA0Iohbw==","shasum":"45bda634d7c473b68cb063f8ff49650de75c8e2b","tarball":"https://registry.npmjs.org/@chenglou/freerange/-/freerange-0.0.5.tgz","fileCount":41,"unpackedSize":942584,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFZniJLZjCqERnZ/FZEENT4lVorjTMGAG5x3AKh0yFitAiEA+1jcICo0yycjRW2Ck8T7N7j6+Dorgtz9Kp6peW2lSFo="}]},"_npmUser":{"name":"chenglou","email":"chenglou92@gmail.com"},"directories":{},"maintainers":[{"name":"chenglou","email":"chenglou92@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/freerange_0.0.5_1788436894829_0.2698210255454092"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-21T00:22:46.302Z","modified":"2026-09-03T12:01:35.184Z","0.0.1":"2026-07-21T00:22:46.786Z","0.0.2":"2026-07-21T12:24:26.066Z","0.0.3":"2026-07-28T23:51:15.547Z","0.0.4":"2026-08-01T01:49:03.234Z","0.0.5":"2026-09-03T12:01:35.014Z"},"bugs":{"url":"https://github.com/chenglou/freerange/issues"},"license":"MIT","homepage":"https://github.com/chenglou/freerange#readme","repository":{"type":"git","url":"git+https://github.com/chenglou/freerange.git"},"description":"Static numeric range analysis for TypeScript","maintainers":[{"name":"chenglou","email":"chenglou92@gmail.com"}],"readme":"# Freerange\n\nFreerange shows you the range of every `number` in your TypeScript codebase, letting you find potential `NaN`, `Infinity`, division by zero, out-of-bounds array indexes, and more.\n\n- **Uses the official TypeScript API**. Not a new language, not a fork. No annotations, no library functions.\n- **Static**. Freerange works at compile (build) time, like TS. No need to start your app. AI agents can now guarantee UI layouts without ever touching the browser!\n- **Fast**. Uses a negligible fraction of TypeScript's analysis time.\n- **Robust**. Adversarially tested by agents against thousands of edge cases.\n\nFreerange is deliberately designed to cater to a useful (and growing) subset of TypeScript, and gives concrete guidance for moving important calculations into that subset, so that your code and math can meet in the middle to unlock the most proof power without much ergonomics drawbacks. AI agents are especially well-suited to refactor such code, and we highly recommend you asking them to do so. However, if you/they do find an unsupported TS feature truly valuable, please file an issue!\n\n## Install\n\n```sh\nbun install --dev @chenglou/freerange # npm install works too of course\n```\n\n## API\n\nThere's no API =). Your TypeScript code provides enough information for Freerange's analysis. We recommend that your agents shape code in the analysis-friendly ways described below.\n\n## Commands\n\n- `fr`: print project errors and warnings\n- `fr --audit`: print every function's contracts, plus refactor suggestions to help Freerange analyze better. Great for agents\n\nPass a file path to either command to filter down to just that file's report.\n\n`fr` directly uses TypeScript under the hood, so it naturally respects your `tsconfig`. We output TS errors before our analysis, so technically, you can swap out your explicit `tsc --noEmit` command for `fr` and nothing changes!\n\n## Examples\n\n### 1: Catch UI Sizing Bug\n\n```ts\nfunction gridColumnCount(containerWidth: number) {\n  return Math.floor(containerWidth / 240)\n}\n\nfunction gridItemWidth(containerWidth: number) {\n  return containerWidth / gridColumnCount(containerWidth)\n}\n\ngridItemWidth(200)\n```\n\n`bun fr` (or `npx fr`) outputs:\n\n```zsh\nindex.ts:9:1 - error [inferred-requirement]: call to gridItemWidth violates its nonzero divisor requirement (division at index.ts:6:10)\n```\n\nHow it works: Freerange follows `200` into `gridItemWidth`, then through the call to `gridColumnCount`. It works out that `Math.floor(200 / 240)` is `0`, then catches the later division by that result. TypeScript only knows that these values are numbers; Freerange follows their ranges through both functions.\n\n### 2: Static `console.assert`\n\nDid you know that `console.log` has a lesser-known sibling: `console.assert`? When the assertion is true, it stays silent. When the assertion is false, it reports a failure.\n\nBy itself, `console.assert` isn't as universally useful as `console.log`. Freerange changes that by analyzing `console.assert` **statically**:\n\n```ts\nexport function itemColumn(itemIndex: number, columnCount: number): number {\n  console.assert(Number.isInteger(columnCount))\n  console.assert(columnCount >= 1)\n\n  const index = Math.max(0, Math.floor(itemIndex))\n  const column = index % columnCount\n\n  console.assert(column < columnCount)\n  return column\n}\n```\n\nIn the example above, calling `itemColumn(0, 2.2)` produces an error (`columnCount` should be an integer) **at compile time**, not at runtime! No need to start a browser to know that the code is wrong here.\n\n`console.assert` calls at the very beginning of a function, before any other statement, are caller requirements. Like parameter types, every caller must satisfy them.\nAny `console.assert` later in the function will be proven by Freerange for the function itself. Otherwise, Freerange reports an error.\n\nLeading assertions can compare two inputs with `===`, `<`, `<=`, `>`, or `>=`. `!==` still needs one fixed finite number. A caller that proves the comparison satisfies the requirement immediately; otherwise the same requirement passes to its caller:\n\n```ts\nfunction clamp(minimum: number, value: number, maximum: number): number {\n  console.assert(minimum <= maximum)\n  return Math.min(maximum, Math.max(minimum, value))\n}\n```\n\nFor example, `clamp(0, opacity, 1)` satisfies the requirement, while `clamp(1, opacity, 0)` is an error.\n\nFor simplicity and predictability, `console.assert` currently works only in named top-level functions and accepts simple numeric checks:\n- `Number.isInteger`, `Number.isFinite`, `Number.isNaN`\n- Comparisons (`===`, `!==`, `<`, `>`, `<=`, `>=`) using number literals, object paths, and `array.length`\n- References to module constants. For caller requirements, the constant must resolve to a numeric literal\n\nWe also don't support aliasing `console.assert`, e.g. `const assert = console.assert`.\n\nFor more complex assertions, like inline calculations, extract them into variables:\n\n```ts\nconst availableWidth = frame.right - frame.left\nconsole.assert(availableWidth >= 0)\n```\n\n(You can strip `console.assert` in production with bundler or Bun's drop feature, as you may already be doing.)\n\n#### Things Worth Asserting\n\nThere are infinitely many assertable things. Here are some good, non-noisy ones:\n- Guarantee that two UI items don't overlap:\n  ```ts\n  console.assert(input.bottom < content.top)\n  ```\n- Guarantee that a virtualized list never renders more items than intended:\n  ```ts\n  const visibleItemCount = endIndex - startIndex\n  console.assert(visibleItemCount <= MAX_VISIBLE_ITEM_COUNT)\n  ```\n- Ensure that two separately calculated values are equal:\n  ```ts\n  const frame = {\n    input: {bottom: inputBottom},\n    inputTray: {bottom: inputBottom},\n  }\n  console.assert(frame.inputTray.bottom === frame.input.bottom)\n  ```\n\nEvery plain `number` parameter already requires a finite, non-`NaN` value. The same requirement applies to numeric fields selected from a fixed-shape object parameter. Freerange also checks whether a divisor may be `0` and the other conditions shown by `fr --audit`. You don't need to assert the same information explicitly.\n\n## Writing Analyzable TypeScript\n\nFreerange 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:\n\n- Named, synchronous top-level functions. Both `function size(...) {}` and direct `const size = (...) => ...` declarations work. Freerange follows calls between functions in the same file\n- Numbers, booleans, strings, nullable values, plain objects, tagged unions, dense arrays, and fixed tuples\n- `if`/`else`, ternaries, non-fallthrough `switch`, `&&`, `||`, `!`, `??`, `for`, `while`, and `for...of` loops\n- Arithmetic, comparisons, object field and array reads, selected `Math` operations, and `Number.isInteger`, `Number.isFinite`, and `Number.isNaN`\n\nFreerange 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.\n\nUse 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.\n\nBefore 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]`.\n\n### Numeric Analysis\n\nFor 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.\n\nFreerange also supports numeric phantom types, e.g. `type Pixels = number & {readonly __brand: 'pixels'}`.\n\n#### No common-subexpression elimination\n\nFreerange 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:\n\n```ts\nexport function progressBar(value: number, start: number, end: number): number {\n  if (end - start === 0) return 0\n  return (value - start) / (end - start)\n}\n```\n\nCalculate the subtraction once when the check and division must use the same result:\n\n```ts\nexport function progressBar(value: number, start: number, end: number): number {\n  const span = end - start\n  if (span === 0) return 0\n  return (value - start) / span\n}\n```\n\nFreerange recognizes aliases, repeated reads of the same immutable field, repeated array reads using the same stored index, and the same argument passed to multiple parameters. A newly evaluated calculation or function call is a new value. Audit code: `[guard-derived-value]`.\n\n#### Ranges use inclusive endpoints\n\nFreerange 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.\n\n#### Branches merge into one continuous range\n\nFreerange combines both branch results into one continuous range. Here `width` becomes `240..480`, which includes `300` even though neither branch returns it:\n\n```ts\nexport function previewRatio(compact: boolean): number {\n  const width = compact ? 240 : 480\n  return 100 / (width - 300)\n}\n```\n\nKeep a calculation inside each branch when it depends on the separate alternatives:\n\n```ts\nexport function previewRatio(compact: boolean): number {\n  if (compact) return 100 / (240 - 300)\n  return 100 / (480 - 300)\n}\n```\n\nA broad but safe range may need no rewrite.\n\n#### A number remembers at most one excluded value\n\nAfter `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:\n\n```ts\nconst divisor = code - 240\nif (divisor === 0) return 0\nreturn 100 / divisor\n```\n\nFreerange does not retain arbitrary sets such as \"every number except 240 and 300.\"\n\n#### No transitive reasoning between comparisons\n\nFreerange retains a direct ordered comparison between the same values when every incoming path establishes it, including through supported helpers in the same file. For example, from `left <= right` and `lowerOffset <= upperOffset`, it can prove `left - upperOffset <= right - lowerOffset`. Freerange does not combine `left <= middle` and `middle <= right` to prove `left <= right`:\n\n```ts\nif (left > middle) throw new Error('out of order')\nif (middle > right) throw new Error('out of order')\nconsole.assert(left <= right) // unproven\n```\n\nFrom `left === right`, Freerange can prove `left - right === 0` and preserve equality when both sides add or subtract the same value. It also works when both sides multiply or divide by the same value whose sign is known. Freerange does not otherwise replace `left` with `right` inside larger calculations.\n\nMultiplying both sides by the same nonnegative value preserves order; multiplying by the same nonpositive value reverses it. Division has the corresponding rule but excludes zero: a positive divisor preserves order and a negative divisor reverses it. An unknown sign or different factors remain unproven:\n\n```ts\nif (left > right || divisor <= 0) return\nconst dividedLeft = left / divisor\nconst dividedRight = right / divisor\nconsole.assert(dividedLeft <= dividedRight)\n```\n\nThese rules apply only when neither result may be `NaN`. The proofs are non-strict because floating-point rounding may make two results equal.\n\nWhen one value is built from another, keep the relationship visible in that calculation:\n\n```ts\nconst gap = Math.max(0, requestedGap)\nconst left = navRight + gap\nconsole.assert(navRight <= left)\n```\n\nIf the values arrive independently, Freerange may leave the relationship unproven.\n\n#### Branch results keep their range, not each branch's relationships\n\nFreerange does not transfer a different relationship from every branch onto the joined result:\n\n```ts\nconsole.assert(minimum <= maximum)\nconst result = value > maximum ? maximum : value < minimum ? minimum : value\nconsole.assert(minimum <= result) // unproven\n```\n\nWhen the intended operation is a `Math.min`/`Math.max` clamp, writing that operation directly keeps its bounds visible to Freerange. Freerange does not transfer relationships onto branch results or values replaced by a loop.\n\n#### No algebraic inversion of conditions\n\nFreerange 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:\n\n```ts\nexport function previewScale(width: number): number {\n  if (width <= 5) return 1\n  return 100 / width\n}\n```\n\n#### No algebraic normalization\n\nFreerange does not rewrite expressions using associativity, commutativity, or distributivity. Those rules do not always preserve JavaScript floating-point results:\n\n```ts\nconst amount = 9_007_199_254_740_992\nconst first = 3 + amount + 2 // 9007199254740998\nconst second = 1 + amount + 4 // 9007199254740996\n```\n\nChoose 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]`.\n\n#### Numeric truthiness is unsupported\n\nFreerange 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]`.\n\n### Functions\n\nPut important calculations in named synchronous functions. A React component, callback, or async function can call a plain helper:\n\n```tsx\nexport function fittedImageHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {\n  return (frameWidth * Math.max(1, imageHeight)) / Math.max(1, imageWidth)\n}\n\nfunction ImageCard(props: {frameWidth: number; imageWidth: number; imageHeight: number}) {\n  const height = fittedImageHeight(props.frameWidth, props.imageWidth, props.imageHeight)\n  return <img style={{height}} />\n}\n```\n\nLiteral 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.\n\n#### A function return does not include how the value was calculated\n\nFreerange 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`\":\n\n```ts\nfunction span(start: number, end: number): number {\n  return end - start\n}\n\nexport function progressBar(value: number, start: number, end: number): number {\n  return (value - start) / span(start, end)\n}\n```\n\nWhen 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.\n\n#### Imported function bodies are not analyzed\n\nFreerange does not follow imported functions. Keep a numeric rule that needs analysis in a supported helper with explicit inputs:\n\n```ts\nexport function labelWidthFromMeasurement(measuredWidth: number): number {\n  return Math.max(120, measuredWidth + 32)\n}\n```\n\nAn 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.\n\n#### No higher-order function analysis\n\nFreerange does not analyze callbacks passed to higher-order functions such as `reduce`, `map`, or `filter`. For a simple scalar aggregation, write the loop directly:\n\n```ts\nexport function totalWidth(widths: number[]): number {\n  let total = 0\n  for (let index = 0; index < widths.length; index += 1) {\n    total += widths[index]!\n  }\n  return total\n}\n```\n\nThis 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]`.\n\n### Objects, Arrays, and Changing State\n\nFreerange reads plain objects, fixed tuples, dense arrays, and tagged unions through at most eight nested levels. For typed objects, it analyzes the fields used in the file and follows them through assignments, returns, arrays, and calls between functions in the file. Numeric fields defined by your project become caller requirements; library fields such as `MouseEvent.clientX` are assumptions. Object literals still evaluate and retain every field they construct.\n\nWhen a conditional or nullish expression chooses between different object types, name the chosen value before reading one of its fields, e.g. `const point = useFallback ? fallback : measured; return point.x`. Give each union case a tag, use an exhaustive non-fallthrough `switch`, and keep deeply nested or unclassifiable data outside important numeric helpers.\n\nFreerange 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.\n\n#### No object and array writes\n\nFreerange 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:\n\n```ts\nexport function moveRight(point: {x: number; y: number}, distance: number): {x: number; y: number} {\n  return {x: point.x + distance, y: point.y}\n}\n```\n\nObject 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.\n\n#### Reads of changing state are not referentially transparent\n\nReferential 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:\n\n```ts\nexport function viewportScale(): number {\n  const viewportWidth = window.innerWidth\n  if (viewportWidth === 0) return 0\n  return 100 / viewportWidth\n}\n```\n\nKeep separate reads when two observations are intentional, such as two clock reads used to measure elapsed time.\n\n#### Array reads require dense arrays and valid indexes\n\nUse `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]`.\n\n### Loops\n\n#### Loops find stable ranges, not exact formulas\n\nFreerange 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:\n\n```ts\nexport function fixedTotal(): number {\n  let total = 0\n  for (let index = 0; index < 3; index += 1) {\n    total += 2\n  }\n  return total\n}\n```\n\nFreerange 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.\n\nCode 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.\n\n## Recommended TypeScript Config\n\nThe only TypeScript compiler option that Freerange mandates is `strictNullChecks` (otherwise the analysis is too unsafe), which is enabled when `strict` is on. We generally recommend enabling the options below as well. They aren't necessary for Freerange's analysis, but they help AI agents and humans write safer code that is more likely to be analyzable:\n\n```jsonc\n{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true\n  }\n}\n```\n\n## `fr --audit` Output\n\nFreerange uses a few terms consistently:\n\n- `requires`: a condition the caller must satisfy. The function's guarantees assume the condition is true.\n- `ensures`: a guarantee about the returned value whenever the function returns.\n- `assumes`: an input condition Freerange accepts without proving, such as an array being dense or every element of a `number[]` being finite.\n- `proves`: a successful static `console.assert` check.\n- `unsupported`: Freerange cannot analyze the function because it uses code outside the analyzed subset. Freerange shows the first blocker you can potentially refactor.\n- `partially supported`: Freerange can analyze some, but not all, of the function.\n- `skipped`: some top-level statements in the modules weren't analyzed.\n\n### Caller Requirements\n\nEvery plain `number` parameter must be finite and not `NaN`, even when the function does not read it. The same rule applies to numeric fields selected from fixed-shape object parameters. 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.\n\nDivision 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.\n\nA 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.\n\nAn `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.\n\nAlways 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.\n\n## Development\n\n```sh\nbun install\nbun run check\n```\n\n## Credits\n\n[Infer](https://github.com/facebook/infer), [AlphaProof](https://deepmind.google/blog/ai-solves-imo-problems-at-silver-medal-level/)\n","readmeFilename":"README.md"}