harn-stdlib 0.8.119

Embedded Harn standard library source catalog
Documentation
// std/disclosure — render surface-native authorship disclosure from ActorChain.
//
// Import: import { render } from "std/disclosure"
import { deep_merge } from "std/collections"

type DisclosureRenderOptions = {project?: bool, project_root?: string, env?: bool, config?: dict}

const __BUILTIN_DISCLOSURE_CONFIG = """
[defaults]
email_domain = "actors.harn.invalid"

[surfaces.git]
kind = "text"
template = "{{ if delegated }}Co-Authored-By: {{ current.name }} <{{ current.email }}>{{ for actor in prior_actors }}\nAssisted-by: {{ actor.name }} <{{ actor.email }}>{{ end }}{{ else }}Co-Authored-By: {{ origin.name }} <{{ origin.email }}>{{ end }}"

[surfaces.slack]
kind = "text"
template = "AI-assisted by {{ current.label }} for {{ origin.label }}."

[surfaces.github]
kind = "github_author_choice"
mode = "human_author_agent_coauthor"
author = "origin"
co_author = "current"
"""

fn __dict(value) -> dict {
  if type_of(value) == "dict" {
    return value
  }
  return {}
}

fn __as_config(value) -> dict {
  let dict = __dict(value)
  if contains(dict.keys(), "disclosure") {
    return __dict(dict.disclosure)
  }
  return dict
}

fn __as_project_config(value) -> dict {
  let dict = __dict(value)
  if contains(dict.keys(), "disclosure") {
    return __dict(dict.disclosure)
  }
  return {}
}

fn __parse_config_toml(text: string, label: string, project: bool = false) -> dict {
  let parsed = try {
    toml_parse(text)
  }
  if is_ok(parsed) {
    let data = unwrap(parsed)
    if project {
      return __as_project_config(data)
    }
    return __as_config(data)
  }
  throw "std/disclosure: invalid TOML in " + label
}

fn __read_config_file(path: string, project: bool = false) -> dict {
  if path == "" || !harness.fs.exists(path) {
    return {}
  }
  return __parse_config_toml(harness.fs.read_text(path), path, project)
}

fn __looks_like_toml(text: string) -> bool {
  let trimmed = trim(text)
  return contains(text, "\n") || starts_with(trimmed, "[")
}

fn __project_config(options: dict) -> dict {
  if options?.project == false {
    return {}
  }
  let root = options?.project_root ?? project_root()
  if root == nil || root == "" {
    return {}
  }
  return __read_config_file(path_join(root, "harn.toml"), true)
}

fn __env_config(options: dict) -> dict {
  if options?.env == false {
    return {}
  }
  var config = {}
  let path = harness.env.get("HARN_DISCLOSURE_CONFIG_PATH")
  if path != nil && path != "" {
    config = deep_merge(config, __read_config_file(to_string(path)))
  }
  let raw = harness.env.get("HARN_DISCLOSURE_CONFIG")
  if raw == nil || raw == "" {
    return config
  }
  let text = to_string(raw)
  if __looks_like_toml(text) {
    return deep_merge(config, __parse_config_toml(text, "HARN_DISCLOSURE_CONFIG"))
  }
  if harness.fs.exists(text) {
    return deep_merge(config, __read_config_file(text))
  }
  return deep_merge(config, __parse_config_toml(text, "HARN_DISCLOSURE_CONFIG"))
}

fn __config(options) -> dict {
  let opts = __dict(options)
  var config = __parse_config_toml(__BUILTIN_DISCLOSURE_CONFIG, "built-in disclosure config")
  config = deep_merge(config, __project_config(opts))
  config = deep_merge(config, __env_config(opts))
  config = deep_merge(config, __as_config(opts?.config))
  return config
}

fn __chain_subjects(chain) -> list<string> {
  require type_of(chain) == "dict", "std/disclosure: chain must be an ActorChain dict"
  require chain?.sub != nil && chain.sub != "", "std/disclosure: chain.sub is required"
  var subjects = []
  var node = chain?.act
  while node != nil {
    require type_of(node) == "dict", "std/disclosure: chain.act entries must be dicts"
    require node?.sub != nil && node.sub != "", "std/disclosure: chain.act.sub is required"
    subjects = subjects + [to_string(node.sub)]
    node = node?.act
  }
  return subjects + [to_string(chain.sub)]
}

fn __subject_parts(subject: string) -> dict {
  let split_at = subject.index_of(":")
  if split_at < 0 {
    return {kind: "principal", id: subject}
  }
  return {kind: substring(subject, 0, split_at), id: substring(subject, split_at + 1)}
}

fn __slug(value: string) -> string {
  let lowered = lowercase(trim(value))
  let cleaned = regex_replace("[^a-z0-9._+-]+", "-", lowered)
  let stripped = regex_replace("(^-+|-+$)", "", cleaned)
  if stripped == "" {
    return "actor"
  }
  return stripped
}

fn __profile_for(subject: string, config: dict) -> dict {
  let identities = __dict(config?.identities)
  if contains(identities.keys(), subject) {
    return __dict(identities[subject])
  }
  return {}
}

fn __principal(subject: string, config: dict, role: string, position: int) -> dict {
  let profile = __profile_for(subject, config)
  let parts = __subject_parts(subject)
  let name = to_string(profile?.name ?? profile?.display_name ?? profile?.label ?? parts.id ?? subject)
  let label = to_string(profile?.label ?? profile?.slack_label ?? profile?.display_name ?? name)
  let email_domain = to_string(config?.defaults?.email_domain ?? "actors.harn.invalid")
  let email = to_string(profile?.email ?? (__slug(parts.kind + "+" + parts.id) + "@" + email_domain))
  var principal = {
    subject: subject,
    kind: parts.kind,
    id: parts.id,
    role: role,
    position: position,
    name: name,
    label: label,
    email: email,
  }
  if profile?.github != nil {
    principal = principal + {github: to_string(profile.github)}
  }
  if profile?.github_login != nil {
    principal = principal + {github: to_string(profile.github_login)}
  }
  if profile?.login != nil {
    principal = principal + {github: to_string(profile.login)}
  }
  if profile?.slack != nil {
    principal = principal + {slack: to_string(profile.slack)}
  }
  if profile?.slack_id != nil {
    principal = principal + {slack: to_string(profile.slack_id)}
  }
  return principal
}

fn __context(chain, config: dict) -> dict {
  let subjects = __chain_subjects(chain)
  let origin_index = len(subjects) - 1
  var principals = []
  for {index, value} in subjects.enumerate() {
    let role = if index == 0 {
      "current"
    } else if index == origin_index {
      "origin"
    } else {
      "actor"
    }
    principals = principals + [__principal(value, config, role, index)]
  }
  let origin = principals[origin_index]
  let actors = if len(principals) > 1 {
    principals.slice(0, origin_index)
  } else {
    []
  }
  let current = principals[0]
  let prior_actors = if len(actors) > 1 {
    actors.slice(1, len(actors))
  } else {
    []
  }
  return {
    chain: chain,
    principals: principals,
    subjects: subjects,
    current: current,
    origin: origin,
    actors: actors,
    prior_actors: prior_actors,
    delegated: len(actors) > 0,
  }
}

fn __surface_config(config: dict, surface: string) -> dict {
  let surfaces = __dict(config?.surfaces)
  if !contains(surfaces.keys(), surface) {
    throw "std/disclosure: unknown disclosure surface `" + surface + "`"
  }
  let surface_config = __dict(surfaces[surface])
  if len(surface_config.keys()) == 0 {
    throw "std/disclosure: disclosure surface `" + surface + "` must be a table"
  }
  return surface_config
}

fn __select_principal(ctx: dict, selector) {
  let name = to_string(selector ?? "")
  if name == "origin" {
    return ctx.origin
  }
  if name == "current" {
    return ctx.current
  }
  if name == "none" || name == "" {
    return nil
  }
  for principal in ctx.principals {
    if principal.subject == name || principal.role == name {
      return principal
    }
  }
  throw "std/disclosure: unknown principal selector `" + name + "`"
}

fn __render_text(surface_config: dict, ctx: dict) -> string {
  let template = surface_config?.template
  require template != nil && template != "", "std/disclosure: text disclosure surfaces require a template"
  return render_string(to_string(template), ctx)
}

fn __render_github(surface_config: dict, ctx: dict) -> dict {
  let author = __select_principal(ctx, surface_config?.author ?? "origin")
  var co_author = __select_principal(ctx, surface_config?.co_author ?? "current")
  if author != nil && co_author != nil && author.subject == co_author.subject {
    co_author = nil
  }
  return {
    surface: "github",
    kind: "github_author_choice",
    mode: to_string(surface_config?.mode ?? "human_author_agent_coauthor"),
    author: author,
    co_author: co_author,
    principals: ctx.principals,
    actor_chain: ctx.chain,
  }
}

/**
 * Render a surface-native authorship disclosure artifact from an ActorChain.
 *
 * Built-in surface config is overlaid by `[disclosure]` in `harn.toml`, then
 * `HARN_DISCLOSURE_CONFIG_PATH` / `HARN_DISCLOSURE_CONFIG`, then
 * `options.config`. Supported built-in surfaces are `git`, `slack`, and
 * `github`.
 *
 * @effects: [fs-read, env-read]
 * @errors: [TypeError, ValueError]
 */
pub fn render(chain, surface: string, options: DisclosureRenderOptions = {}) -> any {
  let config = __config(options)
  let surface_config = __surface_config(config, surface)
  let ctx = __context(chain, config)
  let kind = to_string(surface_config?.kind ?? "text")
  if kind == "text" {
    return __render_text(surface_config, ctx)
  }
  if kind == "github_author_choice" {
    return __render_github(surface_config, ctx)
  }
  throw "std/disclosure: unsupported disclosure surface kind `" + kind + "`"
}