diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 825f18c80..927de5ed9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -68,7 +68,7 @@ jobs:
       - name: Build and test in release mode (windows)
         if: matrix.os == 'windows-latest'
         run: |
-          opam exec -- dune runtest -p merlin-lib,dot-merlin-reader,merlin
+          opam exec -- dune runtest -p merlin-lib,dot-merlin-reader,ocaml-index,merlin
 
       - name: Build and test in release mode (macos/linux)
         if: matrix.os != 'windows-latest'
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 1821f53b3..521fb1dd4 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -34,4 +34,4 @@ jobs:
       uses: cachix/install-nix-action@v27
       with:
         nix_path: nixpkgs=channel:nixos-unstable
-    - run: nix develop -c dune build @check @runtest -p merlin-lib,dot-merlin-reader,merlin
+    - run: nix develop -c dune build @check @runtest -p merlin-lib,dot-merlin-reader,ocaml-index,merlin
diff --git a/merlin-lib.opam b/merlin-lib.opam
index 8f4e8c3e2..c78fbd3fb 100644
--- a/merlin-lib.opam
+++ b/merlin-lib.opam
@@ -11,7 +11,7 @@ build: [
 ]
 depends: [
   "ocaml" {>= "5.2" & < "5.3"}
-  "dune" {>= "2.9.0"}
+  "dune" {>= "3.0.0"}
   "csexp" {>= "1.5.1"}
   "menhir"    {dev & >= "20201216"}
   "menhirLib" {dev & >= "20201216"}
diff --git a/ocaml-index.opam b/ocaml-index.opam
new file mode 100644
index 000000000..f0615f66c
--- /dev/null
+++ b/ocaml-index.opam
@@ -0,0 +1,33 @@
+# This file is generated by dune, edit dune-project instead
+opam-version: "2.0"
+synopsis: "A tool that indexes value usages from cmt files"
+description:
+  "ocaml-index should integrate with the build system to index codebase and allow tools such as Merlin to perform project-wide occurrences queries."
+maintainer: ["ulysse@tarides.com"]
+authors: ["ulysse@tarides.com"]
+license: "MIT"
+homepage: "https://github.com/ocaml/merlin/ocaml-index"
+bug-reports: "https://github.com/ocaml/merlin/issues"
+depends: [
+  "dune" {>= "3.0.0"}
+  "ocaml" {>= "5.2"}
+  "merlin-lib" {>= "5.1-502"}
+  "odoc" {with-doc}
+]
+build: [
+  ["dune" "subst"] {dev}
+  [
+    "dune"
+    "build"
+    "-p"
+    name
+    "-j"
+    jobs
+    "--promote-install-files=false"
+    "@install"
+    "@runtest" {with-test}
+    "@doc" {with-doc}
+  ]
+  ["dune" "install" "-p" name "--create-install-files" name]
+]
+dev-repo: "git+https://github.com/ocaml/merlin.git"
diff --git a/src/ocaml-index/CHANGES.md b/src/ocaml-index/CHANGES.md
new file mode 100644
index 000000000..b14a5452e
--- /dev/null
+++ b/src/ocaml-index/CHANGES.md
@@ -0,0 +1,10 @@
+1.0 (2024-06-18)
+----------------
+
+### Added
+
+- Initial release.
+- The `aggregate`` command that finishes reduction of shapes in cmt files and
+  store the output in a single index file.
+- The `stats` command that prints information about an index file.
+- The `dump` command that prints all locs of an index.
diff --git a/src/ocaml-index/README.md b/src/ocaml-index/README.md
new file mode 100644
index 000000000..a78ab6c84
--- /dev/null
+++ b/src/ocaml-index/README.md
@@ -0,0 +1,62 @@
+# ocaml-index
+
+Ocaml-index is a tool that indexes values from CMT files. Its current purpose is
+to provide project-wide occurrences for OCaml codebases. The tool iterate on
+given cmt's occurrences list (`cmt_ident_occurrences`) and determines the
+definition of every value found in it. It then write an index to disk where
+values corresponding to the same definition are grouped together. The tool can
+also take multiple input files, index them and merge the results into a single
+index.
+
+# Usage
+
+## Process cmt files and merge indexes
+
+
+> ocaml-index aggregate [-o _output_file_] _cmt_file_ ... _index_file_ ... [-I _dir_] ... [--no-cmt-load-path]
+
+
+- Input `cmt` files are indexed and merged into the final output
+- Input index files are directly merged into the output
+- If no input files is provided an empty index is created
+- The default output file name is `project.ocaml-index`
+
+### Load path:
+Identifying definitions while processing `cmt` files may require loading any of
+the `cmt` files of every transitive dependency of the compilation unit. By
+default the `cmt_load_path` of the first input `cmt` file will be used to search
+for these other units. One can add more paths to the load path using the `-I`
+option. Usage of the cmt's loadpath can be disabled using the
+`--no-cmt-load-path` option.
+
+### Paths:
+By default, the paths stored in the cmt's locations are relative to the
+directory where the compiler was called. for build systems that do not always
+call the compiler from the same root folder it might be useful to rewrite these
+paths.
+
+Using the `--root <path>` option stores the given path in the output file.
+Additionally, the ` --rewrite-root` option will prepend `root` to all paths in
+indexed location.
+
+[Note: this feature is not used in the reference Dune rules, it might evolve in
+the future if needed]
+
+## Querying indexes
+
+The tool does not provide actual queries but one can dump an entire index:
+
+> ocaml-index dump _index_file_ ...
+
+Or only print the number of definitions it stores:
+
+> ocaml-index stats _index_file_ ...
+
+```bash
+$ ocaml-index stats _build/default/src/dune_rules/.dune_rules.objs/cctx.ocaml-index
+Index ".../cctx.ocaml-index" contains:
+- 28083 definitions
+- 86850 locations
+- 0 approximated definitions
+- 0 compilation units shapes
+```
diff --git a/src/ocaml-index/bin/dune b/src/ocaml-index/bin/dune
new file mode 100644
index 000000000..2e4c04990
--- /dev/null
+++ b/src/ocaml-index/bin/dune
@@ -0,0 +1,9 @@
+(executable
+ (name ocaml_index)
+ (public_name ocaml-index)
+ (package ocaml-index)
+ (libraries lib ocaml_typing merlin_index_format)
+ (flags
+  :standard
+  -open Ocaml_typing
+  -open Merlin_index_format))
diff --git a/src/ocaml-index/bin/ocaml_index.ml b/src/ocaml-index/bin/ocaml_index.ml
new file mode 100644
index 000000000..a4fc1f3f9
--- /dev/null
+++ b/src/ocaml-index/bin/ocaml_index.ml
@@ -0,0 +1,105 @@
+(** The indexer's binary *)
+
+open Lib
+
+let usage_msg =
+  "ocaml-index [COMMAND] [-verbose] <file1> [<file2>] ... -o <output>"
+
+let verbose = ref false
+let debug = ref false
+let input_files = ref []
+let build_path = ref []
+let output_file = ref "project.ocaml-index"
+let root = ref ""
+let rewrite_root = ref false
+let store_shapes = ref false
+let do_not_use_cmt_loadpath = ref false
+
+type command = Aggregate | Dump | Stats
+
+let parse_command = function
+  | "aggregate" -> Some Aggregate
+  | "dump" -> Some Dump
+  | "stats" -> Some Stats
+  | _ -> None
+
+let command = ref None
+
+let anon_fun arg =
+  match !command with
+  | None -> (
+      match parse_command arg with
+      | Some cmd -> command := Some cmd
+      | None ->
+          command := Some Aggregate;
+          input_files := arg :: !input_files)
+  | Some _ -> input_files := arg :: !input_files
+
+let speclist =
+  [
+    ("--verbose", Arg.Set verbose, "Output more information");
+    ("--debug", Arg.Set debug, "Output debugging information");
+    ("-o", Arg.Set_string output_file, "Set output file name");
+    ( "--root",
+      Arg.Set_string root,
+      "Set the root path for all relative locations" );
+    ( "--rewrite-root",
+      Arg.Set rewrite_root,
+      "Rewrite locations paths using the provided root" );
+    ( "--store-shapes",
+      Arg.Set store_shapes,
+      "Aggregate input-indexes shapes and store them in the new index" );
+    ( "-I",
+      Arg.String (fun arg -> build_path := arg :: !build_path),
+      "An extra directory to add to the load path" );
+    ( "--no-cmt-load-path",
+      Arg.Set do_not_use_cmt_loadpath,
+      "Do not initialize the load path with the paths found in the first input \
+       cmt file" );
+  ]
+
+let set_log_level debug verbose =
+  Log.set_log_level Error;
+  if verbose then Log.set_log_level Warning;
+  if debug then Log.set_log_level Debug
+
+let () =
+  Arg.parse speclist anon_fun usage_msg;
+  set_log_level !debug !verbose;
+  (match !command with
+  | Some Aggregate ->
+      let root = if String.equal "" !root then None else Some !root in
+      Index.from_files ~store_shapes:!store_shapes ~root
+        ~rewrite_root:!rewrite_root ~output_file:!output_file
+        ~build_path:!build_path
+        ~do_not_use_cmt_loadpath:!do_not_use_cmt_loadpath !input_files
+  | Some Dump ->
+      List.iter
+        (fun file ->
+          Index_format.(
+            read_exn ~file |> pp Format.std_formatter))
+        !input_files
+  | Some Stats ->
+      List.iter
+        (fun file ->
+          let open Merlin_index_format.Index_format in
+          let { defs; approximated; cu_shape; root_directory; _ } =
+            read_exn ~file
+          in
+          Printf.printf
+            "Index %S contains:\n\
+             - %i definitions\n\
+             - %i locations\n\
+             - %i approximated definitions\n\
+             - %i compilation units shapes\n\
+             - root dir: %s\n\n"
+            file (Uid_map.cardinal defs)
+            (Uid_map.fold
+               (fun _uid locs acc -> acc + Lid_set.cardinal locs)
+               defs 0)
+            (Uid_map.cardinal approximated)
+            (Hashtbl.length cu_shape)
+            (Option.value ~default:"none" root_directory))
+        !input_files
+  | _ -> Printf.printf "Nothing to do.\n%!");
+  exit 0
diff --git a/src/ocaml-index/lib/dune b/src/ocaml-index/lib/dune
new file mode 100644
index 000000000..f21f905bd
--- /dev/null
+++ b/src/ocaml-index/lib/dune
@@ -0,0 +1,17 @@
+(library
+ (name lib)
+ (libraries
+  ocaml_typing
+  ocaml_parsing
+  ocaml_utils
+  merlin_utils
+  merlin_analysis
+  merlin_index_format)
+ (flags
+  :standard
+  -open Ocaml_typing
+  -open Ocaml_parsing
+  -open Ocaml_utils
+  -open Merlin_utils
+  -open Merlin_analysis
+  -open Merlin_index_format))
diff --git a/src/ocaml-index/lib/index.ml b/src/ocaml-index/lib/index.ml
new file mode 100644
index 000000000..05d62307d
--- /dev/null
+++ b/src/ocaml-index/lib/index.ml
@@ -0,0 +1,181 @@
+module Kind = Shape.Sig_component_kind
+open Index_format
+
+let with_root ?root file =
+  match root with None -> file | Some root -> Filename.concat root file
+
+let add_root ~root (lid : Longident.t Location.loc) =
+  match root with
+  | None -> lid
+  | Some root ->
+      let pos_fname = Filename.concat root lid.loc.loc_start.pos_fname in
+      {
+        lid with
+        loc =
+          {
+            lid.loc with
+            loc_start = { lid.loc.loc_start with pos_fname };
+            loc_end = { lid.loc.loc_end with pos_fname };
+          };
+      }
+
+let merge m m' =
+  Shape.Uid.Map.union
+    (fun _uid locs locs' -> Some (Lid_set.union locs locs'))
+    m m'
+
+(** Cmt files contains a table of declarations' Uids associated to a typedtree
+    fragment. [add_locs_from_fragments] gather locations from these *)
+let gather_locs_from_fragments ~root ~rewrite_root map fragments =
+  let to_located_lid (name : string Location.loc) =
+    { name with txt = Longident.Lident name.txt }
+  in
+  let add_loc uid fragment acc =
+    match Misc_utils.loc_of_decl ~uid fragment with
+    | None -> acc
+    | Some lid ->
+        let lid = to_located_lid lid in
+        let lid = if rewrite_root then add_root ~root lid else lid in
+        Shape.Uid.Map.add uid (Lid_set.singleton lid) acc
+  in
+  Shape.Uid.Tbl.fold add_loc fragments map
+
+module Reduce_conf = struct
+  let fuel = 10
+
+  let try_load ~unit_name () =
+    let cmt = Format.sprintf "%s.cmt" unit_name in
+    match Cmt_cache.read (Load_path.find_normalized cmt) with
+    | cmt_item ->
+        Log.debug "Loaded CMT %s" cmt;
+        cmt_item.cmt_infos.cmt_impl_shape
+    | exception Not_found ->
+        Log.warn "Failed to load file %S in load_path: @[%s@]\n%!" cmt
+        @@ String.concat "; " (Load_path.get_path_list ());
+        None
+
+  let read_unit_shape ~unit_name =
+    Log.debug "Read unit shape: %s\n%!" unit_name;
+    try_load ~unit_name ()
+end
+
+let init_load_path_once ~do_not_use_cmt_loadpath =
+  let loaded = ref false in
+  fun ~dirs cmt_loadpath ->
+    if not !loaded then (
+      let cmt_visible, cmt_hidden =
+        if do_not_use_cmt_loadpath then ([], [])
+        else (cmt_loadpath.Load_path.visible, cmt_loadpath.Load_path.hidden)
+      in
+      let visible = List.concat [ cmt_visible; dirs ] in
+      Load_path.(init ~auto_include:no_auto_include ~visible ~hidden:cmt_hidden);
+      loaded := true)
+
+let index_of_cmt ~root ~rewrite_root ~build_path ~do_not_use_cmt_loadpath
+    cmt_infos =
+  let {
+    Cmt_format.cmt_loadpath;
+    cmt_impl_shape;
+    cmt_modname;
+    cmt_uid_to_decl;
+    cmt_ident_occurrences;
+    cmt_initial_env;
+    cmt_sourcefile;
+    cmt_source_digest;
+    _;
+  } =
+    cmt_infos
+  in
+  init_load_path_once ~do_not_use_cmt_loadpath ~dirs:build_path cmt_loadpath;
+  let module Reduce = Shape_reduce.Make (Reduce_conf) in
+  let defs =
+    if Option.is_none cmt_impl_shape then Shape.Uid.Map.empty
+    else
+      gather_locs_from_fragments ~root ~rewrite_root Shape.Uid.Map.empty
+        cmt_uid_to_decl
+  in
+  (* The list [cmt_ident_occurrences] associate each ident usage location in the
+     module with its (partially) reduced shape. We finish the reduction and
+     group together all the locations that share the same definition uid. *)
+  let defs, approximated =
+    List.fold_left
+      (fun ((acc_defs, acc_apx) as acc) (lid, (item : Shape_reduce.result)) ->
+        let lid = if rewrite_root then add_root ~root lid else lid in
+        let resolved =
+          match item with
+          | Unresolved shape -> Reduce.reduce_for_uid cmt_initial_env shape
+          | Resolved _ when Option.is_none cmt_impl_shape ->
+              (* Right now, without additional information we cannot take the
+                 risk to mix uids from interfaces with the ones from
+                 implementations. We simply ignore items defined in an interface. *)
+              Internal_error_missing_uid
+          | result -> result
+        in
+        match Locate.uid_of_result ~traverse_aliases:false resolved with
+        | Some uid, false -> (add acc_defs uid (Lid_set.singleton lid), acc_apx)
+        | Some uid, true -> (acc_defs, add acc_apx uid (Lid_set.singleton lid))
+        | None, _ -> acc)
+      (defs, Shape.Uid.Map.empty)
+      cmt_ident_occurrences
+  in
+  let cu_shape = Hashtbl.create 1 in
+  Option.iter (Hashtbl.add cu_shape cmt_modname) cmt_impl_shape;
+  let stats =
+    match cmt_sourcefile with
+    | None -> Stats.empty
+    | Some src -> (
+        let rooted_src = with_root ?root src in
+        try
+          let stats = Unix.stat rooted_src in
+          let src = if rewrite_root then rooted_src else src in
+          Stats.singleton src
+            {
+              mtime = stats.st_mtime;
+              size = stats.st_size;
+              source_digest = cmt_source_digest;
+            }
+        with Unix.Unix_error _ -> Stats.empty)
+  in
+  { defs; approximated; cu_shape; stats; root_directory = None }
+
+let merge_index ~store_shapes ~into index =
+  let defs = merge index.defs into.defs in
+  let approximated = merge index.approximated into.approximated in
+  let stats = Stats.union (fun _ f1 _f2 -> Some f1) into.stats index.stats in
+  if store_shapes then
+    Hashtbl.add_seq index.cu_shape (Hashtbl.to_seq into.cu_shape);
+  { into with defs; approximated; stats }
+
+let from_files ~store_shapes ~output_file ~root ~rewrite_root ~build_path
+    ~do_not_use_cmt_loadpath files =
+  Log.debug "Debug log is enabled";
+  let initial_index =
+    {
+      defs = Shape.Uid.Map.empty;
+      approximated = Shape.Uid.Map.empty;
+      cu_shape = Hashtbl.create 64;
+      stats = Stats.empty;
+      root_directory = root;
+    }
+  in
+  let final_index =
+    Ocaml_utils.Local_store.with_store (Ocaml_utils.Local_store.fresh ())
+    @@ fun () ->
+    List.fold_left
+      (fun into file ->
+        let index =
+          match Cmt_cache.read file with
+          | cmt_item ->
+              index_of_cmt ~root ~rewrite_root ~build_path
+                ~do_not_use_cmt_loadpath cmt_item.cmt_infos
+          | exception _ -> (
+              match read ~file with
+              | Index index -> index
+              | _ ->
+                  Log.error "Unknown file type: %s" file;
+                  exit 1)
+        in
+        merge_index ~store_shapes index ~into)
+      initial_index files
+  in
+  write ~file:output_file final_index
diff --git a/src/ocaml-index/lib/log.ml b/src/ocaml-index/lib/log.ml
new file mode 100644
index 000000000..ecce9f5ed
--- /dev/null
+++ b/src/ocaml-index/lib/log.ml
@@ -0,0 +1,29 @@
+module Level = struct
+  type t = Debug | Warning | Error
+
+  let int_of_t = function Debug -> 0 | Warning -> 1 | Error -> 2
+
+  let string_of_t = function
+    | Debug -> "debug"
+    | Warning -> "warning"
+    | Error -> "error"
+
+  let ( >= ) a b = int_of_t a >= int_of_t b
+end
+
+let log_level = ref Level.Error
+let set_log_level level = log_level := level
+
+let log ~level =
+  let print =
+    let formatter =
+      if level = Level.Error then Format.err_formatter else Format.std_formatter
+    in
+    Format.fprintf formatter "[%s] %s\n%!" (Level.string_of_t level)
+  in
+  if Level.(level >= !log_level) then Format.kasprintf print
+  else Format.ikfprintf ignore Format.std_formatter
+
+let debug fmt = log ~level:Level.Debug fmt
+let warn fmt = log ~level:Level.Warning fmt
+let error fmt = log ~level:Level.Error fmt
diff --git a/src/ocaml-index/lib/log.mli b/src/ocaml-index/lib/log.mli
new file mode 100644
index 000000000..a02cb6e70
--- /dev/null
+++ b/src/ocaml-index/lib/log.mli
@@ -0,0 +1,9 @@
+module Level : sig
+  type t = Debug | Warning | Error
+end
+
+val set_log_level : Level.t -> unit
+val log : level:Level.t -> ('a, Format.formatter, unit, unit) format4 -> 'a
+val debug : ('a, Format.formatter, unit, unit) format4 -> 'a
+val warn : ('a, Format.formatter, unit, unit) format4 -> 'a
+val error : ('a, Format.formatter, unit, unit) format4 -> 'a
diff --git a/src/ocaml-index/tests/dune b/src/ocaml-index/tests/dune
new file mode 100644
index 000000000..e304a31f6
--- /dev/null
+++ b/src/ocaml-index/tests/dune
@@ -0,0 +1,10 @@
+(alias
+ (name ocaml-index-test-deps)
+ (deps
+  (package ocaml-index)))
+
+(cram
+ (package ocaml-index)
+ (applies_to :whole_subtree)
+ (deps
+  (alias ocaml-index-test-deps)))
diff --git a/src/ocaml-index/tests/tests-dirs/cmd.t b/src/ocaml-index/tests/tests-dirs/cmd.t
new file mode 100644
index 000000000..ade4fee5a
--- /dev/null
+++ b/src/ocaml-index/tests/tests-dirs/cmd.t
@@ -0,0 +1,16 @@
+  $ ocaml-index aggregate
+  $ ocaml-index aggregate --debug
+  [debug] Debug log is enabled
+
+  $ ocaml-index --help
+  ocaml-index [COMMAND] [-verbose] <file1> [<file2>] ... -o <output>
+    --verbose Output more information
+    --debug Output debugging information
+    -o Set output file name
+    --root Set the root path for all relative locations
+    --rewrite-root Rewrite locations paths using the provided root
+    --store-shapes Aggregate input-indexes shapes and store them in the new index
+    -I An extra directory to add to the load path
+    --no-cmt-load-path Do not initialize the load path with the paths found in the first input cmt file
+    -help  Display this list of options
+    --help  Display this list of options
diff --git a/src/ocaml-index/tests/tests-dirs/flags.t b/src/ocaml-index/tests/tests-dirs/flags.t
new file mode 100644
index 000000000..1ea8399ca
--- /dev/null
+++ b/src/ocaml-index/tests/tests-dirs/flags.t
@@ -0,0 +1,33 @@
+  $ cat >main.ml <<EOF
+  > let x = 42
+  > let () = print_int x
+  > EOF
+
+  $ ocamlc -bin-annot -bin-annot-occurrences -c main.ml
+
+  $ ocaml-index aggregate main.cmt
+
+Default output file:
+  $ ls project.ocaml-index
+  project.ocaml-index
+
+Set output file:
+  $ ocaml-index aggregate main.cmt -o out
+  $ ls out
+  out
+
+No root dir was given:
+  $ ocaml-index stats project.ocaml-index | grep root
+  - root dir: none
+
+We provide one:
+  $ ocaml-index aggregate main.cmt --root /tmp/
+  $ ocaml-index stats project.ocaml-index | grep root
+  - root dir: /tmp/
+
+Rewrite locations:
+  $ ocaml-index aggregate main.cmt --root /tmp/ --rewrite-root
+  $ ocaml-index dump project.ocaml-index | grep File
+     "x": File "/tmp/main.ml", line 1, characters 4-5;
+     "x": File "/tmp/main.ml", line 2, characters 19-20
+     "print_int": File "/tmp/main.ml", line 2, characters 9-18
diff --git a/src/ocaml-index/tests/tests-dirs/index-project.t b/src/ocaml-index/tests/tests-dirs/index-project.t
new file mode 100644
index 000000000..d521d6918
--- /dev/null
+++ b/src/ocaml-index/tests/tests-dirs/index-project.t
@@ -0,0 +1,134 @@
+  $ cat >main.ml <<EOF
+  > let x = Foo.x + Foo.y
+  > let y = Foo.y + Bar.z
+  > type pouet = Foo.t
+  > let _, z = let x = 1 in x + y, 42
+  > module A = struct 
+  >   let ina = 42
+  >   let _ = ina
+  > end
+  > let _ = A.ina
+  > module _ = Foo
+  > include Foo
+  > EOF
+
+  $ cat >foo.ml <<EOF
+  > type t
+  > let x = 42
+  > let y = 36 + Bar.z + x
+  > EOF
+
+  $ cat >bar.ml <<EOF
+  > let z = 42
+  > EOF
+
+  $ ocamlc -bin-annot -bin-annot-occurrences -c bar.ml foo.ml main.ml
+
+  $ ocaml-index aggregate -o main.uideps main.cmt
+  $ ocaml-index aggregate -o foo.uideps foo.cmt
+  $ ocaml-index aggregate -o bar.uideps bar.cmt
+
+  $ ocaml-index dump main.uideps
+  13 uids:
+  {uid: Foo; locs:
+     "Foo": File "main.ml", line 10, characters 11-14;
+     "Foo": File "main.ml", line 11, characters 8-11
+   uid: Bar.0; locs: "Bar.z": File "main.ml", line 2, characters 16-21
+   uid: Foo.0; locs: "Foo.t": File "main.ml", line 3, characters 13-18
+   uid: Foo.1; locs: "Foo.x": File "main.ml", line 1, characters 8-13
+   uid: Foo.2; locs:
+     "Foo.y": File "main.ml", line 1, characters 16-21;
+     "Foo.y": File "main.ml", line 2, characters 8-13
+   uid: Main.0; locs: "x": File "main.ml", line 1, characters 4-5
+   uid: Main.1; locs:
+     "y": File "main.ml", line 2, characters 4-5;
+     "y": File "main.ml", line 4, characters 28-29
+   uid: Main.2; locs: "pouet": File "main.ml", line 3, characters 5-10
+   uid: Main.3; locs: "z": File "main.ml", line 4, characters 7-8
+   uid: Main.4; locs:
+     "x": File "main.ml", line 4, characters 15-16;
+     "x": File "main.ml", line 4, characters 24-25
+   uid: Main.5; locs:
+     "ina": File "main.ml", line 6, characters 6-9;
+     "ina": File "main.ml", line 7, characters 10-13;
+     "A.ina": File "main.ml", line 9, characters 8-13
+   uid: Main.6; locs: "A": File "main.ml", line 5, characters 7-8
+   uid: Stdlib.53; locs:
+     "+": File "main.ml", line 1, characters 14-15;
+     "+": File "main.ml", line 2, characters 14-15;
+     "+": File "main.ml", line 4, characters 26-27
+   }, 0 approx shapes: {}, and shapes for CUS .
+
+  $ ocaml-index dump foo.uideps
+  5 uids:
+  {uid: Bar.0; locs: "Bar.z": File "foo.ml", line 3, characters 13-18
+   uid: Foo.0; locs: "t": File "foo.ml", line 1, characters 5-6
+   uid: Foo.1; locs:
+     "x": File "foo.ml", line 2, characters 4-5;
+     "x": File "foo.ml", line 3, characters 21-22
+   uid: Foo.2; locs: "y": File "foo.ml", line 3, characters 4-5
+   uid: Stdlib.53; locs:
+     "+": File "foo.ml", line 3, characters 11-12;
+     "+": File "foo.ml", line 3, characters 19-20
+   }, 0 approx shapes: {}, and shapes for CUS .
+
+
+
+  $ ocaml-index -o test.uideps main.cmt foo.cmt bar.cmt
+  $ ocaml-index dump test.uideps
+  13 uids:
+  {uid: Foo; locs:
+     "Foo": File "main.ml", line 10, characters 11-14;
+     "Foo": File "main.ml", line 11, characters 8-11
+   uid: Bar.0; locs:
+     "z": File "bar.ml", line 1, characters 4-5;
+     "Bar.z": File "foo.ml", line 3, characters 13-18;
+     "Bar.z": File "main.ml", line 2, characters 16-21
+   uid: Foo.0; locs:
+     "t": File "foo.ml", line 1, characters 5-6;
+     "Foo.t": File "main.ml", line 3, characters 13-18
+   uid: Foo.1; locs:
+     "x": File "foo.ml", line 2, characters 4-5;
+     "x": File "foo.ml", line 3, characters 21-22;
+     "Foo.x": File "main.ml", line 1, characters 8-13
+   uid: Foo.2; locs:
+     "y": File "foo.ml", line 3, characters 4-5;
+     "Foo.y": File "main.ml", line 1, characters 16-21;
+     "Foo.y": File "main.ml", line 2, characters 8-13
+   uid: Main.0; locs: "x": File "main.ml", line 1, characters 4-5
+   uid: Main.1; locs:
+     "y": File "main.ml", line 2, characters 4-5;
+     "y": File "main.ml", line 4, characters 28-29
+   uid: Main.2; locs: "pouet": File "main.ml", line 3, characters 5-10
+   uid: Main.3; locs: "z": File "main.ml", line 4, characters 7-8
+   uid: Main.4; locs:
+     "x": File "main.ml", line 4, characters 15-16;
+     "x": File "main.ml", line 4, characters 24-25
+   uid: Main.5; locs:
+     "ina": File "main.ml", line 6, characters 6-9;
+     "ina": File "main.ml", line 7, characters 10-13;
+     "A.ina": File "main.ml", line 9, characters 8-13
+   uid: Main.6; locs: "A": File "main.ml", line 5, characters 7-8
+   uid: Stdlib.53; locs:
+     "+": File "foo.ml", line 3, characters 11-12;
+     "+": File "foo.ml", line 3, characters 19-20;
+     "+": File "main.ml", line 1, characters 14-15;
+     "+": File "main.ml", line 2, characters 14-15;
+     "+": File "main.ml", line 4, characters 26-27
+   }, 0 approx shapes: {}, and shapes for CUS .
+
+  $ ocaml-index stats foo.uideps test.uideps
+  Index "test.uideps" contains:
+  - 13 definitions
+  - 29 locations
+  - 0 approximated definitions
+  - 0 compilation units shapes
+  - root dir: none
+  
+  Index "foo.uideps" contains:
+  - 5 definitions
+  - 7 locations
+  - 0 approximated definitions
+  - 0 compilation units shapes
+  - root dir: none
+  
diff --git a/src/ocaml-index/tests/tests-dirs/interfaces.t b/src/ocaml-index/tests/tests-dirs/interfaces.t
new file mode 100644
index 000000000..2232ed41b
--- /dev/null
+++ b/src/ocaml-index/tests/tests-dirs/interfaces.t
@@ -0,0 +1,23 @@
+  $ cat >main.mli <<'EOF'
+  > type t = Float.t
+  > EOF
+
+  $ ocamlc -bin-annot -bin-annot-occurrences -c main.mli
+
+  $ ls
+  main.cmi
+  main.cmti
+  main.mli
+
+  $ ocamlobjinfo -quiet -index main.cmti
+  Indexed shapes:
+  Unresolved: CU Stdlib . "Float"[module] . "t"[type]  :
+    Float.t (File "main.mli", line 1, characters 9-16)
+
+  $ ocaml-index aggregate main.cmti -o main.index
+
+  $ ocaml-index dump main.index
+  1 uids:
+  {uid: Stdlib__Float.81; locs:
+     "Float.t": File "main.mli", line 1, characters 9-16
+   }, 0 approx shapes: {}, and shapes for CUS .
diff --git a/src/ocaml-index/tests/tests-dirs/local-shape-and-include.t b/src/ocaml-index/tests/tests-dirs/local-shape-and-include.t
new file mode 100644
index 000000000..120db8e51
--- /dev/null
+++ b/src/ocaml-index/tests/tests-dirs/local-shape-and-include.t
@@ -0,0 +1,56 @@
+  $ cat >main.ml <<EOF
+  > let f = String.equal
+  > module B : sig 
+  >   val g : string -> string -> bool
+  > end = struct
+  >   module C = struct
+  >     include External
+  >     let g = equal
+  >   end
+  >   let g = C.g
+  > end
+  > EOF
+
+  $ cat >external.ml <<EOF
+  > let equal = String.equal
+  > EOF
+
+  $ ocamlc -bin-annot -bin-annot-occurrences -c external.ml main.ml
+
+  $ ocaml-index aggregate -o main.uideps main.cmt
+
+  $ ocaml-index aggregate -o test.uideps main.uideps
+
+  $ ocaml-index dump main.uideps
+  9 uids:
+  {uid: External; locs: "External": File "main.ml", line 6, characters 12-20
+   uid: External.0; locs: "equal": File "main.ml", line 7, characters 12-17
+   uid: Main.0; locs: "f": File "main.ml", line 1, characters 4-5
+   uid: Main.1; locs:
+     "g": File "main.ml", line 7, characters 8-9;
+     "C.g": File "main.ml", line 9, characters 10-13
+   uid: Main.2; locs: "C": File "main.ml", line 5, characters 9-10
+   uid: Main.3; locs: "g": File "main.ml", line 9, characters 6-7
+   uid: Main.4; locs: "g": File "main.ml", line 3, characters 6-7
+   uid: Main.5; locs: "B": File "main.ml", line 2, characters 7-8
+   uid: Stdlib__String.173; locs:
+     "String.equal": File "main.ml", line 1, characters 8-20
+   }, 0 approx shapes: {}, and shapes for CUS .
+
+
+  $ ocaml-index dump test.uideps
+  9 uids:
+  {uid: External; locs: "External": File "main.ml", line 6, characters 12-20
+   uid: External.0; locs: "equal": File "main.ml", line 7, characters 12-17
+   uid: Main.0; locs: "f": File "main.ml", line 1, characters 4-5
+   uid: Main.1; locs:
+     "g": File "main.ml", line 7, characters 8-9;
+     "C.g": File "main.ml", line 9, characters 10-13
+   uid: Main.2; locs: "C": File "main.ml", line 5, characters 9-10
+   uid: Main.3; locs: "g": File "main.ml", line 9, characters 6-7
+   uid: Main.4; locs: "g": File "main.ml", line 3, characters 6-7
+   uid: Main.5; locs: "B": File "main.ml", line 2, characters 7-8
+   uid: Stdlib__String.173; locs:
+     "String.equal": File "main.ml", line 1, characters 8-20
+   }, 0 approx shapes: {}, and shapes for CUS .
+
diff --git a/src/ocaml-index/tests/tests-dirs/transitive-deps.t b/src/ocaml-index/tests/tests-dirs/transitive-deps.t
new file mode 100644
index 000000000..416469105
--- /dev/null
+++ b/src/ocaml-index/tests/tests-dirs/transitive-deps.t
@@ -0,0 +1,53 @@
+  $ cat >main.ml <<EOF
+  > let x = List.init Foo.x (fun n -> n)
+  > EOF
+
+  $ mkdir lib1
+  $ cat >lib1/foo.ml <<EOF
+  > include Bar
+  > EOF
+
+  $ mkdir lib2
+  $ cat >lib2/bar.ml <<EOF
+  > let x = 21
+  > EOF
+
+  $ ocamlc -bin-annot -bin-annot-occurrences -c lib2/bar.ml
+  $ ocamlc -bin-annot -bin-annot-occurrences -c lib1/foo.ml -I lib2
+
+# Here we have an implicit transitive dependency on lib2:
+  $ ocamlc -bin-annot -bin-annot-occurrences -c main.ml -I lib1 -I /Users/ulysse/tmp/occurrences/_opam/lib/fpath
+
+# We pass explicitely the implicit transitive dependency over lib2:
+  $ ocaml-index aggregate -o main.uideps main.cmt -I lib2
+  $ ocaml-index aggregate -o lib1/foo.uideps lib1/foo.cmt
+  $ ocaml-index aggregate -o lib2/bar.uideps lib2/bar.cmt
+
+  $ ocaml-index aggregate -o test.uideps main.uideps lib1/foo.uideps lib2/bar.uideps
+
+  $ ocaml-index dump main.uideps
+  4 uids:
+  {uid: Bar.0; locs: "Foo.x": File "main.ml", line 1, characters 18-23
+   uid: Main.0; locs: "x": File "main.ml", line 1, characters 4-5
+   uid: Main.1; locs: "n": File "main.ml", line 1, characters 34-35
+   uid: Stdlib__List.45; locs:
+     "List.init": File "main.ml", line 1, characters 8-17
+   }, 0 approx shapes: {}, and shapes for CUS .
+
+  $ ocaml-index dump lib1/foo.uideps
+  1 uids:
+  {uid: Bar; locs: "Bar": File "lib1/foo.ml", line 1, characters 8-11 },
+  0 approx shapes: {}, and shapes for CUS .
+
+  $ ocaml-index dump test.uideps
+  5 uids:
+  {uid: Bar; locs: "Bar": File "lib1/foo.ml", line 1, characters 8-11
+   uid: Bar.0; locs:
+     "x": File "lib2/bar.ml", line 1, characters 4-5;
+     "Foo.x": File "main.ml", line 1, characters 18-23
+   uid: Main.0; locs: "x": File "main.ml", line 1, characters 4-5
+   uid: Main.1; locs: "n": File "main.ml", line 1, characters 34-35
+   uid: Stdlib__List.45; locs:
+     "List.init": File "main.ml", line 1, characters 8-17
+   }, 0 approx shapes: {}, and shapes for CUS .
+
diff --git a/tests/test-dirs/occurrences/project-wide/dune b/tests/test-dirs/occurrences/project-wide/dune
index 45fd154be..9b429078a 100644
--- a/tests/test-dirs/occurrences/project-wide/dune
+++ b/tests/test-dirs/occurrences/project-wide/dune
@@ -1,10 +1,11 @@
 (cram
  (applies_to :whole_subtree)
- (enabled_if %{bin-available:ocaml-index}))
+ (deps
+  %{bin:ocaml-index}))
 
 (cram
  (applies_to pwo-ml-gen)
- (enabled_if (and %{bin-available:ocaml-index} (>= %{read:version/dune.txt} "3.16"))))
+ (enabled_if (>= %{read:version/dune.txt} "3.16")))
 
 (subdir
  version