fix: address PR review feedback (C1-C4, I1-I5)

Critical fixes:
- C1: Remove unused treeSitter.nodeTypes from configs and schema — extraction
  logic is TS/JS-specific, nodeTypes gave false impression of generality
- C2+C3: Remove blocking temp dir prompt, default to .understand-anything/tmp/,
  update all 5 prompt templates to use project-local paths instead of /tmp/
- C4: Fix false-positive framework detection — use "next": (with quotes) for
  Next.js, remove cors/body-parser from Express keywords

Important fixes:
- I1: Change FrameworkConfig.language to languages[] array — React/Express/Vue
  now correctly listed under both typescript and javascript
- I2: Fix getByExtension case sensitivity — .TS now resolves same as .ts
- I3: languageKeyFromPath returns null instead of throwing for unknown extensions
- I4: Duplicate framework registration is now a no-op instead of corrupting array
- I5: SKILL.md now says "skip silently" when language/framework snippet not found

Also: getForLanguage returns a defensive copy, 3 new tests added (157 total).
This commit is contained in:
Sreeram
2026-03-23 21:53:02 +05:30
Unverified
parent 42074bb210
commit ae898a28b2
23 changed files with 83 additions and 103 deletions
@@ -81,6 +81,30 @@ describe("FrameworkRegistry", () => {
});
});
it("returns frameworks for all listed languages (cross-language)", () => {
const registry = FrameworkRegistry.createDefault();
// React lists both typescript and javascript
const tsFrameworks = registry.getForLanguage("typescript");
const jsFrameworks = registry.getForLanguage("javascript");
expect(tsFrameworks.some((f) => f.id === "react")).toBe(true);
expect(jsFrameworks.some((f) => f.id === "react")).toBe(true);
});
it("does not duplicate on re-registration", () => {
const registry = new FrameworkRegistry();
registry.register(djangoConfig);
registry.register(djangoConfig);
expect(registry.getForLanguage("python")).toHaveLength(1);
});
it("getForLanguage returns a copy, not the internal array", () => {
const registry = new FrameworkRegistry();
registry.register(djangoConfig);
const result = registry.getForLanguage("python");
result.push(reactConfig);
expect(registry.getForLanguage("python")).toHaveLength(1);
});
describe("createDefault", () => {
it("registers all 10 built-in framework configs", () => {
const registry = FrameworkRegistry.createDefault();
@@ -7,20 +7,6 @@ export const javascriptConfig = {
treeSitter: {
wasmPackage: "tree-sitter-javascript",
wasmFile: "tree-sitter-javascript.wasm",
nodeTypes: {
function: [
"function_declaration",
"arrow_function",
"function_expression",
"method_definition",
],
class: ["class_declaration"],
import: ["import_statement"],
export: ["export_statement"],
call: ["call_expression"],
string: ["string", "string_fragment"],
parameter: ["formal_parameters"],
},
},
concepts: [
"closures",
@@ -7,24 +7,6 @@ export const typescriptConfig = {
treeSitter: {
wasmPackage: "tree-sitter-typescript",
wasmFile: "tree-sitter-typescript.wasm",
nodeTypes: {
function: [
"function_declaration",
"arrow_function",
"function_expression",
"method_definition",
],
class: ["class_declaration"],
import: ["import_statement"],
export: ["export_statement"],
call: ["call_expression"],
string: ["string", "string_fragment"],
parameter: [
"formal_parameters",
"required_parameter",
"optional_parameter",
],
},
},
concepts: [
"generics",
@@ -12,11 +12,17 @@ export class FrameworkRegistry {
register(config: FrameworkConfig): void {
const parsed = FrameworkConfigSchema.parse(config);
// Prevent duplicate registration
if (this.byId.has(parsed.id)) return;
this.byId.set(parsed.id, parsed);
const existing = this.byLanguage.get(parsed.language) ?? [];
existing.push(parsed);
this.byLanguage.set(parsed.language, existing);
for (const lang of parsed.languages) {
const existing = this.byLanguage.get(lang) ?? [];
existing.push(parsed);
this.byLanguage.set(lang, existing);
}
}
getById(id: string): FrameworkConfig | null {
@@ -24,7 +30,7 @@ export class FrameworkRegistry {
}
getForLanguage(langId: string): FrameworkConfig[] {
return this.byLanguage.get(langId) ?? [];
return [...(this.byLanguage.get(langId) ?? [])];
}
getAllFrameworks(): FrameworkConfig[] {
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const djangoConfig = {
id: "django",
displayName: "Django",
language: "python",
languages: ["python"],
detectionKeywords: [
"django",
"djangorestframework",
@@ -3,8 +3,8 @@ import type { FrameworkConfig } from "../types.js";
export const expressConfig = {
id: "express",
displayName: "Express",
language: "javascript",
detectionKeywords: ["express", "express-validator", "cors", "body-parser"],
languages: ["javascript", "typescript"],
detectionKeywords: ["\"express\":", "express-validator", "express-session"],
manifestFiles: ["package.json"],
promptSnippetPath: "./frameworks/express.md",
entryPoints: [
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const fastapiConfig = {
id: "fastapi",
displayName: "FastAPI",
language: "python",
languages: ["python"],
detectionKeywords: ["fastapi", "uvicorn", "starlette"],
manifestFiles: [
"requirements.txt",
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const flaskConfig = {
id: "flask",
displayName: "Flask",
language: "python",
languages: ["python"],
detectionKeywords: [
"flask",
"flask-restful",
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const ginConfig = {
id: "gin",
displayName: "Gin",
language: "go",
languages: ["go"],
detectionKeywords: ["github.com/gin-gonic/gin"],
manifestFiles: ["go.mod"],
promptSnippetPath: "./frameworks/gin.md",
@@ -3,8 +3,8 @@ import type { FrameworkConfig } from "../types.js";
export const nextjsConfig = {
id: "nextjs",
displayName: "Next.js",
language: "typescript",
detectionKeywords: ["next", "@next/font", "@next/image"],
languages: ["typescript", "javascript"],
detectionKeywords: ["\"next\":", "@next/font", "@next/image"],
manifestFiles: ["package.json"],
promptSnippetPath: "./frameworks/nextjs.md",
entryPoints: [
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const railsConfig = {
id: "rails",
displayName: "Ruby on Rails",
language: "ruby",
languages: ["ruby"],
detectionKeywords: [
"rails",
"railties",
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const reactConfig = {
id: "react",
displayName: "React",
language: "typescript",
languages: ["typescript", "javascript"],
detectionKeywords: ["react", "react-dom", "@types/react"],
manifestFiles: ["package.json"],
promptSnippetPath: "./frameworks/react.md",
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const springConfig = {
id: "spring",
displayName: "Spring Boot",
language: "java",
languages: ["java", "kotlin"],
detectionKeywords: [
"spring-boot",
"spring-boot-starter",
@@ -3,7 +3,7 @@ import type { FrameworkConfig } from "../types.js";
export const vueConfig = {
id: "vue",
displayName: "Vue",
language: "typescript",
languages: ["typescript", "javascript"],
detectionKeywords: ["vue", "@vue/cli-service", "nuxt", "vite-plugin-vue"],
manifestFiles: ["package.json"],
promptSnippetPath: "./frameworks/vue.md",
@@ -25,7 +25,7 @@ export class LanguageRegistry {
}
getByExtension(ext: string): LanguageConfig | null {
const key = ext.startsWith(".") ? ext : `.${ext}`;
const key = (ext.startsWith(".") ? ext : `.${ext}`).toLowerCase();
return this.byExtension.get(key) ?? null;
}
@@ -1,18 +1,13 @@
import { z } from "zod";
// Tree-sitter node type mappings for a language
// Tree-sitter grammar configuration for a language.
// Only wasmPackage and wasmFile are needed for grammar loading.
// The extraction logic in tree-sitter-plugin.ts is currently TS/JS-specific;
// when grammars for other languages are added, language-specific extractors
// should be registered via the customAnalyzer escape hatch.
export const TreeSitterConfigSchema = z.object({
wasmPackage: z.string(),
wasmFile: z.string(),
nodeTypes: z.object({
function: z.array(z.string()),
class: z.array(z.string()),
import: z.array(z.string()),
export: z.array(z.string()),
call: z.array(z.string()),
string: z.array(z.string()),
parameter: z.array(z.string()),
}),
});
export type TreeSitterConfig = z.infer<typeof TreeSitterConfigSchema>;
@@ -43,7 +38,7 @@ export type LanguageConfig = z.infer<typeof LanguageConfigSchema>;
export const FrameworkConfigSchema = z.object({
id: z.string().min(1),
displayName: z.string().min(1),
language: z.string().min(1),
languages: z.array(z.string().min(1)).min(1),
detectionKeywords: z.array(z.string()).min(1),
manifestFiles: z.array(z.string()).min(1),
promptSnippetPath: z.string().min(1),
@@ -201,17 +201,13 @@ export class TreeSitterPlugin implements AnalyzerPlugin {
this.languages = langs;
}
private languageKeyFromPath(filePath: string): string {
private languageKeyFromPath(filePath: string): string | null {
const ext = extname(filePath).toLowerCase();
// Special case: .tsx needs its own grammar
if (ext === ".tsx") return "tsx";
const lang = this._extensionToLang.get(ext);
if (!lang) {
throw new Error(`Unsupported file extension: ${ext}`);
}
return lang;
return this._extensionToLang.get(ext) ?? null;
}
/**
@@ -304,6 +300,7 @@ export class TreeSitterPlugin implements AnalyzerPlugin {
);
}
const langKey = this.languageKeyFromPath(filePath);
if (!langKey) return null;
const lang = this._languages.get(langKey);
if (!lang) {
// Language grammar not loaded — graceful degradation
@@ -20,29 +20,19 @@ Analyze the current codebase and produce a `knowledge-graph.json` file in `.unde
Determine whether to run a full analysis or incremental update.
1. **STOP and ask the user where temporary scripts and intermediate files should be written.** You MUST wait for the user's response before proceeding to step 2 or any other step. Do NOT read prompt templates, launch subagents, or begin any analysis until this question is answered.
Ask the user:
> Where should I write temporary scripts and intermediate files during analysis?
> 1. **Project directory** (recommended): `.understand-anything/tmp/` — stays within the project
> 2. **System temp**: `/tmp/` — may require permissions outside the project
After the user responds, store the chosen path as `$TMP_DIR`. Create it: `mkdir -p $TMP_DIR`.
All subagent prompts reference `$TMP_DIR` for script files and intermediate JSON. When dispatching subagents, replace any `/tmp/ua-` paths in the prompt templates with `$TMP_DIR/ua-` so scripts and results are written to the user's chosen location.
2. Set `PROJECT_ROOT` to the current working directory.
3. Get the current git commit hash:
1. Set `PROJECT_ROOT` to the current working directory.
2. Get the current git commit hash:
```bash
git rev-parse HEAD
```
4. Create the intermediate output directory:
3. Create the intermediate and temp output directories:
```bash
mkdir -p $PROJECT_ROOT/.understand-anything/intermediate
mkdir -p $PROJECT_ROOT/.understand-anything/tmp
```
5. Check if `$PROJECT_ROOT/.understand-anything/knowledge-graph.json` exists. If it does, read it.
6. Check if `$PROJECT_ROOT/.understand-anything/meta.json` exists. If it does, read it to get `gitCommitHash`.
7. **Decision logic:**
4. Check if `$PROJECT_ROOT/.understand-anything/knowledge-graph.json` exists. If it does, read it.
5. Check if `$PROJECT_ROOT/.understand-anything/meta.json` exists. If it does, read it to get `gitCommitHash`.
6. **Decision logic:**
| Condition | Action |
|---|---|
@@ -57,7 +47,7 @@ Determine whether to run a full analysis or incremental update.
```
If this returns no files, report "Graph is up to date" and STOP.
8. **Collect project context for subagent injection:**
7. **Collect project context for subagent injection:**
- Read `README.md` (or `README.rst`, `readme.md`) from `$PROJECT_ROOT` if it exists. Store as `$README_CONTENT` (first 3000 characters).
- Read the primary package manifest (`package.json`, `pyproject.toml`, `Cargo.toml`, `go.mod`, `pom.xml`) if it exists. Store as `$MANIFEST_CONTENT`.
- Capture the top-level directory tree:
@@ -113,8 +103,8 @@ For each batch, dispatch a subagent using the prompt template at `./file-analyze
**Build the combined prompt template:**
1. Read the base template at `./file-analyzer-prompt.md`.
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`) and append its content after the base template under a `## Language Context` header. These files are in the `languages/` subdirectory next to this SKILL.md file. Use `ls ./languages/` to discover available language files if needed.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. These files are in the `frameworks/` subdirectory next to this SKILL.md file. Use `ls ./frameworks/` to discover available framework files if needed.
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. Use `ls ./languages/` to discover available language files if needed.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file. Use `ls ./frameworks/` to discover available framework files if needed.
Then for each batch pass the combined template content as the subagent's prompt, appending the following additional context:
@@ -172,8 +162,8 @@ Merge all file-analyzer results into a single set of nodes and edges. Then perfo
**Build the combined prompt template:**
1. Read the base template at `./architecture-analyzer-prompt.md`.
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`) and append its content after the base template under a `## Language Context` header. These files are in the `languages/` subdirectory next to this SKILL.md file.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`), read the file at `./languages/<language-id>.md` (e.g., `./languages/python.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file.
3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/<framework-id-lowercase>.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file.
Pass the combined content as the subagent's prompt, appending the following additional context:
@@ -416,7 +406,7 @@ Pass these parameters in the dispatch prompt:
3. Clean up intermediate files:
```bash
rm -rf $PROJECT_ROOT/.understand-anything/intermediate
rm -rf $TMP_DIR
rm -rf $PROJECT_ROOT/.understand-anything/tmp
```
4. Report a summary to the user containing:
@@ -164,7 +164,7 @@ For each pair of groups with imports between them, determine the dominant direct
Before writing the script, create its input JSON file:
```bash
cat > /tmp/ua-arch-input.json << 'ENDJSON'
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-arch-input.json << 'ENDJSON'
{
"fileNodes": [<file nodes from prompt>],
"importEdges": [<import edges from prompt>]
@@ -177,7 +177,7 @@ ENDJSON
After writing the script, execute it:
```bash
node /tmp/ua-arch-analyze.js /tmp/ua-arch-input.json /tmp/ua-arch-results.json
node $PROJECT_ROOT/.understand-anything/tmp/ua-arch-analyze.js $PROJECT_ROOT/.understand-anything/tmp/ua-arch-input.json $PROJECT_ROOT/.understand-anything/tmp/ua-arch-results.json
```
If the script exits with a non-zero code, read stderr, diagnose the issue, fix the script, and re-run. You have up to 2 retry attempts.
@@ -186,7 +186,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t
## Phase 2 -- Semantic Layer Assignment
After the script completes, read `/tmp/ua-arch-results.json`. Use the structural analysis as the primary input for your layer decisions. Do NOT re-read source files or re-analyze imports -- trust the script's results entirely.
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-arch-results.json`. Use the structural analysis as the primary input for your layer decisions. Do NOT re-read source files or re-analyze imports -- trust the script's results entirely.
### Step 1 -- Evaluate Directory Groups as Layer Candidates
@@ -111,7 +111,7 @@ The script must write this exact JSON structure to the output file:
Before writing the script, create its input JSON file. **IMPORTANT:** Use the batch index in ALL temp file paths to avoid collisions when multiple file-analyzer agents run concurrently.
```bash
cat > /tmp/ua-file-analyzer-input-<batchIndex>.json << 'ENDJSON'
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-<batchIndex>.json << 'ENDJSON'
{
"projectRoot": "<project-root>",
"allProjectFiles": [<full file list from scan>],
@@ -126,9 +126,9 @@ After writing the script, execute it. **Use the batch index in every temp file p
```bash
# For Node.js scripts:
node /tmp/ua-file-extract-<batchIndex>.js /tmp/ua-file-analyzer-input-<batchIndex>.json /tmp/ua-file-extract-results-<batchIndex>.json
node $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-<batchIndex>.js $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-<batchIndex>.json $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json
# For Python scripts:
python3 /tmp/ua-file-extract-<batchIndex>.py /tmp/ua-file-analyzer-input-<batchIndex>.json /tmp/ua-file-extract-results-<batchIndex>.json
python3 $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-<batchIndex>.py $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-<batchIndex>.json $PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json
```
If the script exits with a non-zero code, read stderr, diagnose the issue, fix the script, and re-run. You have up to 2 retry attempts.
@@ -137,7 +137,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t
## Phase 2 -- Semantic Analysis
After the script completes, read `/tmp/ua-file-extract-results-<batchIndex>.json`. Use these structured results as the foundation for your analysis. Do NOT re-read the source files unless the script skipped a file or you need to understand a specific code pattern that the script could not capture.
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json`. Use these structured results as the foundation for your analysis. Do NOT re-read the source files unless the script skipped a file or you need to understand a specific code pattern that the script could not capture.
For each file in the script's `results` array, produce `GraphNode` and `GraphEdge` objects by combining the script's structural data with your expert judgment.
@@ -134,7 +134,7 @@ The script must write this exact JSON structure to the output file:
After writing the script, execute it:
```bash
node /tmp/ua-graph-validate.js "<graph-file-path>" "/tmp/ua-review-results.json"
node $PROJECT_ROOT/.understand-anything/tmp/ua-graph-validate.js "<graph-file-path>" "$PROJECT_ROOT/.understand-anything/tmp/ua-review-results.json"
```
If the script exits with a non-zero code, read stderr, diagnose the issue, fix the script, and re-run. You have up to 2 retry attempts.
@@ -143,7 +143,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t
## Phase 2 -- Review and Decision
After the script completes, read `/tmp/ua-review-results.json`. Do NOT re-read the original graph file -- trust the script's results entirely.
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-review-results.json`. Do NOT re-read the original graph file -- trust the script's results entirely.
Review the `issues` and `warnings` arrays and render your decision:
@@ -141,7 +141,7 @@ The script must write this exact JSON structure to the output file:
After writing the script, execute it:
```bash
node /tmp/ua-project-scan.js "<project-root>" "/tmp/ua-scan-results.json"
node $PROJECT_ROOT/.understand-anything/tmp/ua-project-scan.js "<project-root>" "$PROJECT_ROOT/.understand-anything/tmp/ua-scan-results.json"
```
(Or the equivalent for bash/Python, depending on which language you chose.)
@@ -152,7 +152,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t
## Phase 2 -- Description and Final Assembly
After the script completes, read `/tmp/ua-scan-results.json`. Do NOT re-run file discovery commands or re-count lines -- trust the script's results entirely.
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-scan-results.json`. Do NOT re-run file discovery commands or re-count lines -- trust the script's results entirely.
**IMPORTANT:** The final output must NOT contain the `scriptCompleted`, `rawDescription`, or `readmeHead` fields. These are intermediate script fields only. Strip them when assembling the final JSON.
@@ -132,7 +132,7 @@ Create a lookup of each node ID to its `summary`, `type`, `tags` (default to emp
Before writing the script, create its input JSON file:
```bash
cat > /tmp/ua-tour-input.json << 'ENDJSON'
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-tour-input.json << 'ENDJSON'
{
"nodes": [<nodes from prompt>],
"edges": [<edges from prompt>],
@@ -146,7 +146,7 @@ ENDJSON
After writing the script, execute it:
```bash
node /tmp/ua-tour-analyze.js /tmp/ua-tour-input.json /tmp/ua-tour-results.json
node $PROJECT_ROOT/.understand-anything/tmp/ua-tour-analyze.js $PROJECT_ROOT/.understand-anything/tmp/ua-tour-input.json $PROJECT_ROOT/.understand-anything/tmp/ua-tour-results.json
```
If the script exits with a non-zero code, read stderr, diagnose the issue, fix the script, and re-run. You have up to 2 retry attempts.
@@ -155,7 +155,7 @@ If the script exits with a non-zero code, read stderr, diagnose the issue, fix t
## Phase 2 -- Pedagogical Tour Design
After the script completes, read `/tmp/ua-tour-results.json`. Use the structural analysis as your primary guide for designing the tour. Do NOT re-read source files or re-analyze the graph -- trust the script's results entirely.
After the script completes, read `$PROJECT_ROOT/.understand-anything/tmp/ua-tour-results.json`. Use the structural analysis as your primary guide for designing the tour. Do NOT re-read source files or re-analyze the graph -- trust the script's results entirely.
### Step 1 -- Choose the Starting Point