diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,145 @@
 
 ---
 
+## [0.17.1] - unreleased
+
+Development version. Opens the 0.17.x line — the first release built entirely against
+published 0.17.0 hosts (Hackage / PyPI / Maven Central), with the `hostOverrides` shims
+that were needed while 0.16.1 lacked the effect type and I/O primitives removed.
+
+### New features
+
+- **Generated term-reference DSLs** ([#467](https://github.com/CategoricalData/hydra/issues/467)):
+  kernel *term* modules now project generated DSL surfaces, completing the story begun with
+  type-module DSLs and the generated primitive wrappers (`hydra.dsl.lib.*`, 0.17.0). A
+  signature read-back in the DSL pass reuses the main pass's inference, and a demand-curated
+  set of 19 kernel term modules (plus the json/test/extract modules) emits one typed,
+  rename-safe reference per definition into every target language — e.g. Haskell
+  `Hydra.Dsl.Strip.deannotateType`, Java `hydra.dsl.Strip.deannotateType(...)`, Python
+  `hydra.dsl.strip.deannotate_type(...)`. The Java and Python coder sources were pivoted onto
+  the generated references, retiring ~700 stringly-typed inline `var("hydra....")` references
+  and shrinking Python's hand registry to the non-generatable remainder (byte-identical
+  generated output before and after). Follow-ups:
+  [#555](https://github.com/CategoricalData/hydra/issues/555) (typed references to derived
+  encode/decode/show functions via `TypedName`),
+  [#556](https://github.com/CategoricalData/hydra/issues/556) (non-kernel packages).
+- **Package validation** ([#574](https://github.com/CategoricalData/hydra/issues/574),
+  [#575](https://github.com/CategoricalData/hydra/issues/575)): a translingual `hydra.validate.*`
+  framework carries per-package `ValidationProfile`s (error/warning rules, error/warning caps) and a
+  catalog of checks — undeclared-package-dependency detection (now a fatal gate, #574),
+  definition-name convention (PascalCase), definition ordering, documentation completeness, and
+  conflicting-variant-name detection — layered on the existing core term/type well-formedness rules.
+  A kernel-strict / others-relaxed policy keeps `/sync` green fleet-wide while the documentation
+  backlog drains. See the new [Validation](https://github.com/CategoricalData/hydra/wiki/Validation)
+  wiki page for the full per-package constraint specification.
+- **DSL-authoring registration guards** ([#554](https://github.com/CategoricalData/hydra/issues/554)):
+  fail-fast authoring-time validation for the host-native DSL sources — every DSL-authoring module in
+  Java/`hydra-jvm`, Python, and Scala now asserts its definitions are fully registered
+  (`Defs.checkComplete` / `check_complete`), backed by a Haskell kernel-source registration-completeness
+  scanner, so a definition added to a source file but omitted from its `definitions` list fails loudly
+  instead of silently vanishing from the generated output.
+- **`hydra-build` package** ([#546](https://github.com/CategoricalData/hydra/issues/546),
+  [#529](https://github.com/CategoricalData/hydra/issues/529),
+  [#530](https://github.com/CategoricalData/hydra/issues/530)): the build-orchestration logic that had
+  lived only in host scripts is now translingual Hydra. Pure module-list utilities became
+  `hydra.build.modules` (#529) and the orphan-reconcile decision became `hydra.build.reconcile` (#530),
+  each with a translingual test suite; the `hydra.build.*` modules were then extracted into a dedicated
+  `hydra-build` package (#546), separate from the kernel.
+- **Metadata sidecar + content/version-addressed freshness**
+  ([#415](https://github.com/CategoricalData/hydra/issues/415)): dist manifests now carry a
+  `moduleFormatVersion` field, and the generator's freshness basis is content/version-addressed — the
+  groundwork for reproducible, cache-correct incremental generation.
+- **Structured generation-provenance record**
+  ([#523](https://github.com/CategoricalData/hydra/issues/523)): each generation run now records a
+  structured `Generation` provenance entry (digest v2), replacing the ad-hoc shell threading of
+  generation metadata.
+- **Worker + issue hierarchy** ([#557](https://github.com/CategoricalData/hydra/issues/557)): a
+  principled agent/worker framework — roles and parentage mirroring the GitHub issue tree, a
+  cross-worktree message transport with a verify-after-copy protocol, a blocking-aware design-question
+  (`decisions/`) channel, an issue-proposal queue with the `/issues` command, an autonomy-dial banner,
+  and cross-machine staging coordination. Contributor-facing process only; no change to generated code.
+
+### API and usability
+
+- **DSL usability passes** ([#430](https://github.com/CategoricalData/hydra/issues/430),
+  [#553](https://github.com/CategoricalData/hydra/issues/553)): reviewed the host-native Java, Python,
+  and Scala DSL surfaces for ergonomics and cross-host parity (documented def shapes, added a Scala
+  projection helper), bringing the three authoring dialects into closer alignment.
+
+### Fixed
+
+Bug fixes landed this cycle, grouped by area (see each issue for detail):
+
+- **TypeScript** — reactivated `hydra.lib.*` primitive tests surfaced encoder gaps: lazy show-calling
+  let bindings and compact bare-string `LiteralType` expansion in the test converter
+  ([#564](https://github.com/CategoricalData/hydra/issues/564)),
+  `binaryToBytes` registration + base64 binary decode in `decodeUtf8`
+  ([#561](https://github.com/CategoricalData/hydra/issues/561)),
+  `hydra.test.build.*` emission after the #546 split
+  ([#570](https://github.com/CategoricalData/hydra/issues/570)), and `HYDRA_DEFAULT_IMPLS` support in
+  the test runner ([#536](https://github.com/CategoricalData/hydra/issues/536)).
+- **Tests and CI** — restored the default-primitive-implementation testing solution
+  ([#388](https://github.com/CategoricalData/hydra/issues/388),
+  [#549](https://github.com/CategoricalData/hydra/issues/549)), reactivated previously-ignored kernel
+  test cases ([#513](https://github.com/CategoricalData/hydra/issues/513),
+  [#550](https://github.com/CategoricalData/hydra/issues/550)), wired the regression harnesses into CI
+  and split them out of the monolithic Haskell job for timeout headroom
+  ([#535](https://github.com/CategoricalData/hydra/issues/535),
+  [#563](https://github.com/CategoricalData/hydra/issues/563)), added Scala/Go/Lisp
+  self-containment gates to `verify-distribution.sh`
+  ([#537](https://github.com/CategoricalData/hydra/issues/537)), a manifest-JSON-tracked CI check
+  ([#541](https://github.com/CategoricalData/hydra/issues/541)), a per-package test-digest freshness
+  fix ([#551](https://github.com/CategoricalData/hydra/issues/551)), overlay-file restoration in
+  `test-stale-output-prune.sh` ([#558](https://github.com/CategoricalData/hydra/issues/558)), and a
+  sync-freshness fingerprint that now sees host-native DSL sources
+  ([#562](https://github.com/CategoricalData/hydra/issues/562)).
+- **Kernel and coders** — Java binary-literal signed-byte emission
+  ([#528](https://github.com/CategoricalData/hydra/issues/528)), `hydra.json.Decode.fromJson`
+  parametric-type (Application/Forall) support
+  ([#531](https://github.com/CategoricalData/hydra/issues/531)), stale 2-param `Coder` shape in
+  Java/Scala test bootstrap ([#552](https://github.com/CategoricalData/hydra/issues/552)), and a
+  kernel dead-code/TODO cleanup ([#538](https://github.com/CategoricalData/hydra/issues/538)).
+- **Lib and build** — `hydra.lib.files` copy/removeDirectory/status overlays across all nine hosts
+  ([#525](https://github.com/CategoricalData/hydra/issues/525)), `hydra.build.*` regeneration into the
+  Lisp dialects ([#544](https://github.com/CategoricalData/hydra/issues/544)), Scheme maps
+  last-writer-wins in `union` ([#545](https://github.com/CategoricalData/hydra/issues/545)),
+  standalone-kernel SHA dependency ([#542](https://github.com/CategoricalData/hydra/issues/542)),
+  the lexicon's empty Types section ([#539](https://github.com/CategoricalData/hydra/issues/539)), and
+  a duplicated generated-file header in `generate-head-haskell-build.py`
+  ([#540](https://github.com/CategoricalData/hydra/issues/540)).
+
+- **TypeScript overlay-import rewrite** ([#565](https://github.com/CategoricalData/hydra/issues/565)):
+  the TypeScript coder's lib overlay-import rewrite no longer captures `hydra.lib.defaults`, which is a
+  generated (not overlaid) module; excluding it fixes spurious import references in generated TypeScript.
+
+- **Lexicon generation** (`bin/regenerate-lexicon.sh`): the generator had been failing
+  silently since the encoder/decoder modules moved to in-memory synthesis
+  ([#448](https://github.com/CategoricalData/hydra/issues/448)) — the script masked the
+  failure and left a stale `docs/hydra-lexicon.txt` missing the four effectful primitive
+  libraries (`hydra.lib.{effects,files,system,text}`). The synthesized coder modules are
+  now included in the lexicon universe, the script fails loudly, and the regenerated
+  lexicon covers all 267 primitives across 17 library modules.
+- **`bin/prepare-release.sh`**: the non-blocking Java quality check invoked Gradle
+  subprojects (`:hydra-rdf4j:test` etc.) that were removed in
+  [#511](https://github.com/CategoricalData/hydra/issues/511), so it always warned without
+  checking anything; it now runs the `dist/java/hydra-pg` package tests (the current home
+  of the former binding tests).
+- **`.gitignore`**: the ignore rules for the overlaid hand-written Haskell runtime still
+  named pre-[#501](https://github.com/CategoricalData/hydra/issues/501) paths, so 41
+  duplicate copies under `dist/haskell/` had become tracked; the rules now match the
+  `Hydra/Overlay/` location and the duplicates are untracked.
+
+### Documentation
+
+- Corrected the 0.17.0 Maven coordinates throughout (Java artifacts publish under group
+  `net.fortytwo.hydra.java`, Scala under `net.fortytwo.hydra.scala`,
+  [#519](https://github.com/CategoricalData/hydra/issues/519)); added the npm and Scala
+  release channels to the README; added TypeScript/Scala/Lisp sections to Getting started;
+  repointed stale pre-#418/#501 DSL paths in the architecture docs; added GitHub issue and
+  PR templates.
+
+---
+
 ## [0.17.0] - 2026-07-01
 
 Major themes: first-class support for effects and I/O in the kernel, TypeScript and
diff --git a/hydra-scala.cabal b/hydra-scala.cabal
--- a/hydra-scala.cabal
+++ b/hydra-scala.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.39.1.
+-- This file has been generated from package.yaml by hpack version 0.38.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           hydra-scala
-version:        0.17.0
+version:        0.17.1
 synopsis:       Hydra's Scala coder: emit Scala source from Hydra modules
 description:    Hydra is an implementation of the LambdaGraph data model, which takes advantage of an isomorphism between labeled hypergraphs and typed lambda calculus: in Hydra, "graphs are programs, and programs are graphs". Scala support for Hydra
 category:       Data
@@ -40,6 +40,7 @@
   build-depends:
       base >=4.19.0 && <4.22
     , containers >=0.6.7 && <0.8
-    , hydra-jvm ==0.17.0
+    , hydra-jvm ==0.17.1
+    , hydra-kernel ==0.17.1
     , scientific >=0.3.7 && <0.4
   default-language: Haskell2010
diff --git a/src/main/haskell/Hydra/Scala/Coder.hs b/src/main/haskell/Hydra/Scala/Coder.hs
--- a/src/main/haskell/Hydra/Scala/Coder.hs
+++ b/src/main/haskell/Hydra/Scala/Coder.hs
@@ -11,6 +11,7 @@
 import qualified Hydra.Coders as Coders
 import qualified Hydra.Constants as Constants
 import qualified Hydra.Core as Core
+import qualified Hydra.Docs as Docs
 import qualified Hydra.Environment as Environment
 import qualified Hydra.Error.Checking as Checking
 import qualified Hydra.Error.Core as ErrorCore
@@ -23,6 +24,7 @@
 import qualified Hydra.Graph as Graph
 import qualified Hydra.Inference as Inference
 import qualified Hydra.Json.Model as Model
+import qualified Hydra.Lexical as Lexical
 import qualified Hydra.Overlay.Haskell.Lib.Eithers as Eithers
 import qualified Hydra.Overlay.Haskell.Lib.Equality as Equality
 import qualified Hydra.Overlay.Haskell.Lib.Lists as Lists
@@ -82,6 +84,27 @@
           Core.applicationFunction = fterm,
           Core.applicationArgument = (Core.TermVariable avar)})
 
+-- | Collect the set of free type variables occurring in a type (#589)
+collectTypeVars :: Core.Type -> S.Set Core.Name
+collectTypeVars typ = collectTypeVarsGo (Strip.deannotateType typ)
+
+-- | Recursively collect free type variables from a type (#589)
+collectTypeVarsGo :: Core.Type -> S.Set Core.Name
+collectTypeVarsGo t =
+    case (Strip.deannotateType t) of
+      Core.TypeVariable v0 -> Sets.singleton v0
+      Core.TypeFunction v0 -> Sets.union (collectTypeVarsGo (Strip.deannotateType (Core.functionTypeDomain v0))) (collectTypeVarsGo (Strip.deannotateType (Core.functionTypeCodomain v0)))
+      Core.TypeApplication v0 -> Sets.union (collectTypeVarsGo (Strip.deannotateType (Core.applicationTypeFunction v0))) (collectTypeVarsGo (Strip.deannotateType (Core.applicationTypeArgument v0)))
+      Core.TypeList v0 -> collectTypeVarsGo (Strip.deannotateType v0)
+      Core.TypeSet v0 -> collectTypeVarsGo (Strip.deannotateType v0)
+      Core.TypeOptional v0 -> collectTypeVarsGo (Strip.deannotateType v0)
+      Core.TypeEffect v0 -> collectTypeVarsGo (Strip.deannotateType v0)
+      Core.TypeMap v0 -> Sets.union (collectTypeVarsGo (Strip.deannotateType (Core.mapTypeKeys v0))) (collectTypeVarsGo (Strip.deannotateType (Core.mapTypeValues v0)))
+      Core.TypePair v0 -> Sets.union (collectTypeVarsGo (Strip.deannotateType (Core.pairTypeFirst v0))) (collectTypeVarsGo (Strip.deannotateType (Core.pairTypeSecond v0)))
+      Core.TypeEither v0 -> Sets.union (collectTypeVarsGo (Strip.deannotateType (Core.eitherTypeLeft v0))) (collectTypeVarsGo (Strip.deannotateType (Core.eitherTypeRight v0)))
+      Core.TypeForall v0 -> collectTypeVarsGo (Strip.deannotateType (Core.forallTypeBody v0))
+      _ -> Sets.empty
+
 -- | Construct a Scala package from a Hydra module and its definitions
 constructModule :: t0 -> Graph.Graph -> Packaging.Module -> [Packaging.Definition] -> Either Errors.Error Syntax.Pkg
 constructModule cx g mod defs =
@@ -102,6 +125,74 @@
           typeDeclStats,
           termDeclStats])})))))
 
+-- | Filter/rewrite a raw type-application list against callee-scheme over-generalization (#589)
+correctTypeApps :: Core.Name -> [Core.Type] -> Graph.Graph -> Either t0 [Core.Type]
+correctTypeApps name fallbackTypeApps g =
+    Optionals.cases (Lexical.lookupBinding g name) (Right fallbackTypeApps) (\el -> Optionals.cases (Core.bindingTypeScheme el) (Right fallbackTypeApps) (\ts ->
+      let schemeType = Core.typeSchemeBody ts
+          allSchemeVars = Lists.filter (\vv -> isSimpleName vv) (Core.typeSchemeVariables ts)
+          schemeTypeVars = collectTypeVars schemeType
+          usedFlags = Lists.map (\vv -> Sets.member vv schemeTypeVars) allSchemeVars
+          usedSchemeVars = filterByFlags allSchemeVars usedFlags
+          nParams = countFunctionParams schemeType
+          peeled = peelDomainTypes nParams schemeType
+          calleeDoms = Pairs.first peeled
+          calleeCod = Pairs.second peeled
+          overgenSubst = detectAccumulatorUnification calleeDoms calleeCod usedSchemeVars
+          keepFlags =
+                  Lists.map (\vv -> Logic.and (Sets.member vv schemeTypeVars) (Logic.not (Maps.member vv overgenSubst))) allSchemeVars
+          schemeVars = filterByFlags allSchemeVars keepFlags
+          filteredFallback0 =
+                  Logic.ifElse (Equality.equal (Lists.length allSchemeVars) (Lists.length fallbackTypeApps)) (filterByFlags fallbackTypeApps keepFlags) fallbackTypeApps
+          filteredFallback =
+                  Logic.ifElse (Maps.null overgenSubst) filteredFallback0 (Lists.map (\t -> substituteTypeVarsWithTypes overgenSubst t) filteredFallback0)
+      in (Right filteredFallback)))
+
+-- | Count the curried function parameters of a type (#589)
+countFunctionParams :: Core.Type -> Int
+countFunctionParams t =
+    case (Strip.deannotateType t) of
+      Core.TypeFunction v0 -> Math.add 1 (countFunctionParams (Core.functionTypeCodomain v0))
+      _ -> 0
+
+-- | Detect callee-scheme type vars forced together/to-concrete by the callee's own domain shape (#589)
+detectAccumulatorUnification :: [Core.Type] -> Core.Type -> [Core.Name] -> M.Map Core.Name Core.Type
+detectAccumulatorUnification doms cod tparams =
+
+      let tparamSet = Sets.fromList tparams
+          allPairs = Lists.bind doms (\d -> extractInOutPair d)
+          groupedByInput = groupPairsByFirst allPairs
+          selfRefSubst = selfRefSubstitution groupedByInput
+          directPairs = Lists.bind doms (\d -> extractDirectReturn tparamSet d)
+          groupedDirect = groupPairsByFirst directPairs
+          directInputVars = Sets.fromList (Lists.map (\p -> Pairs.first p) directPairs)
+          codVar =
+                  case (Strip.deannotateType cod) of
+                    Core.TypeVariable v0 -> Just v0
+                    _ -> Nothing
+          directRefSubst = directRefSubstitution directInputVars codVar groupedDirect
+          codSubst =
+                  Optionals.cases (findPairFirst cod) Maps.empty (\cv -> Logic.ifElse (Maps.member cv selfRefSubst) Maps.empty (Optionals.cases (findSelfRefVar groupedByInput) Maps.empty (\refVar -> Logic.ifElse (Equality.equal cv refVar) Maps.empty (Maps.singleton cv refVar))))
+          domVars = Sets.fromList (Lists.bind doms (\d -> Sets.toList (collectTypeVars d)))
+          danglingSubst =
+                  Optionals.cases (findPairFirst cod) Maps.empty (\cv -> Logic.ifElse (Sets.member cv domVars) Maps.empty (Optionals.cases (findSelfRefVar groupedByInput) Maps.empty (\refVar -> Maps.singleton cv (Core.TypeVariable refVar))))
+      in (Maps.union (Maps.union (Maps.union (nameMapToTypeMap selfRefSubst) (nameMapToTypeMap codSubst)) danglingSubst) (nameMapToTypeMap directRefSubst))
+
+-- | Compute the direct-return substitution over grouped accumulator pairs (#589)
+directRefSubstitution :: (Eq t0, Ord t0) => (S.Set t0 -> Maybe t0 -> M.Map t0 [t0] -> M.Map t0 t0)
+directRefSubstitution directInputVars codVar grouped =
+    Lists.foldl (\subst -> \entry -> directRefSubstitutionProcessGroup directInputVars codVar subst (Pairs.first entry) (Pairs.second entry)) Maps.empty (Maps.toList grouped)
+
+-- | Unify safe co-occurring vars onto inVar for the direct-return accumulator pattern (#589)
+directRefSubstitutionProcessGroup :: (Eq t0, Ord t0) => (S.Set t0 -> Maybe t0 -> M.Map t0 t0 -> t0 -> [t0] -> M.Map t0 t0)
+directRefSubstitutionProcessGroup directInputVars codVar subst inVar outVars =
+
+      let selfRefCount = Lists.length (Lists.filter (\vv -> Equality.equal vv inVar) outVars)
+          nonSelfVars = Lists.filter (\vv -> Logic.not (Equality.equal vv inVar)) outVars
+          safeNonSelfVars =
+                  Lists.filter (\vv -> Logic.and (Logic.not (Sets.member vv directInputVars)) (Logic.not (Equality.equal (Just vv) codVar))) nonSelfVars
+      in (Logic.ifElse (Logic.and (Equality.gte selfRefCount 2) (Logic.not (Lists.null safeNonSelfVars))) (Lists.foldl (\s -> \vv -> Maps.insert vv inVar s) subst safeNonSelfVars) subst)
+
 -- | Drop N domain types from a function type, returning the remaining type
 dropDomains :: Int -> Core.Type -> Core.Type
 dropDomains n t =
@@ -173,7 +264,7 @@
           cod = dropDomains paramCount typ
           zippedParams = Lists.zip (Lists.take paramCount paramNames) (Lists.take paramCount doms)
           freeTypeVars =
-                  Lists.filter (\v -> Logic.not (Lists.elem 46 (Strings.toList (Core.unName v)))) (Sets.toList (Variables.freeVariablesInType typ))
+                  Lists.nub (Lists.filter (\v -> Logic.not (Lists.elem 46 (Strings.toList (Core.unName v)))) (Variables.freeVariablesInTypeOrdered typ))
           tparams = Lists.map (\tv -> Utils.stparam tv) freeTypeVars
           letBindings = extractLetBindings term
           gWithTypeVars =
@@ -385,7 +476,7 @@
             Core.TermProject _ -> encodeTerm cx g substitutedBody
             Core.TermCases _ -> encodeTerm cx g substitutedBody
             Core.TermUnwrap _ -> encodeTerm cx g substitutedBody
-            Core.TermVariable v1 -> Eithers.bind (Eithers.mapList (\targ -> encodeType cx g targ) typeArgs) (\stypeArgs -> Optionals.cases (Maps.lookup v1 (Graph.graphPrimitives g)) (Eithers.bind (encodeTerm cx g substitutedBody) (\svar -> Right (Utils.sapplyTypes svar stypeArgs))) (\_prim -> Right (Utils.sapplyTypes (Utils.sprim v1) stypeArgs)))
+            Core.TermVariable v1 -> Eithers.bind (correctTypeApps v1 typeArgs g) (\correctedTypeArgs -> Eithers.bind (Eithers.mapList (\targ -> encodeType cx g targ) correctedTypeArgs) (\stypeArgs -> Optionals.cases (Maps.lookup v1 (Graph.graphPrimitives g)) (Eithers.bind (encodeTerm cx g substitutedBody) (\svar -> Right (Utils.sapplyTypes svar stypeArgs))) (\_prim -> Right (Utils.sapplyTypes (Utils.sprim v1) stypeArgs))))
             _ -> encodeTerm cx g substitutedBody
         Core.TermTypeLambda v0 -> encodeTerm cx (Scoping.extendGraphForTypeLambda g v0) (Core.typeLambdaBody v0)
         Core.TermApplication v0 ->
@@ -786,6 +877,33 @@
       Core.TypeForall v0 -> extractCodomain (Core.forallTypeBody v0)
       _ -> t
 
+-- | Extract direct-return (input-var, output-var) pairs from a domain type (#589)
+extractDirectReturn :: S.Set Core.Name -> Core.Type -> [(Core.Name, Core.Name)]
+extractDirectReturn tparamSet t = extractDirectReturnGo tparamSet t
+
+-- | Recursive worker for extractDirectReturn (#589)
+extractDirectReturnGo :: S.Set Core.Name -> Core.Type -> [(Core.Name, Core.Name)]
+extractDirectReturnGo tparamSet t =
+    case (Strip.deannotateType t) of
+      Core.TypeFunction v0 ->
+        let dom = Strip.deannotateType (Core.functionTypeDomain v0)
+            cod = Core.functionTypeCodomain v0
+        in case dom of
+          Core.TypeVariable v1 -> Logic.ifElse (Sets.member v1 tparamSet) (case (Strip.deannotateType cod) of
+            Core.TypeFunction v2 ->
+              let midArg = Strip.deannotateType (Core.functionTypeDomain v2)
+                  retPart = Strip.deannotateType (Core.functionTypeCodomain v2)
+              in case midArg of
+                Core.TypeVariable v3 -> Logic.ifElse (Sets.member v3 tparamSet) [] (case retPart of
+                  Core.TypeVariable v4 -> Logic.ifElse (Sets.member v4 tparamSet) (Lists.singleton (v1, v4)) []
+                  _ -> [])
+                _ -> case retPart of
+                  Core.TypeVariable v3 -> Logic.ifElse (Sets.member v3 tparamSet) (Lists.singleton (v1, v3)) []
+                  _ -> []
+            _ -> []) (extractDirectReturnGo tparamSet cod)
+          _ -> extractDirectReturnGo tparamSet cod
+      _ -> []
+
 -- | Extract domain types from a function type
 extractDomains :: Core.Type -> [Core.Type]
 extractDomains t =
@@ -794,6 +912,21 @@
       Core.TypeForall v0 -> extractDomains (Core.forallTypeBody v0)
       _ -> []
 
+-- | Extract an (input-var, output-var) accumulator pair from a domain type (#589)
+extractInOutPair :: Core.Type -> [(Core.Name, Core.Name)]
+extractInOutPair t =
+    case (Strip.deannotateType t) of
+      Core.TypeFunction v0 -> case (Strip.deannotateType (Core.functionTypeDomain v0)) of
+        Core.TypeVariable v1 ->
+          let retType = unwrapReturnType (Core.functionTypeCodomain v0)
+          in case (Strip.deannotateType retType) of
+            Core.TypePair v2 -> case (Strip.deannotateType (Core.pairTypeFirst v2)) of
+              Core.TypeVariable v3 -> Lists.singleton (v1, v3)
+              _ -> []
+            _ -> []
+        _ -> []
+      _ -> []
+
 -- | Extract let bindings from a term
 extractLetBindings :: Core.Term -> [Core.Binding]
 extractLetBindings t =
@@ -865,6 +998,10 @@
         Syntax.paramDataDecltpe = (Just sftyp),
         Syntax.paramDataDefault = Nothing})))
 
+-- | Keep elements of xs whose corresponding flag is true (#589)
+filterByFlags :: [t0] -> [Bool] -> [t0]
+filterByFlags xs flags = Lists.map (\p -> Pairs.first p) (Lists.filter (\p -> Pairs.second p) (Lists.zip xs flags))
+
 -- | Find the domain type from annotations
 findDomain :: t0 -> Graph.Graph -> M.Map Core.Name Core.Term -> Either Errors.Error Core.Type
 findDomain cx g meta =
@@ -879,6 +1016,15 @@
       Lists.map toElImport (Sets.toList elImps),
       (Lists.map toPrimImport (Sets.toList primImps))])))
 
+-- | If t is Pair(var, _), return the first component's variable name (#589)
+findPairFirst :: Core.Type -> Maybe Core.Name
+findPairFirst t =
+    case (Strip.deannotateType t) of
+      Core.TypePair v0 -> case (Strip.deannotateType (Core.pairTypeFirst v0)) of
+        Core.TypeVariable v1 -> Just v1
+        _ -> Nothing
+      _ -> Nothing
+
 -- | Find the Scala domain type for a function from annotations
 findSdom :: t0 -> Graph.Graph -> M.Map Core.Name Core.Term -> Either Errors.Error (Maybe Syntax.Type)
 findSdom cx g meta =
@@ -893,6 +1039,25 @@
         _ -> Right Nothing
       _ -> Eithers.bind (encodeType cx g t) (\st -> Right (Just st))))
 
+-- | Find a type variable that is its own accumulator output, if any (#589)
+findSelfRefVar :: (Eq t0, Ord t0) => (M.Map t0 [t0] -> Maybe t0)
+findSelfRefVar grouped =
+
+      let selfRefs = Lists.filter (\entry -> Lists.elem (Pairs.first entry) (Pairs.second entry)) (Maps.toList grouped)
+      in (Optionals.map (\entry -> Pairs.first entry) (Lists.maybeHead selfRefs))
+
+-- | Group a list of pairs into a map keyed by first component (#589)
+groupPairsByFirst :: Ord t0 => ([(t0, t1)] -> M.Map t0 [t1])
+groupPairsByFirst pairs =
+    Lists.foldl (\m -> \p ->
+      let k = Pairs.first p
+          vv = Pairs.second p
+      in (Maps.alter (\mv -> Optionals.cases mv (Just (Lists.singleton vv)) (\vs -> Just (Lists.concat2 vs (Lists.singleton vv)))) k m)) Maps.empty pairs
+
+-- | True if a type-variable name has no namespace qualifier (#589)
+isSimpleName :: Core.Name -> Bool
+isSimpleName name = Equality.equal (Lists.length (Strings.splitOn "." (Core.unName name))) 1
+
 -- | Convert a Hydra module to Scala source code
 moduleToScala :: Packaging.Module -> [Packaging.Definition] -> t0 -> Graph.Graph -> Either Errors.Error (M.Map String String)
 moduleToScala mod defs cx g =
@@ -900,6 +1065,29 @@
       let s = Serialization.printExpr (Serialization.parenthesize (Serde.pkgToExpr pkg))
       in (Right (Maps.singleton (Names.moduleNameToFilePath Util.CaseConventionCamel (File.FileExtension "scala") (Packaging.moduleName mod)) s)))
 
+-- | Lift a Name->Name substitution map to a Name->Type map (#589)
+nameMapToTypeMap :: Ord t0 => (M.Map t0 Core.Name -> M.Map t0 Core.Type)
+nameMapToTypeMap m = Maps.map (\vv -> Core.TypeVariable vv) m
+
+-- | Peel up to n curried domain types off a function type (#589)
+peelDomainTypes :: Int -> Core.Type -> ([Core.Type], Core.Type)
+peelDomainTypes n t =
+    Logic.ifElse (Equality.lte n 0) ([], t) (case (Strip.deannotateType t) of
+      Core.TypeFunction v0 ->
+        let rest = peelDomainTypes (Math.sub n 1) (Core.functionTypeCodomain v0)
+        in (Lists.cons (Core.functionTypeDomain v0) (Pairs.first rest), (Pairs.second rest))
+      _ -> ([], t))
+
+-- | Compute the self-reference substitution over grouped accumulator pairs (#589)
+selfRefSubstitution :: (Eq t0, Ord t0) => (M.Map t0 [t0] -> M.Map t0 t0)
+selfRefSubstitution grouped =
+    Lists.foldl (\subst -> \entry -> selfRefSubstitutionProcessGroup subst (Pairs.first entry) (Pairs.second entry)) Maps.empty (Maps.toList grouped)
+
+-- | Unify every co-occurring var onto inVar when inVar is its own accumulator output (#589)
+selfRefSubstitutionProcessGroup :: (Eq t0, Ord t0) => (M.Map t0 t0 -> t0 -> [t0] -> M.Map t0 t0)
+selfRefSubstitutionProcessGroup subst inVar outVars =
+    Logic.ifElse (Lists.elem inVar outVars) (Lists.foldl (\s -> \vv -> Logic.ifElse (Equality.equal vv inVar) s (Maps.insert vv inVar s)) subst outVars) subst
+
 -- | Strip wrap eliminations from terms (newtypes are erased in Scala)
 stripWrapEliminations :: Core.Term -> Core.Term
 stripWrapEliminations t =
@@ -920,6 +1108,39 @@
           _ -> t
       _ -> t
 
+-- | Substitute type variables in t per subst (#589)
+substituteTypeVarsWithTypes :: M.Map Core.Name Core.Type -> Core.Type -> Core.Type
+substituteTypeVarsWithTypes subst t = substituteTypeVarsWithTypesGo subst (Strip.deannotateType t)
+
+-- | Recursive worker for substituteTypeVarsWithTypes (#589)
+substituteTypeVarsWithTypesGo :: M.Map Core.Name Core.Type -> Core.Type -> Core.Type
+substituteTypeVarsWithTypesGo subst t =
+    case (Strip.deannotateType t) of
+      Core.TypeVariable v0 -> Optionals.cases (Maps.lookup v0 subst) t (\rep -> rep)
+      Core.TypeFunction v0 -> Core.TypeFunction (Core.FunctionType {
+        Core.functionTypeDomain = (substituteTypeVarsWithTypesGo subst (Core.functionTypeDomain v0)),
+        Core.functionTypeCodomain = (substituteTypeVarsWithTypesGo subst (Core.functionTypeCodomain v0))})
+      Core.TypeApplication v0 -> Core.TypeApplication (Core.ApplicationType {
+        Core.applicationTypeFunction = (substituteTypeVarsWithTypesGo subst (Core.applicationTypeFunction v0)),
+        Core.applicationTypeArgument = (substituteTypeVarsWithTypesGo subst (Core.applicationTypeArgument v0))})
+      Core.TypeList v0 -> Core.TypeList (substituteTypeVarsWithTypesGo subst v0)
+      Core.TypeSet v0 -> Core.TypeSet (substituteTypeVarsWithTypesGo subst v0)
+      Core.TypeOptional v0 -> Core.TypeOptional (substituteTypeVarsWithTypesGo subst v0)
+      Core.TypeEffect v0 -> Core.TypeEffect (substituteTypeVarsWithTypesGo subst v0)
+      Core.TypeMap v0 -> Core.TypeMap (Core.MapType {
+        Core.mapTypeKeys = (substituteTypeVarsWithTypesGo subst (Core.mapTypeKeys v0)),
+        Core.mapTypeValues = (substituteTypeVarsWithTypesGo subst (Core.mapTypeValues v0))})
+      Core.TypePair v0 -> Core.TypePair (Core.PairType {
+        Core.pairTypeFirst = (substituteTypeVarsWithTypesGo subst (Core.pairTypeFirst v0)),
+        Core.pairTypeSecond = (substituteTypeVarsWithTypesGo subst (Core.pairTypeSecond v0))})
+      Core.TypeEither v0 -> Core.TypeEither (Core.EitherType {
+        Core.eitherTypeLeft = (substituteTypeVarsWithTypesGo subst (Core.eitherTypeLeft v0)),
+        Core.eitherTypeRight = (substituteTypeVarsWithTypesGo subst (Core.eitherTypeRight v0))})
+      Core.TypeForall v0 -> Core.TypeForall (Core.ForallType {
+        Core.forallTypeParameter = (Core.forallTypeParameter v0),
+        Core.forallTypeBody = (substituteTypeVarsWithTypesGo subst (Core.forallTypeBody v0))})
+      _ -> t
+
 -- | Create an element import statement
 toElImport :: Packaging.ModuleName -> Syntax.Stat
 toElImport ns =
@@ -953,3 +1174,11 @@
       in (Syntax.TypeVar (Syntax.VarType {
         Syntax.varTypeName = Syntax.NameType {
           Syntax.nameTypeValue = s}}))
+
+-- | Unwrap a (possibly curried/applied) type down to its ultimate return type (#589)
+unwrapReturnType :: Core.Type -> Core.Type
+unwrapReturnType t =
+    case (Strip.deannotateType t) of
+      Core.TypeFunction v0 -> unwrapReturnType (Core.functionTypeCodomain v0)
+      Core.TypeApplication v0 -> unwrapReturnType (Core.applicationTypeArgument v0)
+      _ -> t
diff --git a/src/main/haskell/Hydra/Scala/Language.hs b/src/main/haskell/Hydra/Scala/Language.hs
--- a/src/main/haskell/Hydra/Scala/Language.hs
+++ b/src/main/haskell/Hydra/Scala/Language.hs
@@ -7,6 +7,7 @@
 import qualified Hydra.Ast as Ast
 import qualified Hydra.Coders as Coders
 import qualified Hydra.Core as Core
+import qualified Hydra.Docs as Docs
 import qualified Hydra.Error.Checking as Checking
 import qualified Hydra.Error.Core as ErrorCore
 import qualified Hydra.Error.File as ErrorFile
diff --git a/src/main/haskell/Hydra/Scala/Serde.hs b/src/main/haskell/Hydra/Scala/Serde.hs
--- a/src/main/haskell/Hydra/Scala/Serde.hs
+++ b/src/main/haskell/Hydra/Scala/Serde.hs
@@ -7,6 +7,7 @@
 import qualified Hydra.Ast as Ast
 import qualified Hydra.Coders as Coders
 import qualified Hydra.Core as Core
+import qualified Hydra.Docs as Docs
 import qualified Hydra.Error.Checking as Checking
 import qualified Hydra.Error.Core as ErrorCore
 import qualified Hydra.Error.File as ErrorFile
diff --git a/src/main/haskell/Hydra/Scala/Utils.hs b/src/main/haskell/Hydra/Scala/Utils.hs
--- a/src/main/haskell/Hydra/Scala/Utils.hs
+++ b/src/main/haskell/Hydra/Scala/Utils.hs
@@ -7,6 +7,7 @@
 import qualified Hydra.Ast as Ast
 import qualified Hydra.Coders as Coders
 import qualified Hydra.Core as Core
+import qualified Hydra.Docs as Docs
 import qualified Hydra.Error.Checking as Checking
 import qualified Hydra.Error.Core as ErrorCore
 import qualified Hydra.Error.File as ErrorFile
@@ -67,10 +68,10 @@
       Syntax.applyDataFun = fun,
       Syntax.applyDataArgs = args})
 
--- | Apply explicit type parameters to a Scala expression (e.g. f[A, B])
+-- | Apply explicit type parameters to a Scala expression (e.g. f[A, B]); a no-op for an empty type-arg list (#589)
 sapplyTypes :: Syntax.Data -> [Syntax.Type] -> Syntax.Data
 sapplyTypes fun typeArgs =
-
+    Logic.ifElse (Lists.null typeArgs) fun (
       let typeToStr = \t -> typeToString t
           typeStrings = Lists.map typeToStr typeArgs
           typeArgStr =
@@ -94,7 +95,7 @@
               Syntax.selectDataName = Syntax.NameData {
                 Syntax.nameDataValue = (Syntax.PredefString (Strings.cat2 rawName typeArgStr))}})))
           _ -> fun
-        _ -> fun
+        _ -> fun)
 
 -- | Create a Scala assignment expression
 sassign :: Syntax.Data -> Syntax.Data -> Syntax.Data
