Skip to content

Commit c3d049f

Browse files
author
jgstern-agent
committed
feat(frameworks): Yesod detection + pattern set (WI-vabiv / UAT BUG-16)
UAT-2026-04-13 BUG-16 flagged Yesod (Haskell web framework, the Rails-inspired one used by haskellers) as missing from framework detection. The three other frameworks named in the tracker item (http4s, play, flask-appbuilder) already had YAML patterns in the repo — Yesod was the only genuine gap. Two pieces: 1. Detection: `HASKELL_FRAMEWORKS["yesod"] = ["yesod", "yesod-core", "yesod-auth", "yesod-persistent"]` in profile.py picks Yesod up from *.cabal build-depends, package.yaml hpack deps, and stack.yaml extra-deps — same plumbing Servant and Scotty already use. 2. Pattern set: new frameworks/yesod.yaml covering Yesod's conventions since Template Haskell quasi-quoters like `[parseRoutes| ... |]` aren't statically parseable by hypergumbo. Instead we match the stable conventions: - `mkYesod` / `mkYesodData` / `mkYesodSubData` / `mkYesodSubDispatch` / `parseRoutes` quasi-quoter calls (concept=application / router). - Warp runner calls: `warp`, `warpEnv`, `warpDebug`, `warpTLS`, `toWaiApp`, `toWaiAppPlain` (concept=application). - `Yesod` / `YesodSubsite` / `YesodSubDispatch` base_class matches (concept=application). - `RenderRoute` / `ParseRoute` router-class memberships. - `YesodPersist` / `YesodAuth` / `YesodBreadcrumbs` standard mixins (concept=database / auth). - Handler naming convention: function-kind symbols whose names match `^(get|post|put|delete|patch|head|options)[A-Z]...R$` get concept=route. These ARE the Yesod routes — the TH-generated dispatcher calls them by name. HTTP-method extraction for everything past GET/POST is a later materializer expansion; for now the concept attachment is enough to recognize the handlers downstream. - `Handler` / `HandlerFor` / `HandlerT` / `HandlerM` monad type membership (concept=route). - `Widget` components, `YForm` / `MForm` / `AForm` forms, `PersistEntity` models. 3 tests: - Detection from *.cabal - Detection from package.yaml - yesod.yaml loads via load_framework_patterns with the expected concept set ({application, router, route, auth, model, ...}) so typos / schema drift in the YAML fail fast. Signed-off-by: jgstern-agent <josh-agent@iterabloom.com>
1 parent 4f224cf commit c3d049f

5 files changed

Lines changed: 211 additions & 4 deletions

File tree

.ci/affected-tests.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# Test selection manifest
2-
# Generated by smart-test at 2026-04-15T08:49:45-04:00
2+
# Generated by smart-test at 2026-04-15T09:17:20-04:00
33
# Mode: targeted
44
# Baseline: 02dba9744d2c86e26f06565aad4ebcae7ef0f4a8
5-
# Changed files: 31
6-
# Changed source files: 6
7-
# Selected tests: 65
5+
# Changed files: 35
6+
# Changed source files: 7
7+
# Selected tests: 66
88
#
99
# === CHANGED_SOURCE_FILES ===
1010
packages/hypergumbo-core/src/hypergumbo_core/cli.py
1111
packages/hypergumbo-core/src/hypergumbo_core/_hf_noise.py
1212
packages/hypergumbo-core/src/hypergumbo_core/io_boundary.py
13+
packages/hypergumbo-core/src/hypergumbo_core/profile.py
1314
packages/hypergumbo-core/src/hypergumbo_core/sketch_embeddings.py
1415
packages/hypergumbo-lang-mainstream/src/hypergumbo_lang_mainstream/php.py
1516
packages/hypergumbo-tracker/src/hypergumbo_tracker/cli.py
@@ -37,6 +38,7 @@ packages/hypergumbo-core/tests/test_io_boundary.py
3738
packages/hypergumbo-core/tests/test_locale.py
3839
packages/hypergumbo-core/tests/test_max_tier.py
3940
packages/hypergumbo-core/tests/test_no_first_party_priority.py
41+
packages/hypergumbo-core/tests/test_partial_install_warnings.py
4042
packages/hypergumbo-core/tests/test_profile.py
4143
packages/hypergumbo-core/tests/test_pyffi_linker.py
4244
packages/hypergumbo-core/tests/test_rubyffi_linker.py

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This changelog tracks the **tool version** (package releases). The **schema vers
1212

1313
### Added
1414

15+
- **Yesod framework detection + pattern set** (WI-vabiv / UAT BUG-16): Yesod (Haskell — haskellers) is now detected from `*.cabal` / `package.yaml` dependencies (`yesod`, `yesod-core`, `yesod-auth`, `yesod-persistent`) and a new `frameworks/yesod.yaml` ships patterns for the Yesod conventions: `mkYesod` / `mkYesodData` / `mkYesodSubData` / `parseRoutes` quasi-quoter calls, Warp runner (`warp`/`warpTLS`/`toWaiApp`), `Yesod` / `YesodSubsite` typeclass memberships, `RenderRoute` / `ParseRoute` router class, standard mixin typeclasses (`YesodPersist`, `YesodAuth`, `YesodBreadcrumbs`), and the `<method><Resource>R` handler naming convention (getHomeR, postUserR, deleteUserR, ...). Route materialization for the non-GET/non-POST methods is a later materializer expansion; concepts are attached today so downstream analysis recognizes the handlers as web-handler entry points.
1516
- **Elixir I/O primitive catalog** (WI-vibur / UAT BUG-09b): new `io_primitives/elixir.yaml` fixes the "0 boundaries on plausible (Phoenix/Ecto)" gap. Covers Elixir stdlib (`File.read`/`write`/`stream!`, `IO.puts`/`write`/`read`, `System.cmd`/`get_env`, `Logger.*`), the idiomatic HTTP-client galaxy (`HTTPoison`, `Tesla`, `Req`, `Finch`, `Mint.HTTP*`, plus Erlang `:httpc`), Phoenix server surface (`Phoenix.Router.get/post/...`, `Phoenix.Controller.render/json`, `Plug.Conn.send_resp/read_body`, `Phoenix.Channel.broadcast`, `Phoenix.Endpoint.broadcast`), databases (Ecto.Repo read/write verbs, `Ecto.Multi`, `Postgrex.query`, `MyXQL.query`, `Redix.command`), and IPC (`GenServer.call/cast`, `Process.send`, `Oban.insert`, `Task.async`). `elixir` added to `_CATALOG_PARENTS` with `erlang` as parent so atom-access into Erlang (`:gen_tcp.send`, `:ets.lookup`, `:file.read_file`) is still matched. Elixir-specific `ambiguous_names` prevents Elixir pipe/Enum verbs and scope functions from producing short-name false positives.
1617
- **Kotlin I/O primitive catalog** (WI-rujos / UAT BUG-09d): new `io_primitives/kotlin.yaml` with Kotlin-specific entries. Kotlin was previously aliased to `java.yaml` verbatim via `_CATALOG_ALIASES`, which produced only 1 boundary (net_send) on detekt because Kotlin idiom uses extension functions and top-level stdlib functions that have no Java analog. The new catalog covers: `kotlin.io` File extensions (`readText`, `writeText`, `forEachLine`, `useLines`, `copyTo`, `walk`), `kotlin.io.path` Path extensions (Kotlin 1.5+), top-level `println`/`print` (receiver `kotlin.io.ConsoleKt`), ktor client + server, `android.util.Log`, `kotlin-logging` (`mu.KLogger` and the 5.x relocated `io.github.oshai.kotlinlogging.KLogger`), and Exposed ORM read/write. Kotlin-specific `ambiguous_names` prevent scope functions (`apply`, `run`, `let`, `use`) and coroutine/Flow verbs (`send`, `receive`, `collect`) from producing short-name false positives. `kotlin` moved from `_CATALOG_ALIASES` to `_CATALOG_PARENTS` so the Java parent still provides the raw `java.io/java.net/JDBC/SLF4J` entries for Kotlin code that uses those APIs directly.
1718

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# Yesod framework patterns (ADR-0003)
3+
#
4+
# Yesod is a Rails-inspired web framework for Haskell. Route registration
5+
# happens in a Template Haskell quasi-quoter (`parseRoutes`) that hypergumbo
6+
# can't fully evaluate, so detection falls back to the stable conventions
7+
# Yesod enforces on handler naming and typeclass membership:
8+
#
9+
# - Every route handler is named `<method><Resource>R` with the method
10+
# lowercase and the resource capitalized, and has kind=function.
11+
# Examples: getHomeR, postUserR, getUserR, deleteUserR, putBookR.
12+
# - Yesod applications implement the `Yesod` typeclass; subsites
13+
# implement `YesodSubsite`.
14+
# - Route types derive `RenderRoute` / `ParseRoute`.
15+
# - Convention: quasi-quoter declarations via `mkYesod` or `mkYesodData`.
16+
#
17+
# UAT-2026-04-13 BUG-16 flagged the gap on haskellers. WI-vabiv.
18+
19+
id: yesod
20+
language: haskell
21+
22+
patterns:
23+
# ==================== USAGE-BASED PATTERNS (v1.1.x) ====================
24+
# mkYesod "App" [parseRoutes| ... |] — declares the route table.
25+
# We can't parse the inside of the quasi-quoter without TH evaluation,
26+
# but detecting the mkYesod call itself identifies the app root.
27+
- concept: application
28+
usage:
29+
kind: "^call$"
30+
name: "^mkYesod$"
31+
32+
# mkYesodData for declaring route data type separately
33+
- concept: application
34+
usage:
35+
kind: "^call$"
36+
name: "^mkYesodData$"
37+
38+
# mkYesodSubData / mkYesodSubDispatch — subsite boundaries
39+
- concept: application
40+
usage:
41+
kind: "^call$"
42+
name: "^(mkYesodSubData|mkYesodSubDispatch)$"
43+
44+
# parseRoutes quasi-quoter call (some projects use it directly)
45+
- concept: router
46+
usage:
47+
kind: "^call$"
48+
name: "^parseRoutes$"
49+
50+
# Warp runner — what actually serves the app (common Yesod pattern:
51+
# `warpEnv . toWaiAppPlain`, `warp 3000 app`).
52+
- concept: application
53+
usage:
54+
kind: "^call$"
55+
name: "^(warp|warpEnv|warpDebug|warpTLS|toWaiApp|toWaiAppPlain)$"
56+
57+
# ==================== DEFINITION-BASED PATTERNS (v1.0.x) ====================
58+
59+
# Yesod typeclass instance — marks the app root type
60+
- concept: application
61+
base_class: "^Yesod$"
62+
63+
# YesodSubsite / YesodSubDispatch — subsite class memberships
64+
- concept: application
65+
base_class: "^(YesodSubsite|YesodSubDispatch)$"
66+
67+
# RenderRoute / ParseRoute — route-type typeclass membership
68+
- concept: router
69+
base_class: "^(RenderRoute|ParseRoute)$"
70+
71+
# YesodPersist / YesodAuth / YesodBreadcrumbs — standard mixin typeclasses
72+
# covering the common Yesod extension surface
73+
- concept: database
74+
base_class: "^YesodPersist$"
75+
76+
- concept: auth
77+
base_class: "^YesodAuth$"
78+
79+
# Handler naming convention — function-kind symbols whose names match
80+
# the <method><Resource>R pattern. These ARE the Yesod routes: the
81+
# framework's TH-generated dispatcher calls them by name, so the
82+
# handler function IS the route entry.
83+
#
84+
# Notes on HTTP method: Yesod encodes the method in the prefix
85+
# (getHomeR → GET /home). The current route materializer applies
86+
# the get-prefix convention for GET and do-prefix for POST; other
87+
# methods (post/put/delete/patch/head/options) attach the concept
88+
# but don't yet materialize a route symbol with an HTTP method.
89+
# That's a materializer expansion for a later item — these patterns
90+
# still enrich symbols with concept=route so downstream analysis
91+
# recognizes them as web-handler entry points.
92+
- concept: route
93+
symbol_name: "^getR$|^get[A-Z][A-Za-z0-9_']*R$"
94+
symbol_kind: "^function$"
95+
96+
- concept: route
97+
symbol_name: "^postR$|^post[A-Z][A-Za-z0-9_']*R$"
98+
symbol_kind: "^function$"
99+
100+
- concept: route
101+
symbol_name: "^putR$|^put[A-Z][A-Za-z0-9_']*R$"
102+
symbol_kind: "^function$"
103+
104+
- concept: route
105+
symbol_name: "^deleteR$|^delete[A-Z][A-Za-z0-9_']*R$"
106+
symbol_kind: "^function$"
107+
108+
- concept: route
109+
symbol_name: "^patchR$|^patch[A-Z][A-Za-z0-9_']*R$"
110+
symbol_kind: "^function$"
111+
112+
- concept: route
113+
symbol_name: "^headR$|^head[A-Z][A-Za-z0-9_']*R$"
114+
symbol_kind: "^function$"
115+
116+
- concept: route
117+
symbol_name: "^optionsR$|^options[A-Z][A-Za-z0-9_']*R$"
118+
symbol_kind: "^function$"
119+
120+
# Handler type signatures (some projects declare explicit Handler types)
121+
- concept: route
122+
base_class: "^Handler$"
123+
124+
# Widgets — Yesod-style reusable UI fragments
125+
- concept: component
126+
base_class: "^Widget$"
127+
128+
# Form declarations — YForm / MForm / AForm
129+
- concept: form
130+
base_class: "^(YForm|MForm|AForm)$"
131+
132+
# Persistent (database) typeclass — common pairing with Yesod
133+
- concept: model
134+
base_class: "^PersistEntity$"
135+
136+
# HandlerFor / HandlerM — Yesod monad type aliases. Not all projects
137+
# use these as base classes, but matching catches the ones that do.
138+
- concept: route
139+
base_class: "^(HandlerFor|HandlerT|HandlerM)$"
140+
141+
linkers:
142+
- http

packages/hypergumbo-core/src/hypergumbo_core/profile.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,8 @@
393393
HASKELL_FRAMEWORKS = {
394394
"servant": ["servant", "servant-server"],
395395
"scotty": ["scotty"],
396+
# Yesod — Rails-inspired Haskell web framework (haskellers et al.)
397+
"yesod": ["yesod", "yesod-core", "yesod-auth", "yesod-persistent"],
396398
}
397399

398400
# Clojure framework detection patterns (from deps.edn, project.clj)

packages/hypergumbo-core/tests/test_profile.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,66 @@ def test_detects_haskell_servant_from_package_yaml(tmp_path: Path) -> None:
16611661
assert "servant" in data["profile"]["frameworks"]
16621662

16631663

1664+
def test_detects_haskell_yesod_framework_from_cabal(tmp_path: Path) -> None:
1665+
"""WI-vabiv: detect Yesod from *.cabal dependency.
1666+
1667+
Yesod (UAT BUG-16 / haskellers) is the Rails-inspired Haskell web
1668+
framework; detection wires it up like Servant / Scotty so the new
1669+
yesod.yaml patterns are loaded when a repo declares the dependency.
1670+
"""
1671+
(tmp_path / "Main.hs").write_text("main = putStrLn \"Hello\"\n")
1672+
(tmp_path / "myapp.cabal").write_text("""name: myapp
1673+
version: 0.1.0.0
1674+
build-depends:
1675+
base >=4.7 && <5,
1676+
yesod,
1677+
yesod-core,
1678+
yesod-auth
1679+
""")
1680+
1681+
out_path = tmp_path / "out.json"
1682+
run_behavior_map(repo_root=tmp_path, out_path=out_path, include_sketch_precomputed=False)
1683+
1684+
data = json.loads(out_path.read_text())
1685+
assert "yesod" in data["profile"]["frameworks"]
1686+
1687+
1688+
def test_detects_haskell_yesod_from_package_yaml(tmp_path: Path) -> None:
1689+
"""WI-vabiv: detect Yesod from package.yaml (hpack)."""
1690+
(tmp_path / "Main.hs").write_text("main = putStrLn \"Hello\"\n")
1691+
(tmp_path / "package.yaml").write_text("""name: myapp
1692+
dependencies:
1693+
- base >= 4.7 && < 5
1694+
- yesod
1695+
- yesod-persistent
1696+
""")
1697+
1698+
out_path = tmp_path / "out.json"
1699+
run_behavior_map(repo_root=tmp_path, out_path=out_path, include_sketch_precomputed=False)
1700+
1701+
data = json.loads(out_path.read_text())
1702+
assert "yesod" in data["profile"]["frameworks"]
1703+
1704+
1705+
def test_yesod_framework_yaml_loads(tmp_path: Path) -> None:
1706+
"""WI-vabiv: yesod.yaml loads via load_framework_patterns and declares
1707+
the expected concepts (application, router, route, auth, model, etc.).
1708+
Guards against typos or schema drift in the new file.
1709+
"""
1710+
from hypergumbo_core.framework_patterns import load_framework_patterns
1711+
1712+
pattern_def = load_framework_patterns("yesod")
1713+
assert pattern_def is not None, "yesod.yaml must load"
1714+
assert pattern_def.id == "yesod"
1715+
assert pattern_def.language == "haskell"
1716+
1717+
concepts = {p.concept for p in pattern_def.patterns}
1718+
for required in ("application", "router", "route", "auth", "model"):
1719+
assert required in concepts, (
1720+
f"yesod pattern set missing concept: {required}"
1721+
)
1722+
1723+
16641724
# Clojure framework detection tests
16651725

16661726

0 commit comments

Comments
 (0)