# Diff Analysis The commands here take a unified diff (with `--git`, the `git diff` of the working tree) and examine what a change affects. `context` returns the impact, `impact` stops with exit 1 while impacts are unresolved, and `review` returns the impact, co-changes, public API changes, and dead symbols in one run. `dead-code` and `cochange` also work without a diff. Rust declarations touched only by LF/CRLF conversion are excluded as impact origins when the old and new ASTs prove their complete text, ownership, and attributes unchanged. Only physical CRLF is normalized; changes to literal spaces, blank lines, or `\r` / `\n` escapes remain visible. Duplicate names, changed paths, reconstruction/parse failures, large inputs, and deep ownership retain the conservative result. This applies to working-tree, staged, and external diffs, including the impact portion of review. ## context: Smart Context (Diff → Impact Analysis) `context` takes a unified diff and analyzes the scope of the change, to support AI code review. Function signature changes are matched on identifier boundaries, so two different functions whose names only share a prefix, such as `foo` and `foo_bar`, are not confused. ```bash # Get the git diff automatically and analyze the impact (recommended) astro-sight context --dir . --git # Analyze staged changes astro-sight context --dir . --git --staged # Use a custom base astro-sight context --dir . --git --base HEAD~3 # Pipe from stdin git diff HEAD~1 | astro-sight context --dir . # Pass the diff as an inline string astro-sight context --dir . --diff "$(git diff HEAD~1)" # Read the diff from a file git diff HEAD~1 > /tmp/changes.diff astro-sight context --dir . --diff-file /tmp/changes.diff ``` `--base` of `context` / `impact` / `review` is passed straight to `git diff` / `git show` / `git blame`, so values that start with `-`, contain NUL, or are empty are rejected with `INVALID_REQUEST` (to keep something like `--output=/path` from being taken as an option). Output: ```json { "changes": [ { "path": "src/engine/symbols/mod.rs", "hunks": [{ "old_start": 10, "old_count": 5, "new_start": 10, "new_count": 8 }], "affected_symbols": [ { "name": "extract_symbols", "kind": "function", "change_type": "modified" } ], "signature_changes": [ { "name": "extract_symbols", "old_signature": "fn extract_symbols(...)", "new_signature": "fn extract_symbols(..., include_refs: bool)" } ], "impacted_callers": [ { "path": "src/commands.rs", "name": "cmd_symbols", "line": 166 } ] } ] } ``` For Rust, `affected_symbols[].kind` is `impl` when an impl header changes, an impl without methods is added, or deleted lines may include a method declaration. Adding a method without deleting lines reports the method without reporting its owning type as changed. Edits with deleted lines keep the `impl` record because a removed method cannot be ruled out from the new tree alone. An attribute-only change also keeps the `impl` record because the method symbol does not include attributes. The `symbols` command still includes the impl owner for method ownership. Rust signature attribution checks the declared function name in the AST. A parameter named `text` in `apply(text: &str)` does not make that line the signature of a separate `text` method. A new method in a mixed addition/deletion hunk is reported as `added` when it is a unique callable directly inside a file-root inherent impl, all its lines are added, and the old AST proves that the name did not belong to a callable, associated item, or import. Existing same-name items, trait or nested impls, generating macros or attributes, uncertain Unicode identity, parse failures, reconstruction failures, and possible removals in other files keep the classification conservative. This does not change the existing proofs for body-only edits, compatibility, or private methods. Callers fall into 3 groups by confidence and by how breaking they are. "Blocking" here means a finding that makes `impact` or `review --hook` return exit 1. - `impacted_callers`: actual call sites. Those left outside the diff are blocking for `impact`. References to symbols whose number of arguments changed, and references that cannot be decided, also stay here - `low_confidence_callers`: generic names whose owner (the class or type a method belongs to) cannot be determined, and same-name references in TS/Rust without evidence of a direct import - `informational_callers`: function-value references to symbols whose name and number of arguments did not change, and import lines of modified symbols whose name did not change. Not blocking ### JS / TS references independent of the signature For top-level JS / TS / TSX functions, `modified_closed_in_diff` (`api.mod_closed`) excludes same-name occurrences in another file when their use does not depend on the function signature. Both revisions must contain a unique direct top-level function declaration without visible rebinding. Changes from values or other symbol kinds do not use this proof. The supported sites are noncomputed `property_signature` names with directly written noncallable predefined types such as `boolean`, `string`, and `number`, literal types, or unions of those types; and parameters used only for truthiness. Type aliases, callable properties, method signatures, and mock object keys remain unproven. Parameter proofs additionally require file-wide strict mode, established by a static import/export statement or an exact `"use strict"` directive in the file directive prologue. Dynamic import, function-local directives, and class-only strictness are not used as evidence. Sloppy or unknown strictness remains unproven because reflective argument access can occur outside the function. Property-declaration proofs do not require strict mode. Parameters must be plain identifiers or direct object-pattern shorthands. Every same-name use in the function, including nested closures, is checked. Only ternary / `if` / `while` / `do` / `for` conditions and prefix `!` operands are accepted, through bounded parentheses. A call, value escape, return, assignment, same-name declaration, or type use keeps the entire binding unproven. Defaults, rest parameters, renamed destructuring, and constructor parameter properties are excluded. Functions referring to `arguments` remain unproven because parameters can be accessed indirectly. Parse errors, clues for `with` or `eval`, ambient declarations, and declaration files prevent this proof. Unicode escapes in any identifier node kind also prevent it; escaped names are not guessed. API closure still requires at least one actual target call verified against the changed argument count and changed lines. Alias imports use the separate proof below. Single and batch `refs` occurrences and dead-code counts are unchanged. `context` and `impact` also route these proven sites in other files to `low_confidence_callers`. Both revisions must contain a unique direct function declaration, and the modified declaration itself must be a top-level function with a signature change. Mixed same-name affected methods or other declarations prevent this proof. Removed or added declarations, symbol-kind changes, and import sites do not qualify. Disabling the confidence filter disables this route. Direct `impact --staged` and review fallback without a staged snapshot remain conservative. When `review --staged` uses a complete index snapshot, only references inside that snapshot provide evidence. ### TypeScript Named Import Aliases For a unique ordinary top-level TS/TSX function, a direct relative `import { total as sum } from './pricing'` can contribute calls to `api.mod_closed`. The import must resolve to the changed declaration's file with no competing candidate. Every use of the local alias must resolve to that import and be a direct call. Inner parameters and local declarations that shadow the alias are independent. Both the original-name callers and alias callers must satisfy the changed function's argument count and the existing changed-line checks, including multiline calls. A complete index snapshot supplies the evidence for `review --staged`. Re-exports, type-only imports, function-value escapes, `typeof`, binding the value to another name, unsupported calls, and uncertain resolution remain blocking. An additional JS/TS/TSX inventory checks escaped identifiers and module-object transports outside the name index. Namespace/default imports, export stars, and direct `require`/dynamic imports are accepted only when they resolve to a different relative file; an unrelated but unresolved transport also prevents closure. `rootDirs`, `moduleSuffixes`, `extends`, configuration escapes, unreadable inputs, parse errors, links, and dynamic-code clues prevent the proof. Names are simple ASCII identifiers. Type-member obligations retain their existing policy. The inventory is bounded to 10,000 sources, 2 MiB per source, 64 MiB total, 100,000 AST nodes, and 100,000 walk entries. Alias-reference scanning has separate limits of 64 aliases and 100,000 nodes. Configuration checks allow 1 MiB per file, 10,000 directory entries, and 128 ancestor levels. Exceeding a limit keeps the API blocking. File selection follows the existing ignore/generated-source policy; external users and arbitrary runtime reflection are outside this proof. `refs`, dead-code counts, and ordinary occurrence output are unchanged. ### TypeScript Function-Value Handoffs When an exported function's explicit return type and a receiving interface's callback return type change together, unchanged `execute({ clone: cloneText })` handoffs can close both API changes as `mod_closed`. The producer, interface, and consumer must be unique directly exported root declarations. The interface must contain one required function-type property, and the consumer must receive that interface as its only required parameter. Both old and new callback assignments are checked. Other signature tokens, including the consumer's explicit return contract, must stay equal. The proof handles `void`, primitive types, and mutable/readonly arrays. It limits the consumer body to discarded callback calls, primitive literal returns, and returning an array callback result's `.length` as `number`. Direct producer calls, an unchanged receiving interface, callback mutation/extraction/escape, inferred consumer returns, and unrecognized uses remain unproven. It checks every observed producer/interface/consumer reference and module transport in both source versions before closing either API change. Standalone `impact --git` uses the same diff-scoped evidence; ordinary references and `context` output remain intact. The inventory is limited to 512 TS/TSX files, 8 MiB across both versions, 100,000 AST nodes, depth 128, and 2,048 candidate comparisons. Aliased/default/namespace imports, reexports, dynamic loaders, JS or embedded sources, JSDoc, parse/read failures, links, and uncertain configuration (`extends`, `rootDirs`, `moduleSuffixes`, `paths`, or `baseUrl`) prevent this proof. This verifies the observed internal handoffs and bounded consumer dependencies, without establishing general API compatibility or replacing TypeScript checking. ### TypeScript Required Property Additions Adding a required property to an exported TS / TSX interface or object type alias can appear in `modified_closed_in_diff` (`api.mod_closed`) when every site that produces a value of the type, and every function that receives one, is followed in the same diff. Each reference to the type must be one of these: - The exact return type of a function or method. Every `return` must produce a proven value (defined below). Async functions are limited to the next form; generators are excluded - A top-level ordinary function declaration returning `Row[]`, `Array`, or `readonly Row[]`, or an async declaration returning `Promise`, `Promise>`, `Promise`, or `Promise`. Every array return must call `.map` on a required parameter with an array type, or on an array literal, using an inline arrow with one unannotated parameter that returns only proven values. Expression and block bodies are supported; neither the callback nor the outer function may fall through. `Promise` must directly return a proven value. Receiver aliases, writes, and escapes; callback parameter writes; same-named methods on arbitrary objects; `filter`, `flatMap`, spreads, assertions, and `await` remain unproven. Declarations or imports of `Array`, `ReadonlyArray`, or `Promise`, prototype references, and aliases of these built-ins within the analysis scope also prevent the proof. Prototype changes made by external code are not resolved - `const x: Row = ` or ` satisfies Row`, where the value is proven - A required parameter without a default value in a top-level function declaration (`function label(row: Row)`). The body must not reassign the parameter, bind its value to another name or store it in an object or array, or read its type with `typeof`. Reading its members (`row.id`) and passing it to a call are fine. A separate name search must find at least one direct call of the function, and every reference to it must be an import, an unaliased export, or a direct call that passes a proven value at that position - An unaliased `export { Row }`. Occurrences in the value namespace (a same-name variable, JSX component, or property name) do not refer to the type and are ignored A proven value is one of: an object literal that contains each added property exactly once, on a changed line, with a value whose type fits the added member (a key with an escape, such as `n\u006fte`, names a different property than written, so any such key prevents the proof); a call to a top-level function declaration whose return type is exactly the type (a call through a namespace, `rows.toRow()`, does not count); or an identifier bound to a required parameter or variable annotated exactly with the type. Value types are checked against `string`, `number`, `boolean`, `bigint`, `null`, `undefined`, literal types (bigint literals included), unions and arrays of these, and `any` / `unknown`. For property values, an unannotated variable is typed only when it is a `const` with a primitive literal, because object and array literals widen. Inside a property value, `satisfies` and `as const` (except on a primitive literal) change how the literal is typed, so such values are not proven; `undefined` is not trusted when the file rebinds that name. For a required addition, a local `const x: Row = ` inside a function or block is audited only through uses that resolve to that lexical declaration. Same-name variables and parameters in other files or scopes are excluded. A `typeof` use, alias, container storage, or duplicate declaration of the same binding prevents the proof. Passing a different inner binding to a consumer requires a separate proof of that value. Narrowing and removal retain their existing audits. Required additions also accept `const picked = makeRow(); label(picked)` in TS/TSX when a simple const declaration precedes the reference and its initializer directly calls a unique top-level function with an explicit return annotation exactly matching the target type. Unaliased relative imports use the existing resolution checks. Uses of the variable are audited against its lexical binding; `typeof`, aliases, container storage, duplicate declarations, and writes prevent the proof. This covers local variables and private top-level variables in modules. Exporting/importing the variable, JS, script globals, `let`, destructuring, alias chains, assertions, generic/overloaded functions, explicit type arguments, spread arguments, namespace calls, async functions, and nullable return types remain unproven. Parentheses around the call are accepted. Required additions also support copies such as `{ ...snapshot, title }`. Every spread source must be an identifier proven to have the target type (including the direct-call const results described above), or a direct call to a unique ordinary top-level function explicitly returning that type. Copied added properties need no changed-line evidence. A later explicit override still requires the existing value-type and changed-line proof; the last proven spread can overwrite an earlier explicit value. Unknown spreads remain unproven regardless of their position. Sources involving `any`, `Partial`, assertions, generic/overloaded calls, explicit type arguments, spread arguments, arrays, or Promises are excluded, as are copies with computed/escaped keys, duplicate explicit added keys, or methods. The exception to the spread-source alias audit applies only when the copy is returned by an ordinary top-level function explicitly annotated with the target type, initializes `const x: Row`, or occurs under `satisfies Row`. The destination's type binding, value, callers, and producer-name `typeof`/alias audits must still pass through the existing type-reference obligations. Unannotated copy variables and containers do not qualify. Property narrowing and removal do not use this copy proof. A reference in another file counts only when it is an unaliased named import whose relative specifier resolves to exactly one existing file, the declaration file. A `tsconfig*.json` or `jsconfig*.json` mentioning `rootDirs` or `moduleSuffixes` between the importing file and the workspace root prevents the proof. The names that bind produced values (functions, variables, variables initialized with `satisfies`, and the objects or arrays that hold such a value) are also searched. Any of the following prevents the proof: - A `typeof` use, which copies the type without naming it, as in `ReturnType` - An aliased import or re-export of the name - Binding the value to another name or storing it in an object or array (`const make = toRow`, `{ toRow }`), also through conditional expressions, `&&` / `||` / `??`, and comma expressions - Taking the value out of a container and binding it to another name (`const it = box.item`, `const [first] = list`), including destructuring with a key not written as an identifier (`const { "item": picked } = box`, `{ [key]: picked }`, `{ 0: picked }`) - A value taken out of an unnamed container (`[ satisfies Row][0]`), because where it goes cannot be followed by name - One of these names, or the name of a receiving function, written as a string in a value index, a destructuring key, or a type index (`rows["toRow"]`, `(typeof rows)["toRow"]`), which takes the value out of a namespace object without naming it. This includes strings wrapped in `as const`, parentheses, conditional expressions, computed keys (`{ ["toRow"]: make }`), and type unions (`["toRow" | "makeRow"]`), and it also stops on an unrelated dictionary index (`labels["toRow"]`) - A mention in an embedded source such as `.vue` - A mention of the type or one of these names in a JSDoc comment (`/** ... */`) in a JS-family file. JSDoc is type checked under `checkJs` but is a comment, so it is not a reference; the type is read even without `{...}`. Ordinary comments are not read as types and are ignored A producer exported as default or under another name, or one that is a class member, also prevents the proof, because its users cannot be found by name; a class member's type is copied through the class type (`Repo["find"]`, `implements Repo`). Taking a value out with a string held in a variable or passed to a function (`rows[key]`, `get(rows, "toRow")`), and a type carried by inference when the value is passed as a callback (`ids.map(toRow)`), are not tracked. For this kind of change, a reference line inside the changed lines is not accepted as evidence. Required additions outside the supported form stay blocking even when every reference line changed: generic declarations, `extends`, index signatures, required methods, call / construct signatures, member types outside the list above, string-named properties, member names that collide (including an optional member added at the same time) or cannot be compared as written (numeric names such as `1` and `0x1`, escapes), and self-referential types. References in other positions (`Row[]` and `Promise` outside the returns above, property types of other types, `implements`, `as Row`, type arguments, callbacks, methods, arrow functions, optional parameters) also keep the change blocking. Before this proof, such a change was closed when every reference line changed. The proof checks references to the type name and to the names of the functions, variables, and containers that produce values, within the syntax listed above. Outside the limited const proof above, types carried by inference through call results, arguments, or returns are not tracked, for example assigning `toRow()` to a variable annotated with a different type whose `note` property conflicts, or `typeof r` after `const r = toRow("a")`. ### TypeScript Existing Property Type Narrowing Removing atoms from a required property's scalar union in an exported top-level interface or object type alias can also produce `api.mod_closed` under a limited proof: for example, `string | null` to `string`, `string | undefined` to `string`, or `"ready" | "done"` to `"ready"`. The new atom set must be a proper subset of the old set, compared as written. All properties of the type must have simple scalar types; names, declaration headers, and other members must remain unchanged. Generics, inheritance, index signatures, function-valued or unknown members, ambiguous declarations, optional-property changes, simultaneous member additions, and quoted or numeric property changes remain unproven. Unsupported existing-member changes and object types whose uniqueness or parsing cannot be verified do not fall back to evidence that every reference line changed. Producers are limited to top-level ordinary functions returning exactly `Row` and `const x: Row`. Every produced value must fit the new type, with the affected property on a changed line. Fallthrough, object literals with methods or getters, and values depending on `any`, `unknown`, type assertions, or non-null assertions remain unproven. Even an explicitly annotated const has its initializer checked for assignability to the annotation and traced through preceding consts, up to 16 links; cycles, forward references, and mutable variables are rejected. Typed parameters use their annotations when their body does not reassign them; the origins of primitive arguments passed by callers are not traced. Array and Promise producers, `satisfies`, arrows, and class members are outside this narrowing proof. Receiving functions need explicit return annotations. Uses of annotated variables and parameters, and direct producer-call results, are checked for writes to narrowed properties: assignment, compound assignment, updates, delete, destructuring targets, and for-loop targets. **Comparisons with literals disjoint from the new type are retained as blocking by policy**; this does not reproduce TypeScript's comparison diagnostics. A whole value may be passed only to a receiving function whose body and calls are checked by the same proof. Unknown calls, aliases of a value or narrowed property, string-key access, unproven member calls, and inferred return types remain unproven. Existing guards for `typeof`, aliased imports and exports, JSDoc, and similar hidden uses still apply. This is not whole-program type checking or runtime write resolution. Inference without a type or producer name, changes through external code or reflection, and values arriving through other types are not traced. `refs` and dead-code occurrence contracts are unchanged. ### TypeScript Property Removals Removing only property signatures from an exported root interface or object type alias can produce `api.mod_closed` when every observed producer and consumer is proven to have followed the change. Declaration headers and remaining members must stay unchanged; all old and new members must be uniquely named identifier properties. Scalar-only changes retain the existing value-type checks. Required and optional properties can be removed; removed members themselves must have scalar types. Generics, inheritance, index signatures, methods, function-valued or unsupported members, self references, ambiguous or unparseable declarations, and mixed additions or type changes stay blocking, including when every reference line was edited. Producers are limited to ordinary root functions returning exactly `Row` and local `const x: Row` values. Remaining required properties must be present with compatible values; unlike additions and narrowing, these fields may be on unchanged lines. Values depending on `any`, `unknown`, assertions, unsafe initializers, spread, computed keys, methods, getters, or fallthrough remain unproven. The existing bounded const-initializer checks apply. Consumers require explicit return annotations and verified direct calls. The audit also follows annotated Row values and direct producer-call results: dynamic or string-key access such as `const key = "extra"; row[key]`, destructuring, aliases, unknown calls, and unverified value escapes remain blocking. `satisfies`, arrow producers, class members, imported Row constants, and inferred returns remain outside this removal proof. Addition and narrowing proofs retain their existing contracts. Scalar-only removal also supports unique ordinary root functions with explicit `Row[]`, `Array`, or `readonly Row[]` returns, and async `Promise` returns. Every return must directly map an array through an expression-bodied arrow returning an object literal. Existing array/builtin audits apply, together with value-type checks and removed-key checks. Callback parameter values are proven only when the receiver is an annotated required array parameter with no writes or escapes. Observed array uses are limited to `toRows(...).map(label)` and `(await toRows(...)).map(label)`. Proven producer declarations and wrapper shapes are registered, and the consumer reference and producer result share one verifier. The consumer must be an ordinary function with one required Row parameter and an explicit primitive/void result; generics, overloads, async functions, and generators remain unproven. Aliases, `typeof`, array escapes, thisArg, spread, optional chains, explicit type arguments, block callbacks, forwarding arrows, `.then`, and indirect await forms are rejected. This does not extend non-scalar invariant proofs, addition, or narrowing. When retained members include scalar arrays, `readonly T[]`, `Array`, `ReadonlyArray`, or `Record`, a separate proof requires unchanged values. Only built-in names used by retained generic annotations are audited. Declarations, imports, aliases, and writes prevent the proof; ordinary value reads such as `Array.isArray` are allowed. `T[]` and `readonly T[]` alone need no built-in-name audit. Producers are unique ordinary root functions with returned object literals and root `const x: Row` literals. After excluding the target type declaration and removed-key entries, the complete file token sequence must match the base revision. Parameter declarations, bindings, and all retained values therefore remain unchanged, including present optional fields. Values are limited to literals, array/object literals, and verified producer parameters; calls, outside variables, named types, and assertions remain unproven. Parameter types are limited to scalar types, `T[]`, and `readonly T[]`; generic parameter annotations remain unproven. New producers, inline argument literals, local constants, map callbacks, and `satisfies` are outside this proof. Limits of 2 MiB per file, 100,000 nodes, and eight value-nesting levels, unmatched producers, or other edits in the same file keep the change blocking. Scalar-only removal, addition, and narrowing retain their existing policies. A separate scan requires removed names to be absent from property/member names, object and destructuring keys, string access keys, and string literal types, including `Pick` and `Row["extra"]`. Matching keys on unrelated objects or types also keep the change blocking. Ordinary string values and comments do not count as keys, except JS JSDoc that may supply types. Files containing escapes are parsed even when the raw removed name is absent; escaped identifier names or keys remain unproven. Unparsed embedded sources containing a removed name or an escape, read failures, relevant parse failures, `keyof Row`, `typeof`, aliases and hidden imports/exports retain the conservative result. Staged review uses the index snapshot. This bounded check does not trace general inference, external code, or reflection; `refs` and dead-code occurrences are unchanged. ### TypeScript String-Union Member Removal Removing members from a directly exported root type alias made only of string literals can produce `api.mod_closed` when the observed value flows are proven. The declaration header and the order and spelling of retained members must stay unchanged. Duplicate or escaped declaration literals, generics, alias expansion, and mixed additions remain unproven. Once this removal shape is recognized, unsupported uses stay blocking even when every reference line was edited. Addition-only/reorder-only changes and old non-string aliases keep their existing classification. When old literal members disappear into a general type such as `string`, this proof also remains conservative. Only same-path, unchanged-name candidates participate; renames retain existing handling. This additional proof does not apply when the old alias itself cannot be recognized, including old-input limits or an alias wrapped in ERROR. Allowed type occurrences are root `const X: Record` objects, root `const X: Mode[]` / `readonly Mode[]` arrays, and required Mode parameters of ordinary root functions with explicit primitive/void results. Primitive types are limited to the `string`, `number`, `boolean`, and `bigint` keywords, plus `void` for results. Other parameters must also be annotated required primitives; optional/default/rest/destructured parameters and generics remain unsupported. Record keys must exactly match the retained set, and array elements must be retained string literals. Cross-file names require a direct, unaliased relative named import with one verified target. Independent same-name types are separated by lexical binding; ambiguous ownership remains unproven. At least one supported slot and observed value use are required; unreferenced or type-only uses do not close the change. Arrays may flow through inline single-parameter expression arrows in a chain of `.filter(...)` calls ending in `.map(...)`. Filters use known boolean consumers or retained-literal comparisons; map results use those verified comparisons, known primitive consumers, or Record index reads. Consumer Mode arguments and Record indices accept only retained string literals, verified consumer parameters, or registered array callback parameters. Mode values may appear only in these calls/indices, retained-literal comparisons, or verified switch discriminants. Aliases, escapes, writes, typeof/keyof, re-exports, unknown arguments, assertions, optional calls, spread, type arguments, and thisArg remain blocking. A supplementary residue scan checks exact cooked values in strings, static templates, literal types, and property keys. An unrelated occurrence of a removed value also blocks the proof; substrings and unrelated interpolated templates do not. Common JS and Unicode escapes are decoded. Numeric-like removed values also disable the raw-name filter; numeric or computed static keys then remain unproven instead of approximating JavaScript number-to-string conversion. Relevant JSDoc, embedded sources, and JSON are audited under the reference scan's ignore/hidden/generated policy. Limits are 100,000 walk entries, 10,000 selected files, 2 MiB per file, 64 MiB total, 512 parsed files, and 100,000 AST nodes. For recognized removal candidates, read, parse, walk, source-link, resolution, builtin, or limit failures retain the conservative result. Parse and node budgets are shared across one API-change detection run. Relative-import ownership audits the caller's logical and physical ancestors up to the filesystem root. Resolution settings in tsconfig/jsconfig are read regardless of ignore rules; unknown settings, project references, and read failures remain unproven. Directory-import candidates with package.json also remain unproven. Sources tried before enumerated candidates are also audited for existence and metadata failures, including implementations before explicit declaration specifiers and declarations or directory entries appended to JS suffixes by legacy resolution modes. Config entries, bytes, and depth have shared limits. A non-ancestor config selected on the command line and external bundler rules such as platform suffixes are outside this proof. Known loader forms such as `createRequire`, member `require`, `require.context`, and `import.meta.glob` also remain unproven. JSX HTML entities are not cooked by the supplementary scan, and JSX target uses remain unsupported. Sources excluded as generated, arbitrary loader aliases, and dynamically synthesized names are outside the proof. The guarantee covers only these observed static flows. External consumers, runtime data, and reflection are not proven. Record constants keep `const_value` and strict-const behavior; standalone impact, refs, and dead-code occurrence contracts are unchanged. ### Rust Parameter Patterns Renaming or destructuring a typed ordinary parameter does not change how Rust callers pass arguments. For example, changing `fn double(_n: i32)` to `fn double(n: i32)`, or renaming a tuple pattern with the same tuple type, does not produce `api.mod` or a function signature impact when both declarations and their import environments can be verified. The body may change at the same time. Displayed signatures keep their parameter names. The proof preserves parameter types, count and order, return types, receivers, generics, bounds, lifetimes, qualifiers, ABI, visibility, enclosing declaration headers and allowed attribute values. Attributes outside the small built-in allowlist, `cfg_attr`, `unsafe(...)` attribute wrappers, parameter attributes, macro invocations in a header, parse errors and ambiguous declarations keep the existing conservative result. Attributes on out-of-line module declarations and external macro expansion are not resolved. Type names are compared as written; this proof does not resolve simultaneous changes to local type definitions or transitive aliases. This comparison applies to function items. Containers keep their existing checks: a one-line trait can still report `api.mod` for the trait itself, and an impl owner can still have an impact. References and dead-code occurrence counts are unchanged. ### Python Relative Import Bindings An unaliased, unconditional module-level `from .cli import main` can bind `main()` to a different function than an unrelated changed `legacy.main`. Impact analysis excludes that use only when the relative module resolves uniquely inside the workspace and contains a single, undecorated, direct definition of the imported function. References to the actual defining module, `.pyi` changes, and import-name occurrences keep their existing handling. `refs` and dead-code reference counts are unchanged. The proof requires package initializers containing only comments, plain string literals, or `pass`. Rebinding, duplicate imports, star imports, dynamic namespace operations (including `__dict__` and frame namespace access), parse failures, and module `__getattr__` prevent it. Absolute imports, aliases, namespace packages, class bodies, type positions, lambdas, and comprehensions keep the conservative result. Arbitrary runtime replacement from another file, custom import hooks, and string-based monkey patching are outside this static proof. Package and target evidence is reused within one analysis and discarded before the next query; imported names unrelated to the changed symbols are skipped. ### Python Optional Parameter Removal Removing optional keyword-only parameters or trailing defaulted positional parameters from a unique, undecorated top-level Python function can produce `api.mod_closed`. Remaining parameters, their modes, annotations and defaults, and the rest of the header must be unchanged. All observed uses in the current source must be direct calls accepted by the new signature, the declaration itself, or an unaliased absolute import resolved to that declaration; at least one call is required. Calls need not have changed in the diff. The hook does not block on their impacts, while ordinary impact and reference output stays available. Aliases, module transport, re-exports, function values, ambiguous bindings, dynamic namespace access, non-ASCII identifiers, variadic or positional-only signatures, parse failures and incomplete inventories remain unproven. The inventory follows the reference scan's ignore and hidden-file scope; other source languages, links or junctions, read/walk failures, and limits of 512 source files, 2 MiB per file, 8 MiB total, 100,000 AST nodes or walk entries, and depth 128 prevent the proof. Staged review uses the index snapshot. This is a repository-local call proof; external consumers and arbitrary runtime reflection are outside its scope. ### Python Local Function Definitions and Attribute Writes Impact analysis can also identify a unique, undecorated function declared directly in the reference file as the owner of a bare use after its declaration. Rebinding, duplicate or conditional definitions, decorators, and the namespace barriers above keep the reference unproven. For a modified function proven to be a normal module function on both sides, the attribute name on the left of a plain assignment is excluded from callers. Attribute calls, function values, augmented assignments, references on the right, and changes to methods or properties retain their existing handling. These two proofs are disabled when `context --staged` or `impact --staged` reads callers from the working tree while using index sources. `review --staged` can apply them within its index snapshot. The `refs` occurrence list and dead-code counts are unchanged. ## impact: Detect Unresolved Impacts (for Stop Hooks) From the result of `context`, `impact` flags impacts on files outside the diff as "unresolved". It is meant for the Stop hook of an AI agent: when affected code is left untouched, it blocks and prompts the agent to continue. `impact` only follows symbols that still exist in the changed tree, so deleted symbols are not detected (exit 0 even if calls remain). To stop on deletions too, use `review --dir . --git --hook`, which detects removed public APIs as `api.rm`. Signature changes are matched on identifier boundaries as in `context`, so a change to a test helper or a derived name does not spread as if the base function had changed. Local symbols declared inside a function are not used as starting points of cross-file impacts. TypeScript/JavaScript, Rust, Python, Go, Java, and Kotlin are supported, and nested Kotlin functions are told apart from top-level functions with the same name. For a unique root TypeScript/TSX function with one inline object parameter, type changes confined to properties that remain optional on both sides can make individual JSX callers informational. The caller must use a direct identifier tag and omit every changed prop. Supplying a prop, even as `undefined`, JSX spreads, type arguments, mismatched tags, and implicit children for a changed `children` prop do not qualify. Any bytes between paired tags, including spaces, tabs and line breaks, count as children. Existing low-confidence results retain priority. The source proof requires unchanged headers, patterns and explicit return types, with no whole-parameter or pattern defaults. Prop types and explicit return types are limited to primitives, literals and static inline objects; named types and other unknown forms remain unproven. Without a return annotation, every outer return must directly contain JSX or a string, number, boolean or null literal after removing parentheses. A final top-level return must prevent fallthrough, and the old/new sets of return categories must match. Nested functions and classes are skipped. File-level JSX pragma comments prevent this inferred-return proof. Required changes, overloads and top-level prop additions/removals do not qualify. Evidence is specific to each source and function name. Other body behavior is not verified, and the global `api.mod` is retained; `refs` and dead-code occurrences are unchanged. For Rust, a private declaration directly in a source file can be used by child modules in other files. Impact analysis includes those callers. For a leaf module with no external children, it excludes same-name references in other files when it can rule out source inclusion. For modules with children, it excludes references outside the declaration's subtree only when the old and new visibility, ordinary module paths, and source layout can be verified. Unclear module layouts or source-generating macros keep the candidate. Private methods inside `impl` blocks use a separate rule. When a unique private inherent method has the same owner before and after the change, method references in descendant modules are included and proven sibling modules are excluded. Bare function calls and imports are excluded. A same-name definition in the reference file, a different written owner, a receiver with no owner evidence in the file, or an unproven module subtree lowers the reference to low confidence. Unproven references with `--staged` are also low confidence. When the only edit in a Rust source file adds `pub(crate)` or `pub` to a direct private item, unchanged references inside its former private subtree are informational. References outside that subtree are excluded because they could not have named the old item. This requires an unchanged ordinary Cargo module layout and no glob imports anywhere in the analyzed project: widening visibility can otherwise introduce an ambiguous name through a glob. Other edits in the same diff, uncertain ownership, and narrower or more complex visibility changes retain the normal blocking classification. For Rust type and free-function references, impact analysis can follow named imports, `pub use` chains, and glob imports through ordinary Cargo modules. It excludes a same-name reference only when the resolved declaration belongs to a different file and the bindings used for that proof are unchanged across the diff. An alias import remains blocking under the original name when it is used, publicly re-exported, or may be used by a child module. An alias proven to come from a standard crate or registry dependency remains informational. A registry dependency counts only when the Cargo configuration Cargo reads for the package (`.cargo/config(.toml)` in the package's ancestors and `$CARGO_HOME/config(.toml)`, which applies wherever the project lives) and the environment are readable and cannot redirect dependency sources (`[patch]`, `[source]`, `paths`), replace the compiler (`rustc`, wrappers, or overrides that `[env]` passes to rustc), or add `--extern` flags. Settings passed with `cargo --config`, and configuration that Cargo reads only when run from a directory below the package root, are not visible. Other external imports, conditional bindings, ambiguous globs, changed bindings, unusual source layouts, and staged analysis retain the conservative result. This ownership proof does not change `refs` or dead-code output. Cargo dependency reachability can also exclude same-name references in a different package when that package cannot reach the changed package. This scan accepts unqualified standard expression macros and built-in derives and attributes. It checks macro definitions and imports across the workspace, including declarations, dependencies and library names that can rebind `std`, `core` or `alloc`; possible shadowing of a used standard name prevents the proof. Dependency checks cover both `dev-dependencies` / `build-dependencies` and the legacy `dev_dependencies` / `build_dependencies` spellings, including target-specific tables. Macro arguments are token trees, so an argument containing `use`, `extern`, a macro definition or an attribute remains unproven. Globs are limited to standard-crate paths and paths containing only `crate`, `self` and `super`. A test module's `use super::*` is accepted; `use crate::facade::*` is not, because `facade` may refer to an external crate through multiple aliases. Unknown macros, custom derives, attribute macros and qualified macro calls (including `std::println!`) remain unproven. The scan does not resolve an unknown macro's defining crate or its dependencies. Existing barriers for `include!`, `path` attributes, custom build layouts, read or parse failures, LFS pointers and other uncertain source configurations remain in place. Removed Rust free-function classification shares this reachability scan with impact analysis; `refs` and dead-code occurrences are unchanged. ```bash # Get the git diff automatically and detect unresolved impacts (recommended) astro-sight impact --dir . --git # Check staged changes astro-sight impact --dir . --git --staged # Use a custom base astro-sight impact --dir . --git --base HEAD~3 # Pipe from stdin git diff HEAD~1 | astro-sight impact --dir . ``` - Nothing unresolved → exit 0 (no output) - Something unresolved → text on stderr + exit 1 - `--dir` is not in a git repository → exit 0 (no output, with or without `--hook`; see "Skipping Directories Outside Git" below) - `--base` is omitted and there is no commit yet (HEAD is unborn) → exit 0 (no output; see the same section) Output (on exit 1): ```text Unresolved impacts found: src/engine/symbols/mod.rs changed [extract_symbols]: → src/service.rs:284 [extract_symbols] → src/commands/api_changes/exported.rs:45 [extract_symbols] ``` Line numbers in the text output are 1-based so that an editor can open them as they are (`line` / `ln` in JSON output are 0-based). An example with claw-hooks (write it in the global configuration `~/.config/claw-hooks/config.toml`; claw-hooks ignores `stop_hooks` written in a project's `.claw-hooks.toml`): ```toml [[stop_hooks]] commands = ["astro-sight impact --git --dir ."] condition = { command_exists = "astro-sight" } ``` If you register the command directly in the Claude Code hooks settings, exit 1 on a finding is treated as a non-blocking error and Claude does not stop. With `stop_hooks` that have a `condition`, as in the example above, claw-hooks returns the command failure to Claude as a block, and Claude stops. ### Skipping Directories Outside Git When a command that accepts `--git` (`context` / `impact` / `review` / `dead-code` / `cochange`) runs in a directory outside git, the failure of the internal `git diff` is not treated as an error. The run is skipped as "nothing to analyze" and ends normally with **exit 0**. This keeps the Claude Code Stop hook from blocking while you edit in a directory outside git, such as `~/.config`. - `--hook` (`review` / `impact`) → no output on stdout or stderr, exit 0 - Normal CLI use → an empty normal result with a machine-readable `skipped` field, exit 0. You can tell "no changes" from "not in git" (`impact`, which has no structured output, prints nothing) ```json { "...": "...", "skipped": { "reason": "not_git_repository", "source": "git", "message": "--git was requested but --dir is not inside a git worktree" } } ``` The check uses `git rev-parse --is-inside-work-tree` (with `LC_ALL=C`), so worktrees, submodules, and bare repositories are detected correctly. **Real errors** (an invalid `--base`, a base that cannot resolve to a commit, git not runnable, a broken repository, missing permissions) return `exit 1`. A repository without any commit is skipped in the same way when `--base` is omitted. Right after `git init` or `git checkout --orphan`, HEAD does not point to a commit yet, so there is no base to compare against. `skipped.reason` is `unborn_head`. An explicit `--base` is never skipped: the diff against that base is analyzed, and a base that does not resolve to a commit, even `--base HEAD`, still returns exit 1. When `cochange` gets `--paths` / `--paths-file`, it continues with those files only, as it does outside git. Whether HEAD is unborn is checked with `git show-ref --exists` (git 2.43 or later); older git does not skip and keeps exit 1. Passing a diff through `--diff` / `--diff-file` / stdin does not go through this check. `review --git` makes at most three attempts when the index or working tree changes during analysis, or when `git diff` reports a short read while indexing a file. Each attempt starts by acquiring the diff again and resolving the base to a commit. That commit stays fixed for the attempt's diff, cochange, and API analysis; moving HEAD alone does not invalidate it. Failed results and temporary snapshots are discarded. If all three attempts fail for these reasons, both normal review and `--hook` return `REVIEW_INPUT_CHANGED` with **exit 2**. A single JSON error is printed to stdout; `--hook` also prints a short instruction to rerun review on stderr. Other errors and blocking hook findings keep exit 1. Explicit `--diff`, `--diff-file`, and stdin inputs are not retried. With `--staged`, the index contents are written to a temporary directory, and impact, API, and dead-code analysis all read those contents. Unstaged edits in the working tree do not enter the result (for large repositories, see "Size limit of the staged snapshot"). ### Limit on Untracked Files `--git` (without `--staged`) includes untracked source files in the analysis as new files (so that references to untracked files created in the same piece of work are not reported as unresolved impacts outside the diff). **Untracked files larger than 256KB or 5,000 lines are left out**, though. Pulling in generated output such as code generator results or huge fixtures would make every exported symbol in them a candidate for the API diff, `review` would take tens of minutes, and the Stop hook would time out. In a measurement, a `review` that finished in 1.75 seconds without untracked files did not finish within 10 minutes after adding an untracked file with 22,000 `pub fn` in total. Tracked files are not limited. A file that has been committed or added can be seen as intentionally put up for review. An untracked file, on the other hand, has not been added yet; whether it will be committed is unknown, and a huge one is likely generated. Excluded files are not dropped silently; they are listed in `truncations` (so that they are not mistaken for reviewed files): ```json { "...": "...", "truncations": [{ "path": "generated.rs", "reason": "untracked_file_too_large", "message": "untracked file excluded from --git analysis: lines 80000 exceeds limit 5000" }] } ``` With `--hook`, this is printed as `trunc: [{"f": "generated.rs", "r": "untracked_file_too_large"}]` (it reports the analyzed scope rather than a finding, so it does not cause exit 1). `impact`, which has no structured JSON, prints it as a `note:` line on stderr. `--staged` / `--diff` / `--diff-file` respect the scope given explicitly and do not pull in untracked files at all. ### Size limit of the staged snapshot `review --git --staged` writes every regular file in the index to a temporary directory before analyzing it. When the repository has more than 50,000 files or 512 MiB in total, it does not write them and reads the working tree instead (only the changed files are read from the index). This is not an error; the switch is reported in `truncations`: ```json { "...": "...", "truncations": [{ "reason": "staged_snapshot_unavailable", "message": "staged snapshot was not created: 61234 files / 734003200 bytes exceed the limit (50000 files / 536870912 bytes); the working tree matched the index, so files were read from the working tree" }] } ``` On this path, references found in the working tree do not match the content being committed when it has unstaged edits. So when the working tree differs from the index, the checks that move a change to a non-blocking category because no references remain (`mod_closed` / `mod_compat` / `rm_dead`) are not used, and the change stays blocking. With `--hook`, this is printed as `trunc: [{"r": "staged_snapshot_unavailable"}]` (it reports the analyzed scope, so it does not cause exit 1 by itself). Staged diffs containing Swift files require the snapshot for protocol witness detection. If it exceeds the limit, `review` and Swift `dead-code --git --staged` return an error, so unstaged conformances cannot remove a staged dead candidate. ### Reporting Sources That Cannot Be Analyzed `dead-code` / `review` also list in `truncations` the **source files that exist in the directory but that no backend could analyze**. Declaring a symbol dead without counting the references inside unreadable files would report live symbols as dead (for example a TypeScript function used only from the `