diff --git a/.agents/skills/architecture/references/code-map.md b/.agents/skills/architecture/references/code-map.md
index 47d21cc..5e68dea 100644
--- a/.agents/skills/architecture/references/code-map.md
+++ b/.agents/skills/architecture/references/code-map.md
@@ -5,8 +5,8 @@
**Hub files (most depended-on):**
- `src/lib/runner.js` - 9 dependents
- `src/lib/target.js` - 9 dependents
+- `src/lib/errors.js` - 8 dependents
- `src/lib/scanner.js` - 8 dependents
-- `src/lib/errors.js` - 7 dependents
- `src/lib/skill-writer.js` - 7 dependents
**Domain clusters:**
@@ -16,7 +16,7 @@
| src | 44 | `src/commands/doc-init.js`, `src/lib/runner.js`, `src/lib/target.js` |
**High-churn hotspots:**
-- `src/commands/doc-init.js` - 33 changes
+- `src/commands/doc-init.js` - 34 changes
- `src/commands/doc-sync.js` - 20 changes
- `src/lib/runner.js` - 17 changes
diff --git a/.agents/skills/repo-scanning/SKILL.md b/.agents/skills/repo-scanning/SKILL.md
index 3531f57..8aaf52c 100644
--- a/.agents/skills/repo-scanning/SKILL.md
+++ b/.agents/skills/repo-scanning/SKILL.md
@@ -27,15 +27,15 @@ You are working on **aspens' repo scanning system** — a fully deterministic an
- **Multi-target detection:** Scanner checks for both `.claude` dir + `AGENTS.md` (Claude Code) and `.codex` dir + `AGENTS.md` (Codex CLI) to inform target selection during `doc init`
- **Detection via marker files:** Languages detected by presence of files like `package.json`, `go.mod`, `Cargo.toml` — not by scanning source extensions
- **Framework detection:** JS/TS from `package.json` deps, Python from `requirements.txt`/`pyproject.toml`/`Pipfile`, Go from `go.mod` contents, Ruby from `Gemfile`
-- **Domain detection:** Scans dirs under source root + repo root, skips `SKIP_DIR_NAMES` set (structural/build/IDE dirs), requires at least one source file via `collectModules()`
+- **Domain detection:** Scans dirs under source root + repo root, skips `SKIP_DIR_NAMES` set (structural/build/IDE/.NET/Java/Rust build dirs), requires at least one source file via `collectModules()`
- **extraDomains:** User-specified domains merged via `mergeExtraDomains()` — marked with `userSpecified: true`, resolved against source root then repo root
- **Source root:** First match of `src`, `app`, `lib`, `server`, `pages` via `findSourceRoot()`
-- **Size estimation:** Lines estimated at ~40 bytes/line from `stat.size`, walk capped at depth 5
+- **Size estimation:** Lines estimated at ~40 bytes/line from `stat.size`, walk capped at depth 5, skips `bin`/`obj`/`target` build output alongside `node_modules`/`dist`/etc.
- **Graph is opt-out:** `scanCommand` builds graph by default (`options.graph !== false`); errors are caught and only logged with `--verbose`
## Critical Rules
-- **`SOURCE_EXTS`**: Only `.py`, `.ts`, `.js`, `.tsx`, `.jsx`, `.rb`, `.go`, `.rs` — adding a language requires updating this set AND the `detectLanguages` indicators
-- **`SKIP_DIR_NAMES`**: Directories like `src`, `app`, `dist`, `node_modules` are skipped in domain detection — adding a skip dir here affects all repos
+- **`SOURCE_EXTS`**: `.py`, `.ts`, `.js`, `.tsx`, `.jsx`, `.mjs`, `.cjs`, `.rb`, `.go`, `.rs`, `.java`, `.kt`, `.kts`, `.cs`, `.fs`, `.fsx`, `.swift`, `.php`, `.ex`, `.exs` — adding a language requires updating this set AND the `detectLanguages` indicators. Import graph / hub / cluster detection remains JS/TS/Python-only; other languages get domain discovery but a minimal atlas.
+- **`SKIP_DIR_NAMES`**: Includes `src`, `app`, `bin`, `obj`, `dist`, `target`, `node_modules`, etc. — skipped in domain detection. `bin`/`obj`/`target` added to avoid .NET/Java/Rust build artifacts.
- **`BOILERPLATE_STEMS`**: `__init__`, `index`, `mod` are excluded from module collection — don't add real module names here
- **TypeScript implies JavaScript**: TS detection in `detectLanguages()` automatically adds JS to the languages array
- **Graph failure is non-fatal**: `buildRepoGraph` errors in `scanCommand()` are caught and silently ignored unless `--verbose`
@@ -46,4 +46,4 @@ You are working on **aspens' repo scanning system** — a fully deterministic an
- **No guidelines directory** — `.claude/guidelines/` does not exist yet for this domain
---
-**Last Updated:** 2026-04-02
+**Last Updated:** 2026-04-16
diff --git a/.claude/skills/repo-scanning/skill.md b/.claude/skills/repo-scanning/skill.md
index 78b9acb..5509474 100644
--- a/.claude/skills/repo-scanning/skill.md
+++ b/.claude/skills/repo-scanning/skill.md
@@ -27,15 +27,15 @@ You are working on **aspens' repo scanning system** — a fully deterministic an
- **Multi-target detection:** Scanner checks for both `.claude` dir + `CLAUDE.md` (Claude Code) and `.codex` dir + `AGENTS.md` (Codex CLI) to inform target selection during `doc init`
- **Detection via marker files:** Languages detected by presence of files like `package.json`, `go.mod`, `Cargo.toml` — not by scanning source extensions
- **Framework detection:** JS/TS from `package.json` deps, Python from `requirements.txt`/`pyproject.toml`/`Pipfile`, Go from `go.mod` contents, Ruby from `Gemfile`
-- **Domain detection:** Scans dirs under source root + repo root, skips `SKIP_DIR_NAMES` set (structural/build/IDE dirs), requires at least one source file via `collectModules()`
+- **Domain detection:** Scans dirs under source root + repo root, skips `SKIP_DIR_NAMES` set (structural/build/IDE/.NET/Java/Rust build dirs), requires at least one source file via `collectModules()`
- **extraDomains:** User-specified domains merged via `mergeExtraDomains()` — marked with `userSpecified: true`, resolved against source root then repo root
- **Source root:** First match of `src`, `app`, `lib`, `server`, `pages` via `findSourceRoot()`
-- **Size estimation:** Lines estimated at ~40 bytes/line from `stat.size`, walk capped at depth 5
+- **Size estimation:** Lines estimated at ~40 bytes/line from `stat.size`, walk capped at depth 5, skips `bin`/`obj`/`target` build output alongside `node_modules`/`dist`/etc.
- **Graph is opt-out:** `scanCommand` builds graph by default (`options.graph !== false`); errors are caught and only logged with `--verbose`
## Critical Rules
-- **`SOURCE_EXTS`**: Only `.py`, `.ts`, `.js`, `.tsx`, `.jsx`, `.rb`, `.go`, `.rs` — adding a language requires updating this set AND the `detectLanguages` indicators
-- **`SKIP_DIR_NAMES`**: Directories like `src`, `app`, `dist`, `node_modules` are skipped in domain detection — adding a skip dir here affects all repos
+- **`SOURCE_EXTS`**: `.py`, `.ts`, `.js`, `.tsx`, `.jsx`, `.mjs`, `.cjs`, `.rb`, `.go`, `.rs`, `.java`, `.kt`, `.kts`, `.cs`, `.fs`, `.fsx`, `.swift`, `.php`, `.ex`, `.exs` — adding a language requires updating this set AND the `detectLanguages` indicators. Import graph / hub / cluster detection remains JS/TS/Python-only; other languages get domain discovery but a minimal atlas.
+- **`SKIP_DIR_NAMES`**: Includes `src`, `app`, `bin`, `obj`, `dist`, `target`, `node_modules`, etc. — skipped in domain detection. `bin`/`obj`/`target` added to avoid .NET/Java/Rust build artifacts.
- **`BOILERPLATE_STEMS`**: `__init__`, `index`, `mod` are excluded from module collection — don't add real module names here
- **TypeScript implies JavaScript**: TS detection in `detectLanguages()` automatically adds JS to the languages array
- **Graph failure is non-fatal**: `buildRepoGraph` errors in `scanCommand()` are caught and silently ignored unless `--verbose`
@@ -46,4 +46,4 @@ You are working on **aspens' repo scanning system** — a fully deterministic an
- **No guidelines directory** — `.claude/guidelines/` does not exist yet for this domain
---
-**Last Updated:** 2026-04-02
+**Last Updated:** 2026-04-16
diff --git a/AGENTS.md b/AGENTS.md
index 0eec347..145a909 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,8 +3,8 @@
**Hub files (most depended-on):**
- `src/lib/runner.js` - 9 dependents
- `src/lib/target.js` - 9 dependents
+- `src/lib/errors.js` - 8 dependents
- `src/lib/scanner.js` - 8 dependents
-- `src/lib/errors.js` - 7 dependents
- `src/lib/skill-writer.js` - 7 dependents
**Domain clusters:**
@@ -14,7 +14,7 @@
| src | 44 | `src/commands/doc-init.js`, `src/lib/runner.js`, `src/lib/target.js` |
**High-churn hotspots:**
-- `src/commands/doc-init.js` - 33 changes
+- `src/commands/doc-init.js` - 34 changes
- `src/commands/doc-sync.js` - 20 changes
- `src/lib/runner.js` - 17 changes
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b793732..936bc57 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
## [Unreleased]
+## [0.7.1] - 2026-04-16
+
+### Fixed
+- **Domain discovery for C#, Java, Swift, PHP, and Elixir** — scanner's `SOURCE_EXTS` only recognized JS/TS/Python/Ruby/Go/Rust files, so `doc init` reported zero domains for projects in other languages even when language detection itself succeeded. Added `.cs`, `.java`, `.swift`, `.php`, `.ex`, `.exs`, `.mjs`, and `.cjs` to the source-extension set. Kotlin (`.kt`/`.kts`) and F# (`.fs`/`.fsx`) files are also counted as source now; full language detection for those will follow in a future release.
+- **Build-output skipping** — repo-size estimation now skips `bin/`, `obj/`, and `target/` directories, and domain detection also skips `obj/`, preventing .NET / Java / Rust build artifacts from polluting module lists or line counts.
+
+### Known Limitations
+- Import graph, hub files, and cluster detection remain JS/TS/Python-only — `doc init` on C#/Java/Swift/PHP/Elixir/Kotlin/Rust/Go/Ruby projects will generate skills and domains but produce a minimal atlas. Full multi-language import parsing is tracked for a future release.
+
## [0.7.0] - 2026-04-10
### Added
diff --git a/package-lock.json b/package-lock.json
index 55ea24f..db662f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "aspens",
- "version": "0.7.0",
+ "version": "0.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aspens",
- "version": "0.7.0",
+ "version": "0.7.1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index c139cf6..9d81984 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "aspens",
- "version": "0.7.0",
+ "version": "0.7.1",
"description": "Keep coding-agent context accurate as your codebase changes",
"type": "module",
"bin": {
@@ -23,7 +23,7 @@
"test": "vitest run",
"start": "node bin/cli.js",
"lint": "echo 'No linter configured yet' && exit 0",
- "postinstall": "echo '\n 📌 aspens v0.7.0: Existing repos should refresh generated docs after upgrading.\n Recommended: aspens doc sync --refresh\n\n 🌲 aspens is in active development — please keep it up to date.\n Run into issues? Let us know: https://github.com/aspenkit/aspens/issues\n'"
+ "postinstall": "echo '\n 📌 aspens v0.7.1: Scanner now recognizes C#, Java, Swift, PHP, and Elixir source files for domain discovery.\n If a prior `doc init` came up empty for one of these languages, re-run it after upgrading.\n\n 🌲 aspens is in active development — please keep it up to date.\n Run into issues? Let us know: https://github.com/aspenkit/aspens/issues\n'"
},
"engines": {
"node": ">=20"
diff --git a/src/lib/context-builder.js b/src/lib/context-builder.js
index 3b3c30c..e8760b2 100644
--- a/src/lib/context-builder.js
+++ b/src/lib/context-builder.js
@@ -1,6 +1,7 @@
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { join, extname, relative } from 'path';
import { execSync } from 'child_process';
+import { SOURCE_EXTS } from './source-exts.js';
/**
* Build context string from a repo scan result.
@@ -186,8 +187,6 @@ function listDirRecursive(dirPath, maxDepth, currentDepth = 0, prefix = '') {
}
}
-const SOURCE_EXTS = new Set(['.js', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.rb', '.java', '.kt', '.swift', '.php', '.ex', '.exs']);
-
function listSourceFiles(dirPath) {
try {
return readdirSync(dirPath)
diff --git a/src/lib/impact.js b/src/lib/impact.js
index 3ec363d..a2c518c 100644
--- a/src/lib/impact.js
+++ b/src/lib/impact.js
@@ -5,12 +5,14 @@ import { buildRepoGraph } from './graph-builder.js';
import { loadConfig, TARGETS } from './target.js';
import { findSkillFiles } from './skill-reader.js';
import { getGitRoot } from './git-helpers.js';
+import { SOURCE_EXTS as SCANNER_SOURCE_EXTS } from './source-exts.js';
+// Freshness analysis scans a broader set than scanner domain detection —
+// includes ecosystem extensions (.scala/.clj/.elm/.vue/.svelte) that scanner
+// doesn't yet detect as languages but which still signal source-file edits.
const SOURCE_EXTS = new Set([
- '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
- '.py', '.rb', '.go', '.rs', '.java', '.cs',
- '.php', '.swift', '.kt', '.kts', '.scala',
- '.clj', '.ex', '.exs', '.elm', '.vue', '.svelte',
+ ...SCANNER_SOURCE_EXTS,
+ '.scala', '.clj', '.elm', '.vue', '.svelte',
]);
const LOW_SIGNAL_DOMAIN_NAMES = new Set([
diff --git a/src/lib/scanner.js b/src/lib/scanner.js
index 5a3631d..e26035a 100644
--- a/src/lib/scanner.js
+++ b/src/lib/scanner.js
@@ -1,5 +1,6 @@
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { join, basename, extname, relative } from 'path';
+import { SOURCE_EXTS } from './source-exts.js';
/**
* Scan a repository and return its tech stack, structure, and domains.
@@ -82,7 +83,8 @@ function estimateRepoSize(repoPath) {
for (const entry of entries) {
if (entry.startsWith('.') || entry === 'node_modules' || entry === '__pycache__' ||
entry === 'dist' || entry === 'build' || entry === '.next' || entry === 'vendor' ||
- entry === '.git' || entry === 'coverage') continue;
+ entry === '.git' || entry === 'coverage' || entry === 'bin' || entry === 'obj' ||
+ entry === 'target') continue;
const full = join(dir, entry);
try {
@@ -317,7 +319,7 @@ function detectStructure(repoPath) {
// Always skip — purely structural, build output, dependencies, IDE
const SKIP_DIR_NAMES = new Set([
- 'src', 'app', 'bin', 'cmd', 'pkg', 'internal', 'vendor',
+ 'src', 'app', 'bin', 'obj', 'cmd', 'pkg', 'internal', 'vendor',
'dist', 'build', 'out', 'output', 'target', 'coverage',
'node_modules', '__pycache__', '.next', '.nuxt', '.cache',
'.github', '.vscode', '.idea', '.git',
@@ -631,4 +633,3 @@ function findSourceRoot(repoPath) {
return repoPath;
}
-const SOURCE_EXTS = new Set(['.py', '.ts', '.js', '.tsx', '.jsx', '.rb', '.go', '.rs']);
diff --git a/src/lib/source-exts.js b/src/lib/source-exts.js
new file mode 100644
index 0000000..8ecb3bc
--- /dev/null
+++ b/src/lib/source-exts.js
@@ -0,0 +1,18 @@
+/**
+ * Canonical set of source-file extensions recognized by the scanner and
+ * context-builder. Keep in sync with `detectLanguages()` indicators in
+ * `scanner.js` when adding a language.
+ *
+ * Note: `graph-builder.js` keeps its own smaller set because it only parses
+ * JS/TS/Python imports.
+ */
+export const SOURCE_EXTS = new Set([
+ '.py',
+ '.ts', '.js', '.tsx', '.jsx', '.mjs', '.cjs',
+ '.rb', '.go', '.rs',
+ '.java', '.kt', '.kts',
+ '.cs', '.fs', '.fsx',
+ '.swift',
+ '.php',
+ '.ex', '.exs',
+]);
diff --git a/tests/scanner.test.js b/tests/scanner.test.js
index 3b8fcee..74a47a1 100644
--- a/tests/scanner.test.js
+++ b/tests/scanner.test.js
@@ -205,6 +205,60 @@ describe('scanRepo', () => {
expect(scan.domains.some(d => d.name === 'auth')).toBe(true);
});
+ it('detects domains from C# source files', () => {
+ const dir = createFixture('csharp-project', {
+ 'MyApp.csproj': '',
+ 'Controllers/UsersController.cs': 'public class UsersController {}',
+ 'Controllers/OrdersController.cs': 'public class OrdersController {}',
+ 'Services/PaymentService.cs': 'public class PaymentService {}',
+ });
+ const scan = scanRepo(dir);
+ expect(scan.languages).toContain('csharp');
+ expect(scan.domains.some(d => d.name === 'controllers')).toBe(true);
+ expect(scan.domains.some(d => d.name === 'services')).toBe(true);
+ });
+
+ it('detects domains and languages from Java, Swift, PHP, Elixir, Kotlin source files', () => {
+ const dir = createFixture('multi-lang-project', {
+ 'pom.xml': '',
+ 'Package.swift': '// swift-tools-version:5.0',
+ 'composer.json': '{}',
+ 'mix.exs': 'defmodule App.MixProject do end',
+ 'services/UserService.java': 'public class UserService {}',
+ 'views/HomeView.swift': 'struct HomeView {}',
+ 'handlers/webhook.php': ' d.name);
+ expect(names).toContain('services');
+ expect(names).toContain('views');
+ expect(names).toContain('handlers');
+ expect(names).toContain('workers');
+ expect(names).toContain('ui');
+ });
+
+ it('skips bin, obj, and target build output directories', () => {
+ const dir = createFixture('build-output-project', {
+ 'MyApp.csproj': '',
+ 'bin/Debug/MyApp.cs': '// compiled artifact',
+ 'obj/project.assets.cs': '// intermediate',
+ 'target/classes/Foo.java': '// build output',
+ 'Services/Real.cs': 'public class Real {}',
+ });
+ const scan = scanRepo(dir);
+ const names = scan.domains.map(d => d.name);
+ expect(names).not.toContain('bin');
+ expect(names).not.toContain('obj');
+ expect(names).not.toContain('target');
+ expect(names).toContain('services');
+ });
+
it('returns empty domains for featureless project', () => {
const dir = createFixture('minimal-project', {
'package.json': '{}',