///| Counts keys by section in a merged view.
pub struct SectionStats {
section : String
key_count : Int
sensitive_count : Int
} derive(@debug.Debug, Eq)
///| Describes how one final value was produced.
pub struct ValueTrace {
path : String
final_layer : String
final_source : String
override_count : Int
sensitive : Bool
} derive(@debug.Debug, Eq)
///| Aggregate view statistics.
pub struct ViewStats {
key_count : Int
section_count : Int
sensitive_count : Int
overridden_count : Int
diagnostics_count : Int
} derive(@debug.Debug, Eq)
pub fn analyze_view(view : ConfigView) -> ViewStats {
{
key_count: view.values.length(),
section_count: section_stats(view).length(),
sensitive_count: sensitive_paths(view).length(),
overridden_count: overridden_values(view).length(),
diagnostics_count: view.diagnostics.length(),
}
}
pub fn section_stats(view : ConfigView) -> Array[SectionStats] {
let sections = collect_analysis_sections(view)
let stats : Array[SectionStats] = []
for section_name in sections {
let mut key_count = 0
let mut sensitive_count = 0
for item in view.values {
if normalized_section(item.section) == section_name {
key_count = key_count + 1
if is_sensitive_path(entry_path(item.section, item.key)) {
sensitive_count = sensitive_count + 1
}
}
}
stats.push({ section: section_name, key_count, sensitive_count })
}
stats
}
fn collect_analysis_sections(view : ConfigView) -> Array[String] {
let sections : Array[String] = []
for item in view.values {
let name = normalized_section(item.section)
if !analysis_contains(sections, name) {
sections.push(name)
}
}
sections
}
fn normalized_section(section : String) -> String {
if section == "" { "" } else { section }
}
fn analysis_contains(values : Array[String], target : String) -> Bool {
for value in values {
if value == target {
return true
}
}
false
}
pub fn sensitive_paths(view : ConfigView) -> Array[String] {
let paths : Array[String] = []
for item in view.values {
let path = entry_path(item.section, item.key)
if is_sensitive_path(path) {
paths.push(path)
}
}
paths
}
pub fn overridden_values(view : ConfigView) -> Array[ResolvedValue] {
let values : Array[ResolvedValue] = []
for item in view.values {
if item.overwritten_by.length() > 0 {
values.push(item)
}
}
values
}
pub fn trace_values(view : ConfigView) -> Array[ValueTrace] {
let traces : Array[ValueTrace] = []
for item in view.values {
let path = entry_path(item.section, item.key)
traces.push({
path,
final_layer: item.source_layer,
final_source: item.source + ":" + item.span.line.to_string(),
override_count: item.overwritten_by.length(),
sensitive: is_sensitive_path(path),
})
}
traces
}
pub fn render_stats_markdown(view : ConfigView) -> String {
let stats = analyze_view(view)
let builder = StringBuilder()
builder.write_string("# Configuration Statistics\n\n")
builder.write_string("- Keys: " + stats.key_count.to_string() + "\n")
builder.write_string("- Sections: " + stats.section_count.to_string() + "\n")
builder.write_string("- Sensitive keys: " + stats.sensitive_count.to_string() + "\n")
builder.write_string("- Overridden keys: " + stats.overridden_count.to_string() + "\n")
builder.write_string("- Diagnostics: " + stats.diagnostics_count.to_string() + "\n\n")
builder.write_string("## Sections\n\n")
builder.write_string("| Section | Keys | Sensitive |\n")
builder.write_string("| --- | --- | --- |\n")
for item in section_stats(view) {
builder.write_string("| " + item.section + " | " + item.key_count.to_string() + " | " + item.sensitive_count.to_string() + " |\n")
}
builder.to_string()
}
pub fn render_trace_markdown(view : ConfigView) -> String {
let builder = StringBuilder()
builder.write_string("| Path | Final Layer | Source | Overrides | Sensitive |\n")
builder.write_string("| --- | --- | --- | --- | --- |\n")
for item in trace_values(view) {
builder.write_string("| " + item.path + " | " + item.final_layer + " | " + item.final_source + " | " + item.override_count.to_string() + " | " + yes_no(item.sensitive) + " |\n")
}
builder.to_string()
}
fn yes_no(value : Bool) -> String {
if value { "yes" } else { "no" }
}
///| Measures how many schema fields are filled by current values or defaults.
pub fn schema_coverage(schema : ConfigSchema, view : ConfigView) -> Int {
let mut total = 0
let mut covered = 0
for schema_section in schema.sections {
for schema_field in schema_section.fields {
total = total + 1
if view.get_path(schema_field.path) != None || schema_field.default_value != None {
covered = covered + 1
}
}
}
if total == 0 {
100
} else {
covered * 100 / total
}
}
pub fn missing_schema_fields(schema : ConfigSchema, view : ConfigView) -> Array[String] {
let missing : Array[String] = []
for schema_section in schema.sections {
for schema_field in schema_section.fields {
if view.get_path(schema_field.path) == None && schema_field.default_value == None {
missing.push(schema_field.path)
}
}
}
missing
}
pub fn render_schema_gap_markdown(schema : ConfigSchema, view : ConfigView) -> String {
let builder = StringBuilder()
builder.write_string("# Schema Gap Report\n\n")
builder.write_string("- Schema: " + schema.name + "\n")
builder.write_string("- Coverage: " + schema_coverage(schema, view).to_string() + "%\n\n")
let missing = missing_schema_fields(schema, view)
if missing.length() == 0 {
builder.write_string("All schema fields are present or have defaults.\n")
} else {
builder.write_string("## Missing Fields\n\n")
for path in missing {
builder.write_string("- " + path + "\n")
}
}
builder.to_string()
}