Metadata-Version: 2.5
Name: periplus-map
Version: 0.5.2
Summary: A schema-first, deterministic codebase mapper, configurable to any stack.
Project-URL: Homepage, https://github.com/camoa/periplus
Project-URL: Repository, https://github.com/camoa/periplus
Project-URL: Changelog, https://github.com/camoa/periplus/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/camoa/periplus/issues
Author: camoa
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: codebase,deterministic,map,static-analysis
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: jsonschema==4.26.0
Requires-Dist: platformdirs==4.11.5
Requires-Dist: ruamel-yaml==0.19.1
Requires-Dist: tree-sitter-go==0.25.0
Requires-Dist: tree-sitter-javascript==0.25.0
Requires-Dist: tree-sitter-php==0.24.1
Requires-Dist: tree-sitter==0.26.0
Provides-Extra: dev
Requires-Dist: mypy==2.3.1; extra == 'dev'
Requires-Dist: pytest-xdist==3.8.0; extra == 'dev'
Requires-Dist: pytest==9.1.1; extra == 'dev'
Requires-Dist: ruff==0.15.0; extra == 'dev'
Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
Description-Content-Type: text/markdown

<div align="center">

# Periplus

**A map of a code base in one JSON file · rutters that say what to find · the same map every run**

[![checks](https://img.shields.io/github/actions/workflow/status/camoa/periplus/checks.yml?branch=main)](https://github.com/camoa/periplus/actions/workflows/checks.yml)
[![License](https://img.shields.io/github/license/camoa/periplus)](LICENSE)
[![Latest tag](https://img.shields.io/github/v/tag/camoa/periplus)](https://github.com/camoa/periplus/tags)
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](pyproject.toml)
<sub>The CI workflow, `.github/workflows/checks.yml`, installs the package with its `dev`
extra for the tests. It runs pytest on Python 3.11 to 3.14, ruff and mypy, and validates
every bundled pack. It does not run the acceptance scripts, and it never maps a Go project.</sub>

Status: **beta**, version 0.5.2.

</div>

> *periplus* (n.): an ancient coastal record of the ports and the distances between them.
>
> *rutter* (n.): a mariner's book of written sailing directions for one coast, from the French
> *routier*; it told a pilot what to look for before charts did.

Periplus makes a map of a code base: a JSON file of nodes and edges. Configuration files called
rutters tell it what to find. The same input always gives the same map, byte for byte. Periplus
never guesses. A thing that no rutter explains appears in the map as a marked unknown.

A **rutter** is a folder of YAML files for one stack, such as PHP, Drupal or Python. It declares
node types and edge kinds. It also says where to look, which files to read and how to name what it
finds. A new stack needs a new rutter, never a change to the engine. File names and settings call
a rutter a *pack*: a rutter lives in a folder named `<pack>@<version>`.

How it is meant to work: a person writes a rutter, often with help from an AI. Periplus then runs
it with no AI. A person or an AI reads the map, where every node and edge carries its file and
line. To improve a map, improve the rutters. The engine stays the same.

## Contents

- [Get started](#get-started)
- [Why Periplus](#why-periplus)
- [What the map holds](#what-the-map-holds)
- [Write a rutter](#write-a-rutter)
- [Read the map](#read-the-map)
- [CLI reference](#cli-reference)
- [Troubleshooting](#troubleshooting)
- [Tests and checks](#tests-and-checks)
- [Known limitations](#known-limitations)
- [License](#license)

## Get started

Periplus needs Python 3.11 or later. It declares support for 3.11 to 3.14.

### 1. Install Periplus

Install it from PyPI:

```
uv tool install periplus-map                # or: pip install periplus-map
periplus --version
```

To install a tagged version from the repository:

```
uv tool install git+https://github.com/camoa/periplus@v0.5.2
```

To install from a local copy of this repository:

```
uv tool install /path/to/periplus           # or: pip install /path/to/periplus
```

After any of these installs, run `periplus skills install` to copy the Claude skills Periplus
ships into `~/.claude/skills`. Run it again after an update to replace the older copies.

#### Use the skills with other agents

The skills follow the [Agent Skills](https://agentskills.io) format: a folder with a `SKILL.md`
file. After `periplus skills install`, the folders `periplus-describe`, `periplus-gaps`, `periplus-map` and
`periplus-rutter` are in `~/.claude/skills`. Copy them to the folder the agent reads, and copy them
again after each update. The folder `~/.agents/skills` serves four agents at once:

```
mkdir -p ~/.agents/skills && rsync -a --delete ~/.claude/skills/periplus-* ~/.agents/skills/
```

- [Codex CLI](https://learn.chatgpt.com/docs/build-skills): `~/.agents/skills`, or `.agents/skills` in a repository.
- [Gemini CLI](https://geminicli.com/docs/cli/skills/): `~/.gemini/skills` or `~/.agents/skills`, or `.gemini/skills` or `.agents/skills` in a workspace.
- [GitHub Copilot in VS Code](https://code.visualstudio.com/docs/copilot/customization/agent-skills): `~/.copilot/skills`, `~/.agents/skills` or `~/.claude/skills`, so the Claude install already works. A repository can use `.github/skills`.
- [Cursor](https://cursor.com/docs/context/skills): `~/.cursor/skills` or `~/.agents/skills`, and it also reads `~/.claude/skills`. A repository can use `.cursor/skills`.

To upgrade an install from PyPI, use the tool you installed with:

```
uv tool upgrade periplus-map
pipx upgrade periplus-map
pip install -U periplus-map
```

Then run `periplus skills install` again, and repeat the copy above for any other agent. Read the
[changelog](CHANGELOG.md) before you upgrade: when a bundled rutter's version changes, update the
pins in your project's settings with `periplus update` and map again.

To update a local install, pull the repository and run `uv tool install /path/to/periplus` again. A
local directory is rebuilt when `pyproject.toml` changes or a file under `src/periplus` does. Run
`uv tool install --reinstall /path/to/periplus` for the form that always rebuilds. The command
`periplus status` prints a `rutters:` digest of the bundled rutters, so you can compare two installs.

The distribution name is `periplus-map`. Its seven runtime dependencies are pinned exactly. One
package that `jsonschema` brings is compiled, so your platform needs a prebuilt wheel or Rust.

To work on Periplus itself, run `uv sync --frozen --all-extras` in the repository. This installs
the development tools into `.venv`.

### 2. Pin a rutter

Run `periplus init` in the project folder. It creates `.periplus/`, a pack folder and a commented
settings file, and pins a rutter when it recognises the stack:

```
cd /path/to/project
periplus init
```

The settings file, `.periplus/settings.yml`, names the rutters to load, each pinned to an exact
version:

```yaml
periplus_version: 0
packs:
  - drupal_basic@0.3.0
folders:
  config: ./config/default
```

The `folders` line moves the config folder from `drupal_basic`'s default, `./config/sync`. Leave
it out when your site uses the default.

In a Drupal project, `periplus init` writes the `drupal_basic@0.3.0` pin for you. It looks for
`drupal/core` in `composer.json`, or for `web/core/lib/Drupal.php`. The rutters that
`drupal_basic` depends on load without a pin of their own. After you update Periplus, run
`periplus update` to bring the pins to the installed versions.

### 3. Map the project

```
periplus map --output map.json
```

`periplus map` also prints a run report. It names each rule and how often it fired, and the files
no rule read. It also names folders it could not list, files that did not parse, and rules it
could not run. The [map report guide](docs/map-report-guide.md) reads each part in turn.

### 4. Ask the map

```
periplus types
periplus find TEXT
periplus show ID
```

[Read the map](#read-the-map) lists the six commands that answer a question about the map.

## Why Periplus

**The map is one file you commit, it is the same on every run, and nothing serves it.**

[`docs/principles.md`](docs/principles.md) gives the reasons for this design: why a new mapper,
and why rutters and not built-in support for each stack. The table compares Periplus with the
kinds of tool that page describes.

| | Periplus | A source indexer | A server-backed tool | An AI summary |
|---|---|---|---|---|
| Framework configuration and templates | Read by rutter rules, as the source files are | Little or nothing | Little or nothing | What the model happens to read |
| What you get | One JSON file to commit, compare and review | An index for the tool's own queries | A database behind a server or viewer | Ranked text made for one prompt, never saved |
| What it needs to run | The command line | The tool | A database, a server or a viewer | A model |
| A new stack | A new rutter, with no engine change | Code in the tool | Code in the tool | A new prompt |

## What the map holds

The map is one JSON file. It holds the nodes, the edges, counts by type and by kind, the packs
loaded and the settings file used. Each node has an `id`, a `type`, a `state` and its `locations`
(file and line). Its `provenance` names the pack, the rule and the rule's declared confidence.

The engine writes three states today:

- `mapped`: a rule found the thing in a file the project holds.
- `referenced`: a file names the thing, but no rule read where it is defined. Drupal core is an
  example when only custom code is mapped.
- `unresolved`: a rule looked for a name and no rule maps it. The node records what was searched.

An unresolved node from a map of a Drupal site, shortened:

```json
{"id": "php.function::Drupal\\my_module\\Plugin\\Action\\json_decode",
 "state": "unresolved", "type": "php.function",
 "unresolved_detail": {"rule": "php_basic/calls_function", "found": "no mapped node of this name",
   "searched_for": "Drupal\\my_module\\Plugin\\Action\\json_decode",
   "types_tried": ["php.function"]}}
```

Each edge has `from`, `to`, `kind`, its locations, an `occurrences` count and its provenance.

**Determinism.** `tests/test_demo_b_map_determinism.py` maps one project in separate processes
under different hash seeds and locales, and the maps are byte-identical. This ran on one Linux
machine. The design aims for identical maps across machines, but no test has
compared two machines or two operating systems yet.

### The rutters provided

What the map holds depends on the rutters loaded. Bundled rutters load by name. Example rutters
are copied into a project's `.periplus/packs`. The tables group them by language and by framework.

[`docs/rutters.md`](docs/rutters.md) lists every bundled rutter with its dependencies.

<details>
<summary><strong>Languages and file types</strong></summary>

| Rutter | Version | What it finds |
|---|---|---|
| `php_basic` | 0.2.0 | PHP classes, interfaces, traits, enums, functions and methods. Inheritance, interfaces, traits, containment, properties and parameters with their types, and call sites. Plain and static calls, and method calls through a typed or assigned property; a call to a PHP builtin function makes nothing. Reads a parse tree. |
| `js_basic` | 0.0.1 | JavaScript scripts, and the functions that have a name of their own, each under its script's path. It reads `.js` files only and maps no calls. Reads a parse tree. |
| `go_basic` | 0.0.3 | Go functions, methods, structs and interfaces, each under its package's import path. Calls written `f(x)` inside one package, and `pkg.F(x)` through an import, to another package of the module or to an unresolved function outside it. It skips test files and `vendor`, and needs the module path in the settings as `values.module`. Reads a parse tree. |
| `yaml_basic` | 0.1.0 | Claims `.yml` and `.yaml` files for rutters that depend on it. No rules of its own. |
| `twig_basic` | 0.1.0 | Claims `.twig` files for rutters that depend on it. No rules of its own. |
| `typescript` | 0.1.0 | TypeScript modules, imports, packages and functions in `.ts` and `.tsx` files, and calls from one function to a function the file binds by name. It reads the files as text; the project sets the folder the `@/` alias points at as `typescript_alias_root`. |
| `react` | 0.1.0 | React components and hooks, named by convention, the components a function renders, the hooks it uses, and the `use client` or `use server` directive. It reads the files as text. |
| `nextjs` | 0.2.0 | Next.js App Router pages, layouts, route handlers with their HTTP methods, middleware and special files under `app`. A `fetch` of a route, a link to a page, and a name or prop that holds a route URL. It reads the files as text. |

</details>

<details>
<summary><strong>Drupal: 3 core rutters and 12 contributed-module rutters</strong></summary>

The three core rutters come first. Each contributed module has a rutter of its own, pinned
beside `drupal_basic` when the site uses that module.

| Rutter | Version | What it finds |
|---|---|---|
| `drupal_basic` | 0.3.0 | Configuration, modules, themes, services, routes, permissions, libraries, templates, hooks, entity types, fields, displays and plugins. The services and entity types that `\Drupal::service('x')`, `$container->get('x')`, `\Drupal::entityQuery('x')` and an entity type manager's `getStorage('x')` name in PHP functions and methods. Hooks from `Hook` attribute methods, and from procedural functions named by their file's name, such as `mymod_cron` in `mymod.module`. Depends on `php_basic`, `yaml_basic` and `twig_basic`. |
| `drupal_js_basic` | 0.0.1 | Drupal behaviors and the behaviors they attach, `drupalSettings` keys that PHP attaches and behaviors read, and the libraries that PHP and templates attach. Depends on `js_basic` and `drupal_basic`. |
| `drupal_more` | 0.1.0 | Base fields an entity class defines and the entity type a reference field targets, the PHP class or trait that reads or writes a field by name, the handler classes an entity annotation names, `Job::create('type')` calls, and `->hasPermission('literal')` checks. Depends on `drupal_basic` and `advancedqueue_basic`. |
| `ai_basic` | 0.0.1 | The AI module's agent plugins and function call plugins, by attribute, each joined to its class. |
| `advancedqueue_basic` | 0.0.1 | The Advanced Queue module's job type plugins, by attribute or annotation, each joined to its class. |
| `config_pages_basic` | 0.0.1 | Each Config Pages type, from its config file, as a bundle of the entity type `config_pages`. |
| `crop_basic` | 0.0.1 | Each Crop API crop type, from its config file, as a bundle of the entity type `crop`. |
| `drupal_next` | 0.1.0 | The next module's sites, per-bundle settings and four plugin kinds, the site's preview and revalidate handlers in the Next.js app, Drupal endpoints with their HTTP methods from routing files and REST resources, the TypeScript calls to an endpoint, JSON:API resources and OAuth scopes. Depends on `drupal_basic`, `php_basic`, `typescript`, `react` and `nextjs`. |
| `drush_basic` | 0.0.1 | Drush commands in the custom modules, from the `Command` attribute or the `@command` tag, each joined to its method and class. |
| `eck_basic` | 0.0.1 | Each Entity Construction Kit entity type, and each of its bundles, from their config files. |
| `paragraphs_basic` | 0.0.1 | Each paragraph type, from its config file, as a bundle of the entity type `paragraph`. |
| `profile_basic` | 0.0.1 | Each Profile type, from its config file, as a bundle of the entity type `profile`. |
| `salesforce_basic` | 0.0.1 | Salesforce mapping field plugins; each mapping with its Salesforce object and its edges to the bundle, entity type and fields it maps; the Salesforce events that subscribers name. |
| `twig_tweak_basic` | 0.0.1 | The block plugins, menus, entity types and field storages that Twig Tweak functions such as `drupal_block('x')` name in templates. |
| `webform_basic` | 0.0.1 | Webform handler plugins, by annotation, each joined to its class, and each webform, from its config file, as a bundle of `webform_submission`. |

</details>

<details>
<summary><strong>Laravel</strong></summary>

| Rutter | Version | What it finds |
|---|---|---|
| `laravel_basic` | 0.0.5 | Laravel routes with the prefixes and names of `Route::prefix()->name()->group()` groups, each joined to its controller method, its resource controller or the view it renders, controllers, models, migrations, Blade views with their includes, extends and component tags, and configuration files with the text keys of the array each returns. The views and keys that `view('x')` and `config('x.y')` calls name in PHP functions and methods. Depends on `php_basic`. Reads text, and a parse tree for the calls. |

</details>

<details>
<summary><strong>Examples</strong></summary>

| Rutter | Version | What it finds |
|---|---|---|
| `python_text` | 0.1.0 | Python modules, top-level classes and functions, imports and calls. Reads text. |
| `typer_text` | 0.1.0 | Typer apps, commands and mounts. Depends on `python_text`. |
| `speckitty` | 0.1.0 | spec-kitty missions and their templates. |

</details>

<details>
<summary><strong>Reference packs</strong></summary>

The bundle also holds five older reference packs. They pass `periplus validate`, but they hold
vocabulary only and cannot make a map.

| Pack | Version |
|---|---|
| `php` | 0.1.0 |
| `drupal` | 0.1.0 |
| `yaml` | 0.0.1 |
| `twig` | 0.0.1 |
| `go` | 0.0.2 |

</details>

The bundled rutters are in `src/periplus/packs`. The examples are in
`examples/second-stack/packs`. The manifest of every rutter in the tables above, except
`yaml_basic`, `twig_basic`, the examples and the reference packs, opens with a list of its known
gaps.

To map a Go module, run Periplus at the module's root. Give the module
path in the settings, because Periplus does not read `go.mod`:

```yaml
periplus_version: 0
packs:
  - go_basic@0.0.3
values:
  module: example.com/app
```

To map a Laravel application, pin `php_basic@0.2.0` and `laravel_basic@0.0.5` and run Periplus
at the application's root. The default Laravel folders need no settings. To move the templates,
set `views_root` under `folders`; a template is named by its path below `views_root`.

New in this release: one install serves every bundled rutter. The Go and JavaScript grammars
are dependencies of the package, so `go_basic`, `js_basic` and `drupal_js_basic` map without a
second install, and the `go` and `js` extras are gone.

## Write a rutter

A rutter for a new stack is a folder of YAML files in the project. You write it, map the
project, read what the map cannot explain, and write the next rule.

### The loop

1. Run `periplus init` in the project folder. It creates `.periplus/packs/` and the settings file
   `.periplus/settings.yml`. It pins a bundled rutter when it recognises the stack. For a new
   stack, write a first rutter and pin it, as in the example below.
2. Run `periplus map --output map.json --format json >report.json`. A map needs at least one
   pinned rutter, and with none it stops at exit 17.
3. Read the unknowns. In the report, `unread` lists the files in the rules' folders that no rule
   read. `skipped` lists each match whose rule could not build a node or edge it emits, with the
   reason. `not_executed` lists the rules the engine cannot run, and why. In the map,
   `counts.nodes_by_state` counts the `referenced` nodes, which a file names but no rule maps. It
   also counts the `unresolved` nodes, which a rule looked for and did not find.
4. Write a rule for those names in a rutter under `.periplus/packs/`. Pin a new rutter under
   `packs` in the settings. To build on a bundled rutter, name it in `depends`, as
   `depends: [php_basic]`. Your rutter may then use its folders, file types and types, and add
   rules. The bundled rutter's own files stay as installed.
5. Run `periplus validate <pack>`. It names each file and place that disagrees with the schemas. A
   rutter that does not validate stops the map.
6. Map again. The unknowns you covered disappear. Go back to step 3.

### Where a rutter and its settings live

A project's own rutters sit in `.periplus/packs/`, one folder per rutter, named
`<pack>@<version>`. Each holds a manifest, `pack.yaml`, and pack files: every other `.yaml` or
`.yml` file, in any subfolder.

The settings file, `.periplus/settings.yml`, names:

- `packs`: the rutters to load, each pinned as `<pack>@<version>`. A pinned rutter's dependencies
  load without a pin of their own.
- `folders`: a new place for a folder that a rutter names. A project that keeps the example's
  lists in `todo` sets `folders: {lists: ./todo/**}`. The rules then read `todo` and every folder
  below it.
- `values`: named text values that any template of any rutter may name, such as `{module}`.
- `pack_paths`: more folders to find rutters in, relative to the project folder.
- `re_include`: globs that take back the files a rutter's `files.exclude` leaves out.

`periplus status` lists where Periplus looks for a pinned rutter: the project's
`.periplus/packs`, the user's configuration folder, the bundled rutters and each `pack_paths`
folder. A bundled rutter, such as `php_basic`, loads by its pin alone.

### A complete small rutter

This rutter reads a line format invented for this example, a list of chores. A line
`chore <name>` declares a chore. `after <name>` at its end names the chore it waits for. Write
these four files in an empty folder.

<!-- example: .periplus/settings.yml -->
```yaml
periplus_version: 0
packs:
  - chores@0.0.1
```

<!-- example: .periplus/packs/chores@0.0.1/pack.yaml -->
```yaml
pack: chores
version: 0.0.1
depends: []
folders:
  lists: ./lists
files:
  types:
    chores:
      endings: [chores]
      reader: text
```

<!-- example: .periplus/packs/chores@0.0.1/rules/chores.yaml -->
```yaml
node_types:
- name: chores.chore
  id_namespace: chores.chore
edge_kinds:
- kind: waits_for
  from: chores.chore
  to: chores.chore
rules:
- rule: chore
  reads: file
  in: [lists]
  match: {file: '*', filetype: chores, text: '(?m)^chore (?<name>\w+)'}
  id: {from: {capture: name}}
  emits: [{node: {type: chores.chore}}]
  confidence: declared
- rule: chore_waits
  reads: file
  in: [lists]
  match: {file: '*', filetype: chores, text: '(?m)^chore (?<name>\w+) after (?<after>\w+)'}
  id: {from: {capture: name}}
  emits: [{edge: {kind: waits_for, from: this_node, to: {from: {capture: after}}}}]
  confidence: declared
```

<!-- example: lists/home.chores -->
```text
# the week
chore dishes
chore laundry after dishes
chore floors after sweeping
```

`pack.yaml` gives the rutter's name and version, a folder `lists`, and a file type `chores` for
files ending in `.chores`, read as text. The pack file declares the node type `chores.chore` and
the edge kind `waits_for`. The rule `chore` fires once per match of its `text` pattern, and the
capture `name` gives the node's id. The rule `chore_waits` adds a `waits_for` edge from that node
to the name after `after`. `periplus validate` refuses one rule that emits both the node and the
edge, so the edge has its own rule. `confidence: declared` says the file states the thing.

Run `periplus validate chores`, then `periplus map --output map.json`. The map holds exactly these
nodes and edges, each with its state or kind and its file and line:

<!-- example-map -->
```text
node chores.chore::dishes   mapped      lists/home.chores:2
node chores.chore::laundry  mapped      lists/home.chores:3
node chores.chore::floors   mapped      lists/home.chores:4
node chores.chore::sweeping referenced
edge chores.chore::laundry  waits_for  chores.chore::dishes    lists/home.chores:3
edge chores.chore::floors   waits_for  chores.chore::sweeping  lists/home.chores:4
```

`sweeping` is `referenced`: line 4 names it, and no rule maps it. That is the kind of unknown
that step 3 of the loop finds. The check `scripts/demo.d/99j-readme-example.sh` runs this example
from this file and compares the map with the lines above.

### What to read next

| Read | For |
|---|---|
| `periplus spec` | The contract: the schemas, the rule format, the engine, and what the format cannot say yet. `periplus spec <pack>` adds what one rutter declares and inherits. |
| [`docs/rutter-author-guide.md`](docs/rutter-author-guide.md) | Start here: the files of a rutter, one rule of each shape, and how to check a rutter. |
| [`docs/rutter-author-guide-text.md`](docs/rutter-author-guide-text.md) | Text files: spans, values from enclosing spans, the line of a node, and a path below a folder. |
| [`docs/rutter-author-guide-drupal.md`](docs/rutter-author-guide-drupal.md) | Parse-tree rules, read through the PHP and Drupal rutters. |
| [`docs/rutter-author-guide-references.md`](docs/rutter-author-guide-references.md) | Calls read from a parse tree, and every key the schemas accept that the engine does not run. |
| [`docs/rutter-author-guide-imports.md`](docs/rutter-author-guide-imports.md) | Import paths and a call's arguments. |
| [`docs/rutter-author-guide-abilities.md`](docs/rutter-author-guide-abilities.md) | A condition on a second child, an id that must fit a pattern, a declared end, and a class constant as a call's argument. |
| [`docs/rutter-author-guide-map-rules.md`](docs/rutter-author-guide-map-rules.md) | Map rules: rules that read the map the file rules made, run in passes, one hop each. |
| `examples/second-stack/packs` | Three working text rutters to copy: `python_text`, `typer_text` and `speckitty`. |
| `periplus validate <pack>` | Checks one rutter's files. It names each disagreement with the schemas, by file and place. |
| [`docs/map-report-guide.md`](docs/map-report-guide.md) | Reading the map report: each part in the order to read it, what it means, and what to do when it is not empty. |

A parse-tree rutter pins its grammar under `grammar` in `pack.yaml`, as `php_basic` does.
Periplus installs the PHP, Go and JavaScript grammars. Another language needs its
`tree-sitter-<language>` package installed beside Periplus, or the map stops at exit 28.

An AI that writes a rutter reads `periplus spec` first, then the guide pages in the order above.
Then it runs the loop. The map's `referenced` and `unresolved` nodes say which rule to write next.

## Read the map

Six commands answer a question about a written map, and stop: `periplus types` counts the node
types and edge kinds, `periplus find TEXT` searches node ids, `periplus show ID` prints a node
and its edges, `periplus neighbors ID` and `periplus affected ID` follow edges out of and into a
node, and `periplus path FROM TO` prints the shortest chains between two. They read the ids the
map holds and nothing more. `periplus check-map` runs the full schema check that these commands
skip. The [navigation guide](docs/navigation.md) gives each command's options, output and exit
codes, with an example you can run.

### Slice and export

`periplus slice` writes a piece of the map to a map file of its own. `periplus export` prints the
whole map as GraphML, DOT or Mermaid. To draw a part of the map, slice it, then export the slice.

```
periplus slice [ID ...] [--path PREFIX] --out FILE [--direction out|in|both] [--depth N] [--kind K] [--limit N] [--map PATH]
periplus export --format graphml|dot|mermaid [--map PATH]
```

`slice` starts from the ids you name and from every node whose first file starts with `--path PREFIX`, and follows edges
from them. `--direction out` and `in` follow edges one way; `both` follows an edge either way at every hop (default `both`).
`--depth N` sets how many edges to follow (default 1), and `--kind K` keeps only edges of kind `K`;
repeat it for several. `--limit N` stops the slice when it would hold more than `N` items (default
500, `0` for no limit): the command exits 32 and writes nothing. An unknown id, or a prefix that
matches nothing, exits 31. `--out` is required, and a slice is a smaller map, so name it so it
cannot replace the full map.

`export` prints to standard output and its warnings to standard error. GraphML names each node by
a generated token, labels it with a short name and carries the map id as its `id` data, with the
node type and state, the edge kind as the edge label, and the file and line where there is one.
DOT and Mermaid label each node with its id and type.

```
periplus slice --path src/Billing/ --out billing.map.json
periplus export --format graphml --map billing.map.json > billing.graphml   # open in Gephi Lite
periplus export --format dot --map billing.map.json > billing.dot           # render with Graphviz
periplus export --format mermaid --map billing.map.json > billing.mmd       # wrap in a mermaid code fence in a pull request
```

Gephi Lite handles light graphs; use Gephi for more than about 10,000 nodes. Mermaid on GitHub may
not draw a graph above 500 edges or 50,000 characters, and the export warns when it passes either.

## CLI reference

Run each command in the project folder.

```
periplus init                      # create .periplus/, a pack folder and a commented settings file
periplus status                    # report the settings, where each value came from, and the pack search path
periplus spec [PACK] [--summary]   # print the rutter contract, and what one rutter declares and inherits
periplus validate PACK             # check one rutter's files against the shipped schemas and name what disagrees
periplus update                    # move each pin to the installed version of that rutter
periplus map --output map.json     # map this project and write the map, with a run report
periplus types                     # count the node types and edge kinds in the written map
periplus check-map                 # check every node and edge of the map against the map schema
periplus find TEXT                 # find the nodes whose id holds a text
periplus show ID                   # show one node and the edges at it
periplus neighbors ID              # list the items an item's edges lead to
periplus affected ID               # list the items whose edges lead to an item
periplus path FROM TO              # show every shortest chain of edges from one item to another
periplus slice ID --out FILE       # write the items around some items or a path to a smaller map
periplus export --format F         # print the map as graphml, dot or mermaid
periplus skills install            # copy the Claude skills Periplus ships into ~/.claude/skills
```

`periplus validate` takes a bare `name`, or `name@version` to pick one of several copies.
`periplus update` leaves project-local and unknown pins as they are. Every command but `init`, `slice`
and `export` takes `--format json`. The nine commands that read a map take `--map PATH`; the
[navigation guide](docs/navigation.md) gives their other options. `periplus --version` prints the
installed version, and `periplus <command> --help` gives each command's options.

## Troubleshooting

- **`periplus map` stops at exit 17.** The project pins no rutter, so there is nothing to map.
  Run `periplus init`, or pin a rutter under `packs` in `.periplus/settings.yml`. Exit 17 also
  marks a map that fails its own schema, which is an engine defect. Nothing is written in
  either case.
- **`periplus map` stops at exit 28: a grammar mismatch.** A rutter's `grammar` in `pack.yaml`
  pins a parser runtime or a grammar version other than the one installed, or one that is not
  installed. The message names the rutter, the package, the version pinned and the version
  found. For a language other than PHP, Go and JavaScript, install its `tree-sitter-<language>`
  package at the pinned version. The check runs before any
  source file is opened.
- **A navigation command stops at exit 29, 30 or 31.** It found no map, the file is not a map,
  or the id names no node. The [navigation guide](docs/navigation.md) gives the remedy for each.

## Tests and checks

The pytest suite tests the engine and the bundled rutters. `scripts/demo.sh` runs the acceptance
scripts in `scripts/demo.d` and stops at the first failure: one for each command, one for each
bundled stack family, and the README and guide examples. Each script maps a small project and
compares the result with what it expects. A person runs them at the end of a task. The CI workflow
behind the checks badge does not run them.

To propose a change, read [`CONTRIBUTING.md`](CONTRIBUTING.md).

## Known limitations

These are the limits a new user meets first. [`docs/known-limitations.md`](docs/known-limitations.md)
lists the rest, each rutter's among them.

- **Keys accepted but not executed.** The schemas accept more than the engine runs, for example
  `resolution` and `excluded_tokens`. A rule with such a key does
  not run. Such a key outside a rule stops the map. `rutter-author-guide-references.md` lists every key.
- **No order among rutters.** Two rules in one rutter on one parse-tree node: the last one wins.
  Nothing settles two rutters that give one attribute two values.
- **One edge kind name for all rutters.** `php_basic`, `go_basic` and `python_text` each declare
  `calls` with different ends. So one project cannot load `php_basic` beside either of the others.
- **Patterns run on Python's `re`, not RE2.** Nothing checks for the RE2 subset, and
  `laravel_basic` uses backreferences and lookarounds. Python `re` refuses only what it cannot
  compile, such as a variable-width lookbehind.
- **An unresolved end takes its first candidate type.** For example, `pathlib.Path` becomes a
  `python.function`. The map states a type it cannot know.
- **One JSON file, and every run is a full run.** There is no split map and no incremental run.

## License

Apache License 2.0. See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).

Built with AI assistance. Periplus uses no AI to make a map.
