This project uses DSP (Data Structure Protocol). The
.dsp/directory is the project entity graph: modules, functions, dependencies, public API. This is your long-term memory of the code structure.Working rules:
- Before changing code — find the affected entities via
dsp-cli search,find-by-source, orread-toc. Read theirdescriptionandimportsto understand the context.- When creating a file/module — immediately call
dsp-cli create-object. For each exported function —create-function(with--owner). Register exports viacreate-shared.- When adding an import — call
dsp-cli add-importwith a shortwhy. For external dependencies — firstcreate-object --kind externalif the entity does not exist yet.- When removing an import / export / file — call
remove-import,remove-shared,remove-entityrespectively. Cascading cleanup is performed automatically.- When renaming/moving a file — call
move-entity. The UID does not change.- Don’t touch DSP if only internal implementation changed, without changing purpose and dependencies.
- Bootstrap — if
.dsp/is empty, traverse the project from the root entrypoint downwards (DFS over imports), documenting every file.Key commands:
dsp-cli init dsp-cli create-object <source> <purpose> [--kind external] [--toc ROOT_UID] dsp-cli create-function <source> <purpose> [--owner UID] [--toc ROOT_UID] dsp-cli create-shared <exporter_uid> <shared_uid> [<shared_uid> ...] dsp-cli add-import <importer_uid> <imported_uid> <why> [--exporter UID] dsp-cli remove-import <importer_uid> <imported_uid> [--exporter UID] dsp-cli remove-shared <exporter_uid> <shared_uid> dsp-cli remove-entity <uid> dsp-cli move-entity <uid> <new_source> dsp-cli update-description <uid> [--source S] [--purpose P] [--kind K] dsp-cli get-entity <uid> dsp-cli get-children <uid> [--depth N] dsp-cli get-parents <uid> [--depth N] dsp-cli search <query> dsp-cli find-by-source <path> dsp-cli read-toc [--toc ROOT_UID] dsp-cli get-stats
The goal of DSP is to store minimal, but sufficient context about a repository/artifact system as a graph “entities → dependencies/public API”, so that an LLM can:
- quickly find the required fragments by UID,
- understand why entities exist and how they are connected,
- avoid having to load the entire source tree into the context window.
DSP is long-term memory and an index of the project for an LLM. At any time, an agent can run project-wide search (grep), find the required entities by descriptions/keywords, and from a found UID expand the whole relationship graph: incoming dependencies, outgoing imports, and recipients via exports. This replaces the need to “remember” the project structure or load it in full — the whole project map is always available through .dsp.
DSP is not documentation for humans and not an AST dump. DSP records:
- the meaning of entities (purpose),
- boundaries (what it imports / what it shares outward),
- reasons for relationships (why something is imported), in a volume sufficient for code generation and refactoring.
DSP works with any data and codebases (Node.js, Python, Go, frontend, backend, infrastructure, etc.).
- Code = graph. Graph nodes are objects and functions. Edges are
imports,shared/exports. - Identity by UID, not by file. A file path is an attribute, not an identifier. Renames/moves must not change the UID.
- “Shared” creates an entity. If part of an object becomes available externally (export/public), it must have its own UID (and its own directory in
.dsp). - Import tracks both “from where” and “what”. For one actual import in code, two links may be recorded:
- to the Object module/provider (where we import from),
- to the specific shared entity (what exactly we use).
- Import completeness (coverage). Any file/artifact that is imported/connected somewhere must be represented in
.dspas an Object with UID andsource:. This includes not only code, but also assets/resources: images (.png/.svg/.webp), styles (.css/.scss), data (.json), wasm, sql, templates, etc. whyis always written next to the imported entity. The feedback is stored inexportsof the imported entity (see §4.3 andaddImport).- Start from roots. Each root is a separate entrypoint with its own TOC file. By default, roots are auto-detected via the LLM; if needed, they are specified manually. Imports are traversed depth-first from each root.
- External dependencies are recorded only. If an entity imports an external library/tool (npm packages, stdlib, SDK, etc.), DSP records the fact of the import and a brief purpose, but does not dive inside the dependency (in Node.js do not go into
node_modules; in Python — intosite-packages; in Go — intovendor/module cache; etc.). At the same time, external dependencies still have anexports index— you can see who imports them and why, so the relationship graph remains complete.
- Entity: a graph node. Two base kinds:
ObjectandFunction. - Object: any “thing” with a UID that is not a function (module/ES module, class, namespace, config, resource file, external dependency, etc.). Variables are considered part of a global or local Object; when shared/exported they become separate entities with their own UID.
- Function: a function/method/handler/pipeline that performs work.
- imports: a list of UIDs of any entities outside the local scope that the current entity uses (imports of modules, libraries, objects, functions; dependencies via constructor/DI in classes). For an Object, this also includes its own methods/functions — so the object “sees” its composition.
- shared: a list of UIDs of entities available outside the local scope (exported functions, objects, variables; public fields/methods of classes).
- exports index: a reverse index “who imports this entity and why”. Maintained for any imported entity (Object, Function, external).
At the repository root, a .dsp/ directory is created. For each entity, a directory is created:
.dsp/<uid>/
UID format (to avoid collisions and indicate type by prefix):
obj-<8 hex>— for objects (example:obj-a1b2c3d4)func-<8 hex>— for functions (example:func-7f3a9c12)
Generation: first 8 characters of uuid4().hex. 4 billion possible UIDs is enough for any project.
For entities inside a file (to avoid binding to line numbers), the UID is anchored to code via a comment marker right before the declaration:
// @dsp func-a1b2c3d4
export function calculateTotal(items) { ... }# @dsp obj-e5f6g7h8
class UserService:The @dsp <uid> marker lets you quickly find an entity in code via grep, is independent of lines/formatting, and does not require renaming symbols.
Important: the UID must be stable for “the same” entity across moves/renames (path/file may change, UID must not).
Each entity directory contains:
description— purpose + link to the source code (path/symbol).imports— list of imports/references to entities (one entry per line).shared— list of UIDs of entities available outside the local scope (exports, public fields/methods).exports/— (created as needed) reverse index: who imports this entity and/or its shared parts, and why. Works for any kind (Object, Function, external).
The description file is a short, human- and LLM-readable block. Minimal recommended template:
source: <repo-relative-path>[#<symbol>]
kind: object|function|external
purpose: <1-3 sentences: what it is and why>
After that (optional), freeform text/markdown sections may follow (no rigid schema), e.g. notes: / contracts:.
Rule for the root file (root entrypoint): its description must include a brief project overview (what the system is, its main pipeline/workflow, and what is public API/boundaries) — as short as possible so it becomes the first “project context” for the LLM.
Minimal format (one line — one dependency):
<imported_uid>
Allowed extension (if you need to encode “via which exporter” or other metadata):
<imported_uid> via=<exporter_obj_uid>
One line — one shared entity UID:
<shared_uid>
The exports/ directory is created for any imported entity (Object, Function, external) and shows who uses it and why.
For an entity without shared (Function, external, or an Object imported as a whole):
.dsp/<uid>/exports/<importer_uid>— a file with text “why it is imported” (1–3 sentences).
For an Object with shared entities, add the following:
.dsp/<uid>/exports/<shared_uid>/description— what is exported (briefly)..dsp/<uid>/exports/<shared_uid>/<importer_uid>— “why this shared is imported” (1–3 sentences).
This gives the LLM answers to three questions:
- who imports the entity and why (via
exports/<importer_uid>), - what can be imported from the object (via
shared), - why a specific shared is imported (via
exports/<shared_uid>/<importer_uid>).
If the same shared UID is re-exported from multiple objects (barrel exports), export indices are kept separately in each exporter.
For each root entrypoint, its own TOC file is created under .dsp/. One root — one TOC.
Naming: .dsp/TOC for a single root, .dsp/TOC-<rootUid> if there are multiple roots.
Format:
<uid_root>
<uid_2>
<uid_3>
...
<uid_N>
Rules:
- TOC[0] is always the root of this entrypoint. This is how the LLM gets a starting point.
- Next — all entities reachable from that root, in documentation order (traversal order during bootstrap).
- Each UID appears in a given TOC exactly once.
- The same entity may be in multiple TOCs (if reachable from multiple roots).
- When documenting new entities — append them to the end of the corresponding TOC.
Purpose:
- A complete overview of all entities reachable from a given root.
- Lets the LLM start navigation from the right entrypoint and dive into its structure.
- In multi-root projects (monorepo, multiple applications), each TOC is an independent map of its subtree.
Below are the operations used by the .dsp generator.
Initialize the .dsp/ directory at the project root. Required first step before any operations. Idempotent — repeated calls are safe.
CLI: dsp-cli init
Parameters:
sourceRef— path to source (+ symbol if applicable),purpose— purpose,kind—object(default) orexternal(for external dependencies).
Actions:
- generate/resolve
objUid(stably), - create
.dsp/<objUid>/, - write
.dsp/<objUid>/description(source, kind, purpose), - create
.dsp/<objUid>/importsif missing (empty), - if needed —
.dsp/<objUid>/shared(empty), - append
objUidto.dsp/TOC.
Actions:
- generate/resolve
funcUid(stably), - create
.dsp/<funcUid>/, - write
.dsp/<funcUid>/description, - create
.dsp/<funcUid>/imports(empty), - if
ownerUidis provided:- append
funcUidto the owner object’simports(so the object “sees” its methods), - create a reverse record
.dsp/<funcUid>/exports/<ownerUid>(sogetParentscan find the owner without a full scan),
- append
- append
funcUidto.dsp/TOC.
Function ownership is determined through the owner’s
imports. Reverse lookup — throughgetParents(funcUid). A standalone function (without an owner) is simply added to a module’ssharedif it is exported.
Actions:
- append
sharedUidsto.dsp/<exporterUid>/shared, - ensure
.dsp/<exporterUid>/exports/exists, - for each
sharedUid, ensure.dsp/<exporterUid>/exports/<sharedUid>/descriptionexists — if the file is created for the first time,descriptionis auto-filled from thepurposeof the shared entity (if it already exists in.dsp).
Actions:
- append
importedUid(and optionallyvia=exporterUid) to.dsp/<importerUid>/imports, - write the reverse feedback “why we import” into
exportsof the imported entity:- if importing a shared entity and
exporterUidis known:- create/update
.dsp/<exporterUid>/exports/<importedUid>/<importerUid>with textwhy,
- create/update
- otherwise (importing the Object as a whole — local module, external package/submodule, side-effect import, etc.):
- ensure
.dsp/<importedUid>/exports/exists, - create/update
.dsp/<importedUid>/exports/<importerUid>with textwhy.
- ensure
- if importing a shared entity and
When one addImport call is enough vs when you need two:
You need two calls when importing both the whole module (or as a namespace) and a specific symbol from it. One call is enough when only one of those happens.
// Example 1: namespace import + named import from the same module
import * as utils from './utils'; // → addImport(thisUid, utilsObjUid, why="formatting utilities")
import { calc } from './utils'; // → addImport(thisUid, calcUid, utilsObjUid, why="total calculation")
// Total: 2 addImport calls
// Example 2: named import only
import { UserService } from './services';
// → addImport(thisUid, userServiceUid, servicesObjUid, why="user management")
// Total: 1 call (to the shared entity, exporter provided via exporterUid)
// Example 3: side-effect import (no specific symbol)
import './polyfills';
// → addImport(thisUid, polyfillsObjUid, why="browser polyfills")
// Total: 1 call (to the whole Object)
// Example 4: default import
import express from 'express';
// → addImport(thisUid, expressObjUid, why="HTTP framework")
// Total: 1 call (to the whole Object, external)Update the description of an existing entity. Typical scenarios: the purpose of a module changed, the file path changed (rename/move), or the description was refined after refactoring.
Actions:
- read
.dsp/<uid>/description, - update specified fields (
source:,purpose:,kind:, freeform sections), - write back
.dsp/<uid>/description.
Update the reason for an import (the why text). Typical scenario: a module still imports a dependency, but uses it for a different purpose.
Actions:
- locate the feedback file in
exports:- if
exporterUidis provided:.dsp/<exporterUid>/exports/<importedUid>/<importerUid>, - otherwise:
.dsp/<importedUid>/exports/<importerUid>,
- if
- overwrite the file contents with
newWhy.
The entity moved (file rename/move). UID stays the same; only the source reference changes.
Actions:
- update
source:in.dsp/<uid>/descriptiontonewSourceRef.
This is a special case of
updateDescription, separated for clarity: the UID does not change on moves.
Remove an import relationship. Typical scenario: an import was removed from code.
Actions:
- remove
importedUidfrom.dsp/<importerUid>/imports, - delete the feedback file from
exports:- if
exporterUidis provided: delete.dsp/<exporterUid>/exports/<importedUid>/<importerUid>, - otherwise: delete
.dsp/<importedUid>/exports/<importerUid>.
- if
The entity is no longer exported. Typical scenario: export was removed from code.
Actions:
- remove
sharedUidfrom.dsp/<exporterUid>/shared, - delete the directory
.dsp/<exporterUid>/exports/<sharedUid>/with all recipient files, - for each recipient (former files under
exports/<sharedUid>/) — removesharedUidfrom theirimports.
Remove an entity from DSP completely. Typical scenario: a file/module was deleted from the project.
Actions:
- Full imports scan: for each entity in
.dsp— remove fromimportsall lines whereuidappears asimportedor as avia=target. This covers direct imports, shared-imports viauid, owner links — everything in one pass. - Clean outgoing links: read
.dsp/<uid>/imports— for each imported entity, delete the feedback file from itsexports/. - Clean shared references in exporters: for each entity — if
uidappears in someone’sshared, removeuidfrom.dsp/<exporterUid>/sharedand delete.dsp/<exporterUid>/exports/<uid>/. - Remove
uidfrom all TOC files. - Delete the
.dsp/<uid>/directory entirely.
Get a full snapshot of an entity. Basic operation — an entry point for any analysis of a specific module.
Returns:
description(source, kind, purpose, notes),imports[]— list of dependency UIDs,shared[]— list of exported entity UIDs (for Objects),exportedTo[]— list of recipients fromexports/(who imports it and why).
Implementation: read files from .dsp/<uid>/.
Get the public API of an entity — what it makes available externally and who uses it. Typical scenario: an agent needs to understand what can be imported from a module/class/function.
Returns for each shared UID:
- description (from
.dsp/<uid>/exports/<sharedUid>/description), - list of recipients with reasons (files under
.dsp/<uid>/exports/<sharedUid>/).
Get everyone who imports this entity, and why. Typical scenario: impact analysis — who will be affected by changes in this module.
Returns: a list of pairs (recipientUid, why).
Implementation — a three-level search (each level complements the previous, with UID deduplication):
- Direct recipients: files under
.dsp/<uid>/exports/(direct imports viaaddImportwithoutexporter). - Via shared exporters: if
uidis present in someone’sshared, read files under.dsp/<exporterUid>/exports/<uid>/(imports viaaddImportwithexporter). - Imports fallback: scan all entities — if
uidis found in someone’simports(e.g., owner relationship) but wasn’t discovered at previous levels.
Get the dependency tree downwards — what this entity imports (and what its dependencies import, etc.). Typical scenarios: understand what a module consists of, which libraries it pulls in.
Parameters:
uid— starting point,depth— traversal depth (default1— direct imports only;Infinity— full tree).
Returns: a tree of nodes { uid, description.purpose, children[] }.
Implementation: recursive reading of imports with a visited set to guard against cycles.
Get the dependency tree upwards — who imports this entity (and who imports those, etc.). Typical scenarios: understand blast radius, find all entry points that use this code.
Parameters:
uid— starting point,depth— traversal depth (default1— direct recipients only;Infinity— up to the root(s)).
Returns: a tree of nodes { uid, description.purpose, why, parents[] }.
Implementation: recursive reading of exports/ with a visited set.
Find the shortest path between two entities in the graph (in any direction along imports edges). Typical scenario: understand how two modules are connected to each other.
Returns: an ordered list of UIDs from fromUid to toUid, or null if no path exists.
Implementation: BFS over the imports graph (bidirectional — via imports and exports).
Full-text search across .dsp. Typical scenarios: find a module by a keyword (“authentication”, “routing”, “cache”), find entities related to a specific file.
Searches for matches in:
description(purpose, source, notes),- file names under
exports/(recipient UIDs).
Returns: a list { uid, matchContext } — the UID and the fragment where the match was found.
Implementation: grep -r "query" .dsp/ over description files.
Find entities by a source file path. Typical scenario: an agent sees a file in code and wants its DSP representation.
Returns a list of UIDs, because one file may contain multiple entities (the module Object + shared functions/classes inside). Matching: exact by source: or by the sourcePath# prefix (for entities inside a file).
Implementation: search for source: across all .dsp/*/description.
Read the project table of contents. Entry point for getting familiar with the project: TOC[0] is the root, then all other entities in documentation order.
Implementation: read .dsp/TOC.
Detect cyclic dependencies in the imports graph. Typical scenario: project audit, finding architectural issues.
Returns: a list of cycles; each cycle is an array of UIDs forming a closed path.
Find “orphan” entities — those that nobody uses except the root. Typical scenario: find dead code and unused modules.
An entity is not considered an orphan if at least one of the following holds:
- it is a root (the first UID in any TOC),
- it appears in
importsof any other entity (asimportedor as avia=target), - its
exports/is non-empty (there is at least one recipient).
Implementation: collect the set of all UIDs appearing in imports (including via= targets), then for the remaining ones check exports/.
Overall statistics for the DSP graph. Typical scenario: quick orientation in the scale of the project.
Returns:
- total number of entities (Object / Function / External),
- number of edges (imports),
- number of shared entities,
- number of cycles (if any),
- number of orphans.
Bootstrap is a simple DFS traversal of dependencies starting from a root file. For each root entrypoint, bootstrap is executed separately with its own TOC file.
Step 1. Identify root entrypoint(s):
- by default — auto-detect via the LLM (package.json
main, framework entrypoint, etc.), - or specify manually,
- if there are multiple roots — run bootstrap for each, creating a separate TOC (
TOC-<rootUid>).
Step 2. Fully document the root file:
createObjectfor the module (UID is written to TOC first),- extract functions →
createFunctionfor each (with ownerUid pointing to this Object), - extract
shared(exports/public API) →createShared, - extract all
imports→addImport, - external dependencies from imports →
createObject(..., kind: external)(append to TOC, but do not descend).
Step 3. Take the first import from the current file that is NOT an external dependency (not a library, not node_modules, not stdlib):
- document it fully (same as Step 2),
- append its UID to TOC.
Step 4. Recursive descent:
- from the just-documented file, take the first non-library import,
- if it exists — document it and repeat Step 4,
- if none exist — go up one level and take the next unprocessed non-library import.
Step 5. Repeat until all reachable non-library files are documented.
Visually:
root (document)
├─ import_A (non-library → document)
│ ├─ import_A1 (non-library → document)
│ │ └─ ... (descend deeper)
│ ├─ import_A2 (external → record kind: external, DO NOT descend)
│ └─ import_A3 (non-library → document)
│ └─ ... no non-library imports → backtrack
├─ import_B (non-library → document)
│ └─ ...
└─ import_C (external → record kind: external, DO NOT descend)
Key rules:
- Traversal uses a
visitedset by UID/sourceRef — no infinite recursion. - External dependencies are recorded as Objects with
kind: external, but their internal structure is not analyzed. - After traversal completes,
.dsp/TOCcontains a complete ordered list of all project entities.
- UID must not depend on the file path.
- UID must survive:
- rename/move,
- code rearrangement,
- formatting,
- small implementation changes.
A new UID is created only if an entity changes its purpose (semantic identity), for example:
- a module/class/function started solving a different problem,
- the entity was “reborn” (the old one was replaced with different logic while keeping the name).
In all other cases, update descriptions/links and keep the UID.
Separate identity between “file as an Object” and “entities inside a file”:
- File as an Object:
uid = obj-<uuid>,source: <filePath>. - Entities inside a file (shared functions, shared objects, exported classes): their own
uid = obj-<uuid>orfunc-<uuid>, anchored to code via a comment marker@dsp <uid>before the declaration.
Example (TypeScript):
// @dsp func-7f3a9c12
export function calculateTotal(items: Item[]): number { ... }
// @dsp obj-b4e82d01
export class OrderService { ... }Example (Python):
# @dsp func-3c19ab8e
def process_payment(order):
...Why a comment, not a line-number binding:
- you can instantly find the UID in code via
grep "@dsp func-7f3a9c12", - it does not depend on line numbers, formatting, code rearrangement,
- it does not require renaming symbols — names stay clean,
- it works for any language (every language has comments).
sourceRef in description is stored as source: <filePath>#<uid>.
The source of truth is
.dsp: after a file rename/move it is enough to updatesource:indescriptionwithout changing the UID.
- the tool must be able to detect cycles in the
importsgraph, - traversal must be resilient to cycles,
- cycles are diagnostic information (not fatal), but must be recorded.
- one shared UID may be available from multiple exporters,
- the
exports indexis maintained per exporter, because the LLM must understand “where it is usually imported from”.
When an agent (LLM) writes new code, it simultaneously calls DSP operations to register the created entities:
- created a new module →
createObject, - created a function →
createFunction, - added an export →
createShared, - added an import →
addImport.
DSP is updated during code generation, not after the fact.
- Dynamic dependencies (e.g.,
import()in JS,importlibin Python) are recorded as normalimportswhen discovered. - If a dynamic import cannot be determined statically — it is added to DSP upon first execution/discovery.
DSP works with any data and codebases. At the same time, DSP does not dive into external dependencies — it only records the fact of import:
- An external import is represented as an
Objectwithkind: externalindescription. descriptionrecords: package/module/version and purpose (why it is imported).- The internal structure of the library is not analyzed. For example: in Node.js — do not go into
node_modules; in Python — do not go intosite-packages; in Go — do not go intovendor/module cache; etc. - The
exports indexis maintained — you can see who imports this library and why. This allows you to immediately get the list of all recipients when updating/replacing a dependency.
This keeps the graph closed (all links point to existing UIDs) and fully navigable, without inflating .dsp with descriptions of third-party internals.
Two logical components work with DSP:
- DSP Builder — a tool (script/agent) that builds and updates
.dsp. It calls operations from §5 and maintains graph integrity. - LLM Orchestrator — an agent (LLM) that uses
.dspas project memory. It reads TOC, searches entities, expands the relationship graph, and builds context for code generation.
DSP Builder is responsible for:
- building
.dsp(bootstrap and ongoing updates), - graph integrity,
- minimal, precise descriptions of entities and dependency reasons.
LLM Orchestrator is responsible for:
- selecting relevant UIDs for a task,
- building “context bundles”:
descriptionof the target entities,- their
imports(+ transitive dependencies if needed), sharedand theexports indexto understand API and usage patterns,
- passing strictly limited sections into the model — without overloading the context.
An agent can find required modules and entities in several ways:
Via TOC:
- Read
.dsp/TOC→ get the full UID list → readdescriptionof the required entities.
Via grep/search over files, including the .dsp directory:
- Search
descriptionfiles — find entities by keywords, purpose, source path. - Search
imports— find dependencies of a specific entity. - Search
exports/— find all recipients (who uses a given entity and why).
-
Completeness at the file level, not at the code-within-file level. “Import completeness” (§2) means every file/module that is imported in the project must have an Object in
.dsp. This is about files and modules — not about every variable inside a file. Within one file, a separate UID is assigned only to an entity that is shared outward (shared) or used from multiple places. Local variables, internal helpers, private fields — remain part of the parent Object, without their own UID. If granularity keeps growing, something is wrong. -
Update recipients via
exports. To update a library/module/symbol, it is enough to openexportsof the imported entity and get the list of importers (recipients) by UID, then update them precisely. -
Change tracking.
git diffshows what changed — a new file was created, a function or an object changed. Changed files are fed to the LLM to update DSP. Changes inside functions often do not require DSP updates, because the description captures purpose, not implementation details — unless imports were added/removed.