diff --git a/CHANGELOG.md b/CHANGELOG.md
index baef1f58..525dace2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,34 @@ uses [Semantic Versioning](https://semver.org/).
to directories through the module path in `go.mod` (and local `replace`
directives), and standard-library or third-party calls are dropped.
`_test.go` files and `vendor/` are skipped.
+- **Zig analyzer.** Functions, containers (`struct`, `enum`, `union`,
+ `opaque`), their member functions (`Point.add`), and generic type functions
+ (`fn List(comptime T: type) type { return struct {...}; }`) become
+ components. Edges cover calls (plain, `alias.fn()`, and method calls on
+ `Self`, parameters, and locals whose type can be inferred), `T{...}`
+ literals, and types named in parameters, return types, fields, and
+ annotated locals. `@import("x.zig")` aliases and re-exports
+ (`const T = @import("x.zig").T;`) resolve to the file that declares the
+ name, as do module names that the root `build.zig` registers with
+ `addModule`; `std`, `builtin`, and third-party packages are dropped. `test`
+ blocks and `zig-cache/`, `.zig-cache/`, `zig-out/` are skipped, `build.zig`
+ is also a build artifact, and `build.zig.zon` is a manifest.
+- **Vue single-file components.** Each `.vue` file becomes a component
+ (`ArticleList.vue::ArticleList`), and its options-API members (`methods`,
+ `computed`, `watch`, lifecycle hooks such as `mounted` or `setup`) become
+ its methods. `
+
+
+"""
+
+SETUP_SFC = """\
+
+
+
+
+
+ {{ loud }}
+
+
+"""
+
+
+def _write(root: Path, rel: str, text: str) -> Path:
+ path = root / rel
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(text, encoding="utf-8")
+ return path
+
+
+@pytest.fixture
+def vue_repo(tmp_path: Path) -> Path:
+ _write(tmp_path, "src/components/ArticleList.vue", OPTIONS_SFC)
+ _write(
+ tmp_path,
+ "src/components/ArticlePreview.vue",
+ "{{ article.title }}
\n"
+ "\n",
+ )
+ _write(tmp_path, "src/components/VPagination.vue", " \n")
+ _write(
+ tmp_path,
+ "src/api/articles.js",
+ "export function fetchArticles() {\n return fetch('/api/articles');\n}\n",
+ )
+ _write(tmp_path, "src/views/Home.vue", SETUP_SFC)
+ _write(tmp_path, "src/components/TagList.vue", " \n")
+ return tmp_path
+
+
+def _analyze(repo: Path, rel: str):
+ path = repo / rel
+ nodes, rels = analyze_vue_file(str(path), path.read_text(), repo_path=str(repo))
+ return {n.id: n for n in nodes}, {(r.caller, r.callee): r for r in rels}
+
+
+# --------------------------------------------------------------------------- #
+# names
+# --------------------------------------------------------------------------- #
+
+
+@pytest.mark.parametrize(
+ "value, expected",
+ [
+ ("article-preview", "ArticlePreview"),
+ ("ArticlePreview", "ArticlePreview"),
+ ("article_list", "ArticleList"),
+ ("app", "App"),
+ ],
+)
+def test_pascal_case(value: str, expected: str) -> None:
+ assert pascal_case(value) == expected
+
+
+def test_component_name_from_path() -> None:
+ assert vue_component_name("src/components/article-list.vue") == "ArticleList"
+ assert vue_component_name("src/views/Settings/index.vue") == "Settings"
+ assert vue_component_name("App.vue") == "App"
+
+
+# --------------------------------------------------------------------------- #
+# options API
+# --------------------------------------------------------------------------- #
+
+
+def test_component_and_members(vue_repo: Path) -> None:
+ nodes, _ = _analyze(vue_repo, "src/components/ArticleList.vue")
+ base = "src/components/ArticleList.vue::"
+ component = nodes[base + "ArticleList"]
+ assert component.component_type == "class"
+ assert component.node_type == "component"
+ assert component.language == "vue"
+ assert component.start_line == 1 and component.end_line == OPTIONS_SFC.count("\n")
+
+ for member in ("load", "setPage", "pages", "type", "mounted"):
+ node = nodes[f"{base}ArticleList.{member}"]
+ assert node.component_type == "method"
+ assert node.class_name == "ArticleList"
+ # spreads and non-function options are not members
+ assert not any(i.endswith(("ArticleList.articles", "ArticleList.name")) for i in nodes)
+ # script-level functions still come from the JavaScript analyzer
+ assert base + "format" in nodes
+
+
+def test_line_numbers_follow_the_vue_file(vue_repo: Path) -> None:
+ nodes, _ = _analyze(vue_repo, "src/components/ArticleList.vue")
+ lines = OPTIONS_SFC.splitlines()
+ fmt = nodes["src/components/ArticleList.vue::format"]
+ assert lines[fmt.start_line - 1].startswith("function format(n)")
+ load = nodes["src/components/ArticleList.vue::ArticleList.load"]
+ assert lines[load.start_line - 1].strip() == "load() {"
+ assert load.source_code.strip().startswith("load() {")
+
+
+def test_template_and_import_edges(vue_repo: Path) -> None:
+ _, rels = _analyze(vue_repo, "src/components/ArticleList.vue")
+ base = "src/components/ArticleList.vue::"
+ component = base + "ArticleList"
+
+ # kebab-case tag -> import resolved through the extension-less path
+ edge = rels[(component, "src/components/ArticlePreview.vue::ArticlePreview")]
+ assert edge.is_resolved
+ # PascalCase tag -> `@/` import
+ assert (component, "src/components/VPagination.vue::VPagination") in rels
+ # globally registered component: bare name for CallGraphAnalyzer
+ assert (component, "GlobalWidget") in rels
+ # built-in tags and package imports are not edges
+ assert not any(callee in ("RouterLink", "mapGetters") for _, callee in rels)
+ # template handlers / bindings / interpolations -> members and script functions
+ assert (component, base + "ArticleList.setPage") in rels
+ assert (component, base + "ArticleList.load") in rels
+ assert (component, base + "format") in rels
+ # named import keeps the exported name
+ assert (component, "src/api/articles.js::fetchArticles") in rels
+
+
+def test_member_call_edges(vue_repo: Path) -> None:
+ _, rels = _analyze(vue_repo, "src/components/ArticleList.vue")
+ base = "src/components/ArticleList.vue::ArticleList."
+ assert (base + "setPage", base + "load") in rels # this.load()
+ assert (base + "type", base + "load") in rels
+ assert (base + "mounted", base + "load") in rels
+ assert (base + "load", "src/components/ArticleList.vue::format") in rels
+ assert (base + "load", "src/api/articles.js::fetchArticles") in rels # aliased import
+
+
+# --------------------------------------------------------------------------- #
+#