Metadata-Version: 2.5
Name: pythograph
Version: 0.2.2
Summary: Static facts for Python (Django, Django REST framework, Flask, SQLAlchemy) services for the isthmus bridge-facts exchange: HTTP route declarations and persistence relation uses from the standard-library ast, without importing the analyzed project.
Project-URL: Homepage, https://github.com/ictechgy/pythograph
Project-URL: Repository, https://github.com/ictechgy/pythograph
Project-URL: Issues, https://github.com/ictechgy/pythograph/issues
Project-URL: Changelog, https://github.com/ictechgy/pythograph/blob/main/CHANGELOG.md
Author: ictechgy
License-Expression: MIT
License-File: LICENSE
Keywords: django,django-rest-framework,flask,http,isthmus,persistence,routes,sqlalchemy,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Framework :: Django
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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

# pythograph

<img src="https://raw.githubusercontent.com/ictechgy/pythograph/main/icon.png" alt="pythograph's blue tit mascot" width="112" height="112" align="right">

[한국어](https://github.com/ictechgy/pythograph/blob/main/README.ko.md)

Static facts for Python services (Django, Django REST framework, Flask, SQLAlchemy), emitted in the
[isthmus](https://github.com/ictechgy/isthmus) bridge-facts exchange format.

pythograph is the Python member of a family of static-analysis CLIs (tsograph for
TypeScript/JavaScript, cartograph for Swift, kartograph for Kotlin, dartograph for Dart, gartograph for
Go, rustograph for Rust, schemagraph for SQL). Each tool reports only what it observes in its own language;
isthmus joins the documents.

The analyzed project is parsed with the standard-library `ast` only. It is never imported or executed,
and pythograph has no runtime dependencies and uses no network (`graph`, `reach`, and `impact` run only the project
root's git to read `revision`).

## Status

| Area | State |
|---|---|
| `pythograph routes --role server`: Django URLconf, Django REST framework routers and views, Flask/Werkzeug rules → `route-decl` facts | Implemented |
| `pythograph schema`: Django models and QuerySets, SQLAlchemy 2.x / Flask-SQLAlchemy 3 mappings and queries, SQL text → persistence `relation-use` facts | Implemented |
| `pythograph graph` / `reach` / `impact`: Python call graph → isthmus `language-traversal` v1 (evidence tiers `direct`/`bound`/`candidate`, `unresolvedCalls`, Django/DRF/Flask dispatch) | Implemented |
| `pythograph routes --role client`: requests, httpx, aiohttp, and urllib calls plus declared HTTP wrappers → `route-call` facts | Implemented |

isthmus-cli 0.10.0 (npm) is the first release that accepts `platform: "python"` http documents (server `route-decl`
including `registration-order`, and client `route-call`), persistence documents, and python `language-traversal`
analyses (see [isthmus compatibility](#isthmus-compatibility)).

## Requirements and installation

- Python 3.10 or newer (Django 5.x needs 3.10+, and pythograph parses the analyzed code with the running
  interpreter, so run it with the same or a newer Python than the project).

Install from [PyPI](https://pypi.org/project/pythograph/):

```sh
uv tool install pythograph
# or
pipx install pythograph

pythograph --version
```

To try unreleased changes, install from GitHub (`uv tool install git+https://github.com/ictechgy/pythograph`, or pin a
release tag such as `@v0.1.0`). A wheel built from a checkout (`uv build`) installs the same way:
`uv tool install dist/pythograph-<version>-py3-none-any.whl`. The release steps are in
[RELEASING.md](https://github.com/ictechgy/pythograph/blob/main/RELEASING.md) (Korean).

## `pythograph routes --role server`

```sh
pythograph routes --role server --project <root> [--service <name>] [--include-tests]
                  [--framework auto|django|flask] [--settings <module>]
                  [--dispatch specificity] [--generated-at <timestamp>] [--format json]
```

Writes a bridge-facts v1 document to stdout: `platform: "python"`, `target: "http"`, `roles: ["server"]`,
`dispatch`, `sourceSets`, and one `route-decl` fact per (route, HTTP method).

- `--project` (required): the project root. `project` is its POSIX realpath and every `location.path` is
  relative to it.
- `--framework`: `auto` (default) detects Django (a `DJANGO_SETTINGS_MODULE` default in `manage.py`,
  `wsgi.py`, or `asgi.py`, or `--settings`) and Flask (a `Flask(...)` or `Blueprint(...)` object). A project
  with both is a usage error until you pick one.
- `--settings`: the Django settings module when the entry files do not name it.
- `--include-tests`: also emit routes declared in test sources (`test_*.py`, `*_test.py`, `tests.py`,
  `conftest.py`, files under `tests/` or `test/`) with `testSource: true` and `sourceSets.tests: "included"`.
- `--dispatch specificity`: declare `specificity` for a Django project and omit `order` (see
  [Decisions](#decisions)).
- `--generated-at`: a fixed `generatedAt` for byte-identical output.
- Exit codes: `0` success (zero facts is still success, not proof of completeness), `2` unreadable project,
  more than 100,000 facts, output over 16 Mi characters, or an internal error (the message states the cause
  and a fix, never source text or absolute paths), `64` usage error. `1` is reserved.

Example (synthetic, compacted; the real output is key-sorted JSON with two-space indentation):

```json
{
  "dispatch": "registration-order",
  "facts": [
    {
      "channel": "/catalog/items/{}/edit/",
      "dynamic": false,
      "kind": "route-decl",
      "location": { "column": 10, "line": 20, "path": "catalog/urls.py" },
      "method": "POST",
      "order": { "group": "django:shop.urls", "index": 7 },
      "paramConstraints": [{ "kind": "int", "segment": 2 }],
      "pathAnchor": "root",
      "symbol": { "qualifiedName": "catalog/views.py#ItemEditView.post", "usr": "catalog/views.py#ItemEditView.post" },
      "trailingSlash": "strict"
    }
  ],
  "format": "bridge-facts",
  "platform": "python",
  "roles": ["server"],
  "target": "http",
  "tool": { "name": "pythograph", "version": "0.2.0" },
  "version": 1
}
```

### What is modeled

Every rule was checked against the installed package sources (Django 5.2.17, djangorestframework 3.18.1,
Flask 3.1.3, Werkzeug 3.1.9). The full table with source files is in [docs/HTTP-ROUTES.md](https://github.com/ictechgy/pythograph/blob/main/docs/HTTP-ROUTES.md)
(Korean).

- **Django**: `ROOT_URLCONF` from the settings module (star imports of project settings modules are
  followed), `urlpatterns` built with lists, `+`, `+=`, `.append`, `.extend`, `.insert`, `path()`, `re_path()`,
  `include()` (module strings, module objects, lists, `(patterns, app_name)` tuples, nested), default and
  registered path converters, `re_path` regular expressions converted from their syntax tree when possible,
  function views (`ANY`, narrowed by `require_http_methods`/`require_GET`/`require_POST`/`require_safe`/
  `api_view`), class views (`as_view()`, handlers along the class chain, `http_method_names`), and the
  registration order (first match wins).
- **Django REST framework**: `SimpleRouter`/`DefaultRouter` (`trailing_slash`, `use_regex_path`, lookup
  settings, `@action` with `detail`, `methods`, `url_path`, and `.mapping`), the `DefaultRouter` API root and
  format-suffix variants, `format_suffix_patterns`, `APIView`, generic views, and viewsets.
- **Flask**: `Flask(...)` and `Blueprint(...)` objects at module level or in app factories, `@route`,
  `@get`/`@post`/`@put`/`@delete`/`@patch`, `add_url_rule`, `register_blueprint` (nested, `url_prefix`),
  `MethodView`/`View`, Werkzeug converters (`string`, `int`, `float`, `uuid`, `path`, `any`, custom),
  `strict_slashes`, and `merge_slashes`.

### How facts are built

- **channel**: the canonical path template. A parameter filling a whole segment is `{}`, a partial segment
  keeps its literal skeleton (`/files/{}.json`), a final parameter that can match `/` is `{**}`, and anything
  else that cannot be proven (a segment with two parameters, a middle catch-all, lookarounds, unanchored
  regular expressions) is `dynamic` with a `route-coverage:` limitation. Literals are in the decoded path
  space, so non-pchar characters are UTF-8 percent-encoded and `%` becomes `%25`.
- **method**: uppercase verbs or `ANY`. `HEAD` next to `GET` and automatic `OPTIONS` are not emitted (isthmus
  matches them with `head-as-get` and `options-any`); an explicitly declared `OPTIONS` is.
- **paramConstraints**: `int`, `slug`, `uuid`, `path` (for `{**}`), or `regex` with the pattern.
- **trailingSlash**: Django and strict Flask rules are `strict`; `/?` in a regex and Flask
  `strict_slashes=False` are `optional`; omitted after `{**}`.
- **order** (Django): `{group: "django:<ROOT_URLCONF>", index: <depth-first position>}`.
- **location**: the route string argument (Django `path()`/`re_path()`, Flask decorator or `add_url_rule`), the
  `router.register()` prefix for DRF routes, or the `@action` decorator for extra actions; 1-based line and
  1-based UTF-8 byte column.

### Symbol ids

`symbol.usr` is `<project-relative POSIX path>#<lexical dotted name>`, outermost declaration first and
without `<locals>`: `catalog/views.py#item_list`, `blog/__init__.py#create_app.index`,
`catalog/views.py#ItemEditView.get`, `orders/views.py#OrderViewSet.list`. Class handlers are named after the
class registered in the URL even when the method is inherited; the call graph (`pythograph graph`) has an
inherited-member node with the same id. Views defined outside the project have no usr and are counted under
`missing-route-usrs:`.

### Limitations

When a value cannot be proven, pythograph does not guess: it emits `dynamic`, `pathAnchor: "base"`, or a
limitation with one of the contract's prefixes, and adds `limitationScopes` only when it can prove an upper
bound. Examples: conditional registrations (`if settings.DEBUG:`) become `route-coverage:` scoped to their
templates; unresolved includes and third-party URL modules are scoped to their include prefix; the Django admin,
`static()`, `django.contrib.staticfiles`, and Flask static files are `framework-provided-routes:` with prefix
scopes (Flask static also with `GET`/`HEAD`); `FORCE_SCRIPT_NAME`, `i18n_patterns`, and blueprints whose
registration is not visible use a `base` anchor with `unresolved-route-prefix:`; a project that does not pin
Django 5, DRF 3, or Flask 3 gets `route-framework-version-unknown:`.

### Decisions

- **Django is `registration-order`, Flask is `specificity`**, as verified from the sources. isthmus releases
  before `f9dcd1d` reject `registration-order`; `--dispatch specificity` declares specificity for Django and omits
  `order`. That approximation can only produce false matches (a shadowed pattern matched), never false errors,
  because isthmus filters by method first and reports a method mismatch only when no candidate accepts the
  method, which is also when Django answers 405.
- **Shadowed patterns are still declarations**; shadowing is the consumer's judgement from `order`.
- **Conditional registrations are scoped limitations**, not declarations.

## `pythograph routes --role client`

```sh
pythograph routes --role client --project <root> [--wrappers <file>] [--service <name>]
                  [--include-tests] [--generated-at <timestamp>] [--format json]
```

Writes a bridge-facts v1 document with `platform: "python"`, `target: "http"`, `roles: ["client"]`, and one
`route-call` fact per HTTP request expression. The full rule table with the verified library sources, the oracle
recording, and the end-to-end trace is in
[docs/HTTP-CLIENTS.md](https://github.com/ictechgy/pythograph/blob/main/docs/HTTP-CLIENTS.md) (Korean).

- **Libraries** (resolved by name, never by a same-named project function): requests 2.34 (top-level functions and
  `Session`), httpx 0.28 (top-level functions, `Client`/`AsyncClient` with `base_url`), aiohttp 3.x (`ClientSession`
  with `base_url`, `aiohttp.request`), and `urllib.request.urlopen` (with `Request(method=)` and `data`). Clients are
  followed through single-assignment locals, `with`/`async with`, module variables, instance fields and class
  attributes whose every assignment is a client, client-typed annotations, and project subclasses of a client class.
- **Base joins** use the isthmus style names: `httpx-base-url` (the base always ends in `/` and every leading `/` of
  the path is stripped, so `/x` stays under the base path), `aiohttp-base-url` (RFC 3986: `/x` replaces the base path;
  path-bearing bases and relative paths need aiohttp 3.11+, absolute URLs on a base session need 3.12+, proven from
  the project's lock/requirement files), and none for requests and urllib. `urllib.parse.urljoin` has no vector yet,
  so only its absolute-URL and `/`-rooted forms are claimed.
- **URL strings**: f-strings, `+`, `%` formatting, `str.format`, provable module constants (bound exactly once, never
  rebound, no `global` or module-attribute writes), class attributes and `__init__` fields with one literal value, and
  proven query-tail locals. An interpolation becomes `{}` only when it fills a whole segment; otherwise the fact is
  `dynamic` with a masked `channelPrefix` (`channel` is `null`, so no raw URL text leaves the tool). A literal host
  gives `root` plus `authority`; a dynamic host or an unknown base gives `base`.
- **Wrappers** (`--wrappers`, isthmus `http-wrappers` v1): `"language": "python"` entries; `owner` is a pythograph class
  id (`api/client.py#Gateway`) for methods and constructors (`name: "__init__"`, dataclasses included) or a module path
  (`api/net.py`) for module functions. `label` is a keyword argument, `index` a positional argument (receiver excluded).
  Unknown fields and malformed entries exit 64; declarations that match nothing report `http-wrapper-unresolved:`.
- **symbol.usr** is the enclosing function, method, class body, or module id — the same ids as `graph`/`reach`/`impact`.
- **Limitations**: `route-call-coverage:` (unmodeled request APIs such as urllib3, `http.client`, `send`/`build_request`,
  URL literals passed to receivers whose client type is unknown, scan gaps), `ambiguous-base-join:`,
  `http-wrapper-undeclared:` (functions that pipe a parameter into a request URL), and `http-wrapper-unresolved:`.

## `pythograph schema`

```sh
pythograph schema --project <root> [--include-tests] [--settings <module>] [--generated-at <timestamp>] [--format json]
```

Writes a bridge-facts v1 document with `platform: "python"`, `target: "persistence"` (or `null` when there are no
facts), and one `relation-use` fact per observed relation or column reference. isthmus joins it with a
`platform: "sql"` document (schemagraph `facts --document <catalog>`) under the persistence rules of
`docs/GRAPH-EXCHANGE.md`. The full rule table with source files is in [docs/PERSISTENCE.md](https://github.com/ictechgy/pythograph/blob/main/docs/PERSISTENCE.md)
(Korean).

- **Django**: model classes (abstract, proxy, multi-table inheritance, `Meta` inheritance) → tables
  (`<app_label>_<model>` truncated by `truncate_name` for the backend's `max_name_length`, or `Meta.db_table`), fields →
  columns (`db_column`, `<name>_id` for foreign keys, many-to-many tables and their columns), app labels from
  `INSTALLED_APPS`/`AppConfig`, the `DATABASES` backend, and django.contrib models. Uses: managers and QuerySet chains,
  lookups (`author__profile__city`, reverse relations, `attname`, `pk`), `values`/`order_by`/`F`/`Q`/aggregates,
  `create`/`update` keywords, related managers and forward relations on proven instances, `raw()`, `RawSQL`,
  `extra(tables=)`, and cursor SQL.
- **SQLAlchemy 2.x / Flask-SQLAlchemy 3**: Declarative classes (`DeclarativeBase`, `declarative_base()`, `db.Model`
  with its snake_case names), mixins, single- and joined-table inheritance, `__table_args__`/`MetaData` schemas, Core
  `Table`, `ForeignKey("t.c")`, `relationship(secondary=)`. Uses: statement entities (`select`, `insert`, `update`,
  `delete`, `session.query`, `session.get`, `join`), `Model.column`, `Model.relationship`, `Model.query`, `filter_by`,
  constructors, `table.c.name`, and `text()`.
- **SQL text**: the family's shared lexical extractor (the same vectors as tsograph, dartograph, cartograph, and
  kartograph) for explicit SQL arguments, and uppercase SQL literals elsewhere (docstrings are skipped).
- **channel** is the relation name as written or mapped (`schema.table` only when qualified; no default schema is
  guessed), **method** is the column, and **symbol.usr** is the enclosing function, method, or model class with the
  same ids as `routes`. A name that depends on an unknown backend, app label, or Flask-SQLAlchemy version, an
  unresolved model or lookup, and SQL built at runtime become `dynamic` facts with a `dynamic-relation-names:`
  limitation instead of guesses.
- Test sources (unless `--include-tests`) and migrations (Django `migrations/`, Alembic `versions/`) are not scanned;
  migrations describe past schemas.

## `pythograph graph` / `reach` / `impact`

```sh
pythograph graph  --project <root> [--include-tests] [--revision <id>] [--generated-at <timestamp>]
pythograph reach  --project <root> [--dispatch direct|bound|candidates] [--max-depth <n>] [--max-reached <n>]
                  [--roots-from <file|->] [--include-tests] [--revision <id>] [--generated-at <timestamp>] [--] <id>...
pythograph impact (same options as reach)
```

Builds the project's Python call graph with the standard-library `ast`. `graph` writes pythograph's own snapshot
(`pythograph-graph` v1); `reach` writes the symbols the roots depend on (`dependencies`) and `impact` the symbols that
depend on them (`dependents`) as isthmus
[`language-traversal` v1](https://github.com/ictechgy/isthmus/blob/main/docs/LANGUAGE-TRAVERSAL.md). Ids are the same
strings as `symbol.usr` in `routes` and `schema`. The full rules are in [docs/GRAPH.md](https://github.com/ictechgy/pythograph/blob/main/docs/GRAPH.md) (Korean).

- **Nodes**: modules (`<path>#<module>`), functions, methods, classes, nested definitions, and inherited members
  (`<registered class>.<member>`, for view handlers and for inherited members called on exact receivers).
- **Edges**: `call`, `new` (the project `__init__`, else the class), `callback`, `reference`, `decorator`, `attribute`
  (class-body attributes), `inherit`, and `dispatch`/`framework` (framework dispatch). Resolution follows imports
  (absolute, relative, aliases, `__init__` re-exports, `*`), module attributes, constructors, `self` and `super()`
  through the C3 MRO of project classes, annotated and return-annotated receivers, module-level instances, and
  properties.
- **Evidence tiers**: statically resolved edges are `direct`; overrides in project subclasses for `self`, `cls`, and
  annotated receivers are `candidate`. A method call on a receiver of unknown or overridable type gets `bound` edges
  (followed by `--dispatch bound`) only when every value observed flowing into the receiver is a project class
  instance: constructors, module-level instances, attributes assigned in `__init__` from constructor parameters (DI),
  function parameters whose call sites are all in the project, and factory return values. Open slots get no `bound`
  edge: public functions, classes, and module globals of a library (a project root with `setup.py`/`setup.cfg` or a
  `pyproject.toml` `[project]`/`[tool.poetry]` table), framework-invoked entry points (functions passed as values,
  decorated functions, classes with framework bases), computed `getattr`/`setattr` and module namespaces, `*args`/
  `**kwargs` spreads, wrapping decorators, monkeypatching writes whose value is unknown, `self`/`cls`, and test sources
  (a separate program). Values that leave through library code and computed-name writes to receivers of unknown type
  are not modeled (reported under `bound-assumptions:`). The default stays `direct`: bound linked no calls in the four
  dogfood apps. Module globals and class-body attributes are exact receivers only when nothing rewrites them.
- **No guessing**: calls without a known target are counted by reason (`parameter`, `untyped-receiver`,
  `dynamic-attribute`, `getattr`, `dynamic-callee`, `unresolved-import`, `framework-callback`, …) as each symbol's
  `unresolvedCalls`. A method call on an untyped receiver is proven external only when no project class, module, or
  attribute write defines that name.
- **Framework dispatch**: a table read with `ast` from the installed Django 5.2.17, DRF 3.18.1, and Flask 3.1.3
  sources links the project hooks that the `as_view()` dispatch path (`dispatch`, `initial`, permission checks,
  `__init__`) and framework implementations (`ModelViewSet.retrieve` → `get_object` → `get_queryset`,
  `ModelSerializer.save` → `create`) call. Objects the framework builds from class attributes (`serializer_class`,
  `permission_classes`) count as `framework-callback` unresolved calls.
- **Snapshot size**: the `graph` snapshot is not an isthmus input, so it may be up to 256 Mi characters (about 2 bytes
  of extra memory per output character while encoding); `reach` and `impact` keep the 16 Mi isthmus input limit.
  The encoding is unchanged, so raising the cap alone changes no snapshot under 16 Mi characters.
- **Traversal documents**: a `dispatch` declaration, per-root lower-bound `evidence`, `unresolvedCalls`, one
  multi-root pass (compared with a per-root oracle on random graphs), `--max-depth`/`--max-reached` truncation, and
  `rootsTruncated`. Roots that are not graph nodes are listed without `symbol`; the document is written and the
  command exits `64`. `revision` is `--revision` or git `HEAD` when the work tree is clean; `graphRevision` is a
  SHA-256 of the graph content.

## Validation

The oracle harness in `experiments/oracle/` imports the synthetic fixtures in a scratch virtual environment
and compares pythograph's facts with Django's resolver traversal, DRF routers, and Flask's `url_map`
(`tests/test_fixtures.py` replays the recorded results offline):

| Target | Precision | Recall |
|---|---|---|
| `fixtures/django/drf-shop` | 69/69 | 58/59 (one intentional dynamic lookahead pattern) |
| `fixtures/flask/blog-app` | 28/28 | 27/27 |
| HackSoftware/Django-Styleguide-Example `a70ef43` (MIT, scratch clone) | 21/21 | 21/22 (the DEBUG-only `static()` route) |

Dogfooding on four public apps (Django-Styleguide-Example, babybuddy, microblog, and netbox, cloned only into a scratch
directory) measured route precision against each framework's resolver, relation-use join rates through schemagraph and
isthmus, handler-to-relation reachability, unresolved-call reasons, and runtime; the results and the fixed issues are in
[DOGFOOD.md](https://github.com/ictechgy/pythograph/blob/main/DOGFOOD.md) (Korean).

The isthmus shared conformance vectors (`conformance/`, vendored from isthmus `2954375` and locked in
`conformance.lock`) pass 100% of the `producer` and `producer:pythograph` cases (135: `template.grammar`,
`template.normalize`, `scope.validate`, `scope.applies`, `dispatch.validate`, and 57 url-compose cases for query tails,
interpolation, normalization, stripping, masking, the `rfc3986`/`httpx-base-url`/`aiohttp-base-url` joins, and wrapper
argument binding); the `dispatch.validate` checker also runs on the routes golden output.

**Client mock-server oracle.** `experiments/client_oracle/` runs the synthetic client `fixtures/client/shop-client`
in a scratch environment against a local `http.server` on 127.0.0.1 (name resolution and connects are redirected, no
external traffic) and records what requests, httpx, aiohttp, and urllib actually send. Recorded 2026-09-30: 35
scenarios, 31 matches, 4 dynamic, 0 mismatches (`tests/test_client_oracle.py` replays it offline).

**Python client × Django server trace.** `experiments/client_e2e/` joins a synthetic Python client
(`fixtures/e2e/py-client`) with the Phase 6 Django server recording through isthmus `trace` (main `3a45450`): all four
selected routes attach the Python call site (exact match) to the server handler, reach the calling screen functions,
and continue to the relation uses and tables (`tests/test_client_e2e.py`).

**Phase 6 exit criterion (Django backend × iOS/Android chain).** `experiments/e2e/` joins a synthetic Django + DRF
server (`fixtures/e2e/shop-api`), a schemagraph catalog of its Django DDL, and route-calls plus reverse traversals of
synthetic iOS (cartograph) and Android (kartograph) clients with isthmus `trace` (workspace). The expected paths of the
three questions match: (a) API → DB tables + DB dependents, (b) API → client call sites → affected client symbols, and
(c) table → API → client. The Android recording needs kartograph `4c09d91` or later (Retrofit route-call usrs and
`baseUrl` joins), which attaches the Android order and checkout calls to the chain. `tests/test_e2e_trace.py` re-checks
the recorded inputs and outputs offline (the table is in
[docs/GRAPH.md](https://github.com/ictechgy/pythograph/blob/main/docs/GRAPH.md#phase-6-종료-조건-django-백엔드--iosandroid-체인)).

Persistence naming vectors (`fixtures/persistence-naming/vectors.json`) are recorded by importing synthetic models
with the real ORMs in a scratch environment (`experiments/persistence/run_naming.py`): Django 5.2.17 `_meta` names
quoted by each backend's `connection.ops` (sqlite3, postgresql, mysql, oracle), and SQLAlchemy 2.0.54 /
Flask-SQLAlchemy 3.1.1 mappers. pythograph matches 100% (Django 136/136 model-backend pairs, SQLAlchemy 9/9 and
Flask-SQLAlchemy 10/10 classes, all tables and columns). Joining `pythograph schema` output for the two persistence
fixtures with schemagraph catalogs of the DDL the ORMs create (`experiments/persistence/run_e2e.py`) gives no isthmus
errors (41 and 20 matches).

## isthmus compatibility

isthmus-cli 0.10.0 (npm, `npm install --global isthmus-cli@0.10.0`) is the released version that accepts
`platform: "python"`: http `route-decl` facts (Django's `registration-order` and `order`, with shadowing diagnostics),
client `route-call` facts (`routes --role client`), persistence `relation-use` facts, and python `forward`/`reverse`
analyses (`language-traversal` v1) in `trace`. Earlier isthmus releases reject python route-decl and route-call
documents as input errors. `--dispatch specificity` remains for those older releases.

## Bounded source scanning

Source reads request at most 4 MiB plus one overflow byte. Directory enumeration consumes at most 200,000 entries
plus one overflow entry, with a maximum depth of 64. An oversized directory is omitted as a whole rather than selecting
an arbitrary filesystem-order subset; files from previously completed directories remain available. Unreadable directory
entries are counted in limitations for routes, schema, and graph output, and prevent closed-world `bound` proofs that
would otherwise rely on missing modules. A successful command with these limitations is still an incomplete analysis.

Every project-relative path component is checked for symbolic links, including module/package resolution and dependency
manifests. Links to locations inside the project are rejected too; the skipped-link count counts each encountered path
once. On systems supporting descriptor-relative `O_NOFOLLOW`, directory scans and source reads open each component
without following links, and verify the opened file before reading. Other systems recheck components and file identity
before reading, but those checks are not atomic. Analyze a stable project tree; this is not a filesystem snapshot or a
sandbox against concurrent changes to the filesystem. The analyzed Python code is never imported or executed.

## Development

```sh
uv sync
uv run ruff check src tests && uv run ruff format --check src tests
uv run mypy
uv run pytest --cov          # line and branch coverage gate: 90%
uv run python scripts/verify_cli_contract.py
```

The framework hook table is regenerated from a scratch virtual environment with Django 5.2.17, DRF 3.18.1, and
Flask 3.1.3 installed: `python experiments/graph/dump_framework_hooks.py --site-packages <path>` (`--check` compares
only).

## License

MIT
