9.4 KiB
Design Reference: pi-project-map
Audience: maintainers and contributors.
Purpose: explain howpi-project-mapworks internally, not how to install or use it.
1. Overview
pi-project-map is a TypeScript/Node.js skill package that generates and maintains hierarchical, paired project-map artifacts for AI coding agents:
.pi-map.index.md— routing-first, sparse directory metadata.pi-map.md— orientation-first, richer directory metadata
It runs as both:
- a standalone CLI (
project-map) - a Pi extension (
pi-extension.ts)
The extension registers tools and event hooks that keep the artifacts fresh and can inject them into agent context at runtime.
Core design principle
The artifacts are navigation aids, not source-of-truth. Source code is always the final authority.
index routes, map orients, source decides.
2. Artifact model
Each non-ignored directory receives a matched pair.
2.1 Shared model
Both files are generated from the same in-memory DirectoryArtifactModel:
interface DirectoryArtifactModel {
dir: string;
role: string;
files: FileEntry[];
arch: string;
dirty?: string;
isRoot: boolean;
parent?: string;
children: string[];
tags: string[];
symbols: string[];
workflows: WorkflowHint[];
}
interface FileEntry {
name: string;
purpose: string;
exports: string[];
deps: string[];
}
2.2 .pi-map.md (rich map)
Rendered by src/format.ts → renderDirectoryMap().
Contains:
dir:line and siblingindex:linkProject Map Protocol(root only)## role## files## arch## tags## symbols## workflows## dirty
2.3 .pi-map.index.md (index)
Rendered by src/format.ts → renderDirectoryIndex().
Contains:
- same protocol (root only)
## role## parent## children## files## links## workflows## dirty
2.4 Why a paired format?
- indexes are cheap — many can be loaded without consuming much context
- maps are dense — loaded only after an index suggests relevance
- paired generation guarantees structural consistency
3. High-level architecture
3.1 Main modules
discover → directory tree, .gitignore-aware
init → full generation
llm-extract → LLM-based file/package analysis
ast-extract → tree-sitter parsing
merge → combine LLM + AST into FileEntry
routing-metadata → tags, symbols, workflow hints
format → render / parse the markdown pair
patch → incremental update after edits
validate → consistency checking with optional repair
retrieve → deterministic query scoring
prompt-injection → runtime context policy
pi-extension → Pi tool/event registration
cli → standalone command dispatcher
config → defaults and .pi-project-map.json loader
3.2 Runtime modes
| Mode | Entry point | LLM client |
|---|---|---|
| Pi extension | pi-extension.ts |
PiLLMClient via Pi runtime |
| Standalone CLI | src/cli/cli.ts |
ExternalLLMClient or KimiLLMClient |
4. Extraction pipeline
4.1 Discovery
src/discover.ts walks the filesystem with ignore, merging built-in exclusions and .gitignore.
4.2 Per-directory generation
src/init.ts → generateDirectoryArtifacts():
processFiles()runs in parallel over directory files- for each file:
extractFileLLM()getspurpose,deps,conceptsextractFileAST()gets exports/imports/calls where possiblemergeFileData()combines both into aFileEntry
extractPackageLLM()produces directoryroleandarchcreateDirectoryModel()builds the shared modelpopulateRoutingMetadata()derivestags,symbols,workflowswriteDirectoryArtifacts()writes both.pi-map.mdand.pi-map.index.md
Directories are processed sequentially; files within a directory are processed concurrently.
4.3 LLM extraction
src/llm/llm-extract.ts:
- prompts are minimal and line-oriented
- binary files are skipped
- files over 500KB are labeled large and skipped
- source is truncated before prompting
- results are cached by SHA-256 of file content
- missing client throws
LLMError
4.4 AST extraction
src/ast/ast-extract.ts uses tree-sitter for supported languages to extract:
- imports / requires
- exported classes, functions, constants
- methods, parameters, return types
- direct calls and raised exceptions
Unsupported languages fall back to LLM-only extraction.
4.5 Merging
src/merge.ts:
- purpose/concepts come from the LLM
- exports come from AST when available
- rich AST symbols are encoded as compact DSL:
class:Foomethod:bar(a: string) → numbercall:bazraise:Error
- deps are deduplicated union of AST + LLM deps
4.6 Routing metadata
src/routing-metadata.ts generates deterministic metadata used by retrieval and injection:
- tags
- symbols
- workflow hints
Caps are configurable via tagCap and workflowHintCap.
5. Patch / validate / reinit behavior
5.1 Patch
src/patch.ts:
- resolve directory containing changed file
- rediscover project tree
- regenerate changed directory pair
- refresh ancestors according to patch mode
Patch mode:
- small — refresh ancestor indexes only
- structural — refresh ancestor map/index pairs
5.2 Validate
src/validate.ts compares artifacts against filesystem and AST.
Important discrepancy types:
missingorphanedstale-signaturedirtystale-mapstale-indexbroken-linkstructural
With --fix, validate builds a repair plan and regenerates directories deepest-first.
5.3 Reinit
reinitPath() is the blunt instrument for widespread staleness.
6. Retrieval architecture
src/retrieve.ts implements deterministic, index-first context retrieval.
- walk the project for paired artifacts
- parse indexes/maps into
DirectoryArtifactModel - normalize the query
- score every directory
- return top-K (default: 3) as a markdown bundle with:
- relevant indexes
- relevant maps
- likely files
- relevant symbols
- instructions to verify from source
No LLM is used during retrieval. It is intentionally separate from automatic prompt injection.
7. Prompt injection architecture
src/prompt-injection.ts and pi-extension.ts implement runtime guidance injection.
7.1 Mode ladder
| Mode | Behavior |
|---|---|
off |
No automatic injection |
advisory |
Visible startup/init hints; no artifact preload |
strong (default) |
Root pair preloaded, budgeted expansion, reinjection on relevant turns |
strict |
Same as strong, plus bypass guard for sensitive edits/architecture reasoning without protocol path |
7.2 Event hooks
The extension currently registers:
session_startbefore_agent_startcontext
Payload fallback scanning is handled inside context-level decision logic; there is no separately registered before_provider_request hook in the current implementation.
7.3 Reinjection policy
shouldReinjectForEvent() decides whether to inject:
- only active in
strongorstrict - skips if the canonical marker is already present in outgoing messages or payload
- triggers on:
agent_startedit_intentarchitecture_sensitivecompactionartifact_change
artifact_changealways forces reinjection
detectEditIntent() and detectArchitectureSensitiveReasoning() provide heuristic fallback for generic turns.
7.4 Protocol path and strict bypass
The protocol path is present when outgoing context contains:
- the canonical root-pair marker/block
- the trust-boundary text
In strict mode, a sensitive turn without the protocol path is blocked with a visible guard. The agent can override with:
[PI_MAP_BYPASS: brief justification]
Empty or whitespace reasons are rejected.
7.5 Budgeted expansion
buildInjectionPayload():
- computes budget as
min(relative, absolute) - default is 15% of context window, capped at 100k tokens
- always includes the root pair
- adds additional pairs shallow-first until budget is exhausted
- prepends a maintenance reminder
Token estimation is best-effort: ceil(char_count / 4).
7.6 Context-window discovery
discoverContextWindow() inspects the Pi runtime model for context metadata and falls back to the absolute cap when unavailable.
8. Known limits and tradeoffs
Correctness vs cost
- init/patch/repair make LLM calls
- large repositories can be expensive
- caching reduces duplicate work
AST coverage
- TypeScript/TSX, Python, and Go have the richest support
- other languages may be partial or LLM-only
Token estimation
- 4 chars/token is only a heuristic
- oversized files may be truncated or skipped
Staleness
- there is no filesystem watcher
- maps go stale when edits happen outside the patch flow
- validate detects but does not prevent staleness
Patch mode inference
- auto-mode heuristics are good but imperfect
- contributors can force structural mode when needed
Strict mode ergonomics
- strict guards can be surprising on casual phrasing
- bypass markers are intentionally explicit and user-visible
Retrieval scoring
- deterministic scoring is reproducible but not semantic-search-smart
- broader queries may still need manual browsing
Cache
- cache grows unless manually cleaned
- corrupted cache files are recovered by starting fresh