My agentic slop goes here. Not intended for anyone else!

more

Changed files
+187 -19
stack
+88 -12
stack/sortal/lib/sortal.ml
···
module Contact = struct
type t = {
handle : string;
···
mastodon : string option;
orcid : string option;
url : string option;
-
atom_feeds : string list option;
}
let make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon
-
?orcid ?url ?atom_feeds () =
{ handle; names; email; icon; github; twitter; bluesky; mastodon;
-
orcid; url; atom_feeds }
let handle t = t.handle
let names t = t.names
let name t = List.hd t.names
let email t = t.email
let icon t = t.icon
let github t = t.github
···
let mastodon t = t.mastodon
let orcid t = t.orcid
let url t = t.url
-
let atom_feeds t = t.atom_feeds
let best_url t =
match t.url with
···
let open Jsont in
let open Jsont.Object in
let mem_opt f v ~enc = mem f v ~dec_absent:None ~enc_omit:Option.is_none ~enc in
-
let make handle names email icon github twitter bluesky mastodon orcid url atom_feeds =
{ handle; names; email; icon; github; twitter; bluesky; mastodon;
-
orcid; url; atom_feeds }
in
map ~kind:"Contact" make
|> mem "handle" string ~enc:handle
···
|> mem_opt "mastodon" (some string) ~enc:mastodon
|> mem_opt "orcid" (some string) ~enc:orcid
|> mem_opt "url" (some string) ~enc:url
-
|> mem_opt "atom_feeds" (some (list string)) ~enc:atom_feeds
|> finish
let compare a b = String.compare a.handle b.handle
···
(match t.icon with
| Some i -> pf ppf "%a: %a@," (styled `Bold string) "Icon" string i
| None -> ());
-
(match t.atom_feeds with
| Some feeds when feeds <> [] ->
-
pf ppf "%a: @[<h>%a@]@," (styled `Bold string) "Atom Feeds"
-
(list ~sep:comma string) feeds
| _ -> ());
pf ppf "@]"
end
···
let with_github = List.filter (fun c -> Contact.github c <> None) contacts |> List.length in
let with_orcid = List.filter (fun c -> Contact.orcid c <> None) contacts |> List.length in
let with_url = List.filter (fun c -> Contact.url c <> None) contacts |> List.length in
-
let with_feeds = List.filter (fun c -> Contact.atom_feeds c <> None) contacts |> List.length in
Logs.app (fun m -> m "Contact Database Statistics:");
Logs.app (fun m -> m " Total contacts: %d" total);
···
Logs.app (fun m -> m " With GitHub: %d (%.1f%%)" with_github (float_of_int with_github /. float_of_int total *. 100.));
Logs.app (fun m -> m " With ORCID: %d (%.1f%%)" with_orcid (float_of_int with_orcid /. float_of_int total *. 100.));
Logs.app (fun m -> m " With URL: %d (%.1f%%)" with_url (float_of_int with_url /. float_of_int total *. 100.));
-
Logs.app (fun m -> m " With Atom feeds: %d (%.1f%%)" with_feeds (float_of_int with_feeds /. float_of_int total *. 100.));
0
(* Command info objects *)
···
+
module Feed = struct
+
type feed_type =
+
| Atom
+
| Rss
+
| Json
+
+
type t = {
+
feed_type : feed_type;
+
url : string;
+
name : string option;
+
}
+
+
let make ~feed_type ~url ?name () =
+
{ feed_type; url; name }
+
+
let feed_type t = t.feed_type
+
let url t = t.url
+
let name t = t.name
+
+
let set_name t name = { t with name = Some name }
+
+
let feed_type_to_string = function
+
| Atom -> "atom"
+
| Rss -> "rss"
+
| Json -> "json"
+
+
let feed_type_of_string = function
+
| "atom" -> Some Atom
+
| "rss" -> Some Rss
+
| "json" -> Some Json
+
| _ -> None
+
+
let json_t =
+
let open Jsont in
+
let open Jsont.Object in
+
let make feed_type url name =
+
match feed_type_of_string feed_type with
+
| Some ft -> { feed_type = ft; url; name }
+
| None -> failwith ("Invalid feed type: " ^ feed_type)
+
in
+
map ~kind:"Feed" make
+
|> mem "type" string ~enc:(fun f -> feed_type_to_string f.feed_type)
+
|> mem "url" string ~enc:(fun f -> f.url)
+
|> opt_mem "name" string ~enc:(fun f -> f.name)
+
|> finish
+
+
let pp ppf t =
+
let open Fmt in
+
pf ppf "%s: %s%a"
+
(feed_type_to_string t.feed_type)
+
t.url
+
(option (fun ppf name -> pf ppf " (%s)" name)) t.name
+
end
+
module Contact = struct
type t = {
handle : string;
···
mastodon : string option;
orcid : string option;
url : string option;
+
feeds : Feed.t list option;
}
let make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon
+
?orcid ?url ?feeds () =
{ handle; names; email; icon; github; twitter; bluesky; mastodon;
+
orcid; url; feeds }
let handle t = t.handle
let names t = t.names
let name t = List.hd t.names
+
let primary_name = name
let email t = t.email
let icon t = t.icon
let github t = t.github
···
let mastodon t = t.mastodon
let orcid t = t.orcid
let url t = t.url
+
let feeds t = t.feeds
+
+
let add_feed t feed =
+
let feeds = match t.feeds with
+
| Some fs -> Some (feed :: fs)
+
| None -> Some [feed]
+
in
+
{ t with feeds }
+
+
let remove_feed t url =
+
let feeds = match t.feeds with
+
| Some fs -> Some (List.filter (fun f -> Feed.url f <> url) fs)
+
| None -> None
+
in
+
{ t with feeds }
let best_url t =
match t.url with
···
let open Jsont in
let open Jsont.Object in
let mem_opt f v ~enc = mem f v ~dec_absent:None ~enc_omit:Option.is_none ~enc in
+
let make handle names email icon github twitter bluesky mastodon orcid url feeds =
{ handle; names; email; icon; github; twitter; bluesky; mastodon;
+
orcid; url; feeds }
in
map ~kind:"Contact" make
|> mem "handle" string ~enc:handle
···
|> mem_opt "mastodon" (some string) ~enc:mastodon
|> mem_opt "orcid" (some string) ~enc:orcid
|> mem_opt "url" (some string) ~enc:url
+
|> mem_opt "feeds" (some (list Feed.json_t)) ~enc:feeds
|> finish
let compare a b = String.compare a.handle b.handle
···
(match t.icon with
| Some i -> pf ppf "%a: %a@," (styled `Bold string) "Icon" string i
| None -> ());
+
(match t.feeds with
| Some feeds when feeds <> [] ->
+
pf ppf "%a:@," (styled `Bold string) "Feeds";
+
List.iter (fun feed ->
+
pf ppf " - %a@," Feed.pp feed
+
) feeds
| _ -> ());
pf ppf "@]"
end
···
let with_github = List.filter (fun c -> Contact.github c <> None) contacts |> List.length in
let with_orcid = List.filter (fun c -> Contact.orcid c <> None) contacts |> List.length in
let with_url = List.filter (fun c -> Contact.url c <> None) contacts |> List.length in
+
let with_feeds = List.filter (fun c -> Contact.feeds c <> None) contacts |> List.length in
+
let total_feeds = List.fold_left (fun acc c ->
+
match Contact.feeds c with
+
| Some feeds -> acc + List.length feeds
+
| None -> acc
+
) 0 contacts in
Logs.app (fun m -> m "Contact Database Statistics:");
Logs.app (fun m -> m " Total contacts: %d" total);
···
Logs.app (fun m -> m " With GitHub: %d (%.1f%%)" with_github (float_of_int with_github /. float_of_int total *. 100.));
Logs.app (fun m -> m " With ORCID: %d (%.1f%%)" with_orcid (float_of_int with_orcid /. float_of_int total *. 100.));
Logs.app (fun m -> m " With URL: %d (%.1f%%)" with_url (float_of_int with_url /. float_of_int total *. 100.));
+
Logs.app (fun m -> m " With feeds: %d (%.1f%%), total %d feeds" with_feeds (float_of_int with_feeds /. float_of_int total *. 100.) total_feeds);
0
(* Command info objects *)
+63 -5
stack/sortal/lib/sortal.mli
···
]}
*)
(** {1 Contact Metadata} *)
module Contact : sig
···
type t
(** [make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon
-
?orcid ?url ?atom_feeds ()] creates a new contact.
@param handle A unique identifier/username for this contact (required)
@param names A list of names for this contact, with the first being primary (required)
···
@param mastodon Mastodon handle (including instance)
@param orcid ORCID identifier
@param url Personal or professional website URL
-
@param atom_feeds List of Atom/RSS feed URLs associated with this contact
*)
val make :
handle:string ->
···
?mastodon:string ->
?orcid:string ->
?url:string ->
-
?atom_feeds:string list ->
unit ->
t
···
(** [name t] returns the primary (first) name. *)
val name : t -> string
(** [email t] returns the email address if available. *)
val email : t -> string option
···
(** [url t] returns the personal/professional website URL if available. *)
val url : t -> string option
-
(** [atom_feeds t] returns the list of Atom/RSS feed URLs if available. *)
-
val atom_feeds : t -> string list option
(** {2 Derived Information} *)
···
]}
*)
+
(** {1 Feed Metadata} *)
+
+
module Feed : sig
+
(** Feed subscription with type and URL.
+
+
A feed represents a subscription to a content source (Atom, RSS, or JSONFeed). *)
+
type t
+
+
(** Feed type identifier. *)
+
type feed_type =
+
| Atom (** Atom feed format *)
+
| Rss (** RSS feed format *)
+
| Json (** JSON Feed format *)
+
+
(** [make ~feed_type ~url ?name ()] creates a new feed.
+
+
@param feed_type The type of feed (Atom, RSS, or JSON)
+
@param url The feed URL
+
@param name Optional human-readable name/label for the feed
+
*)
+
val make : feed_type:feed_type -> url:string -> ?name:string -> unit -> t
+
+
(** [feed_type t] returns the feed type. *)
+
val feed_type : t -> feed_type
+
+
(** [url t] returns the feed URL. *)
+
val url : t -> string
+
+
(** [name t] returns the feed name if set. *)
+
val name : t -> string option
+
+
(** [set_name t name] returns a new feed with the name updated. *)
+
val set_name : t -> string -> t
+
+
(** [feed_type_to_string ft] converts a feed type to a string. *)
+
val feed_type_to_string : feed_type -> string
+
+
(** [feed_type_of_string s] parses a feed type from a string.
+
Returns [None] if the string is not recognized. *)
+
val feed_type_of_string : string -> feed_type option
+
+
(** [json_t] is the jsont encoder/decoder for feeds. *)
+
val json_t : t Jsont.t
+
+
(** [pp ppf t] pretty prints a feed. *)
+
val pp : Format.formatter -> t -> unit
+
end
+
(** {1 Contact Metadata} *)
module Contact : sig
···
type t
(** [make ~handle ~names ?email ?icon ?github ?twitter ?bluesky ?mastodon
+
?orcid ?url ?feeds ()] creates a new contact.
@param handle A unique identifier/username for this contact (required)
@param names A list of names for this contact, with the first being primary (required)
···
@param mastodon Mastodon handle (including instance)
@param orcid ORCID identifier
@param url Personal or professional website URL
+
@param feeds List of feed subscriptions (Atom/RSS/JSON) associated with this contact
*)
val make :
handle:string ->
···
?mastodon:string ->
?orcid:string ->
?url:string ->
+
?feeds:Feed.t list ->
unit ->
t
···
(** [name t] returns the primary (first) name. *)
val name : t -> string
+
(** [primary_name t] returns the primary (first) name.
+
This is an alias for {!name} for clarity. *)
+
val primary_name : t -> string
+
(** [email t] returns the email address if available. *)
val email : t -> string option
···
(** [url t] returns the personal/professional website URL if available. *)
val url : t -> string option
+
(** [feeds t] returns the list of feed subscriptions if available. *)
+
val feeds : t -> Feed.t list option
+
+
(** [add_feed t feed] returns a new contact with the feed added. *)
+
val add_feed : t -> Feed.t -> t
+
+
(** [remove_feed t url] returns a new contact with the feed matching the URL removed. *)
+
val remove_feed : t -> string -> t
(** {2 Derived Information} *)
+36 -2
stack/sortal/scripts/import_yaml_contacts.py
···
return yaml.safe_load(yaml_content)
return None
def convert_to_sortal_format(yaml_data, handle):
"""Convert YAML contact data to Sortal JSON format."""
sortal_contact = {
···
sortal_contact["orcid"] = yaml_data["orcid"]
if "url" in yaml_data:
sortal_contact["url"] = yaml_data["url"]
if "atom" in yaml_data:
-
sortal_contact["atom_feeds"] = yaml_data["atom"]
return sortal_contact
···
print(f"Error: Source directory does not exist: {source_dir}")
return 1
imported_count = 0
error_count = 0
# Process each .md file
for md_file in sorted(source_dir.glob('*.md')):
···
json.dump(sortal_contact, f, indent=2, ensure_ascii=False)
name = sortal_contact.get('names', [handle])[0] if sortal_contact.get('names') else handle
-
print(f"✓ Imported: {handle} ({name})")
imported_count += 1
except Exception as e:
···
print()
print(f"Import complete!")
print(f" Successfully imported: {imported_count}")
print(f" Errors: {error_count}")
print(f" Output directory: {dest_dir}")
···
return yaml.safe_load(yaml_content)
return None
+
def detect_feed_type(url):
+
"""Detect feed type based on URL patterns."""
+
url_lower = url.lower()
+
if 'json' in url_lower or url_lower.endswith('.json'):
+
return 'json'
+
elif 'rss' in url_lower or url_lower.endswith('.rss') or url_lower.endswith('.xml'):
+
return 'rss'
+
else:
+
# Default to atom for most feeds
+
return 'atom'
+
def convert_to_sortal_format(yaml_data, handle):
"""Convert YAML contact data to Sortal JSON format."""
sortal_contact = {
···
sortal_contact["orcid"] = yaml_data["orcid"]
if "url" in yaml_data:
sortal_contact["url"] = yaml_data["url"]
+
+
# Convert atom feeds to new feed structure
if "atom" in yaml_data:
+
atom_feeds = yaml_data["atom"]
+
if atom_feeds:
+
feeds = []
+
for feed_url in atom_feeds:
+
feed_type = detect_feed_type(feed_url)
+
feed_obj = {
+
"type": feed_type,
+
"url": feed_url
+
}
+
feeds.append(feed_obj)
+
sortal_contact["feeds"] = feeds
return sortal_contact
···
print(f"Error: Source directory does not exist: {source_dir}")
return 1
+
# Delete existing contacts to avoid old schema
+
print("Clearing existing contacts...")
+
for existing_file in dest_dir.glob('*.json'):
+
existing_file.unlink()
+
imported_count = 0
error_count = 0
+
total_feeds = 0
# Process each .md file
for md_file in sorted(source_dir.glob('*.md')):
···
json.dump(sortal_contact, f, indent=2, ensure_ascii=False)
name = sortal_contact.get('names', [handle])[0] if sortal_contact.get('names') else handle
+
feed_count = len(sortal_contact.get('feeds', []))
+
total_feeds += feed_count
+
+
feed_info = f" ({feed_count} feed{'s' if feed_count != 1 else ''})" if feed_count > 0 else ""
+
print(f"✓ Imported: {handle} ({name}){feed_info}")
imported_count += 1
except Exception as e:
···
print()
print(f"Import complete!")
print(f" Successfully imported: {imported_count}")
+
print(f" Total feeds: {total_feeds}")
print(f" Errors: {error_count}")
print(f" Output directory: {dest_dir}")