LLM coding agents lose context between tasks. On large codebases they spend most of their tokens on "orientation" — figuring out where things live, what depends on what, and what is safe to change. DSP solves this by externalizing the project's structural map into a persistent, queryable graph stored in a .dsp/ directory next to the code.
DSP is NOT documentation for humans and NOT an AST dump. It captures three things: meaning (why an entity exists), boundaries (what it imports and exposes), and reasons (why each connection exists). This is enough for an agent to navigate, refactor, and generate code without loading the entire source tree into the context window.
Use this skill when:
.dsp/ directory (DSP is already set up).dsp, or structure mappingDSP models the codebase as a directed graph. Nodes are entities, edges are imports and shared/exports.
Two entity kinds exist:
Every entity gets a stable UID: obj-<8hex> for objects, func-<8hex> for functions. File paths are attributes that can change; UIDs survive renames, moves, and reformatting.
For entities inside a file, the UID is anchored with a comment marker in source code:
// @dsp func-7f3a9c12
export function calculateTotal(items) { ... }
# @dsp obj-e5f6g7h8
class UserService:
When an import is recorded, DSP stores a short reason explaining why that dependency exists. This lives in the exports/ reverse index of the imported entity. A dependency graph without reasons tells you what imports what; reasons tell you what is safe to change and who will break.
Each entity gets a small directory under .dsp/:
.dsp/
├── TOC # ordered list of all entity UIDs from root
├── obj-a1b2c3d4/
│ ├── description # source path, kind, purpose (1-3 sentences)
│ ├── imports # UIDs this entity depends on (one per line)
│ ├── shared # UIDs of public API / exported entities
│ └── exports/ # reverse index: who imports this and why
│ ├── <importer_uid> # file content = "why" text
│ └── <shared_uid>/
│ ├── description # what is exported
│ └── <importer_uid> # why this specific export is imported
└── func-7f3a9c12/
├── description
├── imports
└── exports/
Everything is plain text. Diffable. Reviewable. No database needed.
Every file or artifact that is imported anywhere must be represented in .dsp as an Object — code, images, styles, configs, JSON, wasm, everything. External dependencies (npm packages, stdlib, etc.) are recorded as kind: external but their internals are never analyzed.
The skill relies on a standalone Python CLI script dsp-cli.py. If it is missing from the project, download it:
curl -O https://raw.githubusercontent.com/k-kolomeitsev/data-structure-protocol/main/skills/data-structure-protocol/scripts/dsp-cli.py
Requires Python 3.10+. All commands use python dsp-cli.py --root <project-root> <command>.
If .dsp/ is empty, traverse the project from root entrypoint(s) via DFS on imports:
package.json main, framework entry, main.py, etc.)create-object, create-function for each export, create-shared, add-import for all dependenciescreate-object --kind external, add to TOC, but never descend into node_modules/site-packages/etc.search, find-by-source, or read-toc. Read their description and imports to understand context.create-object. For each exported function — create-function (with --owner). Register exports via create-shared.add-import with a brief why. For external deps — first create-object --kind external if the entity doesn't exist.remove-import, remove-shared, remove-entity. Cascade cleanup is automatic.move-entity. UID does not change.| Category | Commands |
|---|---|
| Create | init, create-object, create-function, create-shared, add-import |
| Update | update-description, update-import-why, move-entity |
| Delete | remove-import, remove-shared, remove-entity |
| Navigate | get-entity, get-children --depth N, get-parents --depth N, get-path, get-recipients, read-toc |
| Search | search <query>, find-by-source <path> |
| Diagnostics | detect-cycles, get-orphans, get-stats |
| Code Change | DSP Action |
|---|---|
| New file/module | create-object + create-function + create-shared + add-import |
| New import added | add-import (+ create-object --kind external if new dep) |
| Import removed | remove-import |
| Export added | create-shared (+ create-function if new) |
| Export removed | remove-shared |
| File renamed/moved | move-entity |
| File deleted | remove-entity |
| Purpose changed | update-description |
| Internal-only change | No DSP update needed |
python dsp-cli.py --root . init
python dsp-cli.py --root . create-object "src/app.ts" "Main application entrypoint"
# Output: obj-a1b2c3d4
python dsp-cli.py --root . create-function "src/app.ts#start" "Starts the HTTP server" --owner obj-a1b2c3d4
# Output: func-7f3a9c12
python dsp-cli.py --root . create-shared obj-a1b2c3d4 func-7f3a9c12
python dsp-cli.py --root . add-import obj-a1b2c3d4 obj-deadbeef "HTTP routing"
python dsp-cli.py --root . search "authentication"
python dsp-cli.py --root . get-entity obj-a1b2c3d4
python dsp-cli.py --root . get-children obj-a1b2c3d4 --depth 2
python dsp-cli.py --root . get-recipients obj-a1b2c3d4
python dsp-cli.py --root . get-path obj-a1b2c3d4 func-7f3a9c12
python dsp-cli.py --root . find-by-source "lodash"
# Output: obj-11223344
python dsp-cli.py --root . get-recipients obj-11223344
# Shows every module that imports lodash and WHY — lets you systematically replace it
why reason when recording an import — this is where most of DSP's value liveskind: external for third-party libraries without analyzing their internals.dsp/ diffs like code diffs — review them, keep them accurate.dsp/ for internal-only changes that don't affect purpose or dependenciesmove-entity instead)This skill connects naturally to: