///|
pub(all) struct SubmissionIdentity {
applicant_name : String
phone : String
email : String
github_owner : String
repository_name : String
mooncakes_owner : String
package_name : String
} derive(Eq, Debug)
///|
pub fn SubmissionIdentity::SubmissionIdentity(
applicant_name? : String = "",
phone? : String = "",
email? : String = "",
github_owner? : String = "",
repository_name? : String = "",
mooncakes_owner? : String = "",
package_name? : String = "",
) -> SubmissionIdentity {
{
applicant_name,
phone,
email,
github_owner,
repository_name,
mooncakes_owner,
package_name,
}
}
///|
pub(all) struct IdentityEvidence {
key : String
value : String
source : String
note : String
} derive(Eq, Debug)
///|
pub fn IdentityEvidence::IdentityEvidence(
key? : String = "",
value? : String = "",
source? : String = "",
note? : String = "",
) -> IdentityEvidence {
{ key, value, source, note }
}
///|
pub(all) struct IdentityFinding {
id : String
severity : Severity
title : String
expected : String
actual : String
source : String
detail : String
} derive(Eq, Debug)
///|
pub(all) struct IdentityReport {
canonical : SubmissionIdentity
evidence : Array[IdentityEvidence]
findings : Array[IdentityFinding]
score : Int
verdict : String
} derive(Eq, Debug)
///|
pub fn identity_consistency(
canonical : SubmissionIdentity,
evidence : Array[IdentityEvidence],
) -> IdentityReport {
let findings = Array::new(capacity=evidence.length() + 2)
for item in evidence {
let expected = expected_identity_value(canonical, item.key)
if expected == "" {
findings.push(
identity_warn(
"identity.unknown-key",
"Unknown identity evidence key",
"",
item.value,
item.source,
"Evidence key `" +
item.key +
"` is not part of the canonical identity profile.",
),
)
} else if item.value.trim().is_empty() {
findings.push(
identity_fail(
item.key,
"Identity evidence is missing",
expected,
item.value,
item.source,
"Add the expected value to the referenced submission material.",
),
)
} else if normalized_identity(item.value) == normalized_identity(expected) {
findings.push(
identity_pass(
item.key,
"Identity evidence matches",
expected,
item.value,
item.source,
),
)
} else {
findings.push(
identity_fail(
item.key,
"Identity evidence does not match",
expected,
item.value,
item.source,
"Use one applicant identity across README, submission text, git config, repository URL and Mooncakes package metadata.",
),
)
}
}
let score = identity_score(findings)
{ canonical, evidence, findings, score, verdict: identity_verdict(findings) }
}
///|
pub fn acceptance_identity_report(
canonical : SubmissionIdentity,
snapshot : ProjectSnapshot,
submission_text : String,
git_user_name? : String = "",
git_user_email? : String = "",
git_remote? : String = "",
) -> IdentityReport {
let manifest = parse_manifest(snapshot.moon_mod)
let repository = if git_remote.trim().is_empty() {
manifest.repository
} else {
git_remote
}
let evidence = identity_evidence_from_snapshot(
canonical, snapshot, submission_text, manifest, repository, git_user_name, git_user_email,
)
identity_consistency(canonical, evidence)
}
///|
pub fn identity_markdown(report : IdentityReport) -> String {
report.to_markdown()
}
///|
pub fn identity_json(report : IdentityReport) -> String {
report.to_json()
}
///|
pub fn IdentityReport::has_mismatch(self : IdentityReport) -> Bool {
for finding in self.findings {
if finding.severity == Fail {
return true
}
}
false
}
///|
pub fn IdentityReport::to_markdown(self : IdentityReport) -> String {
let out = StringBuilder()
out.write_string("# ReviewProof Identity Proof\n\n")
out.write_string("- Verdict: " + self.verdict + "\n")
out.write_string("- Score: " + self.score.to_string() + "/100\n")
out.write_string("- GitHub owner: " + self.canonical.github_owner + "\n")
out.write_string(
"- Mooncakes owner: " + self.canonical.mooncakes_owner + "\n",
)
out.write_string("- Package: " + self.canonical.package_name + "\n\n")
out.write_string("## Evidence\n\n")
for finding in self.findings {
out.write_string("- [" + severity_text(finding.severity) + "] ")
out.write_string(finding.id + ": " + finding.title)
if finding.source != "" {
out.write_string(" -- " + finding.source)
}
if finding.severity != Pass {
out.write_string(" expected=`" + finding.expected + "`")
out.write_string(" actual=`" + finding.actual + "`")
}
out.write_string("\n")
}
out.to_string()
}
///|
pub fn IdentityReport::to_json(self : IdentityReport) -> String {
let out = StringBuilder()
out.write_string("{")
out.write_string("\"verdict\":\"" + json_escape(self.verdict) + "\",")
out.write_string("\"score\":" + self.score.to_string() + ",")
out.write_string(
"\"github_owner\":\"" + json_escape(self.canonical.github_owner) + "\",",
)
out.write_string(
"\"mooncakes_owner\":\"" +
json_escape(self.canonical.mooncakes_owner) +
"\",",
)
out.write_string(
"\"package\":\"" + json_escape(self.canonical.package_name) + "\",",
)
out.write_string("\"findings\":[")
for i in 0.. 0 {
out.write_string(",")
}
out.write_string("{")
out.write_string("\"id\":\"" + json_escape(finding.id) + "\",")
out.write_string(
"\"severity\":\"" + severity_text(finding.severity) + "\",",
)
out.write_string("\"source\":\"" + json_escape(finding.source) + "\",")
out.write_string("\"expected\":\"" + json_escape(finding.expected) + "\",")
out.write_string("\"actual\":\"" + json_escape(finding.actual) + "\"")
out.write_string("}")
}
out.write_string("]}")
out.to_string()
}
///|
fn identity_evidence_from_snapshot(
canonical : SubmissionIdentity,
snapshot : ProjectSnapshot,
submission_text : String,
manifest : PackageManifest,
repository : String,
git_user_name : String,
git_user_email : String,
) -> Array[IdentityEvidence] {
[
IdentityEvidence(
key="applicant.name",
value=present_value(submission_text, canonical.applicant_name),
source="submission",
note="Applicant name should appear in the submission document.",
),
IdentityEvidence(
key="contact.phone",
value=present_value(submission_text, canonical.phone),
source="submission",
note="Phone number should match the registration form.",
),
IdentityEvidence(
key="contact.email",
value=present_value(submission_text, canonical.email),
source="submission",
note="Email should match the registration form.",
),
IdentityEvidence(
key="github.owner",
value=extract_github_owner(repository),
source="git remote or moon.mod repository",
note="Repository owner must match the public GitHub account.",
),
IdentityEvidence(
key="repository.name",
value=extract_github_repo(repository),
source="git remote or moon.mod repository",
note="Repository name should be the submitted project repository.",
),
IdentityEvidence(
key="mooncakes.owner",
value=snapshot.mooncakes_owner,
source="project snapshot",
note="Mooncakes owner should match moon.mod owner.",
),
IdentityEvidence(
key="package.name",
value=snapshot.mooncakes_package,
source="project snapshot",
note="Mooncakes package should match the published package name.",
),
IdentityEvidence(
key="module.owner",
value=manifest.owner,
source="moon.mod name",
note="Module namespace owner is what moon publish validates.",
),
IdentityEvidence(
key="module.package",
value=manifest.package_name,
source="moon.mod name",
note="Module package is the public Mooncakes package slug.",
),
IdentityEvidence(
key="git.user.name",
value=git_user_name,
source="git config user.name",
note="Local commit author name should be the intended GitHub handle.",
),
IdentityEvidence(
key="git.user.email",
value=git_user_email,
source="git config user.email",
note="Local commit email should be the intended applicant email.",
),
]
}
///|
fn expected_identity_value(
canonical : SubmissionIdentity,
key : String,
) -> String {
match key {
"applicant.name" => canonical.applicant_name
"contact.phone" => canonical.phone
"contact.email" => canonical.email
"github.owner" => canonical.github_owner
"repository.name" => canonical.repository_name
"mooncakes.owner" => canonical.mooncakes_owner
"package.name" => canonical.package_name
"module.owner" => canonical.mooncakes_owner
"module.package" => canonical.package_name
"git.user.name" => canonical.github_owner
"git.user.email" => canonical.email
_ => ""
}
}
///|
fn identity_pass(
id : String,
title : String,
expected : String,
actual : String,
source : String,
) -> IdentityFinding {
{ id, severity: Pass, title, expected, actual, source, detail: "" }
}
///|
fn identity_warn(
id : String,
title : String,
expected : String,
actual : String,
source : String,
detail : String,
) -> IdentityFinding {
{ id, severity: Warn, title, expected, actual, source, detail }
}
///|
fn identity_fail(
id : String,
title : String,
expected : String,
actual : String,
source : String,
detail : String,
) -> IdentityFinding {
{ id, severity: Fail, title, expected, actual, source, detail }
}
///|
fn identity_score(findings : Array[IdentityFinding]) -> Int {
let mut score = 100
for finding in findings {
match finding.severity {
Pass => ()
Warn => score -= 4
Fail => score -= 12
}
}
if score < 0 {
0
} else {
score
}
}
///|
fn identity_verdict(findings : Array[IdentityFinding]) -> String {
let mut failures = 0
let mut warnings = 0
for finding in findings {
match finding.severity {
Pass => ()
Warn => warnings += 1
Fail => failures += 1
}
}
if failures > 0 {
"identity-mismatch"
} else if warnings > 0 {
"needs-review"
} else {
"consistent"
}
}
///|
fn normalized_identity(value : String) -> String {
value.trim().to_owned()
}
///|
fn present_value(haystack : String, needle : String) -> String {
if needle == "" {
""
} else if normalized(haystack).contains(needle) {
needle
} else {
""
}
}
///|
fn extract_github_owner(repository : String) -> String {
let tail = github_tail(repository)
if tail == "" {
""
} else {
let parts = tail.split("/").to_array()
if parts.length() >= 2 {
parts[0].trim().to_owned()
} else {
""
}
}
}
///|
fn extract_github_repo(repository : String) -> String {
let tail = github_tail(repository)
if tail == "" {
""
} else {
let parts = tail.split("/").to_array()
if parts.length() >= 2 {
strip_git_suffix(parts[1].trim().to_owned())
} else {
""
}
}
}
///|
fn github_tail(repository : String) -> String {
let trimmed = repository.trim().to_owned()
if trimmed.contains("github.com/") {
let parts = trimmed.split("github.com/").to_array()
if parts.length() >= 2 {
parts[1].to_owned()
} else {
""
}
} else if trimmed.contains("github.com:") {
let parts = trimmed.split("github.com:").to_array()
if parts.length() >= 2 {
parts[1].to_owned()
} else {
""
}
} else {
""
}
}
///|
fn strip_git_suffix(repo : String) -> String {
if repo.has_suffix(".git") {
repo[0:repo.length() - 4].to_owned()
} else {
repo
}
}