Skip to content

Extract

Extract components from a project as standalone packages.

Extraction completes the Drydock lifecycle. boundaries tells you where to cut; extract cuts and materializes a standalone package with dependencies resolved, imports rewritten, and build files generated.

The Safety Property

No output flag means plan only. The command shows you exactly what will happen — which files, what the closure drags in, and what will break — and writes nothing until you give it an output path with -o.

# Plan only (safe; no writes)
python3 -m drydock.cli extract <project> --dir <path>

# Execute (materializes the package)
python3 -m drydock.cli extract <project> --dir <path> -o /output

Selection

Extract requires exactly one selection method:

  • --cluster N — Extract a cluster by id (from drydock boundaries)
  • --dir PATH — Extract everything under a directory path
  • --files FILE,... — Extract an explicit comma-separated file list
  • --seam CONTRACT — Extract a plugin seam: the contract module plus all modules implementing it

Example: By Directory

python3 -m drydock.cli extract . --dir drydock/langs --markdown

Shows a plan as markdown. For JSON:

python3 -m drydock.cli extract . --dir drydock/langs

Example: By Cluster

After running drydock boundaries:

python3 -m drydock.cli boundaries . -o boundaries.json
# See cluster IDs in the output
python3 -m drydock.cli extract . --cluster 0

Example: By Explicit Files

python3 -m drydock.cli extract . --files src/core.py,src/api.py,src/models.py

Example: By Plugin Seam

First, find available seams:

python3 -m drydock.cli extract . --list-seams --markdown

Shows contract-shaped cut points, best first, with extraction commands:

## `extract/rewriters/base.py` — clean (cohesion 1.0)

4 implementers, 0 escaping dependencies

drydock extract . --seam extract/rewriters/base.py -o OUT

## `langs/base.py` — extractable (cohesion 0.875)

6 implementers, 1 escaping dependencies

drydock extract . --seam langs/base.py -o OUT

Then extract the seam:

# Plan only
python3 -m drydock.cli extract . --seam extract/rewriters/base.py

# Execute
python3 -m drydock.cli extract . --seam extract/rewriters/base.py -o /output

A seam is a contract (interface, protocol, trait) plus the modules that implement it. Extraction includes the contract and all implementers; consumers stay behind and keep depending on the contract from outside — which is what the plug point is for. See Plugin Seams for how cohesion predicts extraction safety.

The Closure

Extraction distinguishes between files you selected and files the closure pulled in. This is the most important thing to read in the plan.

Selected files are what you asked for. Closure files are dependencies that weren't selected but are needed to make the selection work. If you selected only src/api.py and it imports from src/core.py, the closure will pull in src/core.py automatically (unless you use --no-closure).

# Plan shows closure
python3 -m drydock.cli extract . --dir mycomponent --markdown

Output will have sections: - Selected (N) — The files you chose - Pulled by closure (M) — Dependencies needed to make them work

If closure is large (e.g., a simple module pulls in half the codebase), that signals tight coupling — extraction may not be safe.

Strict Mode: --no-closure

Turn off closure to see what breaks if you extract only your selection:

python3 -m drydock.cli extract . --dir mycomponent --no-closure

Now every unincluded dependency becomes a blocking error. This is what you want when extracting an interface to reimplement underneath.

Package Init Handling

By default, Drydock synthesizes minimal __init__.py markers rather than copying originals. This matters because a re-exporting __init__.py drags its entire package along.

Example: extracting one Drydock subpackage went from 11 files to 25 when copying originals. Synthesis brought it back to 12.

# Generated (minimal)
# langpack/__init__.py - just marks it as a package

To use the original __init__.py files instead:

python3 -m drydock.cli extract . --dir drydock/langs -o output --copy-package-inits

Component Naming

By default, component name is inferred from the selection: - --dir drydock/langs → component is langs - --cluster 5 → component is cluster-5 - --files ... → component is extracted

Override with --component:

python3 -m drydock.cli extract . --dir drydock/langs --component langpack

What Gets Generated

After execution, the output directory contains:

output/
├── pyproject.toml          # Python package metadata (if applicable)
├── go.mod                  # Go module file (if applicable)
├── package.json            # JavaScript package (if applicable)
├── Cargo.toml              # Rust crate (if applicable)
├── .gitignore              # Git exclusions
├── README.md               # Component documentation (auto-generated)
├── EXTRACTION.md           # Detailed extraction plan (filled-in template)
├── drydock-manifest.json   # Metadata + provenance
└── <component>/            # The extracted files
    ├── __init__.py         # (Python)
    ├── module.py
    └── subpackage/

pyproject.toml (Python)

Minimal, unpinned dependencies:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "langpack"
version = "0.1.0"
description = "Extracted component from Drydock"
requires-python = ">=3.10"

TODO items you must fix: - Pin dependency versions (currently *) - Add a license - Add description and author

.gitignore

Includes build artifacts, cache directories, and distribution folders for all supported languages.

README.md

Auto-generated from extraction metadata. Lists: - Files extracted and their reason (selected, closure, auto-generated) - External dependencies - Installation command - Known issues and warnings

EXTRACTION.md

Filled-in version of the extraction plan template. Details: - Source project and commit - All extracted files - Closure breakdown - External dependencies - Issues (blocking and warnings)

drydock-manifest.json

Machine-readable provenance:

{
  "component": "langpack",
  "extracted_at": "2026-08-22T16:29:47Z",
  "source": {
    "project": "Drydock",
    "root": "/path/to/Drydock",
    "commit": "2600b4..."
  },
  "files": [
    {
      "source": "drydock/langs/base.py",
      "destination": "langpack/langs/base.py",
      "reason": "selected",
      "rewrites": 5
    }
  ],
  "languages": ["python"],
  "external_deps": {}
}

Import Rewriting

Import rewriting is conservative and per-language:

  • Python — Rewrites import/from statements to new package paths
  • JavaScript/TypeScript — Rewrites ES6 and CommonJS imports
  • Go — Updates package imports
  • Rust — Updates crate references (minimal; layout preservation works better)
  • C#, Java — Basic namespace/package rewrites

Relative imports are deliberately NOT rewritten. The planner preserves relative layout, so from ..utils import x stays valid and needs no rewrite. In a real extraction (29 TypeScript files, 79 relative imports), all stayed valid with zero rewrites.

Path aliases (@/components in TypeScript, @ in Python) depend on tsconfig.json / pyproject.toml and need manual attention after extraction.

Limitations & Honesty

What Works

  • Closure detection — Drydock pulls in all internal dependencies automatically
  • Layout preservation — Relative imports stay valid with zero rewrites
  • Manifest generation — Metadata for tracing back to source
  • Package scaffolding — Build files and .gitignore for immediate use

What Doesn't (by design)

  1. Import rewriting is conservative — When in doubt, we don't rewrite. Unconfident rewrites become visible breakages in the extracted copy, not silent corruptions.

  2. JS/TS path aliases need manual work — TypeScript @/components paths require updating tsconfig.json after extraction. Drydock doesn't rewrite because it doesn't understand your build config.

  3. Dependency versions are unpinned — Generated pyproject.toml has requires-python = "*" and dependencies = [...] with no versions. You must fix these before publishing.

  4. No license is set — The generated pyproject.toml has no license field. Add one before publishing.

  5. Closure can be large — If extracting a single file pulls in 100 others, that's a signal of tight coupling. Extraction is still possible, but the extracted copy isn't independent.

Practical Example

Extract Drydock's language parsers as a standalone package:

# 1. See the plan
python3 -m drydock.cli extract . --dir drydock/langs --markdown

# 2. Execute to /tmp/langpack
python3 -m drydock.cli extract . --dir drydock/langs --component langpack -o /tmp/langpack

# 3. Inspect
tree /tmp/langpack
# Output:
# /tmp/langpack/
# ├── EXTRACTION.md
# ├── README.md
# ├── drydock-manifest.json
# ├── pyproject.toml
# ├── .gitignore
# └── langpack/
#     ├── __init__.py
#     ├── core/
#     │   ├── __init__.py
#     │   └── models.py          (pulled by closure)
#     └── langs/
#         ├── __init__.py
#         ├── base.py
#         ├── builtins.py
#         ├── csharp.py
#         ├── go.py
#         ├── java.py
#         ├── javascript.py
#         ├── python_ast.py
#         └── rust.py

# 4. Test it
cd /tmp/langpack
pip install -e .
python3 -c "from langpack.langs import python_ast; print(python_ast.__name__)"

MCP Server

The extract command is available in the MCP server with dry_run=True by default (safe):

# Your AI assistant sees:
drydock_extract(
    project_path: str,
    cluster: int = -1,
    dir: str = "",
    files: str = "",
    component: str = "",
    closure: bool = True,
    dry_run: bool = True,  # Always plan-only unless explicitly overridden
)

A model gets a reviewable plan, never a surprise write.

Flags Reference

Flag Type Default Purpose
--cluster int -1 (disabled) Extract by boundary cluster ID
--dir str "" Extract directory path
--files str "" Comma-separated file list
--seam str "" Extract a plugin seam (contract module)
--list-seams bool False List all plugin seams (contract-shaped cut points) and exit
--component str (inferred) Component / root package name
--no-closure bool False Strict mode: fail on external deps
--force bool False Overwrite non-empty destination
--allow-blocking bool False Proceed despite blocking issues
-o, --output str (plan-only) Write to path; required for execution
-m, --markdown bool False Render plan as markdown
-c, --compact bool False Compact JSON (no indentation)
-q, --quiet bool False Suppress progress output
--no-tests bool False Exclude test files
--max-files int ∞ Stop after N files (partial results flagged)
--skip-dir str (standard) Additional directory to skip
--follow-symlinks bool False Follow symlinked directories

Exit Codes

Code Meaning
0 Success
1 Errors or blocking issues in plan
2 Usage error (missing args, invalid paths)

See Also