Context file for Claude Code. This repo is the *-api interface scaffold + design docs for an
extensible IDE framework that runs on-device (Android/ART) and edits & builds Android/Java projects
without hosting Gradle. Implementations (*-impl) are not written yet.
Language: Kotlin for the framework and SPI; Java for compiler-backend adapters (Eclipse JDT, javac). Target language for the IDE's users is Java first.
docs/architecture-core-foundation.md— full architecture: project model, two-level graph (composite project graph + intra-project module graph), build-system abstraction + Gradle compatibility layer, extension-point contracts, concurrency model, roadmap.docs/completion-and-incremental-ast.md— code-completion design: error-tolerant AST, the completion-token (dummy-identifier) technique, scope/binding resolution, incremental reparse.docs/plugin-system.md— the unified plugin model: thePluginSPI (plugin-api) +PluginManager(plugin-impl), how the IDE's own built-ins (ide-core/BuiltInPlugins.kt) dogfood it in place of imperative host wiring, the extension-point + scoped-service substrate,FILE_TYPE_EProuting, and platform ports as application-scoped host services (ide-core/PlatformPorts.kt).
platform-core no domain knowledge; depended on by all
└─ vfs-api
└─ project-model-api
├─ build-api
└─ language-api
deps-api → project-model-api ; deps-impl → deps-api (Maven resolver: POM transitives + BOM platforms)
index-api → language-api ; index-impl → index-api ; lang-jdt → index-api (members index + index-backed completion)
lang-xml → { language-api, index-api } (XML LanguageBackend: tolerant parser + completion; Android-agnostic)
kotlin-compiler-deps = the ONE unshaded Kotlin compiler (`-for-ide` split incl. cli/K2JVMCompiler) + IntelliJ platform jars + support libs (no kotlin-compiler-embeddable anywhere)
lang-kotlin → { language-api, index-api, kotlin-compiler-deps } + kotlin-metadata-jvm + ow2-asm (editor-only Kotlin LanguageBackend: PSI parse → own symbols/inference/completion; registered in ide-core)
android-sdk-metadata → android-support + ow2-asm (build-time generator: attrs.xml + android.jar → metadata asset)
analysis-api → language-api, index-api (diagnostics/analyzers/quick-fixes; compiler unified as a diagnostic provider) ; analysis-impl → analysis-api (the engine)
block-api → language-api ; block-impl → block-api (projectional/block editor: DOM→BlockTree projection + surgical BlockEdit→DocumentEdit)
android-support → { build-api, build-engine, project-model-impl } (the Android plugin: facet, module types, variants, native APK pipeline)
ide-ui (Compose Multiplatform UI, commonMain — no project deps)
ide-core (IdeServices + IdeServicesBackend, the shared engine→UI bridge) → { ide-ui, the impls }
ide-desktop (JVM launcher) → ide-core ; ide-android (Android launcher) → ide-core
platform-core(dev.ide.platform): ExtensionPoint/ExtensionRegistry, MessageBus, ActivityManager/ ActivityScope (read/write-lock discipline), ProgressReporter, Disposable, ContentHash, PluginId.vfs-api(dev.ide.vfs): VirtualFile, VirtualFileSystem, VfsEvent (sealed), VfsListener.project-model-api(dev.ide.model,dev.ide.model.graph): Workspace/Project/Module/SourceSet/ ContentRoot, OrderEntry (Library/Module/Sdk/PlatformDependency= a BOM) + DependencyScope (+exported= api semantics; + an optionalvariantconfig qualifier = thedebugImplementation/flavor semantics, null = shared), ClasspathSnapshot, LibraryTable/SdkTable, ModuleType, Variant, Facet, ProjectModelTransaction, ProjectGraph/ModuleGraph, CoordinationPolicy. Build-variant filtering:Module.classpath(scope, transitive, variant)takes an active config-name set ({main, free, debug, freeDebug}); null = include everything (back-compat), a non-null set drops any entry whosevariantisn't in it (shared entries always stay) — direct + the module- dependency closure.Variant.configurationsexposes that set generically (android-support'sAndroidVariantfills it from the unfiltered candidate source-set names), soproject-model-implfilters without knowing Android. Persisted as nested[dependencies.<config>]tables inmodule.toml(bare[dependencies]= shared, byte-identical to the pre-variant format). Defines Coordinate, BuildSystemId, LanguageLevel. AlsoFileIconProvider/IconTarget- the
platform.fileIconEP (pluggable project-tree icons → string icon ids; resolverFileIconRegistry - built-in
DefaultFileIconProviderin project-model-impl;AndroidFileIconProviderin android-support).
- the
build-api(dev.ide.build): BuildSystem SPI; generic incremental Task/TaskInputs/TaskOutputs/ TaskGraph/TaskExecutor engine (Gradle-mimic: model → task DAG → fingerprint up-to-date checks → cache).build-engine(dev.ide.build.engine): impl of build-api's engine — live-content fingerprints, persistent per-taskBuildCache,TaskGraphImpl(Kahn levels),TaskExecutorImpl(up-to-date skip + bounded-parallel + cancel + per-task status events), and the nativeJavaBuildSystem:compileJava → jar(createBuildGraph) pluscreateInterpretRunGraphwhoseInterpretExecTaskruns a console app's main by INTERPRETING its compiled bytecode on the:jvm-interpbytecode VM (AlwaysRun, the Gradleapplication-pluginrunequivalent), over aJavaCompileport. Kotlin/Java mixed modules: an injectedKotlinCompileport (mirror ofJavaCompile, impl in lang-kotlin) drives acompileKotlintask (KotlinBuild.kt) registered ahead ofcompileJavafor any module with.kt. Kotlin emits to akotlin-classessibling dir; that dir joins the Java compile classpath (Java → Kotlin), thejar/classeslifecycle (now multi-dir), and the run runtime classpath, while kotlinc is fed the module's.javafor resolution (Kotlin → Java). Standard ordering (Kotlin first); a Java edit re-runscompileKotlin; incrementality holds. Console runs use the bytecode interpreter, ONE path for desktop AND device (the oldjava-forkJavaExecTaskand the on-device dex-runcreateDexRunGraph/JavaDexTask/DexExecTask+RunDexBackend/RunDexer/DexRunner/DexClassLoaderRunner/ForkedDalvikRunnerare all GONE): the run task hands the runtime classpath (class dirs + library jars, no dexing) to an injectedProgramInterpreter(ProgramRun.kt; implVmProgramInterpreterin jvm-build, so build-engine stays VM-free).VmProgramInterpreterbuilds a FRESHVmper run (statics reset), interprets the user's + libraries' classes from the classpath and bridges only the platform/stdlib namespaces (java/,kotlin/,android/,… → the real runtime), so nothing downloaded or user-built is loaded by the host class loader or dexed — the Play dynamic-code answer. Runs are cancellable even mid-compute (the VM loop checks a cancel flag every ~1024 instructions; the run thread is also interrupted to break a blocked stdin read). The run sandbox now lives at the VM bridge (RunBridge), not bytecode instrumentation:System.exit/Runtime.exit/halt→ControlledExit(ends the run not the IDE — an in-processSecurityManageris unsupported on ART), and network/file/reflection/process calls consult the injectedPermissionBrokerviaGuards.enforce(blocks + prompts on an undecided category,SecurityExceptionon deny). Best-effort over a curated API set, not a hardened sandbox. stdio: the interpreter redirects the process-globalSystem.out/err/into the run'sProgramIofor the run's duration (so interpreted output AND bridged stdlib I/O — Kotlin'sreadln— both reach the run console). On device the host injectsVmProgramInterpreter(peerFactory = DexPeerFactory()), which dexes the small generated peer classes the VM needs when an interpreted object is handed to real platform code (aComparator, aRunnable). Threading: the VM is multi-threaded — aThreadan interpreted program starts is a REAL host thread whoserunre-enters the interpreter, so threads run in genuine parallel; each thread keeps its frame stack thread-local,synchronized/wait/notifyon interpreted objects use real per-objectVmMonitors (ReentrantLock + Condition), class init follows JLS 12.4.2, and the shared class / resolution / peer state is concurrent.java.util.concurrent(atomics, executors, locks,ThreadLocal) is bridged to the real runtime, so it works unchanged. The run ends whenmainAND every non-daemon thread it started have finished (JVM exit semantics); Stop interrupts the wholeThreadGroup. It is a best-effort concurrency model, not a hardened JMM (raw non-volatilefield visibility is relaxed). Tradeoffs: compute-heavy programs run much slower than native, and an unsupportedinvokedynamicbootstrap fails withVmUnsupportedException. JDT compile backend (JdtBatchCompiler/JdtSourceCompiler, ecj batch →.class) lives in lang-jdt. Wired into the IDE: the top-bar Run button + liveBuildConsole(status pill, step graph, streamed log) viaIdeBackend.buildState/runBuild/stopBuild. Roadmap steps 4–5 done.android-support(dev.ide.android.support+.tools/.tasks): the Android plugin (roadmap step 7). Pure kotlin/jvm — never links android.jar; invokes SDK tools at runtime behind injected ports.AndroidFacet(+BuildType/ProductFlavor) +AndroidFacetCodec([android]TOML table; ints asLong, build types/flavors as inline-table arrays).BuildTypealso carries the R8/ProGuard config (minifyEnabled,shrinkResources,proguardFiles,consumerProguardFiles, inlineproguardRules);AndroidFacetcarriesr8FullMode(default on) +coreLibraryDesugaringEnabled(codec emits these only when non-default).AndroidAppModuleType/AndroidLibModuleType;AndroidVariants(build-type×flavor cross-product → active source sets, Gradle naming).AndroidBuildSystemwires the incremental DAGmergeResources → aapt2Compile → aapt2Link(+R,+extra-pkg R) → [compileKotlin →] compileJava(per module) → dexBuilder → {mergeProjectDex, mergeLibDex, mergeExtDex} → packageApk → signover build-engine — faithful to AGP: ONEdexBuildertask (DexArchiveBuilderTask) archives the three scopes (project / sub-module / external) into per-class dex archives (Dexer.dexArchive, D8DexFilePerClassFile). The project is per-class-file incremental (a.classmanifestof content hashes → only changed classes re-dex, with the unchanged ones as D8's desugaring classpath); sub-module/external scopes are per-jar content-hash buckets (an unchanged lib is reused, not re-dexed). The scope merges (DexMergeTaskunder AGP'smergeProjectDex/mergeLibDex/mergeExtDexnames) run only when their scope changed. minSdk ≥ 21 → native multidex (scopes stay split, packaged separately); minSdk < 21 → onemergeDex(MERGE_ALL).mergeExtDexis threshold-gated (extMergeThreshold, AGP's LIBRARIES_MERGING_THRESHOLD 50/500): per-lib below it, merge-all above.packageApkrenumbers the dex layers into oneclasses*.dexset. The dexer suppresses benign D8 desugaring warnings (e.g. guava's MethodHandle helpers below min-api 26). Packaging native libs + Java resources (AGP-faithful,tasks/PackagingMerge.kt): two merge tasks feedpackageApk/BundleTask, mirroring AGP'smerge<Variant>NativeLibs/merge<Variant>JavaResource.MergeNativeLibsTaskcollects every.so(modulesrc/<set>/jniLibs+ dep-lib jniLibs + exploded-AARjni.sounderlibinside dep jars) into one<abi>-laid-out dir the packager maps underlib;MergeJavaResourcesTaskcollects Java resources (modulesrc/<set>/resources+ non-class entries of sub-module/external jars,.classskipped) intomerged-java-res.jarcopied to the APK root (bundle: underroot/). Both apply the facet'spackaging { }(AndroidPackaging/ResourcePackaging/JniLibsPackaging, new[android.packaging]inline table in the codec) layered over AGP defaults (PackagingRules: exclude signatures/MANIFEST.MF/maven/.kotlin_module/… ; mergeMETA-INF/services/**). Precedence exclude → merge → pickFirst → first-wins-with-warning (AGP errors on the last; we're lenient); sources offered project-first..sopackaged as-is (no NDK strip on device). NewContentRole.JNI_LIBS;android-app/-libsource sets gainedsrc/<set>/{resources,jniLibs}roots. Editable in the IDE via the Module Config Packaging tab (ModulesTab.Packaging/PackagingPane→ModuleService.get/updatePackagingOptions→facet.packaging; AGP defaults shown read-only). The block is omitted from the[android]codec map when default so it lives on its own tab (not the generic Settings fields), andupdateModuleConfigoverlays UI values on the existing encoded facet so a Settings save can't clobber it. VerifiedAndroidPackagingBuildTest(SDK) +PackagingMergeTest(unit) +ModuleConfigTest(config UI). Minify/release path:minifyEnabledswaps the dexBuilder→merge chain for oneR8MinifyTask(minify<Variant>WithR8) that shrinks+optimizes+obfuscates+dexes in a single R8 pass. Keep rules are gathered AGP-style (ProguardConfig.kt): aapt2--proguardmanifest/layout rules +proguardFiles(bundled defaults underresources/proguard/resolved likegetDefaultProguardFile(...), plus module files) + dep-lib/AARconsumerProguardFiles+ inlineproguardRules;r8FullModepicks full vs ProGuard-compat;mapping.txt→outputs/mapping/<variant>/.shrinkResourceslinks proto (aapt2--proto-format), R8 shrinks resources in-process (AndroidResourceProvider/Consumer), thenConvertResourcesTaskconverts back to binary.coreLibraryDesugaringEnableddrives D8(debug)/R8(release)java.*rewriting + anL8DexTaskthat dexes the kept-wholedesugar_jdk_libsruntime (injectedDesugarLibartifacts; no-op when the host ships none). TheShrinkerport takes aShrinkRequest; the desugar config folds into the dex cache key only when enabled (a no-desugaring build's cache is unchanged). Verified end-to-end through real R8/D8/L8 (AndroidProguardConfigTest,AndroidResourceShrinkTest,AndroidCoreLibraryDesugaringTest). Plus a realmergeResources(dep android-lib + AAR + app res). Tool portsAapt2/Dexer/ApkSigner(tools/) split by ART reality: native aapt2/zipalign via ProcessBuilder (Aapt2Subprocess); pure-Java D8/apksigner either subprocess (D8Dexer/ApkSignerTool, desktop) or in-process via their APIs (D8InProcessDexer→com.android.tools.r8.D8,ApksigSigner→com.android.apksig, the on-device path). FactoriesAndroidBuildSystem.subprocess(...)/.inProcess(...); r8+apksig arecompileOnly+test here,:ide-androidbundles them. Library-aware:AndroidLibraries+AarExtractorroute JAR/AAR deps — code→compile/dex, AAR res→aapt2(merged into app R), AAR assets→assets/, AAR jni→lib/. Multi-module: compiles the whole module-dependency closure and dexes every output; dep android-lib res merged. Variant-aware: a dependency android-lib is built in the variant matching the app being assembled (AndroidVariants. matchLibraryVariant: exact name → same build type with dimension-aware flavor match → debuggability fallback → the lib's default) —registerAndroidLibraryuses that variant's source roots / res / R / classpath, not all non-test source sets; the app likewise merges each dep lib's matching-variant res.AndroidVariant. configurations(the unfiltered candidate names) drives the dep-classpath filter. Kotlin: when a module (app or android-lib) has.kt,AndroidKotlinCompileTaskruns before itscompileJava(K2 against android.jar + the lib's own non-final R via the injectedKotlinCompileport); thekotlin-classesoutput joins the Java compile classpath and the project dex scope (DexArchiveBuilderTasktakes multiple project-class roots), and a lib's Kotlin output sits in thekotlin-classessibling so dependers find it.android-app/android-libsource sets gained asrc/<set>/kotlinSOURCE root (plussrc/<set>/resources= Java resources andsrc/<set>/jniLibs= native libs; see the packaging note above). Decoupled (reusable) library R: each android-lib gets a non-final R from its OWN res (generateR--non-final-ids→compileR, compile-only, kept OUT of the lib's dexed output → getstatic not inlined); the app generates+dexes the FINAL R for all lib pkgs (--extra-packages) → lib compiled once, app-independent, no dup R.SampleAndroidProject(appandroid-app →featureandroid-lib →corejava-lib) is the Android test fixture (bootstrapDemo/seedDemo); editable manifest+res, android.jar SDK (detected on desktop, bundled asset on-device). The Java sample isSampleProject/bootstrapJavaDemo(the Java-test fixture). The launchers no longer seed a demo on first run — both start on the project picker. The file tree surfaces the manifest + res/assets (IdeServices.manifestPath+treeRootsDetailed, which keeps each root's source-set name +ContentRoles so the tree picks distinct icons). Verified on desktop (skip when no SDK):AndroidApkBuildTest(both wirings),AndroidLibraryAwareTest(JAR+AAR),AndroidMultiModuleTest(app→util→coreall 3 dexed),AndroidIncrementalDexTest(edit app → dexBuilder re-archives only the app;mergeExtDexskipped, i.e. lib not re-dexed),AndroidPerClassDexTest(edit one of two app classes → only its per-class.dexchanges),AndroidMonoDexTest(minSdk<21 → singlemergeDex/classes.dex),AndroidLibResourceMergeTest(decoupled-R invariants + res in arsc),SampleAndroidProjectTest(sample → signed APK). Registered into the host viaAndroidSupport.register(moduleTypes, codecs). Gotchas: D8 needs a jar not a class dir (DexTask jars in-process); D8 rejects bytecode newer than it supports (compile at the module languageLevel ≤ 17);List<Path> + Pathbinds theIterableoverload (a Path isIterable<Path>) — append with+ listOf(p). Seedocs/android-support.md.
language-api(dev.ide.lang+.dom/.incremental/.resolve/.completion): LanguageBackend SPI, SourceAnalyzer/SourceCompiler, CompilationContext; backend-neutral DOM (DomNode/ParsedFile/NodeKind,nodeAt); IncrementalParser (DocumentSnapshot/DocumentEdit/reparse); resolve (Symbol/Scope/TypeRef); completion (CompletionService/CompletionContext/CompletionItem). Also theplatform.languageBackendEP (LANGUAGE_BACKEND_EP): backends are contributed, andide-corepicks one per file by matching the file'sLanguageId(.java→jdt,.xml→xml) — adding a language is a registration, not a host edit.lang-xml(dev.ide.lang.xml+.completion): the XML LanguageBackend (Android layouts/values/ manifest/drawables/menus).XmlTreeParser— a hand-written error-tolerant parser (never throws on the half-typed buffer; unclosed/mismatched tags recover via an open-name stack withxml.*diagnostics) → neutral DOM (XmlNode/XmlParsedFile,XmlNodeKinds;ATTR_VALUErange is the text between quotes). Full reparse per change (XML files are small;INCREMENTALnot advertised).XmlSourceAnalyzerexposes the incremental parser + completion + well-formedness diagnostics. Deliberately Android-AGNOSTIC:XmlContextScannercomputes where the caret is (XmlCompletionPosition: TAG_NAME/ATTRIBUTE_NAME/ ATTRIBUTE_VALUE + tag/parentTag/attr/prefix/range) andXmlCompletionServicemerges candidates from injectedXmlCompletionContributors — the host supplies what belongs there. The Android knowledge lives inAndroidXmlContributor(ide-core), driven by the SDK-derivedAndroidSdkMetadata(the curatedAndroidWidgetCatalogis GONE) bundled as a classpath asset + mergedResourceRepository/index → CompletionItems: hierarchy-correct attributes (incl. parent*_Layoutparams like RelativeLayout'slayout_below/toEndOf), enum/flag/boolean values,@type/namerefs filtered by accepted ResourceType,@+id/. Custom-view tags: layout TAG_NAME completion offers framework widgets (simple names) plus customViewsubclasses discovered from the module's library classpath (Maven jars + AARclasses.jar) by their fully-qualified name —CustomViewScanner(android-support) ASM-scans the classpath for View subclasses (seeded with the bundled SDK widget map so a chain bottoming out at a framework base likeandroid.widget.Buttonstill resolves; cross-jar ancestry handled), content-fingerprint cached under.platform/caches/custom-views/.IdeServices.customViewWidgetsfeeds them in;XmlCompletionService.nameMatchesnow also matches a candidate's local name after.so typingMaterialButtoncompletes the FQN. Editor QoL: auto-close tags (XmlEditing.tagToClose—>after<TextView…inserts</TextView>) + caret hops past the closing"after an attribute-value completion (CodeEditor.accept).analyzeXml(ide-core) merges XML well-formedness with the resource-reference checks into the one editor Diagnostic stream. Phases 1–4 done + manifest completion + create-XML/-dir + the namespace:fix- namespace-aware attribute matching; phase 5 (non-layout templates, framework
@android:resources) indocs/xml-language-support.md. Phase 4 inspections/quick-fixes: pure detection inXmlLintRules(missingxmlns:android, hardcoded string, missinglayout_width/height),IdeServices.xmlFindingsattaches fixes + the index-backed unresolved-resource check (repository fallback while the index builds); fixes reach the lightbulb via the XML branch ofeditorActions/applyEditorAction— extract-to-@stringappends tores/values/strings.xml, create-resource, add-attr, add-namespace. Phase 3 resource index:AndroidResourceIndex(android-supportindex/, onplatform.index) keyed by resource name →ResourceDeclValue(type,name,file,offset), parsing res XML viaResourceFileScanner;IndexScope.resourceRoots IndexServiceImpl.indexSourcewalks res.xml(disjoint from Java inputs — baselines safe);IdeServicesfeeds res roots (project+dep+AAR, keyed"type/name"), reindexes onsave; completion (resource names), go-to-def, and the unresolved-resource inspection now resolve via the index (repository fallback while it builds). Matching:XmlCompletionService.nameMatchesmatches the local name after://(typelayout_w→android:layout_width); the popup bolds the matched run wherever it falls (CompletionPopup).:fix (extraWordCharsinCompletionSession.kt/CodeEditor.kt)- create XML/dir (
NewXmlFileDialogFile/Directory modes,FlowRowchips,IdeBackend.createDirectory). Phase 2 metadata (android-support/metadata/):LayoutMetadatainterface, top-levelWidget/AttributeSpec,AttrsXmlParser(javax.xml; framework + custom attrs.xml),SdkMetadataCodec(compact text assetCAMETA1),AndroidSdkMetadata(hierarchy-aware attr inheritance via android.jar superclass map;app:-prefixed reuse for custom views;reference-attr type hints by name). The:android-sdk-metadatagenerator (ASMClassReaderover android.jar) produces the asset, committed/bundled atandroid-support/src/main/resources/android-sdk-metadata.txt(API 36);AndroidSdkMetadata.bundled()loads it (cached), andIdeServices.sdkLayoutMetadata()prefers a<workspace>/.platform/android-sdk-metadata.txtoverride (e.g. another API level) + merges per-module custom-view attrs (customAttrsMetadata). Manifest:AndroidManifestCatalogrouted when the file isAndroidManifest.xml.:fix: editor word-boundary logic is language-aware viaextraWordChars(path)(XML adds:@?+/.-) inide-uiCompletionSession.kt/CodeEditor.kt—android:/@string/no longer dismiss the popup; Java unchanged. Create XML:res/folder tree nodes (TreeNode.resDirPath) expose+→NewXmlFileDialog(Layout/Values/Drawable/Menu templates) via the existingcreateFileseam. Verified:lang-xmlparser/completion tests,android-supportAndroidSdkMetadataTest,ide-coreAndroidXmlCompletionTest(widget/attr/value/manifest/SDK/custom-view).
- namespace-aware attribute matching; phase 5 (non-layout templates, framework
lang-kotlin(dev.ide.lang.kotlin+.parse/.symbols/.resolve/.completion): the editor-only Kotlin LanguageBackend (docs/kotlin-completion-backend.md). Uses the Kotlin compiler ONLY to parse — a resolution-free standalone PSI host (KotlinParserHost: a singletonKotlinCoreEnvironment→text → KtFile, error-tolerant) adapted to the neutral DOM (KotlinParsedFile/KotlinDomNode,KtElement → NodeKindinKotlinNodeKinds) — and discards all of its resolution/FIR, building its OWN symbols/inference/completion on top. The editor backend does NO codegen (that's a separate track — see below). Two symbol sources (KotlinSymbolService): project.ktparsed to PSI → declaration index (SourceIndex, incl. extensions); classpath binaries branch on@kotlin.Metadata— Kotlin libs decoded viakotlin-metadata-jvm(KotlinMetadata, ASM-extracts the annotation → real Kotlin shape: extensions, properties, nullability), plain Java/Android via ASM bytecode (JavaBytecode). Kotlin mapped types (String/List/… have no.class) bridged by a hardcoded Kotlin→Java map + builtin supertype chains (Builtins); extension index keyed by receiver FQN (walks supertypes) makeslist.map/string.trimappear on.. Resolution + the declared-type-driven inference subset (KotlinResolver: scopes from PSI parents, locals-from-initializers, member/call return types, constructor calls,a.b().cchains) → member/name/type completion (KotlinCompletionService, completion-token splice). Default Kotlin imports inDefaultImports. Desktop-verified (CI_CORE_ONLY=true ./gradlew :lang-kotlin:test, 12 tests): parse spike (cold ~210ms/warm ~3ms),@Metadatadecode vs the real stdlib jar, and end-to-end completion (stdlib extensions, source members, inference chain, scope, type, go-to-def). Gotchas: the compiler/platform is:kotlin-compiler-deps— the UNSHADED-for-idesplit + realcom.intellij.*platform jars (versionskotlinForIde/intellijPlatformin the catalog, NOT Maven Central; the same world the K2 Analysis API links, so ONE PSI everywhere);createForProductionneeds@OptIn(K1Deprecation::class)and aCompilerConfiguration.create(...)-built configuration (extension storage), and the K1KotlinToJVMBytecodeCompileris gone — the registrar compile path drives the K2cli.pipelinephases. Registered inide-core(LANGUAGE_BACKEND_EP+.kt/.kts→kotlin routing inlanguageFor+indexService/extensionCacheDirinjection inanalyzerFor; analyzer isDisposable). The classpath extension scan is lazy + persistent — per-jar content-keyed.kxtcache under.platform/caches/kotlin-ext, and jars with noMETA-INFkotlin_moduleare skipped without decoding a class. Kotlin codegen is wired into the build (compile/):KotlinJvmCompiler(in-process K2K2JVMCompiler; stdlib located viaUnit::class,android.jarboot +-no-jdkon ART, hostjdkHomeon desktop). The build drives it through lang-kotlin's OWNcompileKotlinbuild task (build/KotlinCompileTask, which callsIncrementalKotlinCompilerdirectly and applies the Compose plugin when needed) — there is no build-engine compile port; jvm-build'sJavaPluginregisters the task (Java interop both ways; see the jvm-build + android-support bullets). Desktop-verified end-to-end (KotlinJavaInteropBuildTestin ide-core builds + RUNS a mixed module). Incremental below the task level (compile/IncrementalKotlinCompiler+KotlinAbi, docs phase 3):KotlinCompileTaskroutes through it — a plain-text manifest sidecar (<kotlin-classes>.ic/, outside the dir the engine fingerprints) tracks per-source content hashes, the source→.classmap (via-Xreport-output-files), and a per-class ABI snapshot. A body-only edit recompiles just the changed.kt(unchanged classes copied out as a read-only binary classpath +-Xfriend-pathsforinternal), keeping the rest byte-for-byte; any ABI/structural/context change falls back to a full module recompile (conservative — no file→file dep graph).KotlinAbihashes the public surface only (header + non-private member sigs +@kotlin.Metadata; bodies/privates excluded) so a body edit is ABI-stable, but a class with an inline fun is hashed whole (its body is ABI). VerifiedIncrementalKotlinCompilerTest. The ART go/no-go was cleared by the K2-on-ART spike (docs/kotlin-compiler-on-art.md); running the wired (incremental) build path on device is what's left. Compose-aware editing:KotlinResolver.composableContextAt(offset)decides a position's Compose calling-convention status (COMPOSABLE / NON_COMPOSABLE / UNKNOWN) by walking PSI boundaries — a@Composablefunction/accessor or a@Composable-typed lambda is composable, aninlinelambda is transparent (inherits the enclosing scope), a plain non-inline lambda resets it — faithful to the compiler'sComposableCallChecker(isComposable/isInlinealready ride onKotlinSymbol/KotlinTypefrom source/metadata/bytecode). Completion boosts@Composablecallables to the top in a composable context (Android Studio's weigher; a boost inrank(), NOT a filter — non-composable code stays available). The diagnostickt.composableInvocation(KotlinSourceAnalyzer.composableInvocation) flags a@Composablecall from a confidently-non-composable context as an ERROR (the compiler'sCOMPOSABLE_INVOCATION); UNKNOWN backs off, so the parse-only model never false-positives. VerifiedKotlinComposeContextTest.deps-api(dev.ide.deps): DependencyResolver (Maven coordinates → jars/aars, conflict policy).resolvetakes aplatforms: List<Coordinate>of imported BOMs (Gradleplatform(...)).deps-impl(dev.ide.deps.impl):MavenDependencyResolver— fetch/parse POMs (parent chain + imported BOMdependencyManagementmerged), transitive walk with scope-narrowing/exclusions/cycle-guard, conflict resolution (NEWEST/PINNED/FAIL_ON_CONFLICT),.aar→classes.jar+res/assetsexplosion, disk cache (.platform/caches/resolved-deps, cache-first → offline). BOM platforms: aplatformscoordinate supplies the version for any versionless (blank-version) coordinate — direct OR transitive — without overriding an explicit version (plainplatform()semantics; earlier platforms win). Persisted in the model asPlatformDependency(a newOrderEntry;{platform = "g:n:v"}inmodule.toml, no classpath artifact). IdeServices wires it:addPlatformimports a BOM;addDependencyaccepts a versionlessgroup:name(resolved against the module's platforms, then pinned to the resolved version at add time — a later BOM bump needs a re-add). The Dependencies screen has a Library/Platform(BOM) toggle + a direct-add row for typed/versionless coordinates. Gradle Module Metadata variant selection (GradleModuleMetadata.kt+GmmVariantSelector.kt): when a coordinate publishes a*.modulethe resolver reads it and picks the artifact variant by Gradle attributes (loadNode→selectVariant: hard-filter category/usage/library- elements, soft-scoreplatform.typeandroidJvm>jvm>common +jvm.environment; usage ladder accepts java-/kotlin- api>runtime), followsavailable-at(KMP root →-androidplatform module), and uses that variant'sdependencies/files. Download by the file'surl, not its logicalname(AGP publishes a KMP AAR as name…-release.aarbut url…-android-<v>.aar— usingname404s every KMP AAR). Also honors GMMdependencyConstraints(atomic-group/BOM alignment applied to GAs in the graph),version.strictlypins (not bumped by newest-wins), multi-file variants (extra files →ResolvedArtifact.extraClassesRoots), and a GMM sources variant (KMP-sourcesattach, vs the root-coordinate classifier guess). Cross-module capability conflict resolution (Gradle-faithful): a module that declares it also provides another's capability (guava →com.google.collections:google-collections) supersedes it — Gradle picks one (the highest capability version, likeselectHighestVersion) and evicts the loser entirely (substituting it with the winner). The result is then pruned to the reachable graph (an evicted/superseded module's now-unreachable transitives drop out), so resolution is accurate rather than relying on a downstream dedup. Falls back to the POM when no.moduleexists, so non-GMM libs resolve exactly as before; consumer attributes default to Android (VariantRequest). This is whatMavenClasspath.dedupeForAndroidDex's KMP-android/-jvmcollapse used to paper over — that ranker is now removed (only the bundled-stdlib-vs-Maven newest-wins stays).addDependency/addModuleDependency/addPlatformtake an optionalvariant(a build-variant config likedebug→ persisted as a[dependencies.debug]declaration).search()(the "Add dependency" picker) queries BOTH Maven Central's Solr endpoint AND Google's Maven repo index (master-index.xml+ per-groupgroup-index.xml, crawled + POM-packaging-probed, all session-cached), merging the hits (Central first, deduped by group:name) —androidx.*/com.google.android.*artifacts live ONLY on Google Maven (not mirrored to Central), so a Central-only search never surfaced them even though they resolved fine once added. Verified:MavenDependencyResolverTest(BOM + GMM-androidselection / available-at / name≠url AAR / constraints / strictly / multi-file / kotlin-runtime / sources / capability eviction (highest version) + reachable-graph prune / offline / Google-Maven search (index match, POM packaging, stable-version preference, full-coordinate paste)),GradleModuleParserTest,GmmVariantSelectorTest+PersistenceRoundTripTest.index-api(dev.ide.index): indexing SPI —IndexExtension/IndexService/IndexInput, theplatform.indexEP, shared value types +SourceDeclarationScanner. Seedocs/indexing-infrastructure.md.index-impl(dev.ide.index.impl): the engine. Static (SDK+library) side = disk-backed segments (Segment/SegmentWriter): one immutable.segper artifact (keyed by content hash), queried in place — sorted names + varint-delta postings + optional trigram dict, read on demand through a bounded LRUBlockCache(default 4 MB cap); the only resident state is a sparse term index (every 64th term → offset,String[]/long[]), so heap is flat regardless of index size — the §2 low-RAM design (block-cache variant of mmap). Source side = in-memoryIndexData(TreeMap+ postings + trigram), incremental on edit. SharedScoring(prefix/fuzzy, identical ranking); per-artifact cache reused across launches (opened, not loaded);Closeable/invalidate()shut channels. Built-insclassNames,packages,sourceSymbols; the bytecodemembersindex lives inlang-jdt(needs ecj). Completion queries this for unimported-class (auto-import) + package completion; palette uses it for go-to-symbol. Seedocs/indexing-infrastructure.md.analysis-api(dev.ide.analysis): diagnostics/analyzer/quick-fix SPI — ONEDiagnosticmodel + one pipeline (compiler errors and analyzer findings merge into the same stream).AnalyzershapesFileAnalyzer/ProjectAnalyzerover tiersSYNTAX/SEMANTIC/PROJECT;DiagnosticSink/ProjectDiagnosticSink,AnalysisTarget/ProjectAnalysisScope;QuickFix/WorkspaceEdit(reusesDocumentEdit)/QuickFixProvider(fixes keyed byDiagnostic.code, incl. compiler codes);DiagnosticProvider(the compiler, unified);AnalysisService,AnalysisProfile, batchLintReport; EPsplatform.analyzer/platform.quickFixProvider/platform.diagnosticProvider. Interfaces only. Seedocs/analysis-api.md.analysis-impl(dev.ide.analysis.impl): the engine behind analysis-api.AnalysisEngine : AnalysisServiceruns FileAnalyzers (one shared DOM walk → node-kind gating viainterestedIn), the compiler (DiagnosticProvider), and ProjectAnalyzers; applies theAnalysisProfile(enable/severity) andSuppressionFilter(@Suppressscoped to the enclosing decl via the DOM +// noinspectionnext-line); publishes a version-gated set (PublishedState, replace-not-append) toAnalysisListeners. Per-tier debounce + cancellation (SchedulerConfig, coroutines);applyruns fixes atomically + re-analyzes;lintis the batch sweep. Host-decoupled via theAnalysisEnvironmentport (targets/language/apply).block-api(dev.ide.block): the projectional (block) editor SPI —BlockNode/BlockSlot/BlockPart(ordered fields + slots)/SlotCategory/BlockTree,BlockMapping+BlockTemplate+ProjectionContext- the
platform.blockMappingEP, theBlockEditsealed set (SetField/ReplaceWithText/Delete/InsertTemplate/Move/Wrap), andBlockProjectionService(project/blockAt/computeEdit). Typed slots:ValueKind(BOOLEAN/NUMBER/STRING/OBJECT/TYPE/UNKNOWN) onBlockSlot(what the position expects) andBlockNode(what the expression produces) drives the Scratch-style socket shapes; inferred syntactically today, with theValueKindOraclehook (engine asks it FIRST) so a semantic resolver can refine later. Blocks are a live projection of the shared DOM (not a parallel model); a block edit compiles to a minimalDocumentEditso untouched code survives byte-for-byte. Seedocs/block-based-editing.md.
- the
block-impl(dev.ide.block.impl): the engine.BlockProjectionEnginedoes gap-carving projection (interleave the literal source between child ranges as read-only chrome with each child projected into a slot) + the surgicalBlockEdit→DocumentEditpipeline;JavaBlockMappingdecomposes statements + key expressions (control flow, calls, member/name refs, infix, literals; containers → one foldable list slot), collapsing the rest to editable opaque text slots. Call collapse: a pure-name receiver (System.out) becomes an editablequalifierfield in the call block's header, and fluent chains (sb.append(x).append(y)) flatten into ONE method_call block —name/name1/… fields + one ARGUMENT slot per arg, real-range chrome between (cursor gap-walk, sodefaultSerializeround-trips byte-for-byte; never synthesize separator text). ValueKind inference is syntactic and pure (valueKindFor/expectedValueKind/primitiveKindin JavaBlockMapping.kt): literals/operators/casts produce kinds; if/while/do/assert conditions, typed initializers and returns expect them; the engine consults aValueKindOraclefirst. Verified against the real JDT DOM (BlockProjectionTest, 12 tests). Gotcha: projection ids are deterministic per text —computeEdit/applyBlockEditre-project the same buffer to resolve refs and hold no state, so the host must pass the text the displayed tree was projected from. Wired into the IDE viaIdeBackend.projectBlocks/applyBlockEdit(neutralUiBlockNode/UiBlockEditDTOs, both carrying lowercasevalueKindstrings) →IdeServices.projectBlocks/computeBlockEdit(inide-core) over the per-module incremental parser. Theide-uiblock view (editor/BlockEditor.kt+editor/blocks/BlockShapes.kt/BlockDrag.kt/BlockCompletion.kt/SlotCompletion.kt, behind the Code/Blocks toggle) is a Sketchware/Blockly canvas: solid category-colored puzzle blocks (notch/bump connectors viaGenericShape, overlapped byInterlockColumn), C-shaped control blocks wrapping a colored body rail, typed value sockets (ValueShape, Scratch parity: hexagon=boolean, full pill=number, sharp rect=string, outlined tag=type, rounded=object/unknown — applied to empty sockets AND the value blocks/pills filling them), segmented chain rendering (dimmedqualifier, boldname*fields, thin dividers between chain links), inline completion while editing a socket/field (reuses the code editor's publicCompletionList+CompletionSession; auto-importadditionalEditsheld until commit; socket-type-aware re-ranking viarankForSocket), an index-backed palette search (searchSymbols/searchMembers→ draggable template hits), and drag-and-drop (DragState/dragSource/dropZone: long-press to Move/Delete-on-trash, drag a palette template to Insert) — all mapping to BlockEdits. Verified on desktop (screenshot).ide-ui(dev.ide.ui, Compose MultiplatformcommonMain, desktop (JVM) + Android targets): the reusable IDE UI — design-token theme (faithful todocs/design/tokens.css),CaIcons, components,CodeEditor(gutter + syntax + completion popup + inline diagnostics: severity-colored wavy underlines, a gutter error/warning glyph, and the inline error chip; muted for UNUSED), picker/editor screens. File tree (FileNavigator): icons come from the extensibleTreeIconsregistry keyed byTreeNode.iconId(TreeIcon.Glyph/Folder/Badge, theme-resolvedIconTint); distinct source-set icons (java/resources/android-res/assets/generated), compacted middle packages (com.tyron.codeassistone row, segments kept), and a New-Class flow — hover+on a source-root/package row (or the header+) opensNewFileDialog, whose package chips can target an intermediate level of a compacted package; it callsIdeBackend.createFilethenIdeUiState.refreshTree()+opens.EditorScreendebouncesIdeBackend.analyzeper edit and feeds the result toCodeEditor. Top bar has a Re-index button (IdeBackend.reindex→IndexService.invalidate+ rebuild) and a Run split-button: the chevron opens a searchable dropdown ofIdeBackend.runTasks()(RunControlinEditorChrome);runBuild()runs the default.IdeServices.runTasks/runTaskenumerate a Javarun+ Androidassemble<Variant>(the latter viaAndroidBuildSystem.subprocesswhen an SDK is installed). The block editor (Code/Blocks toggle; details under theblock-implbullet) has typed Scratch-style sockets, segmented chain rendering, inline socket/field completion (reusingCompletionList/CompletionSession+rankForSocket), and index-backed palette search. APermissionDialog(hosted app-wide inCodeAssistApp) overlays a running program when the run sandbox blocks on a guarded call (IdeBackend.permissionRequest→ Allow once/run/always/Deny →answerPermission). On mobile the file-tree + build-console sheets start closed (AppStatekeys their defaults offisMobilePlatform; the console auto-opens on Run); desktop keeps them open. Talks ONLY to theIdeBackendport (dev.ide.ui.backend, paths as strings) — no framework deps. Seedocs/design/. Localization: all user-facing UI text is a Compose-resources string key inide-ui/src/commonMain/composeResources/values/strings.xml(translations invalues-<lang>/, e.g.values-zh/; missing keys fall back tovalues/), read withstringResource(Res.string.<key>)/pluralStringResource(...)(%1$s/%1$dformat args,<plurals>for counts). NEVER hardcode a visible literal in a composable; generic labels reuse a bare shared key, feature-specific ones take a short prefix (dep_,run_, …). Identifiers/route+setting keys/log lines/backend data are NOT localized. Seedocs/localization.md.ide-core(dev.ide.core): the shared engine→UI bridge BOTH launchers run.IdeServicesfaçade (platform-core + project-model-impl + lang-jdt + index-impl + analysis-impl + block-impl + build-engine + android-support) +IdeServicesBackendimplementingIdeBackend. Wires theAnalysisEngineover anIdeAnalysisEnvironment(live-overlay targets) with the JDT compiler as aDiagnosticProvider+ built-in Java analyzers (SystemOutCallAnalyzer,UnusedImportAnalyzerinJavaAnalyzers.kt);IdeBackend.analyzeroutes throughengine.analyzeNow. Test fixtures only (the launchers seed nothing):bootstrapDemo/seedDemo(the Android sample) andbootstrapJavaDemo(the Java sample). Hosts the run sandbox'sPermissionBroker(blocks a running program on a guarded call →IdeBackend'spermissionRequest/answerPermission; remember-logic in the testablePermissionPolicy— once/run/ always, persisted per project), set onGuards.brokeraround each run. The android Run (androidRun:) assembles then installs+launches via an injectedApkInstaller(on-device only).ide-desktop(dev.ide.desktop): JVM Compose launcher —main()opens aProjectManagerover~/.codeassist/projectsand rendersCodeAssistAppover anIdeServicesBackend(starting on the project picker; no demo is seeded). The old Swing/FlatLaf UI is gone.ide-android(dev.ide.android): Android Compose launcher — the device counterpart toide-desktop, running the real engine on ART (the old in-memoryAndroidIdeBackenddemo is gone).AndroidIde.bootstrapcopies the bundledandroid.jarasset and opens aProjectManager.onDeviceoff the main thread (starting on the picker; no demo is seeded);MainActivityrenders the sameCodeAssistApp(:ide-uicommonMain) overide-core'sIdeServicesBackend. Supplies the on-device ports:VmProgramInterpreter(peerFactory = DexPeerFactory())(the bytecode-interpreter console runner) andApkInstallerImpl(PackageInstaller+ the OS install-confirmation, then launch — needsREQUEST_INSTALL_PACKAGES). Build:./gradlew :ide-android:assembleDebug(AGP 9.x; needs the Android SDK — see build notes).
- IDs are
@JvmInline value classwrappers — no stringly-typed APIs. - Open/extensible classifications are string-backed value classes (
NodeKind,BuildSystemId,LanguageId); closed sets are enums (DependencyScope,SymbolKind). - Long-running entry points are
suspend; only touch the model/DOM insideActivityScope.readAction/writeAction. Mutate via*Transaction/Modifiable*thencommit()(atomic, publishes events). - Editor features target the neutral DomNode/Symbol/Scope — never JDT/javac types directly.
- Default language + compile backend is Eclipse JDT/ecj (error recovery, working-copy reconcile, built-in completion, light on ART); javac is an optional compile backend; custom parser slots in later.
Pure-Kotlin, stdlib-only (a suspend modifier on an interface method needs no coroutines dependency).
No Gradle wiring is committed yet. Either add settings.gradle.kts + per-module build.gradle.kts
following the dependency direction above, or type-check the whole scaffold as one unit:
kotlinc $(find . -name '*.kt') -d build/scaffold.jar
(Ask before committing Gradle files — you may want your own group/version conventions.)
Regression suite (opt-in, NOT in check): ./gradlew :lang-jdt:regressionTest :index-impl:regressionTest :jvm-build:regressionTest runs the @Tag("regression") suites against committed JSON baselines under
<module>/baselines/, failing on a regression: completion quality (recall/top-1/top-5/MRR), per-keystroke
ns/op + alloc/op, per-file retained heap + a long-session leak guard, completion at scale (large project
- real-jar classpath), and the IDE build engine compiling/running a large multi-module Java project
incrementally (the dogfooding milestone). Shared harness in
:bench-support(testImplementation only). Re-record a deliberate change with-Dbench.updateBaselines=trueand commit the diff. CI (.github/workflows/ci.yml) runs the unit tests + quality + build-at-scale gates on the pure-JVM framework (CI_CORE_ONLY=truedrops the Android/Compose shells so no SDK is needed). Seedocs/completion-regression-benchmarks.md(incl. the self-hosting goal + gap analysis).
platform-coreimpl — extension registry, message bus, model read/write lock, activities/progress.project-model-impl— model objects, modifiable-model transaction,module.tomlload/save, crash-safe writes. Exit test: build a 3-module project in code, save, reload → identical snapshot.- Graphs + classpath assembly — api/implementation export rules + content hashing.
Exit test: an
implementationdep of a dep is absent from the depender's compile classpath;apiis present. build-engine— Task/TaskGraph/TaskExecutor, input/output fingerprints, up-to-date checks, cache, bounded parallel. Exit test: re-running an unchanged graph does zero work; one input change re-runs only the affected subgraph.- Native build system, Java first (
compileJava → jar) on a JDT compiler backend. Exit test: build & run a multi-module Java CLI on device. - Language backend SPI + JDT analyzer — tolerant DOM, diagnostics, bindings, completion. Exit test: cross-module go-to-definition via the neutral DOM; basic Ctrl-Space completion.
- Native Android pipeline — aapt2/resources, R, D8/R8 dex, package, sign; AndroidFacet, variants.
Exit test: build + install + launch a debug APK on device. In progress (
:android-support): model layer +AndroidBuildSystemDAG done and proven on the desktop JVM (one-module app → real signed APK via native aapt2 + JDT + D8 + apksigner). Left: on-device install/launch, multi-module dexing,android-lib→AAR, IDE Run-button wiring. Seedocs/android-support.md. - Dependency resolver — Maven/POM transitive resolution, conflict policy, aar extraction, offline cache.
Done (
:deps-impl): transitive walk, three conflict policies, AAR explosion, disk cache, and BOM platform support (versionless deps resolved against imported BOMs;PlatformDependencyin the model + the Dependencies-screen Library/Platform flow). Remaining: version ranges/SNAPSHOTs, authenticated repos, LRU cache eviction. - Gradle compat sync — tolerant parser for
settings/build.gradle(.kts)+ version catalogs → model + sync report. - Workspace coordinator + composite builds — parallel/sequential, dependency substitution across linked projects (mixed build systems).
- Later, behind existing contracts: javac backend, custom parser, Kotlin support, incremental indexing, refactoring, NDK.
Refactoring — rename (Java) done:
JdtRename(lang-jdtrename/) resolves the symbol under the caret to a JDT binding key (a constructor maps to its class; locals/params/type-params are file-local) and walksSimpleNames in a parsed file matching that key (+ the constructor-name special-case for a class rename) → identifier ranges.IdeServices.prepareRename/renameorchestrate project-wide: validate the new identifier, pre-filter project.javafiles by substring then parse each with its module analyzer, build a multi-file edit, write disk + the editor overlay, and rename the backing.javafile when a top-level public type's name matched it. Exposed asIdeBackend.prepareRename/rename; the editor triggers it with F2 / Shift-F6 (a prompt inCodeEditor), andIdeUiState.reloadAfterRenamerefreshes open tabs. VerifiedJdtRenameTest(class across files + constructor, method, field, file-local var vs. shadowed field). Not yet: override-hierarchy rename, move-class-to-another-package, Kotlin rename.
Implement platform-core, then project-model-impl, behind the existing *-api interfaces, with
unit tests for steps 1–3's exit criteria. Don't change the *-api contracts without updating
docs/architecture-core-foundation.md.