Metadata-Version: 2.4
Name: assurance-reach
Version: 0.1.1
Summary: What a change to a file reaches, by a code graph such as Graphify's: each hop at its call site, how sure the graph is of it, how far the graph is behind the code, and what it cannot say.
License: Apache-2.0
Project-URL: Source, https://github.com/i-ops-hq/assurance
Keywords: code-graph,impact-analysis,graphify,agents,assurance,dependencies
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Dynamic: license-file

# assurance-reach

**What a change to a file reaches, by a code graph, and how far that graph is behind your code.**

```bash
pip install assurance          # every tool; or: pip install assurance-reach
graphify update .              # writes graphify-out/graph.json; any graph of the same shape works
assurance reach src/billing/money.py
```

It reads a code graph; it does not build one. [Graphify](https://pypi.org/project/graphifyy/) builds
the graph this was written against, from your code alone with no model (`graphify update`), and any
other producer that writes the same shape works too. From the graph it answers one question: if this
file or folder changes, what depends on it, hop by hop, at the line where each dependency is. Then it
says what a graph usually does not: how far behind the code the graph is, and what it could not
determine.

Here it is on [a small project](tests/fixtures/shop) and the graph Graphify built of it:

```
$ assurance reach shop/money.py
What a change to shop/money.py reaches, by graphify-out/graph.json

The graph is current. Since it was built, at 2026-10-05 19:38, none of the 8 files it read has changed, checked by content, and no file of the kinds it reads is new to it.

It reaches 11 symbols in 5 files, to depth 3: 8 by extracted edges, 3 only by inferred ones.

By extracted edges, read from the code:
  shop/cli.py
    L3    cli.py imports_from invoice.py  (2 hops)
  shop/invoice.py
    L3    invoice.py imports_from money.py
    L14   .total() references Money
  shop/report.py
    L3    report.py imports_from invoice.py  (2 hops)
  shop/tax.py
    L3    tax.py imports_from money.py
    L8    tax() references Money
  tests/test_tax.py
    L1    test_tax.py imports_from tax.py  (2 hops)
    L5    test_tax_on_ten() calls tax()  (2 hops)

Only by inferred edges, which the producer inferred rather than read from the code:
  shop/cli.py
    L8    main() calls Invoice  (2 hops)
  shop/invoice.py
    L14   Invoice uses Money  (score 0.95)
  shop/report.py
    L6    monthly() references Invoice  (2 hops)

Not determined: not followed, as not dependencies: contains 8, method 5, rationale_for 7.
```

## What it says

- **What depends on the change, at the call site.** A change reaches what depends on it, so it follows
  dependency edges backwards: the relations Graphify's own blast radius follows (`calls`,
  `indirect_call`, `references`, `imports`, `imports_from`, `dynamic_import`, `re_exports`, `inherits`,
  `extends`, `implements`, `uses`, `mixes_in`, `embeds`, `requires`), and a package manifest's
  `depends_on` and `crate_depends_on`. Each line is a symbol that depends on the change, with the
  relation that carried the hop and the line of the call or import, not of a definition. Three hops by
  default; `--depth N` sets it, and `--depth 0` follows everything.
- **How sure the graph is, never mixed.** A symbol is listed under extracted edges only when a path of
  extracted edges alone reaches it. One reached only through an edge the producer inferred, or marked
  ambiguous, is listed apart, with the score the producer gave the hop where it gave one. A path is as
  sure as its weakest edge.
- **How far the graph is behind the code.** Every report says so, first. With Graphify's manifest,
  every file it read is checked by its content, so a file only touched is not reported as changed:
  which changed, which are gone, and which files of the kinds it reads are new to it. A graph without
  a manifest is checked by modification time, and the report says that is what it did. Whatever it
  could not check, it names, and it does not call a graph current past it.
- **What it could not determine.** Symbols at the last hop with dependents it did not follow; every
  relation it does not follow, by name and count, including any it does not know; reached symbols the
  graph places in no file; records in the graph it could not read, by why. A path the graph holds nothing in is reported as unknown, not as reaching
  nothing.

## From Python

The same answer, as values, for a program that wants more than `--json`:

```python
from pathlib import Path

from assurance_reach.graph import load
from assurance_reach.reach import reach
from assurance_reach.staleness import staleness

# The small project above, and the graph Graphify built of it.
graph = load(Path("tests/fixtures/shop/graphify-out/graph.json"))
found = reach(graph, "shop/money.py", depth=3)
behind = staleness(graph, "shop/money.py")

assert not behind.behind  # the graph is current
assert len(found.reached) == 11
by_extracted = {hit.node.label for hit in found.reached if hit.confidence == "EXTRACTED"}
assert "tax()" in by_extracted and "Invoice" not in by_extracted  # Invoice is reached only by an inferred edge
```

## Did the change do what it said?

Build a graph before the change and one after, declare beforehand which paths the change is
supposed to touch, and ask:

```sh
assurance reach --before before.json --after after.json --declared src/billing/
```

It sorts every dependency change into inside or outside those paths. An edge is
`(source, target, relation)`, so a call that moves down a file is not a change; a change in how sure
the producer is of an edge is reported on its own. Two graphs it cannot compare are refused rather
than diffed, and that includes an after graph the working tree has moved past: an edit made after
the graph was built is invisible to it, so a confident "nothing else changed" would be wrong.

Exit 1 when the outcome did not hold, or when the comparison cannot be trusted. Exit 0 when it held.

## Who owns what a change reaches

`assurance_reach.owners` reads CODEOWNERS and never infers an owner. It implements GitHub's pattern
language, and the last matching rule wins, as GitHub specifies. A line it cannot read does not
quietly drop out: because the last match wins, an unreadable rule that comes *after* the last
readable one that matches may own the file instead, so the answer is **undetermined** rather than
the earlier rule's owner. `assurance clearance`, in `assurance-cli`, is what joins that to authority.

## What it reads

```
nodes  id, label, source_file, source_location
edges  source, target, relation, confidence, source_file, source_location
```

An edge points from the code that depends to what it depends on, and its own `source_file` and
`source_location` are where that happens. `confidence` is `EXTRACTED`, `INFERRED` or `AMBIGUOUS`;
anything else is read as weaker than all three and shown as itself. Graphify writes this as
node-link JSON, edges under `links`; another producer may use `edges`. A `manifest.json` beside the
graph, as Graphify writes one, gives each file's MD5 as it was read. Records that cannot be read are
counted, by why, in `--json`.

Without `--graph`, it uses the nearest `graphify-out/graph.json` above the path, or above the current
folder. The folder a graph covers is the one its `graphify-out` sits in, which is the one Graphify
read, and paths are relative to it; `--root` names another. Graphify run on a folder below the one it
ran from writes the graph's paths from where it ran and records that folder in `.graphify_root`, so
that front is taken off and the graph and its manifest name each file the same way. An absolute
recorded folder is used only when it is a folder here inside the project, and the report says when
it was not.

## For a program

`--json` prints the same answer as `assurance.reach/1`:

- `changed`, the path asked about, relative to the folder the graph covers.
- `graph`: `path`, `root`, `nodes`, `edges`, `built`, `not_read` (records it could not read, by
  why), and `notes`.
- `staleness`: `how`, `files_read`, `changed`, `gone`, `new`, `outside`, `new_search_stopped`,
  `new_search_unlisted` and `changed_path`.
- `start`, the symbols in the changed path.
- `reached`, each with where it is defined (`file`, `line`); the hop that reached it (`relation`,
  `depends_on`, `depends_on_id`, `call_site`, `hop_confidence`, `hop_score`); its `depth`; and the
  `confidence` of the strongest path to it. Following `depends_on_id` leads back to the change.
- `not_determined`: `depth`, `past_depth`, `unfollowed`, `unknown_relations` and `unlocated`.

It exits 0 when it answered, including "nothing" and "unknown", and 2 when it could not: no graph, a
graph it cannot read, or a path outside the folder the graph covers.

## What it is not

- **Not a graph builder.** It never extracts anything from code. A graph is the producer's job.
- **Not a model.** No edge is generated here, and none is guessed: an unknown relation is not followed,
  and is named.
- **Not a gate, yet.** It prints what a change reaches. Deciding whether the person asking may touch
  all of it, and checking afterwards that a change did what it said, are what this is the first step
  toward.

It reads the graph, and the files it names inside the project, on your machine, and sends nothing
anywhere. A graph is often committed, so it is read as input from whoever wrote the repository:
[SECURITY.md](SECURITY.md) says what one cannot make this do.

Apache-2.0.
