///|
/// Final decision for a batch of RFC 9457 documents.
pub(all) enum BatchDecision {
Accept
Review
Reject
} derive(Eq, Debug)
///|
/// Machine-readable categories emitted while analyzing a batch.
pub(all) enum BatchFindingKind {
DocumentLimitExceeded
ParseFailure
ParseDiagnostic
SemanticViolation
AuditWarning
RelativeProblemType
UnregisteredProblemType
MissingRequiredTitle
MissingRequiredStatus
} derive(Eq, Debug)
///|
/// One finding tied to a document index. Batch-wide findings use index -1.
pub struct BatchFinding {
document_index : Int
kind : BatchFindingKind
context : String
}
///|
/// Frequency of one effective problem type URI.
pub struct ProblemTypeFrequency {
type_uri : String
count : Int
}
///|
/// Frequency of one declared HTTP status. None represents a missing status.
pub struct ProblemStatusFrequency {
status : Int?
count : Int
}
///|
/// Frequency of one extension member name across parsed documents.
pub struct ExtensionFrequency {
name : String
count : Int
}
///|
/// Quality gate budgets for batch analysis.
pub struct ProblemBatchPolicy {
max_documents : Int
max_parse_failures : Int
max_semantic_issues : Int
max_audit_issues : Int
max_unregistered_types : Int
require_title : Bool
require_status : Bool
allow_relative_types : Bool
}
///|
/// Aggregate, deterministic evidence for a collection of problem documents.
pub struct ProblemBatchReport {
input_count : Int
parsed_count : Int
parse_failure_count : Int
diagnostic_count : Int
semantic_issue_count : Int
audit_issue_count : Int
about_blank_count : Int
registered_type_count : Int
unregistered_type_count : Int
relative_type_count : Int
missing_title_count : Int
missing_status_count : Int
total_member_count : Int
max_member_count : Int
informational_status_count : Int
successful_status_count : Int
redirection_status_count : Int
client_error_status_count : Int
server_error_status_count : Int
other_status_count : Int
type_frequencies : Array[ProblemTypeFrequency]
status_frequencies : Array[ProblemStatusFrequency]
extension_frequencies : Array[ExtensionFrequency]
findings : Array[BatchFinding]
decision : BatchDecision
quality_score : Int
}
///|
pub fn BatchDecision::name(self : BatchDecision) -> String {
match self {
Accept => "accept"
Review => "review"
Reject => "reject"
}
}
///|
pub fn BatchFindingKind::name(self : BatchFindingKind) -> String {
match self {
DocumentLimitExceeded => "DocumentLimitExceeded"
ParseFailure => "ParseFailure"
ParseDiagnostic => "ParseDiagnostic"
SemanticViolation => "SemanticViolation"
AuditWarning => "AuditWarning"
RelativeProblemType => "RelativeProblemType"
UnregisteredProblemType => "UnregisteredProblemType"
MissingRequiredTitle => "MissingRequiredTitle"
MissingRequiredStatus => "MissingRequiredStatus"
}
}
///|
pub fn BatchFinding::document_index(self : BatchFinding) -> Int {
self.document_index
}
///|
pub fn BatchFinding::kind(self : BatchFinding) -> BatchFindingKind {
self.kind
}
///|
pub fn BatchFinding::context(self : BatchFinding) -> String {
self.context
}
///|
pub fn ProblemTypeFrequency::type_uri(self : ProblemTypeFrequency) -> String {
self.type_uri
}
///|
pub fn ProblemTypeFrequency::count(self : ProblemTypeFrequency) -> Int {
self.count
}
///|
pub fn ProblemStatusFrequency::status(self : ProblemStatusFrequency) -> Int? {
self.status
}
///|
pub fn ProblemStatusFrequency::count(self : ProblemStatusFrequency) -> Int {
self.count
}
///|
pub fn ExtensionFrequency::name(self : ExtensionFrequency) -> String {
self.name
}
///|
pub fn ExtensionFrequency::count(self : ExtensionFrequency) -> Int {
self.count
}
///|
/// Practical defaults: malformed or semantically invalid documents reject;
/// advisory findings remain reviewable within a bounded budget.
pub fn ProblemBatchPolicy::default() -> ProblemBatchPolicy {
{
max_documents: 10_000,
max_parse_failures: 0,
max_semantic_issues: 0,
max_audit_issues: 20,
max_unregistered_types: 100,
require_title: false,
require_status: false,
allow_relative_types: true,
}
}
///|
/// Strict CI policy for interoperable, registry-backed API error documents.
pub fn ProblemBatchPolicy::strict() -> ProblemBatchPolicy {
{
max_documents: 1_000,
max_parse_failures: 0,
max_semantic_issues: 0,
max_audit_issues: 0,
max_unregistered_types: 0,
require_title: true,
require_status: true,
allow_relative_types: false,
}
}
///|
/// Permissive inspection policy that records findings without rejecting them.
pub fn ProblemBatchPolicy::permissive() -> ProblemBatchPolicy {
{
max_documents: 1_000_000,
max_parse_failures: 1_000_000,
max_semantic_issues: 1_000_000,
max_audit_issues: 1_000_000,
max_unregistered_types: 1_000_000,
require_title: false,
require_status: false,
allow_relative_types: true,
}
}
///|
pub fn ProblemBatchPolicy::with_max_documents(
self : ProblemBatchPolicy,
value : Int,
) -> ProblemBatchPolicy {
{ ..self, max_documents: non_negative(value) }
}
///|
pub fn ProblemBatchPolicy::with_max_parse_failures(
self : ProblemBatchPolicy,
value : Int,
) -> ProblemBatchPolicy {
{ ..self, max_parse_failures: non_negative(value) }
}
///|
pub fn ProblemBatchPolicy::with_max_semantic_issues(
self : ProblemBatchPolicy,
value : Int,
) -> ProblemBatchPolicy {
{ ..self, max_semantic_issues: non_negative(value) }
}
///|
pub fn ProblemBatchPolicy::with_max_audit_issues(
self : ProblemBatchPolicy,
value : Int,
) -> ProblemBatchPolicy {
{ ..self, max_audit_issues: non_negative(value) }
}
///|
pub fn ProblemBatchPolicy::with_max_unregistered_types(
self : ProblemBatchPolicy,
value : Int,
) -> ProblemBatchPolicy {
{ ..self, max_unregistered_types: non_negative(value) }
}
///|
pub fn ProblemBatchPolicy::with_require_title(
self : ProblemBatchPolicy,
value : Bool,
) -> ProblemBatchPolicy {
{ ..self, require_title: value }
}
///|
pub fn ProblemBatchPolicy::with_require_status(
self : ProblemBatchPolicy,
value : Bool,
) -> ProblemBatchPolicy {
{ ..self, require_status: value }
}
///|
pub fn ProblemBatchPolicy::with_allow_relative_types(
self : ProblemBatchPolicy,
value : Bool,
) -> ProblemBatchPolicy {
{ ..self, allow_relative_types: value }
}
///|
pub fn ProblemBatchPolicy::max_documents(self : ProblemBatchPolicy) -> Int {
self.max_documents
}
///|
pub fn ProblemBatchPolicy::max_parse_failures(self : ProblemBatchPolicy) -> Int {
self.max_parse_failures
}
///|
pub fn ProblemBatchPolicy::max_semantic_issues(
self : ProblemBatchPolicy,
) -> Int {
self.max_semantic_issues
}
///|
pub fn ProblemBatchPolicy::max_audit_issues(self : ProblemBatchPolicy) -> Int {
self.max_audit_issues
}
///|
pub fn ProblemBatchPolicy::max_unregistered_types(
self : ProblemBatchPolicy,
) -> Int {
self.max_unregistered_types
}
///|
pub fn ProblemBatchPolicy::requires_title(self : ProblemBatchPolicy) -> Bool {
self.require_title
}
///|
pub fn ProblemBatchPolicy::requires_status(self : ProblemBatchPolicy) -> Bool {
self.require_status
}
///|
pub fn ProblemBatchPolicy::allows_relative_types(
self : ProblemBatchPolicy,
) -> Bool {
self.allow_relative_types
}
///|
pub fn ProblemBatchReport::input_count(self : ProblemBatchReport) -> Int {
self.input_count
}
///|
pub fn ProblemBatchReport::parsed_count(self : ProblemBatchReport) -> Int {
self.parsed_count
}
///|
pub fn ProblemBatchReport::parse_failure_count(
self : ProblemBatchReport,
) -> Int {
self.parse_failure_count
}
///|
pub fn ProblemBatchReport::diagnostic_count(self : ProblemBatchReport) -> Int {
self.diagnostic_count
}
///|
pub fn ProblemBatchReport::semantic_issue_count(
self : ProblemBatchReport,
) -> Int {
self.semantic_issue_count
}
///|
pub fn ProblemBatchReport::audit_issue_count(self : ProblemBatchReport) -> Int {
self.audit_issue_count
}
///|
pub fn ProblemBatchReport::about_blank_count(self : ProblemBatchReport) -> Int {
self.about_blank_count
}
///|
pub fn ProblemBatchReport::registered_type_count(
self : ProblemBatchReport,
) -> Int {
self.registered_type_count
}
///|
pub fn ProblemBatchReport::unregistered_type_count(
self : ProblemBatchReport,
) -> Int {
self.unregistered_type_count
}
///|
pub fn ProblemBatchReport::relative_type_count(
self : ProblemBatchReport,
) -> Int {
self.relative_type_count
}
///|
pub fn ProblemBatchReport::missing_title_count(
self : ProblemBatchReport,
) -> Int {
self.missing_title_count
}
///|
pub fn ProblemBatchReport::missing_status_count(
self : ProblemBatchReport,
) -> Int {
self.missing_status_count
}
///|
pub fn ProblemBatchReport::total_member_count(self : ProblemBatchReport) -> Int {
self.total_member_count
}
///|
pub fn ProblemBatchReport::max_member_count(self : ProblemBatchReport) -> Int {
self.max_member_count
}
///|
pub fn ProblemBatchReport::average_member_count(
self : ProblemBatchReport,
) -> Double {
if self.parsed_count == 0 {
0.0
} else {
self.total_member_count.to_double() / self.parsed_count.to_double()
}
}
///|
pub fn ProblemBatchReport::informational_status_count(
self : ProblemBatchReport,
) -> Int {
self.informational_status_count
}
///|
pub fn ProblemBatchReport::successful_status_count(
self : ProblemBatchReport,
) -> Int {
self.successful_status_count
}
///|
pub fn ProblemBatchReport::redirection_status_count(
self : ProblemBatchReport,
) -> Int {
self.redirection_status_count
}
///|
pub fn ProblemBatchReport::client_error_status_count(
self : ProblemBatchReport,
) -> Int {
self.client_error_status_count
}
///|
pub fn ProblemBatchReport::server_error_status_count(
self : ProblemBatchReport,
) -> Int {
self.server_error_status_count
}
///|
pub fn ProblemBatchReport::other_status_count(self : ProblemBatchReport) -> Int {
self.other_status_count
}
///|
pub fn ProblemBatchReport::type_frequencies(
self : ProblemBatchReport,
) -> Array[ProblemTypeFrequency] {
self.type_frequencies.copy()
}
///|
pub fn ProblemBatchReport::status_frequencies(
self : ProblemBatchReport,
) -> Array[ProblemStatusFrequency] {
self.status_frequencies.copy()
}
///|
pub fn ProblemBatchReport::extension_frequencies(
self : ProblemBatchReport,
) -> Array[ExtensionFrequency] {
self.extension_frequencies.copy()
}
///|
pub fn ProblemBatchReport::findings(
self : ProblemBatchReport,
) -> Array[BatchFinding] {
self.findings.copy()
}
///|
pub fn ProblemBatchReport::decision(self : ProblemBatchReport) -> BatchDecision {
self.decision
}
///|
pub fn ProblemBatchReport::quality_score(self : ProblemBatchReport) -> Int {
self.quality_score
}
///|
pub fn ProblemBatchReport::is_accepted(self : ProblemBatchReport) -> Bool {
self.decision == Accept
}
///|
/// Analyze documents independently, preserve deterministic input order, and
/// return aggregate evidence without retaining the original JSON strings.
pub fn analyze_problem_batch(
inputs : Array[String],
policy? : ProblemBatchPolicy = ProblemBatchPolicy::default(),
limits? : Limits = Limits::default(),
) -> ProblemBatchReport {
let findings : Array[BatchFinding] = []
let type_frequencies : Array[ProblemTypeFrequency] = []
let status_frequencies : Array[ProblemStatusFrequency] = []
let extension_frequencies : Array[ExtensionFrequency] = []
let mut parsed_count = 0
let mut parse_failure_count = 0
let mut diagnostic_count = 0
let mut semantic_issue_count = 0
let mut audit_issue_count = 0
let mut about_blank_count = 0
let mut registered_type_count = 0
let mut unregistered_type_count = 0
let mut relative_type_count = 0
let mut missing_title_count = 0
let mut missing_status_count = 0
let mut total_member_count = 0
let mut max_member_count = 0
let mut informational_status_count = 0
let mut successful_status_count = 0
let mut redirection_status_count = 0
let mut client_error_status_count = 0
let mut server_error_status_count = 0
let mut other_status_count = 0
if inputs.length() > policy.max_documents {
findings.push({
document_index: -1,
kind: DocumentLimitExceeded,
context: "batch contains \{inputs.length()} documents; policy allows \{policy.max_documents}",
})
}
for index = 0; index < inputs.length(); index = index + 1 {
match parse_problem_json_with_limits(inputs[index], limits) {
Err(error) => {
parse_failure_count += 1
findings.push({
document_index: index,
kind: ParseFailure,
context: error.to_string(),
})
}
Ok(result) => {
parsed_count += 1
let diagnostics = result.diagnostics()
diagnostic_count += diagnostics.length()
for diagnostic in diagnostics {
findings.push({
document_index: index,
kind: ParseDiagnostic,
context: "member \{diagnostic.member_name()} expected \{diagnostic.expected()} but received \{diagnostic.actual()}",
})
}
let problem = result.problem()
let members = problem.member_count()
total_member_count += members
if members > max_member_count {
max_member_count = members
}
let effective_type = problem.effective_type()
increment_type_frequency(type_frequencies, effective_type)
if problem.is_about_blank() {
about_blank_count += 1
}
match lookup_problem_type(effective_type) {
Some(_) => registered_type_count += 1
None =>
match validate_uri_reference(effective_type) {
Ok(info) if info.is_relative() => {
relative_type_count += 1
findings.push({
document_index: index,
kind: RelativeProblemType,
context: "problem type is a relative URI-reference: \{effective_type}",
})
}
_ => {
unregistered_type_count += 1
findings.push({
document_index: index,
kind: UnregisteredProblemType,
context: "problem type is absent from the offline IANA registry: \{effective_type}",
})
}
}
}
match problem.title() {
None => {
missing_title_count += 1
if policy.require_title {
findings.push({
document_index: index,
kind: MissingRequiredTitle,
context: "policy requires a title",
})
}
}
Some(_) => ()
}
match problem.status() {
None => {
missing_status_count += 1
other_status_count += 1
increment_status_frequency(status_frequencies, None)
if policy.require_status {
findings.push({
document_index: index,
kind: MissingRequiredStatus,
context: "policy requires a status",
})
}
}
Some(status) => {
increment_status_frequency(status_frequencies, Some(status))
if status >= 100 && status <= 199 {
informational_status_count += 1
} else if status <= 299 {
successful_status_count += 1
} else if status <= 399 {
redirection_status_count += 1
} else if status <= 499 {
client_error_status_count += 1
} else if status <= 599 {
server_error_status_count += 1
} else {
other_status_count += 1
}
}
}
for extension in problem.extensions() {
increment_extension_frequency(extension_frequencies, extension.name())
}
let semantic = validate_problem(problem)
semantic_issue_count += semantic.length()
for issue in semantic {
findings.push({
document_index: index,
kind: SemanticViolation,
context: "\{issue.kind().name()}: \{issue.context()}",
})
}
let audit = audit_problem(problem)
audit_issue_count += audit.issues().length()
for issue in audit.issues() {
findings.push({
document_index: index,
kind: AuditWarning,
context: "\{issue.kind().name()}: \{issue.context()}",
})
}
}
}
}
let rejected = inputs.length() > policy.max_documents ||
parse_failure_count > policy.max_parse_failures ||
semantic_issue_count > policy.max_semantic_issues ||
audit_issue_count > policy.max_audit_issues ||
unregistered_type_count > policy.max_unregistered_types ||
(!policy.allow_relative_types && relative_type_count > 0) ||
(policy.require_title && missing_title_count > 0) ||
(policy.require_status && missing_status_count > 0)
let has_review_signal = parse_failure_count > 0 ||
diagnostic_count > 0 ||
semantic_issue_count > 0 ||
audit_issue_count > 0 ||
unregistered_type_count > 0 ||
relative_type_count > 0 ||
missing_title_count > 0 ||
missing_status_count > 0
let decision = if rejected {
Reject
} else if has_review_signal {
Review
} else {
Accept
}
let quality_score = batch_quality_score(
parse_failure_count, diagnostic_count, semantic_issue_count, audit_issue_count,
unregistered_type_count, relative_type_count,
)
{
input_count: inputs.length(),
parsed_count,
parse_failure_count,
diagnostic_count,
semantic_issue_count,
audit_issue_count,
about_blank_count,
registered_type_count,
unregistered_type_count,
relative_type_count,
missing_title_count,
missing_status_count,
total_member_count,
max_member_count,
informational_status_count,
successful_status_count,
redirection_status_count,
client_error_status_count,
server_error_status_count,
other_status_count,
type_frequencies,
status_frequencies,
extension_frequencies,
findings,
decision,
quality_score,
}
}
///|
/// Compact deterministic JSON suitable for CI logs.
pub fn ProblemBatchReport::summary_json(self : ProblemBatchReport) -> String {
"{" +
"\"decision\":" +
json_quote(self.decision.name()) +
"," +
"\"quality_score\":\{self.quality_score}," +
"\"input_count\":\{self.input_count}," +
"\"parsed_count\":\{self.parsed_count}," +
"\"parse_failures\":\{self.parse_failure_count}," +
"\"diagnostics\":\{self.diagnostic_count}," +
"\"semantic_issues\":\{self.semantic_issue_count}," +
"\"audit_issues\":\{self.audit_issue_count}," +
"\"registered_types\":\{self.registered_type_count}," +
"\"unregistered_types\":\{self.unregistered_type_count}," +
"\"relative_types\":\{self.relative_type_count}," +
"\"findings\":\{self.findings.length()}" +
"}"
}
///|
fn increment_type_frequency(
frequencies : Array[ProblemTypeFrequency],
type_uri : String,
) -> Unit {
for i = 0; i < frequencies.length(); i = i + 1 {
if frequencies[i].type_uri == type_uri {
frequencies[i] = { type_uri, count: frequencies[i].count + 1 }
return
}
}
frequencies.push({ type_uri, count: 1 })
}
///|
fn increment_status_frequency(
frequencies : Array[ProblemStatusFrequency],
status : Int?,
) -> Unit {
for i = 0; i < frequencies.length(); i = i + 1 {
if frequencies[i].status == status {
frequencies[i] = { status, count: frequencies[i].count + 1 }
return
}
}
frequencies.push({ status, count: 1 })
}
///|
fn increment_extension_frequency(
frequencies : Array[ExtensionFrequency],
name : String,
) -> Unit {
for i = 0; i < frequencies.length(); i = i + 1 {
if frequencies[i].name == name {
frequencies[i] = { name, count: frequencies[i].count + 1 }
return
}
}
frequencies.push({ name, count: 1 })
}
///|
fn batch_quality_score(
parse_failures : Int,
diagnostics : Int,
semantic_issues : Int,
audit_issues : Int,
unregistered_types : Int,
relative_types : Int,
) -> Int {
let score = 100 -
parse_failures * 25 -
diagnostics * 3 -
semantic_issues * 15 -
audit_issues * 2 -
unregistered_types * 3 -
relative_types * 2
if score < 0 {
0
} else {
score
}
}
///|
fn non_negative(value : Int) -> Int {
if value < 0 {
0
} else {
value
}
}