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 (fromdrydock 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¶
Shows a plan as markdown. For JSON:
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¶
Example: By Plugin Seam¶
First, find available seams:
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).
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:
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.
To use the original __init__.py files instead:
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:
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/fromstatements 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
.gitignorefor immediate use
What Doesn't (by design)¶
-
Import rewriting is conservative — When in doubt, we don't rewrite. Unconfident rewrites become visible breakages in the extracted copy, not silent corruptions.
-
JS/TS path aliases need manual work — TypeScript
@/componentspaths require updatingtsconfig.jsonafter extraction. Drydock doesn't rewrite because it doesn't understand your build config. -
Dependency versions are unpinned — Generated
pyproject.tomlhasrequires-python = "*"anddependencies = [...]with no versions. You must fix these before publishing. -
No license is set — The generated
pyproject.tomlhas no license field. Add one before publishing. -
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¶
- Boundaries (where to cut) — Run this first
- Extending Drydock — Write a custom import rewriter