score API

包 Luna-Flow/mooncake-impact-factor/score 根据四个整数信号计算 MoonBit 包的影响力分数,为分数加上等级和势头类别标签,并把所有结果打包成一个 ScoreSnapshot。每个函数都是纯函数且是全函数:从不中止,也没有隐藏状态。

源码:src/score/impact_factor.mbt。公式背后的数学见 score 设计。

在 moon.pkg 中导入该包:

import {
  "Luna-Flow/mooncake-impact-factor/score",
}

信号

每个评分函数都按以下顺序接收同样的四个信号:

参数含义
dependents依赖本包的包的数量。
recent_dependents这些依赖方中首次出现在近期窗口内的数量。
downloads下载次数,未知时为 0。
days_since_release距最新发布的整天数。

这些函数不检查信号之间是否一致,例如是否满足 recent_dependents <= dependents。负值会被截为 0。索引构建器根据本地注册表快照填写这些信号;参见架构指南。

分数

compute_score

compute_score 返回一个包的影响力分数。

pub fn compute_score(Int, Int, Int, Int) -> Double

设 σ(n)=ln⁡(1+max⁡(n,0))\sigma(n) = \ln(1 + \max(n, 0)),DD 为依赖方数量,RR 为近期依赖方数量,WW 为下载量,tt 为距发布的天数,则结果为

S=m(t) (38 σ(D)+27 σ(R)+22 σ(W))S = m(t)\,\bigl(38\,\sigma(D) + 27\,\sigma(R) + 22\,\sigma(W)\bigr)

其中 m(t)m(t) 是 activity_multiplier(t)。当且仅当三个计数都为 00 或负数时分数为 00,并且任何计数增大时分数都不会减小。按设计分数没有上限,但当计数小于 231−12^{31} - 1 时,分数始终低于约 20942094。

test "compute_score" {
  let score = @score.compute_score(20, 4, 300, 40)
  inspect(score, content="301.7852882193072")
  inspect(@score.compute_score(0, 0, 0, 10), content="0")
}

activity_multiplier

activity_multiplier 返回作用于整个分数的发布时效因子 m(t)m(t)。

pub fn activity_multiplier(Int) -> Double

负的 days_since_release 按 0 处理。

距发布的天数乘数
0 到 301.12
31 到 901.06
91 到 1801.0
181 到 3650.94
366 或更多0.88
test "activity_multiplier" {
  inspect(@score.activity_multiplier(-3), content="1.12")
  inspect(@score.activity_multiplier(90), content="1.06")
  inspect(@score.activity_multiplier(400), content="0.88")
}

clamp_non_negative

clamp_non_negative 在 value 非负时返回 value,否则返回 0。

pub fn clamp_non_negative(Int) -> Int

评分函数会对每个信号应用它。它是公开的,以便调用方用同样的方式准备信号。

test "clamp_non_negative" {
  inspect(@score.clamp_non_negative(-7), content="0")
  inspect(@score.clamp_non_negative(12), content="12")
}

标签

rank_label

rank_label 把分数映射到等级桶 S、A、B、C、D 之一。

pub fn rank_label(Double) -> String
等级条件
Sscore >= 260.0
A180.0 <= score < 260.0
B110.0 <= score < 180.0
C50.0 <= score < 110.0
Dscore < 50.0,或 score 为 NaN
test "rank_label" {
  inspect(@score.rank_label(260.0), content="S")
  inspect(@score.rank_label(259.99), content="A")
  inspect(@score.rank_label(12.0), content="D")
}

compute_momentum_label

compute_momentum_label 把分数增长的快慢分类为 Rising、Hot 或 Stable。

pub fn compute_momentum_label(Double, Double, Double, Int) -> String

参数依次为当前分数 SS、30 天前的分数 S30S_{30}、增长率 rr 和近期依赖方数量 RR。设增长量 G=S−S30G = S - S_{30}:

标签条件
RisingG≥35G \ge 35 且 r≥0.35r \ge 0.35 且 R≥3R \ge 3
Hot不是 Rising,且 G≥18G \ge 18、r≥0.18r \ge 0.18、R≥2R \ge 2
Stable其他情况,包括任一参数为 NaN

该函数不检查 growth_ratio 是否与两个分数相符;compute_score_snapshot 会替你计算它。

test "compute_momentum_label" {
  inspect(@score.compute_momentum_label(140.0, 100.0, 0.4, 3), content="Rising")
  inspect(@score.compute_momentum_label(140.0, 100.0, 0.4, 2), content="Hot")
  inspect(@score.compute_momentum_label(105.0, 100.0, 0.05, 1), content="Stable")
}

快照

ScoreSnapshot

ScoreSnapshot 是对一个包在某一时间点评分的完整结果。

pub struct ScoreSnapshot {
  score : Double
  score_30d_ago : Double
  score_growth_30d : Double
  score_growth_ratio_30d : Double
  rank_label : String
  momentum_label : String
  activity_multiplier : Double
} derive(ToJson, @debug.Debug, @json.FromJson)
字段含义
score当前分数 SS。
score_30d_ago由历史信号计算出的分数 S30S_{30}。
score_growth_30dG=S−S30G = S - S_{30};分数下降时为负。
score_growth_ratio_30d当 S30>0S_{30} > 0 时为 G/S30G / S_{30};否则 G>0G > 0 时为 11,G≤0G \le 0 时为 00。
rank_labelrank_label(score).
momentum_labelcompute_momentum_label(score, score_30d_ago, score_growth_ratio_30d, recent_dependents).
activity_multiplier当前信号对应的 activity_multiplier(days_since_release)。

这些字段在包外是只读的;请用 compute_score_snapshot 构造快照。ToJson 写出一个以字段名为键的对象,这正是 cli 命令的输出格式。FromJson 读回同样的格式,字段缺失或类型错误时抛出 @json.JsonDecodeError。Debug 支持 debug_inspect 和 Repr(...)。该结构体没有实现 Eq;请通过字段或 JSON 形式比较快照。

test "ScoreSnapshot JSON round trip" {
  let snapshot = @score.compute_score_snapshot(8, 2, 120, 12, 0, 0, 0, 0)
  let json = Json(snapshot)
  let back : @score.ScoreSnapshot = @json.from_json(json)
  assert_eq(Json(back), json)
  inspect(back.momentum_label, content="Hot")
}

compute_score_snapshot

compute_score_snapshot 对一个包分别计算当前和 30 天前的分数,并在一次调用中得出增长量、两个标签和乘数。

pub fn compute_score_snapshot(Int, Int, Int, Int, Int, Int, Int, Int) -> ScoreSnapshot

前四个参数是当前信号,后四个是 30 天前的信号,二者都按信号表的顺序排列:

compute_score_snapshot(
  dependents, recent_dependents, downloads, days_since_release,
  historical_dependents, historical_recent_dependents,
  historical_downloads, historical_days_since_release,
)

势头标签使用当前的 recent_dependents。30 天前尚不存在的包,其历史信号都为 0;此时它的 score_30d_ago 为 00,增长率为 11。

test "compute_score_snapshot" {
  let snapshot = @score.compute_score_snapshot(20, 4, 300, 40, 10, 2, 0, 10)
  debug_inspect(
    snapshot,
    content=(
      #|{
      #|  score: 301.7852882193072,
      #|  score_30d_ago: 135.2764584196223,
      #|  score_growth_30d: 166.5088297996849,
      #|  score_growth_ratio_30d: 1.2308780976744749,
      #|  rank_label: "S",
      #|  momentum_label: "Rising",
      #|  activity_multiplier: 1.06,
      #|}
    ),
  )
}

已弃用

ScoreSnapshot 仍保留三个方法,它们是早期 MoonBit 版本根据其派生 trait 隐式生成的。这些方法已从接口文件中隐藏,使用时会产生警告。

已弃用替代方案
ScoreSnapshot::to_json(s)Json(s)
ScoreSnapshot::from_json(json, path)@json.from_json(json)
ScoreSnapshot::to_repr(s)Repr(s)、debug_inspect 或 @debug.to_string