Architecture As Code Python logo

Architecture As Code Python

Organization
l-gevity
architecture-as-code-python

Python implementation of the `architecture-as-code` pattern. Per-package `architecture.toml` files merged into a single `import-linter` config and enforced via `lint-imports` (over a Grimp-built import graph). TRIGGER when: implementing or extending architecture-as-code in a Python repo, debugging an `import-linter` contract, or adapting the assembler. SKIP for routine edits inside a governed package. Reads in conjunction with `architecture-as-code` (the pattern, source of truth for schema, rule placement, anti-patterns, and audit checklist) — this skill defines only the Python-specific encoding, assembler code, and gotchas.

Overview

Publisherl-gevity
Repositoryl-gevity-skills
Skill namearchitecture-as-code-python
Stars
72
Forks
13
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by l-gevity on GitHub. Read the source before you install it.

Installation

Install the Architecture As Code Python AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/l-gevity/l-gevity-skills/tree/main/.agents/skills/architecture-as-code-python
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/l-gevity/l-gevity-skills.git /tmp/l-gevity-skills
mkdir -p .claude/skills
cp -r /tmp/l-gevity-skills/.agents/skills/architecture-as-code-python .claude/skills/architecture-as-code-python
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Architecture As Code Python in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Architecture As Code Python on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Architecture As Code Python is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Architecture-as-Code — Python Implementation

Prerequisite. Read architecture-as-code first. The schema (§1), subsystems (§2), forbidden edges (§3), rule placement (§4), assembler concept (§5), and anti-patterns / audit (§6) are defined there and apply identically here. This file documents only what is Python-specific.

1. File format

  • Filename: architecture.toml. Plain TOML — no Python code, no imports.
  • TOML arrays-of-tables: [[subsystems]], [[forbidden]]. [[components]] is read as a deprecated alias for [[subsystems]] (§3).
  • Pattern syntax: dotted module paths (mypkg.core.tier1). The package and all submodules match by default; set single = true to match only the exact module.
  • Subsystem patterns may overlap (e.g. mypkg.core and mypkg.core.tier1 both match files under mypkg/core/tier1/). That's fine — import-linter checks each contract independently; there's no first-match-wins.
toml
# architecture.toml — example for a package with internal layering
[[subsystems]]
name = "core-facade"
pattern = "mypkg.core.api"           # public sub-package = the facade

[[subsystems]]
name = "core-tier1"
pattern = "mypkg.core.tier1"

[[subsystems]]
name = "core-tier3"
pattern = "mypkg.core.tier3"

[[subsystems]]
name = "core-other"
pattern = "mypkg.core"               # whole-package catch-all, last

[[forbidden]]
# Efferent — self-knowledge, lives in own file.
from = "core-*"
to = "*"
except_to = ["core-*"]
why = "Core purity: no imports outside the core package."

[[forbidden]]
# Internal layering.
from = "core-tier3"
to = "core-tier1"
why = "Tier 3 must go through tier 2."

Parametric "not equal" syntax for cross-domain isolation: !{from.captured.domain} (single braces).

[!NOTE] Facade pattern in Python. Python's idiomatic facade is a sub-package (e.g. mypkg.core.api) exposing public API via __init__.py and __all__. The "single file as facade" pattern from the JS skill maps awkwardly here; prefer a public sub-package plus forbidden edges that ban imports to the internal sub-packages.

2. Specialized contract types

The assembler emits forbidden contracts by default. For two common shapes import-linter offers more specific contract types that produce cleaner violations:

  • Sibling-isolation across N subsystems → one independence contract listing the N module patterns.
  • Strict tier ordering (e.g. tier1 < tier2 < tier3, where higher tiers may import lower) → a layers contract. Optional — explicit [[forbidden]] edges between specific tiers also work.

3. Assembler

A small Python script invoked by pre-commit and CI. Discovers all architecture.toml files, merges them, generates a .importlinter config in INI form, and invokes lint-imports.

Why generate .importlinter rather than mutate pyproject.toml? The generated file is a build artifact (gitignored alongside .import_linter_cache/); the source of truth is the per-package architecture.tomls. import-linter natively reads .importlinter ahead of pyproject.toml.

python
# tools/arch_lint.py
import subprocess
import sys
import tomllib
from pathlib import Path

# Coverage gate (pattern Directive 7): the graph roots must cover every source
# package. Derive them; one hardcoded root silently excludes whatever top-level
# directory lands next.
SOURCE_PACKAGES = sorted(
    p.name
    for p in Path(".").iterdir()
    if p.is_dir() and (p / "__init__.py").exists() and not p.name.startswith(".")
)
GENERATED_CONFIG = Path(".importlinter")

# 1. Discover — recursive walk, skipping the configured ignore-list
#    (.venv, dist, build, .tox, __pycache__, .import_linter_cache).
arch_files = sorted(
    Path(".").rglob("architecture.toml"),
    key=lambda p: -len(p.parts),  # deeper-first
)

# 2. Concat. [[components]] is the deprecated alias for [[subsystems]]
#    (pattern Directive 1): warn on it, reject a file that carries both.
subsystems, forbidden = [], []
for f in arch_files:
    data = tomllib.loads(f.read_text(encoding="utf-8"))
    if "subsystems" in data and "components" in data:
        sys.exit(f"{f}: declare [[subsystems]] or [[components]], not both")
    if "components" in data:
        print(f"{f}: [[components]] is a deprecated alias for [[subsystems]]", file=sys.stderr)
    subsystems.extend(data.get("subsystems", data.get("components", [])))
    forbidden.extend(data.get("forbidden", []))

# 3. Expand wildcards against the live registry.
names = [c["name"] for c in subsystems]

def expand(spec, except_=None):
    if isinstance(spec, dict):
        return spec  # parametric — handled separately
    items = spec if isinstance(spec, list) else [spec]
    def resolve(lst):
        out = []
        for t in lst:
            if t == "*":
                out.extend(names)
            elif t.endswith("*"):
                p = t[:-1]
                out.extend(n for n in names if n.startswith(p))
            else:
                out.append(t)
        return out
    types = resolve(items)
    if except_:
        sub = set(resolve(except_))
        types = [t for t in types if t not in sub]
    return types

# 4. Emit import-linter contracts (INI format).
subsystem_pattern = {c["name"]: c["pattern"] for c in subsystems}
subsystem_single  = {c["name"]: c.get("single", False) for c in subsystems}

if not SOURCE_PACKAGES:
    sys.exit("No source packages discovered — the import graph would be empty.")
ini = ["[importlinter]", "root_packages ="]
ini.extend(f"    {p}" for p in SOURCE_PACKAGES)
ini.append("")
for i, edge in enumerate(forbidden):
    src = expand(edge["from"], edge.get("except"))
    dst = expand(edge["to"],   edge.get("except_to"))
    if not src or not dst:
        continue   # nothing to forbid after exceptions

    src_modules = [subsystem_pattern[n] for n in src]
    dst_modules = [subsystem_pattern[n] for n in dst]
    as_packages = not any(subsystem_single[n] for n in src + dst)

    ini.append(f"[importlinter:contract:{i}]")
    ini.append(f"name = {edge['why'][:80]}")
    ini.append("type = forbidden")
    ini.append("source_modules =")
    ini.extend(f"    {m}" for m in src_modules)
    ini.append("forbidden_modules =")
    ini.extend(f"    {m}" for m in dst_modules)
    if not as_packages:
        ini.append("as_packages = False")
    ini.append("")

GENERATED_CONFIG.write_text("\n".join(ini), encoding="utf-8")

# 5. Invoke lint-imports
result = subprocess.run(
    ["lint-imports", "--config", str(GENERATED_CONFIG)],
    check=False,
)
sys.exit(result.returncode)

Install & run:

bash
pip install import-linter      # pulls in grimp; tomli on Python <3.11
python tools/arch_lint.py      # discover + assemble + lint

Pre-commit hook:

yaml
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: arch-lint
        name: "Architecture lint"
        entry: "python tools/arch_lint.py"
        language: system
        pass_filenames: false

language: system is required — lint-imports must run inside the project's virtualenv to import the analyzed packages.

Gitignore:

.importlinter            # generated by tools/arch_lint.py
.import_linter_cache/    # import-linter's own cache

4. Output Contract

When applying this implementation, emit:

Scope:          <repo / package / subsystem path>
Decision:       Add architecture.toml | Update assembler | Update import-linter config | Blocked
Generated config:<path, if any>
Contracts changed:<forbidden / layers / independence contracts>
Verification:   <lint-imports command / assembler command / Not run + reason>
Next action:    <specific file edit, package install, cache clear, or unresolved question>

5. Python-specific gotchas

[!NOTE] Dynamic imports bypass enforcement. import-linter reads static import and from ... import statements via Grimp. Imports through importlib.import_module(...), __import__, or string-based dispatch don't appear in the graph. If a package uses dynamic imports for plugin loading, mark the entry-point as a single-module declared subsystem (single = true) so its rules are explicit, and consider banning the dynamic style elsewhere.

[!IMPORTANT] The graph root is the coverage gate. import-linter builds its graph from root_package / root_packages. A new top-level package or directory outside that list is not in the graph, so no contract can fail on it and the run still reports success — the Python form of pattern Directive 7 ("the emitted rule's scope must equal the linted source set"). Derive root_packages from the tree at assembly time rather than hardcoding one root, and fail the assembler when the derived list is empty or a top-level source package is missing. import-linter has no native file-existence rule (Directive 8), so that comparison — packages on disk vs. packages in the generated config — is the check that catches an undeclared directory.

[!NOTE] import-linter * is single-segment. In forbidden_modules and similar fields, mypkg.* matches mypkg.foo but not mypkg.foo.bar. The pattern's prefix wildcards (core-*) operate on subsystem names and are expanded by the assembler before contracts are emitted, so they don't hit this limit. But if you write raw module patterns yourself, mind the difference.

[!NOTE] import-linter forbids; it never requires. An aspect coverage check — every subsystem in the governed set uses the mechanism — has no contract type. Express it as a grimp-based test over the same registry; defect-shift-left places that check.

[!NOTE] Cache staleness during refactors. .import_linter_cache/ speeds up subsequent runs but can mislead during heavy refactors. Delete it if results don't match what your import statements actually say.

[!NOTE] Root package must be importable. import-linter imports the root package to walk it. Make sure pip install -e . (or equivalent) has been run in the active venv before invoking the assembler.

[!NOTE] Mixing pattern-level and subsystem-level wildcards. A single rule that uses both mypkg.* (single-segment, import-linter native) and core-* (multi-match, assembler-expanded) will surprise you. Subsystem- name wildcards are the assembler's domain; raw * in import-linter patterns is single-segment only. Don't mix in one rule.

Frequently asked questions

What does the Architecture As Code Python AI skill do?

Python implementation of the `architecture-as-code` pattern. Per-package `architecture.toml` files merged into a single `import-linter` config and enforced via `lint-imports` (over a Grimp-built import graph). TRIGGER when: implementing or extending architecture-as-code in a Python repo, debugging an `import-linter` contract, or adapting the assembler. SKIP for routine edits inside a governed package. Reads in conjunction with `architecture-as-code` (the pattern, source of truth for schema, rule placement, anti-patterns, and audit checklist) — this skill defines only the Python-specific enc...

Why use Architecture As Code Python on TypingMind?

Because you install it once and use it with any model. Architecture As Code Python is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Architecture As Code Python in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/l-gevity/l-gevity-skills/tree/main/.agents/skills/architecture-as-code-python. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Architecture As Code Python?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Architecture As Code Python?

As many as you like. As long as a model supports skills, you can use Architecture As Code Python with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Architecture As Code Python AI skill free?

Yes. It is published on GitHub by l-gevity under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇