Releases: thatsme/giulia
v0.3.6 — Multi-defimpl Phase 2 — function fan-out across sibling impls
Closes the Phase-2 deferral from 4136844 (multi-type defimpl extraction, v0.2.x). defimpl Proto, for: [T1, T2, T3] produces N sibling impl modules sharing one body. Phase 1 emitted N module entries (each with impl_for set), but attributed the body's functions to the first sibling only — siblings 2..N were left with zero functions and therefore invisible to Pass 7's :protocol_impl edge synthesis. The deferral was explicit in the original commit message: "Phase 2 (function-set duplication across siblings for full graph parity) deferred."
The fix is universal — fires for any project that uses the multi-type defimpl form, regardless of protocol, sibling count, or body shape. The deferral became visible after v0.3.5's topology rollup made Pass 7 edges visible at module level. It surfaced concretely while scanning analytics-master (the FOSS Plausible Analytics codebase used as Giulia's complex-shape test bed) — 5 isolated impl modules from one source file traceable to two multi-defimpl declarations:
# Found in the analytics-master test bed at extra/lib/plausible/audit/encoder.ex —
# the same shape would surface in any codebase that uses multi-type defimpl:
defimpl Plausible.Audit.Encoder, for: [Integer, BitString, Float] do
def encode(x, _opts), do: x
end
defimpl Plausible.Audit.Encoder, for: [DateTime, Date, NaiveDateTime, Time] do
def encode(x, _opts), do: to_string(x)
endInteger and DateTime (first siblings) had encode/2 and Pass 7 fired. The other 5 had nothing and were silent.
Fix
Stack representation in Extraction.function_traverse_pre/2 now uses a tagged {:multi, [name1, name2, name3]} for multi-defimpl scopes. The function emitter pattern-matches on the stack head: when it's {:multi, names}, one function record is fanned out per sibling. Single-defimpl scopes unchanged. function_traverse_post/2 still pops one stack entry regardless of shape.
Multi-defimpl fixture (test/fixtures/extraction/protocols_defimpl_multi.expected.exs) updated to require all expected encode/1 records (Integer, BitString, Float, then Atom). The prior fixture only expected Integer + Atom and locked in the bug — that was the test contract that allowed Phase 1 to ship without Phase 2 noticing.
Empirical impact (analytics-master test bed)
Force-rescan after deploy:
| Metric | After v0.3.5 | After v0.3.6 |
|---|---|---|
| Edges | 3631 | 3636 |
| Project unreached | 24 | 19 |
The 5 expected siblings (BitString, Float, Date, NaiveDateTime, Time) all reconnected. The remaining 19 isolates are honest static-analysis blind spots that would surface in any codebase that uses the same patterns — mox-registered mocks, mix data-migration scripts, plugins registered in config/runtime.exs, LiveView .heex callees (slice-a class), exception structs, and template helpers. None are extraction bugs.
Universality
The fix is shape-agnostic — fires for any project that uses defimpl X, for: [...] regardless of N, protocol, or body shape. Per the no-hardcoded-data rule, no library allowlist; no codebase-side opt-in. The analytics-master file just happened to be the first one that exercised the deferred path on a real codebase; the same shape is valid Elixir and could appear in any project.
Tests
59/59 pass across the focused subset (extraction + store + warm_restore). The fixture comparison verifies exact function-record parity per sibling.
v0.3.5 — Topology rolls up Pass 7-11 edges; Graph Explorer isolates panel
Visualization-honesty fix. GET /api/knowledge/topology (the data source for /api/monitor/graph) used Knowledge.Store.all_dependencies/1, which filters edges to those whose both endpoints carry the :module vertex label. That filter silently dropped every edge synthesized by Builder Passes 7-11:
| Pass | Synthesizes | Endpoint shape |
|---|---|---|
7 — protocol_impl |
protocol module → impl's encode/2 etc. |
module → function |
8 — behaviour_impl |
behaviour module → impl's callback function | module → function |
9 — router_dispatch |
router module → controller action | module → function |
10 — mfa_ref / capture_ref / apply_ref |
caller fn → target fn | function → function |
11 — use_import_ref |
caller fn → imported fn | function → function |
Net effect across every analyzed codebase: defimpls of project-defined protocols, Phoenix controller actions, MFA-tuple-dispatched modules, and macro-injected import targets all rendered as isolated nodes in the Graph Explorer — even though dead_code analysis (which uses the full graph) correctly counted them as live. The visual told a different story from the analysis.
The fix is universal — applies to any Elixir project regardless of size or framework. Measured impact below uses three test beds: Giulia itself (170 files, the tool eating its own dogfood), AlexClaw (small Phoenix app), and analytics-master (FOSS Plausible Analytics — a large multi-app Phoenix codebase used as Giulia's complex-shape stress test).
Fix
New Reader.all_dependencies_with_rollup/1 walks every edge in the graph and projects function-vertex endpoints up to their owning module via Graph.vertex_labels/2. Self-loops introduced by intra-module rollup (e.g. a defimpl whose body recurses through its own protocol) are dropped. Function endpoints whose parent module isn't in the project's module set are skipped (no phantom-module invention). /api/knowledge/topology switched to the rollup variant; all_dependencies/1 kept as-is for callers that need a strict module-level filter.
Graph Explorer — isolates panel
priv/static/graph.html now pulls isolated nodes off the Cytoscape canvas into a right-side sidebar split into two sections:
- External boundaries (purple bar,
complexity == 0) —GenServer,Phoenix.Endpoint,Plug,Mix.Project, etc. The indexer records them as vertices because something in the analyzed project doesuse X/@behaviour X/defimpl X for: T, but never traverses their bodies (out of project scope). Meta column shows← NwhereNis the metric-layer's count of project modules referencing them. - Project unreached (orange bar,
complexity > 0) — modules in the analyzed project with no statically-resolved callers/callees. Same class as the slice-a.heexcallee residuals, slice-d runtimeapply/3cases, and slice-z true positives. Meta column showscx N(complexity).
Hide button on the panel header; "Isolates ›" button reappears top-right when the panel is hidden.
Empirical impact (measured against three test bed codebases)
| Codebase | Edges before | Edges after | Isolates before | Isolates after |
|---|---|---|---|---|
| giulia | 886 | 918 | 5 | 0 |
| AlexClaw | 722 | 771 | 12 | 4 |
| analytics-master | 3189 | 3631 | 61 | 31 |
giulia is fully connected — the 5 prior isolates (GenServer, Application, Plug, Supervisor, Mix.Project) all gained at least one rolled-up function-level edge. The big drop on the analytics-master test bed (61 → 31) surfaced a Phase-2 deferral in 4136844 (multi-defimpl extraction) — when scanning that codebase's audit-encoder file the second-and-later siblings of defimpl X, for: [T1, T2, T3] had no functions and therefore no Pass 7 edges to roll up. Closed in v0.3.6.
Tests
4 new tests in test/giulia/knowledge/store_test.exs lock the rollup contract: function-endpoint roll-up, self-loop drop, orphan-function skip, and parity with all_dependencies/1 for direct module↔module edges. 25/25 pass.
v0.3.4 — Warm-restored projects bypass the not_indexed 409 gate
Reliability fix. Persistence.WarmRestore (e8c3fa3, v0.2.x) repopulated the L1 ETS graph from the L2 CubDB cache on every boot so projects survived docker compose restart without re-scanning. But it never touched Context.Indexer's per-project state map. The scan-state gate (075816a, v0.2.x) added strict 409 not_indexed responses on every scan-dependent endpoint when Indexer.status(path) returned %{status: :idle, file_count: 0} — which is exactly what every warm-restored project looked like.
Net effect: warm-restore was a no-op for everything past /api/projects. Selecting a warm-restored project in the Graph Explorer rendered nothing; GET /api/knowledge/topology returned {"error":"project has never been scanned","state":"not_indexed",...} with HTTP 409. Surfaced when opening http://localhost:4000/api/monitor/graph after a daemon restart and noticing only the project scanned in the current daemon lifetime had any data.
The bug is universal — it affected every warm-restored project regardless of language, framework, or size. Verified against three Elixir codebases used as test beds (see "Verified" below).
Fix
New Indexer.register_warm_restored(project_path, file_count) public API + matching handle_cast clause. WarmRestore.run_for/1 calls it after each successful Loader.restore_graph/1, deriving file_count from the restored graph's vertex count via Knowledge.Store.stats(path) (any positive value satisfies the gate). last_scan stays nil — the original scan timestamp isn't recoverable from L2 metadata, and honest is better than fabricated.
Verified
Test beds: AlexClaw (small Phoenix app) and analytics-master (FOSS Plausible Analytics, a large multi-app Elixir/Phoenix codebase used as Giulia's complex-shape test target). Both were warm-restored from L2 cache after a docker compose restart.
- Pre-fix:
GET /api/knowledge/topology?path=/projects/AlexClawreturned 409 / 143 bytes. Same shape on analytics-master. - Post-fix on the same daemon (after warm-restore, no re-scan): 200 / 97974 bytes (AlexClaw); 200 / 450128 bytes (analytics-master).
- 5/5 tests pass in
test/giulia/persistence/warm_restore_test.exs, including a new test that assertsIndexer.status(project)reports:idlewithfile_count > 0afterrun_for/1.
Note
file_count semantics shift slightly for warm-restored projects: previously it was the AST file count from Context.Store.stats(path).ast_files (set at scan-complete). For warm-restored projects it's now the graph's vertex count, which is bigger (modules + functions + structs + behaviours). Honest about its source — last_scan: null distinguishes warm-restored from fresh-scanned in the status payload — but a different number than a fresh scan would show.
v0.3.3 — Indexer post-scan pipeline non-blocking
Reliability fix. The scan task itself was already properly supervised, but Indexer.handle_cast({:scan_complete, ...}) ran the post-scan pipeline (mix compile + Knowledge.Store.rebuild + SemanticIndex.embed_project) inside the GenServer process. While that work ran (10-30+ seconds on a Phoenix app or on Giulia's self-scan), the Indexer mailbox was blocked. Any Indexer.status/1 GenServer.call queued behind it and hit the default 5s timeout — caller crashed, router emitted 500, docker healthcheck eventually restarted the container.
Surfaced specifically when scanning Giulia itself (170 files of meta-AST work — heavier than typical project code; previous Plausible / AlexClaw scans completed within 5s and never tripped the timeout).
Fix
handle_cast({:scan_complete, project_path}, ...) now:
- Computes
stats(fast, in-process) - Sets the project state to
:building(new status atom) - Spawns a
Task.Supervisor.start_child(Giulia.TaskSupervisor, ...)runningrun_post_scan_pipeline/1 - Returns
{:noreply, new_state}immediately
The task casts back {:scan_complete_done, project_path} when the pipeline finishes; that callback flips the status to :idle. Errors inside the pipeline are caught with rescue and logged but never crash the Indexer.
Daemon.Helpers.scan_state/1 recognizes :building as a :pending state — knowledge endpoints return 409 Conflict during the rebuild window with the same UX as :scanning.
Verified
- Concurrent stress: 5 parallel
Indexer.status/1polls during an active force-rescan of Giulia all return instantly with{"status":"building","file_count":170}. Previously these queued in the mailbox and timed out at 5s. - Container survives the stress test (no docker restart).
- 1063 unit tests + 13 properties pass; 0 regressions.
Type spec change
New atom :building added to Giulia.Context.Indexer.scan_status type union. Consumers reading the status field via the typed API should handle this state. The scan_state/1 helper already does.
v0.3.2 — Alias resolution + template scope fix
Patch release. Two fixes that together produce the cleanest dead-code state across canonical codebases since the project began.
Fixed — TestReferences alias resolution (roadmap item 2h)
Giulia.Tools.TestReferences.referenced_functions/1 now resolves alias directives before recording MFA strings. Previously, a test using alias AlexClawTest.Skills.EchoSkill followed by EchoSkill.config_help() recorded "EchoSkill.config_help/0" — a short form the dead-code classifier's :test_only predicate could never match against the real "AlexClawTest.Skills.EchoSkill.config_help/0".
Two-pass walker: first builds the file's short→full alias map (single-segment, multi-alias alias Mod.{A, B}, and alias Mod, as: Other with Sourceror keyword-key unwrapping), then walks for refs and expands head aliases via Map.fetch. Mirrors the alias-resolution pattern from Giulia.Knowledge.Metrics.collect_all_calls/1.
Fixed — TemplateReferences scoped to project source roots
Giulia.Tools.TemplateReferences.scan/1 now scopes its *.heex / *.eex walk to the project's source roots (returned by Giulia.Context.ScanConfig.absolute_roots/1 — typically lib/ plus test/support/ plus whatever mix.exs elixirc_paths/1 adds). Previously walked everything under project_path/**, including deps/ (third-party templates from phoenix_live_dashboard, phoenix codegen, etc.) and _build/, whose qualified function references coincidentally over-exempted local modules.
The over-broad scan was caught by auditing v0.3.1's verification run: AlexClaw went from 6 dead to 0 dead with the wide scan — a too-good-to-be-true result that turned out to be coincidental name overlap with phoenix_live_dashboard templates.
Empirical impact on canonical codebases
| Codebase | v0.3.1 dead | v0.3.2 dead | Notes |
|---|---|---|---|
| AlexClaw | 6 | 0 | All 6 EchoSkill.* test-only entries now correctly resolved through aliases |
| Plausible | 10 | 3 | 7 alias-blind entries resolved; remaining 3 = canonical residual |
The 3 remaining on Plausible are the documented irreducible residual:
Plausible.TestUtils.tmp_dir/0— true positive (no callers anywhere)PlausibleWeb.channel/0— true positive (nouse PlausibleWeb, :channel)PlausibleWeb.Endpoint.app_env_config/0— accept-as-residual (SiteEncrypt variable dispatch, not statically resolvable)
This is the cleanest residual state we've measured.
Migration
L2 caches auto-invalidate via CodeDigest. First daemon restart on v0.3.2 rebuilds graph + metrics from existing ASTs.
v0.3.1 — Slice-a: HEEx/EEx template scanner
Patch release that completes the deferred slice-a from v0.3.0. The :template_pending category was a placeholder for "we know this might be template-callable but we never wrote the parser." Slice-a builds the parser (Giulia.Tools.TemplateReferences), so template-referenced functions are now exempted from the dead-code list at detection time rather than reaching the classifier.
Changed — observable analysis output
/api/knowledge/dead_codeno longer emits:template_pending. The category is removed from the type union. Functions called from*.heex/*.eextemplates are exempted from the response entirely (matching Pass 7-11's exemption pattern). Consumers readingsummary.by_categorywill see the field gone; if you were depending on its presence, switch toMap.get(summary.by_category, :template_pending, 0).- Empirical delta on Plausible: the 3
PlausibleWeb.{EmailView,LayoutView,SiteView}.plausible_url/0entries that v0.3.0 mis-classified as:template_pendingare now correctly exempted — they're called as{plausible_url()}fromtemplates/layout/base_email.html.heexetc., resolved through the conventionaltemplates/<view>/...→<App>Web.<View>Viewmapping.
Added — Giulia.Tools.TemplateReferences
New scanner that walks *.heex / *.eex files and extracts function references in three syntactic surfaces:
- HEEx curly interpolation:
{Module.Sub.fn(args)} - Old EEx interpolation:
<%= Module.Sub.fn(args) %>and<% expr %> - HEEx component invocation:
<Module.Sub.fn args />(qualified) and<.local_fn args />(local)
Returns %{qualified: MapSet, local_per_file: %{path => MapSet}}. The local-per-file map is resolved to a target module via Phoenix path conventions:
- Strip
.heex/.eex/.html; if the resulting.exsibling exists in the project's module index, use that module (LiveView / Component / colocated templates). lib/<app>_web/templates/<view>/<file>.html.heex→<App>Web.<View>View(older Phoenix layout).
Metrics.dead_code_with_asts/3 consumes both signals as additional exemption sets alongside protocol_impl_modules, router_actions, reference_targets, etc.
Known limitation surfaced (not introduced) — TestReferences alias-blindness
The cold-rescan triggered by this slice exposed a pre-existing limitation in Giulia.Tools.TestReferences.referenced_functions/1: it collects qualified Mod.fn/N strings from *_test.exs but does not resolve alias directives. A test using alias AlexClawTest.Skills.EchoSkill then EchoSkill.config_help() records "EchoSkill.config_help/0", not the fully-qualified form, so the classifier's :test_only predicate misses the match. Filed as roadmap item 2h. Fix mirrors the resolve_alias pattern from metrics.ex collect_all_calls/1.
Empirical impact: AlexClaw cold-rescan now lists 6 EchoSkill functions as :genuine dead when they're really :test_only. Plausible has similar regressions in Plausible.PromEx.Plugins.PlausibleMetrics.execute_*_metrics/0 and Plausible.InstallationSupport.Checks.*.report_progress_as/0. Out of scope for this slice; will be addressed in a follow-up patch.
Migration
- L2 caches auto-invalidate via
CodeDigest(TemplateReferences added to@tier_modules). First daemon restart on v0.3.1 rebuilds graph + metrics from existing ASTs. - Backward-compatible response shape —
dead_codekeeps:dead,:count,:total,:summaryfields; only:summary.by_category.template_pendinggoes away.
v0.3.0 — Categorized signals + external tool enrichment
The minor bump (v0.2.x → v0.3.0) is justified by new public API surface: two new endpoints, additive fields on three existing endpoints, a new plugin behaviour, and a new persistence keyspace. Everything is backward-compatible — old consumers reading existing fields keep working.
Headline — dead-code categorization
/api/knowledge/dead_code entries now carry a :category field and the response gains a :summary map. Categorization turns the irreducible residual into honest signal: most entries on real codebases aren't bugs, they're library public API, test-only entry points, or template-only references blocked on the deferred .heex slice.
%{
dead: [
%{module, name, arity, type, file, line,
category: :genuine | :test_only | :library_public_api |
:template_pending | :uncategorized}
],
count, total,
summary: %{
by_category: %{...counts...},
irreducible: integer, # test_only + library_public_api + template_pending
actionable: integer # genuine + uncategorized
}
}Precedence: :test_only > :library_public_api > :template_pending > :genuine. Empirical results on canonical codebases:
| Codebase | Dead | After categorization |
|---|---|---|
| AlexClaw | 1 | 1 genuine |
| Plug | 1 | 1 library_public_api |
| Bandit (post-Pass-10-ext) | 2 | 2 library_public_api |
Headline — external tool enrichment ingestion
Pluggable behaviour Giulia.Enrichment.Source plus a JSON-driven registry (priv/config/enrichment_sources.json). Two sources ship: Credo and Dialyzer. Adding Sobelow / ExDoc / Coverage is one parser module + one JSON line.
New endpoints:
POST /api/index/enrichment— ingest tool output. Body:{tool, project, payload_path}. Validatespayload_pathagainst an allowlist (scan_defaults.json :enrichment_payload_roots) to prevent the endpoint from becoming an arbitrary-file-read primitive. Returns{tool, ingested, targets, replaced}.GET /api/intelligence/enrichments?path=X&mfa=Y(or&module=Y) — uncapped drill-down per-vertex queries. Returns{findings: %{tool => [findings]}, target}. Distinguishes%{}(never ingested for project) from%{credo: []}(ingested, no findings on this target) via a sentinel marker — different signals for consumer agents.
Two consumer endpoints surface findings inline:
pre_impact_check—affected_callers[*].enrichmentscarries per-caller findings so refactor decisions consider type warnings + style issues alongside blast radius.dead_code— entries gain:enrichmentsso type-warning + dead-code residual surface together.
Both apply caps (priv/config/scoring.json :enrichments): errors uncapped, top-3 warnings per entry, drop info, per-response cap of 30 deduplicated by {check, severity}. Capping logic is shared via Giulia.Enrichment.Consumer.
Replace-on-ingest, decoupled lifecycle. Tool ingest cadence (CI on every PR) is decoupled from source rescan cadence — Persistence.Writer.clear_project/1 filters enrichment keys so re-extraction doesn't force re-ingestion.
Provenance per finding: tool_version, run_at, per-file source_digest_at_run. Consumers can flag stale findings on changed files.
Severity is config-driven. Each source carries a severity_map. Credo: 5 entries ("warning" => "error" covers Credo's misleadingly-named real-bug category). Dialyzer: 47 entries covering dialyxir's full warning catalogue. Tunable without recompile.
Telemetry from day one: [:giulia, :enrichment, :ingest | :parse_error | :read].
Known limitation — function line-range precision
Per-function line ranges are derived as next_function.line - 1 after stable per-file sort. This loses precision against multi-clause definitions, defmacro bodies with quote do, multi-arity definitions interleaved with other functions, and @doc heredoc gaps. Empirical impact: most Credo findings on Plausible currently resolve to module-scope rather than function-scope. The three-path arity-resolution waterfall in each parser surfaces this honestly via scope: :module and a :resolution_ambiguous flag rather than guessing wrong arity. Roadmap item 2g — capture true :line_end during AST extraction — predicted to sharpen attribution.
Also since v0.2.2 — what shipped incrementally to users
- Dispatch-edge synthesis (Pass 7-11) — protocol_impl, behaviour_impl, router_dispatch, mfa_ref / capture_ref / apply_ref / mfa_arg_ref, use_import_ref. Net effect across canonical codebases: −97 false-positive
dead_codereports. - Reference-based test detection (slice E2) — replaces filename-matching
has_test; 38 modules correctly reclassified yellow→green on Plausible. - Config externalization —
scoring.json,dispatch_patterns.json,scan_defaults.json,enrichment_sources.json, all auto-invalidated viaCodeDigestenvelope. - MCP server (Build 155) — 71 tools auto-generated from
@skillannotations on sub-routers. Bearer-auth gated byGIULIA_MCP_KEY. - OTP cleanup tier 1 + 2 — supervised SSE inference, ContextManager
:exithandling, state-recovery invariant, ETS heir, reconcile paths. - Correctness invariants —
verify_l2,verify_l3endpoints with stratified sample-identity checks; 15 mix-test drift detectors. - Filter-accountability tests — 11 distinct silent-over-match bugs caught.
- Force-rescan + path validation —
?force=trueon/api/index/scan; 422 on missing/invalid paths or missing project marker. - Multi-type defimpl —
defimpl X, for: [T1, T2, T3]now extracts as N proper impl modules instead ofUnknown.
Migration
- L2 caches auto-invalidate via
CodeDigest. First daemon restart on v0.3.0 rebuilds graph + metrics from existing ASTs (cheap; AST cache unaffected). - All field additions are backward-compatible. Consumers reading existing keys (
dead,count,total,affected_callers, etc.) continue to work without changes.
🤖 Generated with Claude Code
v0.2.2 — MCP Server
MCP Server (Build 155)
Native Model Context Protocol server — AI assistants like Claude Code can now discover and call Giulia's analysis tools directly as structured tool calls, without constructing HTTP requests.
New: MCP Layer
- 74 MCP tools auto-generated from
@skillannotations at boot — REST and MCP always expose identical capabilities - 5 resource templates via
giulia://URI scheme (projects, modules, graph, skills, status) - Bearer token auth via
GIULIA_MCP_KEYenv var (constant-time comparison, MCP disabled when unset) - Anubis StreamableHTTP transport at
/mcpwith Peri schema validation for all tool parameters
New Modules
Giulia.MCP.Server— Anubis MCP server handlingtools/call,tools/list,resources/readGiulia.MCP.ToolSchema— generates tool definitions from sub-router@skillannotationsGiulia.MCP.ResourceProvider— resolvesgiulia://resource URIs to context dataGiulia.Daemon.Plugs.McpAuth— bearer token authentication plugGiulia.Daemon.Plugs.McpForward— deferred forwarder to Anubis transport (avoids persistent_term race)
Documentation
- API.md: MCP section with setup, tool naming conventions, parameter mapping, LLM calling guide
- ARCHITECTURE.md: MCP layer section, Tier 5 in supervision tree,
/mcpin request flow - INSTALLATION.md: Full MCP setup guide (enable, configure, connect, verify)
- SECURITY.md: MCP authentication model,
GIULIA_MCP_KEYin secrets table, deployment hardening - SKILL.md: Access methods comparison (REST vs MCP)
- README.md, CONTRIBUTING.md, ROADMAP.md: Updated for MCP
Dependencies
- Added
anubis_mcp ~> 1.0(Elixir MCP framework)
Client Setup
{
"mcpServers": {
"giulia": {
"type": "http",
"url": "http://localhost:4000/mcp",
"headers": {
"Authorization": "Bearer <GIULIA_MCP_KEY>"
}
}
}
}v0.2.1 — Documentation Alignment
Documentation Alignment (Build 154)
Full audit and update of all project documentation against the actual codebase.
Changes
- ARCHITECTURE.md: Added build reference header, fixed endpoint count (72→88), documented 17 missing modules (Knowledge: 10, Intelligence: 4, Runtime: 3), added TaskSupervisor to supervision tree, expanded Knowledge/Intelligence/Runtime layer sections
- API.md: Added build reference header, updated TOC counts, added 3 missing endpoint docs (
/api/knowledge/topology,/api/monitor/graph,/api/discovery/report_rules) - README.md: Added build reference, updated project status (Build 154, 83 endpoints)
- CLAUDE.md: Rewrote project structure tree from ~88 files to full 119+ file coverage
- mix.exs: Version bump 0.2.0 → 0.2.1
No code changes — documentation only.
v0.2.0 — Conventions Analyzer + Full Codebase Cleanup
What's New
Conventions Analyzer (Build 153)
New GET /api/knowledge/conventions endpoint — 12 AST-based rules that detect coding convention violations across any indexed Elixir project.
Tier 1 (Metadata — instant, ETS-only):
missing_moduledoc— every module needs@moduledocmissing_spec— every public function needs@specmissing_enforce_keys— structs should declare@enforce_keys
Tier 2 (AST Pattern — Sourceror walk):
try_rescue_flow_control— don't useRepo.get!/String.to_integerinside try/rescuesilent_rescue— never swallow errors withrescue _ -> nilruntime_atom_creation— never useString.to_atom/1on runtime stringsprocess_dictionary— avoidProcess.get/putfor application stateunsupervised_task— useTask.Supervisorinstead ofTask.startunless_else— never useunless...elsesingle_value_pipe— don't pipe when there's no chainappend_in_reduce—++insideEnum.reduceis O(n²)if_not— preferunlessoverif not
Full Codebase Cleanup
Giulia now passes her own conventions checker with 0 violations (was 285):
- 85
@specannotations added across providers, tools, storage, inference - 165 single-value pipes converted to direct function calls
- 16
Task.startreplaced withTask.Supervisor.start_child(newGiulia.TaskSupervisor) - 9
if notpatterns refactored - 6
@enforce_keysadded to structs - 3
Process.get/putreplaced withAgentin renderer - 1
String.to_atomsecured withto_existing_atom+ erlang fallback
Bug Fixes
- Fixed stale convention cache returning ghost 0-violation results after re-indexing
- Fixed single-value pipe false positives on chain members (two-phase meta-tracking)
- Fixed
@enforce_keysdetection: reads source file instead of unpopulated ETS attributes
Stats
- 97 files changed, 1,011 insertions, 353 deletions
- 1,732 tests, 0 regressions
- 0 compile warnings