Skip to main content

ff-lntools

English | 한국어

ln-structure, a layout rule for Python and Rust projects built with AI coding agents (Claude Code and others), and lnt, the tool that checks the rule mechanically and generates the navigation docs.

Depends only on tree-sitter (to read Rust). Python 3.10+. Windows / macOS / Linux. Each package picks its language from its own sources (.py or .rs). When that is not decisive, a project with Cargo.toml at its root is treated as Rust (one library crate, layers under src/); otherwise Python.

1. What ln-structure is

Modules are placed into layers named l0, l1, ... by dependency depth alone. Layer names carry no meaning. Where a module lives is decided only by what it imports.

src/shop/
  l0/                       # depends on nothing (standard library only)
    candle/
      candle.py             # class Candle
      __init__.py           # from .candle import Candle
  l1/                       # depends on l0 only
    order/
      order.py              # class Order  (from shop.l0.candle import Candle)
      __init__.py
  l2/                       # depends on l0, l1 only
    report/
      report.py
      __init__.py
  for-agent-layerinfo.md    # module map (generated)
tests/
  l1/order/test_order.py    # example. Test placement is not an ln rule

Eight rules. C6 and C8 apply to Rust only.

Code Rule
C1 Import only from lower layers. Same layer is forbidden too
C2 Import only names on a surface: a module (shop.l1.order) or a layer (shop.l1). Never reach into a module's files or a nested module's internals, never import a name the surface does not publish, and no two modules in one layer may publish the same name
C3 A module's layer = max(layer of its dependencies) + 1. No dependencies means l0. Any third-party package means at least l1
C4 No cycles between modules
C5 Imports inside the package are relative (from ...l1.order import Order), so the package still works when nested inside another project (not applied to Rust: a crate is plugged in as a Cargo dependency, so crate:: paths do not break)
C6 Rust: an impl lives in the module of its type or trait. Attached elsewhere, its code runs without any import showing it
C7 Every source file lives in a module folder. Only surface files and entry scripts (__main__.py; for Rust, Cargo's other targets such as main.rs, bin/, tests/) sit outside. A file outside every module escapes all other checks
C8 Rust: no #[macro_export]. An exported macro lands at the crate root, skips the surfaces, and hides the dependency at the call site. Share a macro between modules with pub(crate) use and a module path

Why.

  • Dependencies written by agents cannot tangle. Cycles are impossible by construction
  • The blast radius of a change is bounded to "layers above", and a tool can compute it exactly
  • Layers have no semantics, so there is no "is this a service or a util" debate. Add a dependency and the module moves up
  • Agents start from the module map (one line of responsibility per module), query signatures with lnt sig, and read the code for detail

Full rules (nested modules, mutual calls, dependency inversion, __init__ patterns): src/lntools/protocol/for-agent-codingprotocol-ln-structure.md (common) and src/lntools/protocol/for-agent-codingprotocol-ln-structure-python.md (Python), src/lntools/protocol/for-agent-codingprotocol-ln-structure-rust.md (Rust), all in Korean.

2. What the tool does

Enforcement does not rely on people remembering rules. Wired into Claude Code hooks, every edit the agent makes is checked and violations come back as errors.

Command Role
lnt init Install protocol docs, CLAUDE.md and Claude Code hooks into a project
lnt check C1-C8, plus files that fail to parse. Exit 1 on violation. --fix first rewrites absolute imports inside the package (C5) to relative ones
lnt blast MODULE Modules affected when this one changes, including consumers through inherited interfaces
lnt map [--json] Modules, layers, dependencies at a glance. --json emits modules, edges, violations and orphans for tools that draw the map, one map per package under src/
lnt sig [TARGET] Public signatures of a layer or module, computed on the spot (not stored)
lnt review Things worth a look, not violations: bypassing dependencies, modules nothing uses, files with more than one class, nested modules that leak hidden types or re-export lower-layer names (shown with the names each outside module imports), module pairs that often change together in git history without an import between them, responsibility lines that end in a verb with no result. Always exit 0
lnt doc Generate the module map (for-agent-layerinfo.md) and every __init__.py, the package root included (module, layer and package surfaces)
lnt doc --check Exit 1 if docs drifted from code
lnt move MODULE lK Relocate a module and rewrite imports, tests mirror, layer __init__, docs

When src/ holds several ln packages, check, doc, review, map and the SessionStart hook cover every package that has sources (--root inside one package narrows it to that package). Commands that take a module (blast, sig, move, doc --stamp) accept package/module; a bare name that exists in several packages is refused.

3. Getting started

Install

uv tool install --python 3.14 ff-lntools

This puts lnt in your user bin folder (~/.local/bin), so it is found whichever Python environment is active. pipx install ff-lntools works the same way. The hooks call lnt, so there is no need to install it into each project's venv or conda env. lnt never imports or builds the target code. It parses Python with ast, which follows the grammar of the Python running lnt, and Rust with tree-sitter. Pick a Python at least as new as the newest one your projects use. If lnt is not on PATH, the hooks fail with "command not found".

Install the Claude Code skill (once)

lnt init --skill

Writes ~/.claude/skills/init-protocol/SKILL.md. Inside Claude Code, /init-protocol then does the same as lnt init below. Skip if you do not use Claude Code.

Set up a project

cd my-project
lnt init

Creates:

my-project/
  CLAUDE.md                                   # only if absent; points to the protocol doc
  .claude/
    for-agent-codingprotocol-ln-structure.md         # language-neutral rules; read when needed, not injected every session
    for-agent-codingprotocol-ln-structure-python.md  # how to apply them in Python: __init__ patterns, import syntax, examples
                                              # (Rust: ...-rust.md, plus clippy.toml at the project root)
    settings.json                             # Claude Code hooks; only "hooks" is merged if the file exists

Write code, check it

Create module folders under src/<package>/l0/, l1/, ... One module = one folder = one file (by default) = one class. ln-structure is object based: a module publishes classes and type aliases only (functions become methods, constants become class attributes, names or files starting with _ stay private). Do not write __init__.py by hand; lnt doc generates it.

lnt check          # rules
lnt doc            # module map and __init__.py files
lnt map            # overview

When lnt check reports C3 ("declared l2, computed l1"), run lnt move <module> l1.

4. How the Claude Code hooks work

lnt init registers two hooks in .claude/settings.json.

{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup|resume|clear|compact",
      "hooks": [{"type": "command", "command": "lnt hook session-start"}]
    }],
    "PostToolUse": [{
      "matcher": "Edit|Write|MultiEdit",
      "hooks": [{"type": "command", "command": "lnt hook post-edit", "timeout": 30}]
    }]
  }
}
  • SessionStart: injects for-agent-layerinfo.md (the module map) into the agent's context, so it starts oriented instead of grepping
  • PostToolUse: runs after every edit to src/**/*.py
    • Violations: exit 2 + stderr. The agent receives an error, with how to resolve each kind of violation, and cannot proceed until fixed
    • No violations: the blast radius and module map drift (fix with lnt doc) are returned as context

Hooks are executed by Claude Code itself, not by the agent. The check happens even if the agent "forgets" the rules.

Projects set up before 0.3.1 have python -m lntools hook ... hooks, which depend on the active Python. Run lnt init again and they become lnt hook ....

5. Docs

One stored doc: for-agent-layerinfo.md at the package root (and one inside each nested module). It lists modules per layer with one line of responsibility each. lnt doc keeps the list in sync between the <!-- lnt:generated:start --> and end markers; the responsibility lines are written by people and survive regeneration. Anything outside the markers is preserved as Notes.

Signatures are not stored. lnt sig computes them from the code on the spot.

$ lnt sig l1.order
## l1.order
Order.__init__(symbol: str, qty: float)  # order.py
Order.fill(qty: float) -> None  # order.py

There is no per-module doc; read the code. What code cannot tell (design reasons, contracts outside Python) goes briefly into the Notes area.

6. FAQ

A lower module needs a higher one. Move it up. Use dependency inversion only when moving cannot work (mutual calls, plugins, code outside the package). See the rules doc, section on calling upward.

Two classes call each other. If they are one unit of behavior, keep them as two files in one module folder. Cycles inside a module are out of scope.

Too many layers. Nest. l3/portfolio/ can contain its own l0/, l1/. From outside only the portfolio surface is visible.

Applying to an existing project. lnt init, then lnt check. Fix violations one at a time with lnt move. If hand-written layerinfo docs exist, lnt doc keeps the one-line descriptions and replaces the rest with the generated block (the old text is in git).

7. Development

git clone https://github.com/gatesplan/ff-lntools
cd ff-lntools
pip install -e .[dev]
python -m pytest -q
python -m lntools check       # this project is itself ln-structure and checks itself

Research protocols (experiment folders, paper notes) live in a separate repository: ff_coding_agent_protocol_md.

Metadata

Release files for ff-lntools 0.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ff-lntools 0.12.0
File Size Uploaded Provenance Actions
ff_lntools-0.12.0.tar.gz 94.5 kB Without provenance Details for ff_lntools-0.12.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ff-lntools 0.12.0
File Interpreter ABI Platform Actions
ff_lntools-0.12.0-py3-none-any.whl Python 3 none any Details for ff_lntools-0.12.0-py3-none-any.whl

Total release size: 221.2 kB

Release files / ff_lntools-0.12.0.tar.gz

Download URL ff_lntools-0.12.0.tar.gz
Size 94.5 kB
Tags Source
SHA-256 checksum
How to use checksums
58a4f560dab01400cc52d911176427ea47f03874c23d6c43959d5d6b38bc572e
BLAKE2b-256 checksum
How to use checksums
618e8c216891e517aa21473d8330b1f7ceec2b6b21d380486d1c0edd0c9ba479
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / ff_lntools-0.12.0-py3-none-any.whl

Download URL ff_lntools-0.12.0-py3-none-any.whl
Size 126.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3edbc4ae7f70e2dcee324f72fb550c6fdd0fcef2510d0c60c7c55e4452340785
BLAKE2b-256 checksum
How to use checksums
31ee0b60d6c69f2ae371035390c5d06fc1c1b34b06236d40900cff40260b6941
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for ff-lntools 0.12.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page