///|
#cfg(any(target="native", target="llvm"))
extern "c" fn moonforensics_process_exit_native(code : Int) = "exit"
///|
#cfg(target="wasm")
fn moonforensics_process_exit_wasm(code : Int) -> Unit = "wasi_snapshot_preview1" "proc_exit"
///|
#cfg(target="wasm-gc")
fn moonforensics_process_exit_wasm_gc(code : Int) -> Unit = "__moonbit_sys_unstable" "exit"
///|
#cfg(target="js")
extern "js" fn moonforensics_process_exit_js(code : Int) -> Bool =
#| function(code) {
#| if (typeof process !== "undefined" && typeof process.exit === "function") {
#| process.exit(code);
#| return true;
#| }
#| return false;
#| }
///|
#cfg(any(target="native", target="llvm"))
fn process_exit(code : Int) -> Unit {
moonforensics_process_exit_native(code)
}
///|
#cfg(target="wasm")
fn process_exit(code : Int) -> Unit {
moonforensics_process_exit_wasm(code)
}
///|
#cfg(target="wasm-gc")
fn process_exit(code : Int) -> Unit {
moonforensics_process_exit_wasm_gc(code)
}
///|
#cfg(target="js")
fn process_exit(code : Int) -> Unit {
if !moonforensics_process_exit_js(code) {
panic()
}
}
///|
fn usage() -> Unit {
println("MoonForensics CLI")
println("Usage:")
println(" moon run cmd/main -- ingest ")
println(
" moon run cmd/main -- analyze [profile-id] [pack-id]",
)
println(" moon run cmd/main -- case [profile-id] [pack-id]")
println(" moon run cmd/main -- verify [expected-sha256]")
println(
"Exit codes: 0 success, 2 usage, 3 input error, 4 verification failure",
)
}
///|
fn read_input(source : String) -> String? {
if source == "" {
None
} else {
Some(source)
}
}
///|
fn ingest(source : String) -> Int {
match read_input(source) {
Some(source) =>
try @moonforensics.parse_jsonl(source) catch {
_ => {
println("ingest failed (exit code 3): invalid JSONL input")
return 3
}
} noraise {
records => {
println("ingest ok (exit code 0): \{records.length()} records")
0
}
}
None => {
println("ingest failed (exit code 3): empty input")
3
}
}
}
///|
fn cli_profile_with_category(
profile_id : String,
category : @moonforensics.EventCategory,
) -> @moonforensics.JsonlMappingProfile {
@moonforensics.JsonlMappingProfile::{
id: profile_id,
version: "1.0.0",
source: @moonforensics.SourceMappingProfile::{
system: @moonforensics.ProfileStringValue::Constant("cli"),
component: @moonforensics.ProfileStringValue::JsonString({
segments: ["source"],
}),
host: Some(
@moonforensics.ProfileStringValue::JsonString({ segments: ["host"], }),
),
instrumentation_scope: None,
},
event_id: @moonforensics.ProfileStringValue::JsonString({
segments: ["event_id"],
}),
event_time: Some({ segments: ["timestamp"], }),
observed_time: None,
severity: Some({ segments: ["severity"], }),
resource: Some(@moonforensics.ResourceMappingProfile::{
kind: @moonforensics.ResourceKind::Service,
id: @moonforensics.ProfileStringValue::JsonString({
segments: ["resource"],
}),
prefix: "",
aliases: [],
attributes_path: None,
}),
category,
event_type: @moonforensics.ProfileStringValue::JsonString({
segments: ["event_type"],
}),
action: Some(
@moonforensics.ProfileStringValue::JsonString({ segments: ["action"], }),
),
outcome: @moonforensics.EventOutcome::Unknown,
body_path: None,
}
}
///|
/// Built-in profiles keep the CLI contract small while making the mapping
/// choice explicit and reproducible. Each profile fixes the semantic category;
/// event fields and resource identity still come from the input object.
fn cli_profile(profile_id : String) -> @moonforensics.JsonlMappingProfile? {
match profile_id {
"flat-v1" =>
Some(
cli_profile_with_category(
profile_id,
@moonforensics.EventCategory::Other("input"),
),
)
"config-v1" =>
Some(
cli_profile_with_category(
profile_id,
@moonforensics.EventCategory::Change,
),
)
"crash-v1" =>
Some(
cli_profile_with_category(
profile_id,
@moonforensics.EventCategory::Process,
),
)
"health-v1" =>
Some(
cli_profile_with_category(
profile_id,
@moonforensics.EventCategory::Availability,
),
)
"latency-v1" =>
Some(
cli_profile_with_category(
profile_id,
@moonforensics.EventCategory::Performance,
),
)
"auto-v1" =>
Some(
cli_profile_with_category(
profile_id,
@moonforensics.EventCategory::Other("input"),
),
)
_ => None
}
}
///|
fn default_pack_for_profile(profile_id : String) -> String {
match profile_id {
"config-v1" => "config"
"crash-v1" => "crash"
"latency-v1" => "latency"
_ => "none"
}
}
///|
fn valid_pack(pack_id : String) -> Bool {
pack_id == "none" ||
pack_id == "config" ||
pack_id == "crash" ||
pack_id == "latency"
}
///|
fn profile_graph(
events : Array[@moonforensics.CanonicalEvent],
pack_id : String,
) -> @moonforensics.IncidentGraph raise @moonforensics.CorrelationRuleError {
match pack_id {
"none" => @moonforensics.build_incident_graph(events, [])
"config" =>
@moonforensics.analyze_with_pack(
events,
@moonforensics.config_analysis_pack(),
)
"crash" =>
@moonforensics.analyze_with_pack(
events,
@moonforensics.crash_analysis_pack(),
)
"latency" =>
@moonforensics.analyze_with_pack(
events,
@moonforensics.latency_analysis_pack(),
)
_ => @moonforensics.build_incident_graph(events, [])
}
}
///|
fn profile_case_collected_at(
events : Array[@moonforensics.CanonicalEvent],
) -> @moonforensics.Timestamp {
for event in events {
match event.event_time {
Some(timestamp) =>
return @moonforensics.Timestamp::{ value: timestamp.raw, }
None => ()
}
}
@moonforensics.Timestamp::{ value: "unknown", }
}
///|
fn profile_case_timeline(
events : Array[@moonforensics.CanonicalEvent],
) -> @moonforensics.Timeline {
let normalized : Array[@moonforensics.NormalizedEvent] = []
for event in events {
normalized.push({
timestamp: event.event_time,
severity: event.severity,
source_id: @moonforensics.normalize_source_id(
event.source.system,
event.source.component,
event.source.host,
),
raw_content: event.raw_content,
})
}
@moonforensics.build_timeline(normalized)
}
///|
fn record_profile_id(record : @moonforensics.JsonlRecord) -> String? {
match record.value {
Json::Object(fields) =>
match fields.get("profile") {
Some(Json::String(value)) =>
if value.trim() == "" {
None
} else {
Some(value.trim().to_owned())
}
_ => None
}
_ => None
}
}
///|
fn map_auto_profile_records(
records : Array[@moonforensics.JsonlRecord],
evidence : @moonforensics.ProfileEvidence,
) -> Array[@moonforensics.CanonicalEvent] raise @moonforensics.ProfileMapError {
let events : Array[@moonforensics.CanonicalEvent] = []
for record in records {
let profile_id = match record_profile_id(record) {
Some(value) => value
None => "flat-v1"
}
let profile = match cli_profile(profile_id) {
Some(profile) => profile
None =>
raise @moonforensics.ProfileMapError::InvalidProfile(
field="profile",
reason="unknown CLI profile: \{profile_id}",
)
}
let mapped = @moonforensics.apply_jsonl_profile([record], profile, evidence)
for event in mapped {
events.push(event)
}
}
events
}
///|
fn profile_case(
source : String,
profile_id : String,
requested_pack : String,
) -> Int {
let selected_pack = if requested_pack == "" {
default_pack_for_profile(profile_id)
} else {
requested_pack
}
if !valid_pack(selected_pack) {
println("case failed (exit code 2): unknown analysis pack")
return 2
}
let profile = match cli_profile(profile_id) {
Some(profile) => profile
None => {
println("case failed (exit code 2): unknown mapping profile")
return 2
}
}
match read_input(source) {
Some(source) =>
try @moonforensics.parse_jsonl(source) catch {
_ => {
println("case failed (exit code 3): invalid JSONL input")
return 3
}
} noraise {
records => {
if records.length() == 0 {
println("case failed (exit code 3): empty input")
return 3
}
let evidence_bytes = @utf8.encode(source)
let document = @moonforensics.EvidenceDocument::{
id: "cli-input",
path: "",
source: @moonforensics.EvidenceSource::{
system: "cli",
component: "case",
host: None,
},
content: evidence_bytes,
}
let manifest = @moonforensics.build_evidence_manifest([document])
let evidence = @moonforensics.ProfileEvidence::{
evidence_id: "cli-input",
path: "",
sha256: Some(manifest.entries[0].sha256),
}
try {
if profile_id == "auto-v1" {
map_auto_profile_records(records, evidence)
} else {
@moonforensics.apply_jsonl_profile(records, profile, evidence)
}
} catch {
_ => {
println("case failed (exit code 3): profile mapping error")
return 3
}
} noraise {
events =>
try profile_graph(events, selected_pack) catch {
_ => {
println("case failed (exit code 3): analysis pack error")
return 3
}
} noraise {
graph => {
let case = @moonforensics.Case::{
case_id: "cli-case",
title: "Profile-driven case analysis",
timezone: "UTC",
collected_at: profile_case_collected_at(events),
evidence: [
@moonforensics.EvidenceItem::{
id: "cli-input",
source: @moonforensics.EvidenceSource::{
system: "cli",
component: "case",
host: None,
},
path: "",
format: @moonforensics.EvidenceFormat::JsonLines,
captured_at: None,
metadata: @moonforensics.EvidenceMetadata::{
entries: [
@moonforensics.MetadataEntry::{
key: "profile_id",
value: profile.id,
},
@moonforensics.MetadataEntry::{
key: "profile_version",
value: profile.version,
},
],
},
},
],
metadata: @moonforensics.EvidenceMetadata::{
entries: [
@moonforensics.MetadataEntry::{
key: "profile_id",
value: profile.id,
},
@moonforensics.MetadataEntry::{
key: "analysis_pack",
value: selected_pack,
},
],
},
}
let report = @moonforensics.ForensicReport::{
case,
summary: "Canonical events were mapped with an explicit profile and analyzed with a declared pack.",
timeline: profile_case_timeline(events),
findings: @moonforensics.DiagnosticReport::{ findings: [], },
evidence_index: manifest,
limitations: [
"Profile mapping is explicit; unmapped input fields are not inferred.",
"Relations are temporal/resource hypotheses, not confirmed root causes.",
],
}
println(@moonforensics.render_markdown_report(report))
println("## 关系图")
println("")
println("- profile: \{profile.id}@\{profile.version}")
println("- analysis pack: \{selected_pack}")
println("- canonical events: \{graph.nodes.length()}")
println("- relations: \{graph.edges.length()}")
println("- diagnostics: \{graph.diagnostics.length()}")
println("case finished (exit code 0)")
0
}
}
}
}
}
None => {
println("case failed (exit code 3): empty input")
3
}
}
}
///|
fn analyze(source : String) -> Int {
match read_input(source) {
Some(source) =>
try @moonforensics.parse_jsonl(source) catch {
_ => {
println("analyze failed (exit code 3): invalid JSONL input")
return 3
}
} noraise {
records => {
let mapping = @moonforensics.JsonlEventMapping::{
system: "cli",
source_field: "source",
event_id_field: "event_id",
timestamp_field: "timestamp",
severity_field: "severity",
resource_field: Some("resource"),
resource_override: None,
kind: @moonforensics.CorrelationKind::Other("input"),
evidence_id: "cli-jsonl",
}
try @moonforensics.adapt_jsonl_records(records, mapping) catch {
_ => {
println(
"analyze failed (exit code 3): required event fields are invalid",
)
return 3
}
} noraise {
events =>
try @moonforensics.correlate_incident_events(events, 60) catch {
_ => {
println(
"analyze failed (exit code 3): invalid correlation window",
)
return 3
}
} noraise {
groups => {
let timeline = @moonforensics.incident_timeline(events)
println("# Analysis")
println("")
println("- records: \{records.length()}")
println("- events: \{events.length()}")
println("- timeline events: \{timeline.events.length()}")
println("- correlation groups: \{groups.length()}")
println(
"- limitations: associations are resource/time observations; no root cause is inferred",
)
println("analyze finished (exit code 0)")
0
}
}
}
}
}
None => {
println("analyze failed (exit code 3): empty input")
3
}
}
}
///|
fn verify(source : String, expected_sha256 : String?) -> Int {
match read_input(source) {
Some(source) => {
let document = @moonforensics.EvidenceDocument::{
id: "cli-input",
path: "",
source: @moonforensics.EvidenceSource::{
system: "cli",
component: "verify",
host: None,
},
content: @utf8.encode(source),
}
let manifest = @moonforensics.build_evidence_manifest([document])
let entry = manifest.entries[0]
match expected_sha256 {
Some(expected) =>
if entry.sha256 != expected {
println(
"verify failed (exit code 4): expected sha256=\{expected}, actual=\{entry.sha256}",
)
4
} else {
println(
"verify ok (exit code 0): manifest matches \{entry.size_bytes} bytes, sha256=\{entry.sha256}",
)
0
}
None => {
println(
"verify ok (exit code 0): \{entry.size_bytes} bytes, sha256=\{entry.sha256}",
)
0
}
}
}
None => {
println("verify failed (exit code 4): empty input")
4
}
}
}
///|
fn main {
let args = @env.args()
if args.length() < 2 {
usage()
process_exit(2)
return
}
let command = args[1]
if command == "--help" || command == "-h" {
usage()
return
}
if args.length() < 3 {
usage()
process_exit(2)
return
}
let path = args[2]
let expected_sha256 = if args.length() >= 4 { Some(args[3]) } else { None }
let exit_code = match command {
"ingest" => ingest(path)
"analyze" =>
if args.length() >= 4 {
let profile_id = args[3]
let requested_pack = if args.length() >= 5 { args[4] } else { "" }
profile_case(path, profile_id, requested_pack)
} else {
analyze(path)
}
"case" => {
let profile_id = if args.length() >= 4 { args[3] } else { "flat-v1" }
let requested_pack = if args.length() >= 5 { args[4] } else { "" }
profile_case(path, profile_id, requested_pack)
}
"verify" => verify(path, expected_sha256)
_ => {
println("unknown command (exit code 2): \{command}")
usage()
2
}
}
if exit_code != 0 {
process_exit(exit_code)
}
}