-
Notifications
You must be signed in to change notification settings - Fork 4
Architecture
aspens/
bin/cli.js # Entry point — Commander CLI setup
src/
commands/
scan.js # aspens scan — tech stack + graph display
doc-init.js # aspens doc init — 3-layer pipeline
doc-sync.js # aspens doc sync — diff classifier, deterministic repair, multi-target publish
doc-graph.js # aspens doc graph — standalone graph rebuild
doc-impact.js # aspens doc impact — context-health report
add.js # aspens add — templates + custom skills
customize.js # aspens customize — inject project context into agents
save-tokens.js # aspens save-tokens — session-optimization settings
lib/
scanner.js # Layer 1 — deterministic repo scanner (languages, frameworks, domains, health)
graph-builder.js # Layer 1 — import graph construction, file metrics, clustering
graph-persistence.js # Graph serialization, code-map (clusters + framework entries), graph-index
source-exts.js # Canonical source-extension set (.cjs/.mjs first-class)
parsers/
typescript.js # es-module-lexer wrapper + default-export + `export * from` re-exports
python.js # Top-level imports, def/class extraction (strips docstrings)
frameworks/
nextjs.js # App Router / Pages Router / middleware entry-point detection
path-resolver.js # tsconfig.json / jsconfig.json `paths:` alias resolution (with `extends` chain)
diff-classifier.js # `isNoOpDiff()` — skip the LLM call on lockfile-only / non-code diffs
target.js # TARGETS table, allowed paths, multi-target merge
target-transform.js # collectSkillsForList + sanitizePublishedContent + assertTargetParity chokepoints
backend.js # Backend abstraction (claude / codex routing)
context-builder.js # Assembles repo context for generation prompts
runner.js # Claude/Codex CLI wrappers, stream parsing, output extraction
skill-writer.js # Writes skill files, generates skill-rules.json, merges settings
skill-reader.js # Parses frontmatter, `triggers:` block, legacy `## Activation` fallback
impact.js # Freshness, coverage, drift, hook health checks
save-tokens.js # Statusline + handoff + precompact-handoff installers
git-helpers.js # Git repo detection, diff, log, changed files (execFileSync)
diff-helpers.js # Diff prioritization, truncation, file selection
git-hook.js # Post-commit hook install/remove
timeout.js # Timeout resolution (flag > env var > fallback)
errors.js # CliError class
prompts/
discover-domains.md # Layer 2 — domain discovery agent prompt
discover-architecture.md # Layer 2 — architecture analysis agent prompt
doc-init.md # Layer 3 — skill generation prompt
doc-init-domain.md # Layer 3 — per-domain skill generation
doc-init-claudemd.md # Layer 3 — orientation doc generation
doc-sync.md # Doc sync prompt
customize-agents.md # Agent customization prompt
partials/ # Shared format specs (skill-format.md, examples.md)
templates/
agents/ # Bundled AI agents (plan, execute, code-reviewer, etc.)
commands/ # Slash commands (handoff/resume, dev-docs)
hooks/ # Hooks (skill-activation, post-tool-use-tracker, save-tokens)
settings/ # Bundled settings.json fragments
tests/ # Vitest test files (439 tests as of v0.8.0)
scanRepo() → { languages, frameworks, domains, entryPoints, size, health }
↓
buildRepoGraph() → { files, edges, ranked, hubs, clusters, hotspots,
frameworkEntryPoints, stats }
↓
runDiscovery() → discoveryFindings (architecture + domains as structured text)
↓
generateChunked() → in-memory skill files (base → domains in parallel → CLAUDE.md)
↓
publishFilesForTargets() → Map<targetId, files[]> (calls transformForTarget per non-source target)
↓ assertTargetParity() ← throws CliError if targets diverge on logical files
↓
sanitizePublishedContent() ← every disk write goes through it (chokepoint sanitizer)
↓
writeSkillFiles() / writeTransformedFiles()
→ .claude/skills/**/*.md, .agents/skills/**/AGENTS.md,
CLAUDE.md, AGENTS.md, .claude/code-map.md
generateCodeMap() emits clusters + framework entry points only — no file counts, edge counts,
hub rankings, or hotspots (those live in .claude/graph.json for programmatic access).
| Package | Purpose | Size |
|---|---|---|
es-module-lexer |
Parse JS/TS import/export statements (WASM) | ~50KB |
commander |
CLI framework | ~50KB |
@clack/prompts |
Interactive terminal prompts | ~30KB |
picocolors |
Terminal color output | ~3KB |
Runtime dependency on either claude CLI or codex CLI for LLM operations, depending on the selected backend.
The graph builder (src/lib/graph-builder.js) is the orchestrator. Language-specific parsing
lives in src/lib/parsers/ and src/lib/frameworks/; alias resolution lives in
src/lib/path-resolver.js.
-
JS/TS —
src/lib/parsers/typescript.jswrapses-module-lexerand adds post-pass handling for inline default exports (export default function Foo,export default class Bar,export default Baz) andexport * from '<spec>'re-exports. Handles.js,.ts,.tsx,.jsx,.mjs,.cjs. -
Python —
src/lib/parsers/python.js. Top-level only (line-anchored regex). Parsesimport X,from X import Y,from .X import Y,from ..X import Y. Strips triple-quoted strings to avoid matching imports in docstrings. Also extracts top-leveldef/classfor export names; SCREAMING_SNAKE constants are deliberately excluded.
src/lib/frameworks/nextjs.js surfaces files Next.js runs implicitly — they have no static
importer, so the priority ranker would otherwise rank them too low. Detected entry points:
-
App Router —
page,layout,route,loading,error,not-found,template,default,global-error(underapp/orsrc/app/) -
Pages Router — every file under
pages/orsrc/pages/ -
Special top-level files —
middleware,instrumentation
Entry points are tagged in the graph and rendered in code-map.md under Framework entry points.
-
Relative imports (
./foo,../bar) — tries extensions (.ts,.tsx,.js,.jsx,.mjs,.cjs), then index files (./foo/index.ts). -
Path aliases (
@/components/Button) —src/lib/path-resolver.jsreadstsconfig.json/jsconfig.jsoncompilerOptions.paths+baseUrl, followsextendschains (capped at 5 levels), and resolves relative to the tsconfig's directory. Searches the repo root, common subdirectories (frontend/,web/,client/,app/,src/), and monorepoapps/*/packages/*. Any resolution that escapes the repo root via../../../is rejected (isInsideRepo()bound-check). -
Python relative (
.utils,..models) — dot-counting for parent traversal, module path to filesystem path. -
Python absolute (
app.services.db) — tries from multiple roots: repo root + detected Python package roots (directories containingpyproject.toml,setup.py,__init__.py, orapp/).
priority = fanIn * 3.0 # how many files depend on this one
+ exportCount * 1.5 # how many things it exports
+ isEntryPoint * 10.0 # main.ts, index.ts, etc.
+ gitChurn * 2.0 # changes in last 6 months
+ 1/(depth+1) * 1.0 # shallower = more architectural
Connected components via BFS on the undirected import graph. Files that import each other (directly or transitively) land in the same cluster. Each cluster is labeled by its primary directory.
For display in code-map.md, single-file clusters are dropped and clusters that share a label
(emitted by the connected-component builder when components are isolated) are merged so the
output doesn't have duplicate cluster headings. Per-cluster files are sorted by fanIn
descending, then path ascending, and capped at 5 files per cluster.
Inter-cluster coupling is computed as the count of cross-cluster import edges.
src/lib/target-transform.js is the v0.8.0 publishing pipeline. Every multi-target write
flows through these three exports:
-
collectSkillsForList(files, pendingBaseSkill, instructionsFile, sourceTarget, repoPath)— merges on-disk skills with the in-flight subset before building the root instructions file's## Skillssection. Without this, a partialdoc syncthat only re-generates one skill would silently drop every unchanged skill from the AGENTS.md / CLAUDE.md skills list. -
sanitizePublishedContent(content, filePath)— defense-in-depth sanitizer that strips forbidden blocks (## Activation,## Key Files, hub/cluster/hotspot tables, framework-entry blocks) from skill and instructions files before they hit disk.code-map.mdis whitelisted because cluster + framework-entry blocks are legitimate there. -
assertTargetParity(perTargetMap)— throwsCliErrorif two configured targets publish a different logical file set (root instructions + per-domain skills). Codex-only syntheticarchitectureskills are carved out bylogicalKeyForFile.
The no-op repair path (repairDeterministicSections() in src/commands/doc-sync.js) runs on every
doc sync invocation where the diff is empty or non-code-bearing — it re-injects the
deterministic ## Skills and ## Behavior sections from on-disk state so missing-section drift
is fixed without an LLM call.
const [domainsResult, archResult] = await Promise.all([
runClaude(discoverDomainsPrompt, ...),
runClaude(discoverArchitecturePrompt, ...),
]);Both discovery runs receive the same graph context. Domain discovery is faster (focused task). Architecture analysis is deeper (reads hub files). Results are merged.
const PARALLEL_LIMIT = 3;
for (let i = 0; i < domains.length; i += PARALLEL_LIMIT) {
const batch = domains.slice(i, i + PARALLEL_LIMIT);
const results = await Promise.all(batch.map(domain => runClaude(...)));
}Base skill is always sequential (generated first, feeds into domain prompts). The target orientation doc (CLAUDE.md or AGENTS.md) is always last.
runner.js wraps the selected backend CLI:
claude -p "prompt" --allowedTools Read,Glob,Grep --output-format stream-json
codex exec --json --sandbox read-only --ask-for-approval never --ephemeral
- Streams/parses structured output for token usage and content extraction
- Auto-scales timeout based on repo size (small: 120s, medium: 300s, large: 600s, very-large: 900s)
- Normalizes backend output into a shared
{ text, usage }interface - Enforces target-aware path safety for
.claude/,.agents/skills/,.codex/,CLAUDE.md, andAGENTS.md