ff-lntools
English | 한국어
ln-structure, a layout rule for Python projects built with AI coding agents (Claude Code and others),
and lnt, the tool that checks the rule mechanically and generates the navigation docs.
Standard library only. Python 3.10+. Windows / macOS / Linux.
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 # mirrors src
Four rules.
| Code | Rule |
|---|---|
| C1 | Import only from lower layers. Same layer is forbidden too |
| C2 | Import only the module surface (shop.l1.order). Never reach into its files or into a nested module's internals |
| 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 |
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 (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-C4. Exit 1 on violation |
lnt blast MODULE |
Modules affected when this one changes, including consumers through inherited interfaces |
lnt map |
Modules, layers, dependencies at a glance |
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. Always exit 0 |
lnt doc |
Generate the module map (for-agent-layerinfo.md) |
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 |
3. Getting started
Install
pip install ff-lntools
Install into every Python environment (venv, conda env) a project uses; the hook calls python -m lntools.
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 # the rules; read when needed, not injected every session
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.
lnt check # rules
lnt doc # docs
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": "python -m lntools hook session-start"}]
}],
"PostToolUse": [{
"matcher": "Edit|Write|MultiEdit",
"hooks": [{"type": "command", "command": "python -m lntools 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, bypassing dependencies of the edited module, 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.
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.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ff_lntools-0.2.0.tar.gz | 36.0 kB | Without provenance | Details for ff_lntools-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ff_lntools-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for ff_lntools-0.2.0-py3-none-any.whl |
Total release size: 84.9 kB
Release files / ff_lntools-0.2.0.tar.gz
| Download URL | ff_lntools-0.2.0.tar.gz |
|---|---|
| Size | 36.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0407efdb7fa8c7d9387311868505c8928f612634304f83ba50871c577be61c77
|
|
BLAKE2b-256 checksum How to use checksums |
c661a6a9fae8b307aa0e0fef1be3735449a6266ed4a5926f1003c5363087e566
|
| 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.2.0-py3-none-any.whl
| Download URL | ff_lntools-0.2.0-py3-none-any.whl |
|---|---|
| Size | 48.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3ec74e60f78c58d6bfe9c30a2b9504dcb5bdea16c231d82782159f58ce16f73a
|
|
BLAKE2b-256 checksum How to use checksums |
a3859033cc03457b22dffb435a7a55f50304e01a233ed0d2ad3502e024b6b061
|
| 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.2.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.