diff --git a/.vscodeignore b/.vscodeignore
index 367279f..f5f0af2 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -5,6 +5,7 @@
**/*.ts
**/eslint.config.mjs
**/tsconfig.json
+diagrams/**
esbuild.mjs
media/**
node_modules/**
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..3e08ff2
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+# Contributing
+
+If you'd like to contribute, the best way right now is to open a bug report, suggestion, or question on [Dim GitHub Discussions](https://github.com/ufukty/dim/discussions). PRs and issues aren't being accepted from users at this stage.
+
+## Internals
+
+Dim's code may not be straightforward to follow, as it employs several optimization techniques that scatter and intertwine logic across files.
+
+### Overview
+
+The extension starts with the lifecycle controller. It is responsible for routing user events received from VS Code to the correct units. Units include the compiled user-config cache and individual editor decorators. Events include changes in the config, active and visible editors, and selections.
+
+
+
+
+
+
+An editor decorator instance is responsible for a single `TextEditor` instance. It holds the UI state, including the per-document toggle and the ranges decorated at the previous iteration.
+
+### Compiled user-config cache
+
+A Dim user-config may contain many RegExes, so compiled results are cached. The compiled config is sensitive to the scope of `TextEditor`. That's a shallow handle VS Code uses to represent a tab's session. Cache invalidation is triggered by the Extensions API event `onDidChangeConfiguration`.
+
+
+
+
+
+
+Cache keys may shift as VS Code returns different `TextEditor` instances for the same "tab" when the user switches between them.
diff --git a/diagrams/config-cache.mmd b/diagrams/config-cache.mmd
new file mode 100644
index 0000000..8aac670
--- /dev/null
+++ b/diagrams/config-cache.mmd
@@ -0,0 +1,12 @@
+ishikawa-beta
+ Recompile user-configuration
+ Raw user-configuration
+ File
+ Editor
+ Workspace
+ Scope
+ Top-level
+ Language-specific
+ Editor scope
+ Document URI
+ Language mode
\ No newline at end of file
diff --git a/diagrams/config-cache@2x.png b/diagrams/config-cache@2x.png
new file mode 100644
index 0000000..9d8fadd
Binary files /dev/null and b/diagrams/config-cache@2x.png differ
diff --git a/diagrams/structure.mmd b/diagrams/structure.mmd
new file mode 100644
index 0000000..c8d6a73
--- /dev/null
+++ b/diagrams/structure.mmd
@@ -0,0 +1,20 @@
+block
+
+columns 3
+
+space:2 ConfigReader
+space:3
+space:2 ConfigCompiler
+space:3
+EditorDecorator space ConfigCache
+space:3
+LifecycleCtrl:3
+space:3
+ExtensionHost:3
+
+ConfigCompiler--"Scope"-->ConfigReader
+ConfigCache--"Scope"-->ConfigCompiler
+LifecycleCtrl--"Invalidate"-->ConfigCache
+LifecycleCtrl--"Event, Command"-->EditorDecorator
+ExtensionHost--"Events, Commands"-->LifecycleCtrl
+EditorDecorator--"Scope"-->ConfigCache
\ No newline at end of file
diff --git a/diagrams/structure@2x.png b/diagrams/structure@2x.png
new file mode 100644
index 0000000..dc0613a
Binary files /dev/null and b/diagrams/structure@2x.png differ
diff --git a/diagrams/watch.sh b/diagrams/watch.sh
new file mode 100644
index 0000000..818ee38
--- /dev/null
+++ b/diagrams/watch.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+fswatch diagrams/*.mmd | while read -r FILE; do
+ mmdc -i "$FILE" -o "${FILE/.mmd/@2x.png}" -s 2 -t neutral;
+done
\ No newline at end of file