The /understand pipeline reads every code file twice during analysis:
once in compute-batches (`extractExports` for the cross-batch neighbour
map) and once again in extract-import-map (per-language config loaders).
Both sites used sequential `readFileSync` loops, so on the iOS repo in
issue #226 (~15k files) the disk-read time was effectively serialised
behind a single libuv thread while the rest of the pool sat idle.
## Changes
- `extractExports` now batches files into `IO_PARALLELISM = 64` slices
and issues all `readFile` calls in each slice through `Promise.all`,
letting libuv's worker-thread pool overlap disk reads. The
tree-sitter parse stays on the main thread because `web-tree-sitter`
is single-threaded WASM — pipelining the I/O while parses run is
where the wall-time savings come from.
- `loadTsConfigs`, `loadGoModules`, `loadPhpAutoloads` and
`buildResolutionContext` switch to async / `Promise.all` for the
same reason. `buildResolutionContext` also runs the three loader
passes concurrently (`Promise.all([...])`) since they're independent.
- A small `readFilesParallel(paths)` helper is added at the top of
`extract-import-map.mjs` so the three loaders share the same
error-preserving shape.
## Why behavior stays identical
- Each loader collects its candidate paths in `files[]` order *before*
issuing reads, then iterates `reads` in the same order to emit
warnings + populate output maps. So stderr order and the final map
contents are byte-identical to the previous sequential loops.
- `extractExports` collects per-file errors in-place in the
`Promise.all` callbacks and emits warnings during the post-read
serial loop, again in chunk order — so warning text and order match
the previous implementation.
- Tree-sitter parsing is unchanged: parses still run serially on the
main thread, just with reads pipelined alongside.
## What's NOT in this PR
- `buildFingerprintStore` and `analyzeChanges` in `core/fingerprint.ts`
have the same sequential pattern. They're left alone here because
they're part of the public `@understand-anything/core` API; making
them async would be a breaking change worth its own discussion.
Internal-only `.mjs` scripts are safe to refactor without API churn.
- No change to scan-project: most of its sync I/O is `statSync`
(metadata, not content) plus a handful of small `.gitignore` /
`.understandignore` reads. The parallelism win is marginal there.
## Verification
- `pnpm lint` clean
- `pnpm --filter @understand-anything/core build` clean
- `pnpm --filter @understand-anything/skill build` clean
- `pnpm test`: 196/196 — including
`test_compute_batches.test.mjs` (19 tests) and
`test_extract_import_map.test.mjs` (40 tests), which exercise both
changed pipelines end-to-end with fixture projects. No output
diff vs main.
Refs #76
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several users have reported that cloning the repo and opening it in Cursor
doesn't always trigger auto-discovery of the .cursor-plugin manifest. Add
the community-reported workaround (Cursor Settings → Plugins, paste repo
URL) to the Cursor section so it isn't only discoverable from issue
threads.
Closes#172
`model: inherit` is a Claude Code-specific keyword that means "use the
parent session's model." Other tools that read the same agent frontmatter
(opencode, codex, etc.) don't understand it and instead try to use
`inherit` as a literal model id, which the configured provider rejects.
Reproduction (from #167): opencode + deepseek runs `/understand`, the
project-scanner subagent dispatches with `model: inherit`, deepseek
returns `ProviderModelNotFoundError`, and the pipeline halts on every
subagent dispatch.
With the field omitted, each platform falls back to its own configured
default:
- Claude Code: user's default subagent model
- opencode / codex / etc.: globally configured model
Note for Claude Code Opus users: subagents will no longer auto-inherit
the Opus session model. If you want the previous behavior, set your
default subagent model globally — that single setting now controls all
nine agents.
Closes#167
Several users have reported that cloning the repo and opening it in Cursor
doesn't always trigger auto-discovery of the .cursor-plugin manifest. Add
the community-reported workaround (Cursor Settings → Plugins, paste repo
URL) to the Cursor section so it isn't only discoverable from issue
threads.
Closes#172
Add to all 8 READMEs (English + 7 translations):
- "Keep learning" section gets inline commands for incremental re-runs, the --auto-update post-commit hook, and scoping /understand to a subdirectory for huge monorepos
- "Under the Hood" gets a new "Tree-sitter + LLM hybrid" subsection explaining the deterministic-vs-semantic split that powers the pipeline
Adds phase status lines, batch progress with total count, and phase
completion confirmations to the skill definition. Users now see
[Phase N/7] headers and Batch X/N during analysis instead of
unnumbered batch lines with no context.
Fixes#182
Tailwind v4's default source detection walks the nearest .git and
collects tracked files via git ls-files. When the dashboard sources
sit inside a gitignored subtree of an ancestor repo (e.g. the default
marketplace install path ~/.claude/plugins/cache/, which is ignored by
~/.claude/.gitignore), detection returns 0 files and the Oxide engine
skips all utility generation — the dashboard renders unstyled.
Adding explicit @source directives is the supported Tailwind v4 escape
hatch and is a no-op for installs where automatic detection works.
Verified: built CSS bundle jumps from ~9 KB to ~55 KB and utility
classes (.flex, .grid, .absolute, .w-full, .h-full) are present.
Fixes#179
- typescript-eslint preset: strict -> recommended for a usable first-pass
baseline (per PR discussion); ratchet up in a follow-up.
- Drop the projectService/parserOptions block. Neither `recommended` nor
`strict` is type-aware, so it was unused; removing it also avoids the
pnpm-workspace tsconfig-resolution failure mode flagged in review.
- Add Node + browser globals via the `globals` package so .mjs scripts and
the dashboard stop hitting `no-undef`.
- Expand ignores: built bundles (**/public/**), Astro generated (.astro/),
and .private/ (eval scratch). Cuts 2400+ errors in vendored output.
- Allow `_`-prefixed unused vars/args/caught errors; skip irregular
whitespace inside comments (json-parser intentionally embeds ZWSP-escaped
block-comment examples in JSDoc).
- Fix the residual 13 genuine errors: drop dead imports/vars, replace
two `as any[]` in schema.ts with `Array<Record<string, unknown>>`,
drop unused destructure in change-classifier, drop unused catch binding
in extract-structure.mjs.
- Add EOF newline to eslint.config.mjs.
- Refresh pnpm-lock.yaml.
- Add `pnpm lint` step to .github/workflows/ci.yml so the tooling
actually enforces something.
pnpm lint now exits 0 locally; 33+13 test files / 1445 tests still pass.
The generated onboarding markdown linked to a nonexistent repository
(anthropics/understand-anything) instead of the actual project URL
(Lum1104/Understand-Anything).
Adds the Trendshift trending-repository badge just below the tagline on the
English README and all 7 localized variants, and to the homepage hero between
the action row and the Enterprise pill.
Adds a Community section near the end of README (English + 7 localized
variants) and a CommunityVideo component on the homepage embedding the
YouTube walkthrough by Better Stack. Section invites future video / blog /
tutorial contributions to be featured here.
import.meta.url resolves through symlinks but pathToFileURL(process.argv[1])
preserves them, so extract-structure.mjs silently exited 0 without writing
output when invoked via the plugin's symlinked install path — the documented
Claude Code / Copilot CLI layout. Compare both sides via realpathSync and add
a post-write existence assertion plus caller-side guidance in the agent.
Closes#162
The repository has a pnpm lint script running 'eslint .' but ESLint
and typescript-eslint were not listed in devDependencies.
Added:
- eslint (^9.0.0)
- @eslint/js (^9.0.0)
- typescript-eslint (^8.0.0)
Added eslint.config.mjs with flat config (ESLint 9+) using
typescript-eslint strict rules. Ignores node_modules, dist, build,
and framework-specific output directories.
- Replace hardcoded hex with var(--color-*) and CJK font stacks with
var(--font-sans) / var(--font-heading) so the overlay tracks the theme
picker and uses the project's typography (DM Serif Display).
- Lift dismiss/visibility state to Dashboard (shouldShowOnboarding +
showOnboarding useState + dismissOnboarding callback). Gate the
Suspense mount with a boolean so the lazy chunk is only fetched on
first visit, matching the PathFinderModal / KeyboardShortcutsHelp
mount pattern.
- Add capture-phase Escape handler (stopPropagation prevents the global
shortcut chain from also firing) and role="dialog" / aria-modal /
aria-labelledby on the card for screen readers.
Schema (packages/core/src/schema.ts) defines node types: file, function,
class, module, concept, config, document, ... The welcome step body only
listed "file, concept, entity, claim", missing the most common code-side
types. Updated all 6 locales (en / zh / zh-TW / ja / ko / ru) to mention
file / class / function from code plus concept / entity / claim from the
knowledge wiki.
- Replace hardcoded Chinese strings with t.onboarding.* via useI18n
- Add `onboarding` namespace to en / zh / zh-TW / ja / ko / ru locales
- Inject @keyframes ua-fade-in (was referenced in inline style but
never defined, so the overlay popped in instead of fading)
Ships the fingerprints baseline fix (e7af9ae): every install since
2.7.0 had a broken Phase 7 step 2.5 that threw TypeError on the first
/understand run and left fingerprints.json empty/missing, which made
every subsequent auto-update escalate to FULL_UPDATE. This release
replaces the LLM-written script with a bundled build-fingerprints.mjs
and reorders Phase 7 to write fingerprints before meta.json.
Anyone upgrading from 2.7.0–2.7.2 should re-run /understand --full
to regenerate a valid baseline.
The Phase 7 step 2.5 code example in SKILL.md called
buildFingerprintStore() with 2 arguments, but the real signature
requires 4 (projectDir, filePaths, registry: PluginRegistry,
gitCommitHash: string). It also omitted the required
`await TreeSitterPlugin.init()`. Any LLM following the example
threw TypeError on `registry.analyzeFile()` and never produced a
baseline — which is why fingerprints.json never existed in a usable
form after a fresh /understand, and is the root cause behind
issue #152's "every auto-update escalates to FULL_UPDATE" cascade.
Replace the LLM-written script with a bundled `build-fingerprints.mjs`
that mirrors `extract-structure.mjs`: resolves @understand-anything/core
via createRequire, initializes TreeSitterPlugin + PluginRegistry
correctly, calls buildFingerprintStore with all four arguments, and
persists via saveFingerprints. Smoke-tested on this repo (3 files,
correct functions/classes/imports extracted).
Reorder Phase 7 so fingerprints are written BEFORE meta.json. If
fingerprint generation fails, the new step explicitly says to abort
Phase 7 — meta.json must not advance without a valid baseline, or
the next auto-update sees a fresh commit hash with no fingerprints
and classifies every file as STRUCTURAL.
Affects every install since 2.7.0 (when the broken example was
introduced). Users running /understand --full on 2.7.3+ will get
a usable fingerprints.json on the first try.
Ships two auto-update fixes:
- #153 (5304ff0): apply .understandignore in Phase 0 so user-excluded
paths don't inflate the structural-change count.
- #152 (dd8b724): LOAD-PATCH-SAVE template for Phase 3d fingerprints
merge, with guard against silent load failure.
Fixes#152. Phase 3d step 3 instructed the LLM to "merge with existing
fingerprints (keep unchanged files as-is)" but the prose was vague
enough that the LLM-written script frequently wrote only the freshly
re-analyzed batch entries to fingerprints.json, discarding every other
file's fingerprint. The next auto-update saw N-batch_size files with
no stored fingerprint → classified as STRUCTURAL → exceeded the 30-file
threshold → FULL_UPDATE permanently, burning hundreds of thousands of
tokens on every subsequent commit.
Replace the four-bullet description with an explicit LOAD-PATCH-SAVE
script template:
1. LOAD ALL existing entries from fingerprints.json (never skip).
2. PATCH or REMOVE each path in filesToReanalyze (inline deletion
handling so the spec doesn't need a separate deletedFiles list).
3. GUARD: if the file existed and was non-empty but loaded as {},
abort the write — silent load failure would otherwise clobber
every fingerprint.
4. SAVE the full dict back.
The reporter's dry-run showed this restores 81/97 files to COSMETIC
classification on their project (zero LLM tokens) instead of all 97
incorrectly forced into STRUCTURAL.
Note: a related ordering bug exists in skills/understand/SKILL.md
Phase 7 (meta.json written before fingerprints.json — silent failure
in step 2.5 leaves stale fingerprints). That's a separate fix in a
different file and is intentionally not bundled here.
Fixes#153. Phase 0 step 7 filters changed files to source extensions
only and never reads `.understandignore`, so files in user-excluded
paths (migrations, vendored code, tests) count as structural changes
and can spuriously escalate the action to FULL_UPDATE. The reporter
saw 50 → 38 structural files after applying their ignore patterns
(below the 30-file FULL_UPDATE threshold, ARCHITECTURE_UPDATE would
have sufficed).
Add step 9 that delegates to `createIgnoreFilter` from
`@understand-anything/core` via $CLAUDE_PLUGIN_ROOT. Same code path
as /understand's project-scanner Step 2.5, so the auto-update honors
the exact same patterns (hardcoded defaults + user .understandignore
files at both standard locations + `!` negation semantics).
If $CLAUDE_PLUGIN_ROOT can't be resolved, fail loud rather than
silently skipping — a silent skip reproduces the original bug.
Ships the fixes that landed on main after the 2.7.0 cut:
- #139 (f3ea1a3): understand-knowledge — Windows path separators in
wikilink resolution + omit empty `category` so KnowledgeMetaSchema's
`z.string().optional()` no longer drops every article node. Closes#151.
- #147 (fafb888): understand-domain — resolve $PLUGIN_ROOT at runtime
for symlink installs.
- f71bad5: understand — persist canonical edge direction during merge,
fixing the 153k auto-correction cascade. Closes#140.
Fixes#140. merge-batch-graphs.py already defaulted missing `direction`
to "forward" when building the dedup key, but the value was never written
back onto the edge — so the generated knowledge-graph.json shipped without
the field and the dashboard validator emitted one auto-correction per
edge (153k on the reporter's Go codebase).
Mirror the dashboard schema validator at packages/core/src/schema.ts:
lowercase the value, map "both"/"mutual" → "bidirectional", fall back to
"forward" for missing or invalid values, and persist the result onto the
edge before it enters edges_by_key. This also closes a latent dedup leak
where "Forward" and "forward" (or "both" and "bidirectional") would have
produced separate dedup keys.
A second pill mirroring the Enterprise one, linking to https://lum.is-a.dev/.
Same gold/amber styling, 0.6rem gap below the Enterprise pill so the two read
as one "contact" group, anim-5 so both pills enter together. Full mobile
breakpoint mirror.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Includes since 2.6.3: Hermes (#91), Cline (#116), KIMI CLI (#134)
platform support; dashboard ACCESS_TOKEN env override; README cleanup
(slogan rewrite, drop outdated overview gifs, move thanks to footer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove invalid allowBuilds from pnpm-workspace.yaml (use onlyBuiltDependencies in root package.json)
- Use data-testid for search input selector (fixes / keyboard shortcut for non-English locales)