///|
/// Ceiling on findings carried by one validate/issues response, matching the
/// bounded-report style of the transaction validators feeding it.
let office_finding_report_limit : Int = 128
///|
/// Ceiling on message characters retained per finding.
let office_finding_message_chars : Int = 512
///|
/// Ceiling on stored cells scanned per workbook while deriving XLSX formula
/// findings; reuses the shared parser-item allowance.
let office_issues_max_scanned_cells : Int = office_read_max_parser_items
///|
/// Ceiling on cached formula-error findings reported per workbook.
let office_issues_max_formula_findings : Int = 64
///|
/// One normalized validate/issues finding in the office.finding/1 shape.
priv struct OfficeFinding {
severity : String
code : String
message : String
location : String?
}
///|
fn office_finding(
severity : String,
code : String,
message : String,
location? : String,
) -> OfficeFinding {
{
severity,
code,
message: bounded_text(message, office_finding_message_chars),
location,
}
}
///|
fn office_finding_json(finding : OfficeFinding) -> Json {
let fields : Map[String, Json] = {
"severity": Json::string(finding.severity),
"code": Json::string(finding.code),
"message": Json::string(finding.message),
}
match finding.location {
Some(location) => fields["location"] = Json::string(location)
None => ()
}
Json::object(fields)
}
///|
fn office_findings_json(findings : Array[OfficeFinding]) -> Json {
Json::array(findings.map(office_finding_json))
}
///|
/// Runs the exact archive-backed package validator used by mutation
/// transactions for the detected format, so read-only verdicts cannot drift
/// from the pre-commit gate.
fn office_package_gate_findings(
source : OfficeReadPackage,
) -> Array[OfficeFinding] raise {
let validator = try {
match source.format {
Xlsx => @office_xlsx.package_validator()
Docx => @office_docx.package_validator()
}
} catch {
error =>
raise CliFailure(
@lib.protocol_error(
"office.validation_unavailable",
"package validator could not be constructed: " +
bounded_text("\{error}", 240),
),
)
}
// mirror the transaction call shape: candidate bytes plus an isolated
// archive fork, so byte-aware or archive-mutating validators keep
// read-only and pre-commit verdicts aligned
let raw = (validator.validate)(
source.format,
source.bytes,
source.archive.clone(),
) catch {
error if office_async_cancelled() => raise error
error =>
raise CliFailure(
@lib.protocol_error(
"office.\{source.format.name()}.validation_failed",
"package validation could not run to completion: " +
bounded_text("\{error}", 240),
details=Json::object({
"file": Json::string(bounded_text(source.file, 160)),
}),
),
)
}
let findings : Array[OfficeFinding] = []
for finding in raw {
if findings.length() >= office_finding_report_limit {
break
}
findings.push(office_finding("error", finding.code, finding.message))
}
findings
}
///|
/// Scans stored cells for cached formula error values (#REF!, #NAME?, ...)
/// and reports them as bounded, actionable warnings with cell locations.
fn office_xlsx_formula_findings(
source : OfficeReadPackage,
findings : Array[OfficeFinding],
cancelled? : () -> Bool = office_async_cancelled,
) -> Unit raise {
let workbook = open_xlsx_read_package(source, cancelled~)
let mut scanned = 0
let mut reported = 0
let mut omitted = 0
for worksheet in workbook.sheets() {
let sheet_name = worksheet.name()
let (max_row, max_col) = worksheet.used_bounds_limited(
maximum_stored_cells=office_issues_max_scanned_cells,
cancelled~,
) catch {
ReadCancelled as error => raise error
_ => continue
}
for row in 1..<=max_row {
for column in 1..<=max_col {
if scanned >= office_issues_max_scanned_cells {
return office_push_formula_omission(findings, omitted, scanned=true)
}
scanned += 1
if scanned % 1024 == 0 {
check_office_read_cancelled(cancelled)
}
// only formula-backed cells qualify: a literal error cell carries no
// cached formula result and is not an actionable formula finding
let formula_info = worksheet.get_cell_formula_info_rc(row, column) catch {
ReadCancelled as error => raise error
_ => None
}
guard formula_info is Some(_) else { continue }
let reference = @xlsx.coordinates_to_cell_name(column, row) catch {
_ => continue
}
match (worksheet.get_cell_value_raw(reference) catch { _ => None }) {
Some(Error(text)) =>
if reported < office_issues_max_formula_findings {
reported += 1
findings.push(
office_finding(
"warning",
"office.xlsx.formula_error_value",
"cell caches formula error value " + bounded_text(text, 16),
location="\{sheet_name}!\{reference}",
),
)
} else {
omitted += 1
}
_ => ()
}
}
}
}
office_push_formula_omission(findings, omitted, scanned=false)
}
///|
fn office_push_formula_omission(
findings : Array[OfficeFinding],
omitted : Int,
scanned~ : Bool,
) -> Unit {
if omitted > 0 {
findings.push(
office_finding(
"warning",
"office.xlsx.formula_findings_omitted",
"\{omitted} additional cached formula error values were omitted from the bounded report",
),
)
}
if scanned {
findings.push(
office_finding(
"warning", "office.xlsx.formula_scan_truncated", "the cached formula error scan stopped at the bounded cell allowance",
),
)
}
}
///|
/// Harvests the tolerant DOCX reader's bounded diagnostics as warnings.
fn office_docx_reader_findings(
source : OfficeReadPackage,
findings : Array[OfficeFinding],
) -> Unit raise {
let main_document_part = validate_docx_read_archive(
source.file,
source.archive,
cancelled=office_async_cancelled,
)
let xml_budget = @xml.xml_read_budget(
max_source_units=docx_cli_max_xml_source_units,
max_tokens=docx_cli_max_xml_tokens,
max_materialized_chars=docx_cli_max_xml_materialized_chars,
max_token_chars=docx_cli_max_xml_token_chars,
cancelled=office_async_cancelled,
)
let annotated = @docx.read_docx_annotated_archive_tolerant_limited(
source.archive,
xml_budget,
max_diagnostics=docx_cli_max_warnings,
max_diagnostic_chars=office_finding_message_chars,
expected_main_document_path=main_document_part,
) catch {
error => {
check_office_read_cancelled(office_async_cancelled)
raise docx_reader_failure(error, source.file)
}
}
// annotation warnings are already merged into the package messages by the
// reader; classify them under their specific code exactly once and keep
// the remaining reader diagnostics under the generic code
let annotation_texts : Set[String] = Set([])
for warning in annotated.annotations().warnings() {
annotation_texts.add(warning)
}
for message in annotated.result().messages {
if findings.length() >= office_finding_report_limit {
break
}
let text = match message {
Error(text) => text
Warning(text) => text
}
let code = if annotation_texts.contains(text) {
"office.docx.annotation_warning"
} else {
"office.docx.reader_diagnostic"
}
findings.push(office_finding("warning", code, text))
}
}
///|
fn office_finding_counts(findings : Array[OfficeFinding]) -> (Int, Int) {
let mut errors = 0
let mut warnings = 0
for finding in findings {
if finding.severity == "error" {
errors += 1
} else {
warnings += 1
}
}
(errors, warnings)
}
///|
fn office_validate_data(
schema : String,
source : OfficeReadPackage,
findings : Array[OfficeFinding],
) -> Json {
let (errors, warnings) = office_finding_counts(findings)
Json::object({
"schema": Json::string(schema),
"file": Json::string(bounded_text(source.file, 160)),
"format": Json::string(source.format.name()),
"valid": Json::boolean(errors == 0),
"findings": office_findings_json(findings),
"error_count": Json::number(errors.to_double()),
"warning_count": Json::number(warnings.to_double()),
})
}
///|
fn office_render_findings_human(findings : Array[OfficeFinding]) -> Unit {
for finding in findings {
let location = match finding.location {
Some(value) => " [" + human_text(value, 160) + "]"
None => ""
}
let message = human_text(finding.message, office_finding_message_chars)
println("\{finding.severity} \{finding.code}\{location}: \{message}")
}
}
///|
fn office_validate_output_mode(matches : @argparse.Matches) -> OutputMode raise {
let json = matches.flags.get_or_default("json", false)
let jsonl = matches.flags.get_or_default("jsonl", false)
if json && jsonl {
raise CliFailure(
@lib.protocol_error(
"office.output_mode_conflict", "--json and --jsonl are mutually exclusive",
),
)
}
if json {
JsonDocument
} else if jsonl {
JsonLines
} else {
Human
}
}
///|
async fn run_validate(matches : @argparse.Matches) -> Unit {
let file = required_value(matches, "file")
let mode = office_validate_output_mode(matches)
let source = read_office_package(file, cancelled=office_async_cancelled)
let findings = office_package_gate_findings(source)
let data = office_validate_data(@lib.SCHEMA_VALIDATE_RESULT, source, findings)
let (errors, _) = office_finding_counts(findings)
if errors > 0 {
raise office_validation_failure(source, errors, data)
}
match mode {
JsonDocument => println(@lib.output_success(data).stringify(indent=2))
JsonLines => println(@lib.output_success(data).stringify())
Human => println("valid \{source.format.name()}")
}
}
///|
/// The non-zero validate verdict still carries the complete bounded
/// office.validate/1 record in the failure envelope's details.
fn office_validation_failure(
source : OfficeReadPackage,
errors : Int,
data : Json,
) -> CliFailure {
CliFailure(
@lib.protocol_error(
"office.validation_failed",
"\{source.format.name().to_upper()} package failed validation with \{errors} finding(s)",
details=data,
),
)
}
///|
async fn run_issues(matches : @argparse.Matches) -> Unit {
let file = required_value(matches, "file")
let mode = office_validate_output_mode(matches)
let source = read_office_package(file, cancelled=office_async_cancelled)
let findings = office_package_gate_findings(source)
// actionable non-fatal findings never mask package invalidity: they are
// collected only when the shared gate accepted the package
if findings.is_empty() {
match source.format {
Xlsx => office_xlsx_formula_findings(source, findings)
Docx => office_docx_reader_findings(source, findings)
}
}
let data = office_validate_data(@lib.SCHEMA_ISSUES_RESULT, source, findings)
match mode {
JsonDocument => println(@lib.output_success(data).stringify(indent=2))
JsonLines => println(@lib.output_success(data).stringify())
Human => {
let (errors, warnings) = office_finding_counts(findings)
office_render_findings_human(findings)
println(
"\{source.format.name()}: \{errors} error(s), \{warnings} warning(s)",
)
}
}
}
///|
fn validate_command() -> @argparse.Command {
let summary = match @lib.find_capability_command("validate") {
Some(command) => command.summary
None => "Validate an XLSX or DOCX package with the mutation gate"
}
Command(
"validate",
about=summary,
positionals=[
PositionArg(
"file",
about="path to an .xlsx or .docx file",
num_args=@argparse.ValueRange::single(),
),
],
flags=[
FlagArg("json", long="json", about="print office.output/1 JSON"),
FlagArg("jsonl", long="jsonl", about="print one office.output/1 line"),
],
)
}
///|
fn issues_command() -> @argparse.Command {
let summary = match @lib.find_capability_command("issues") {
Some(command) => command.summary
None => "Report bounded actionable findings for an XLSX or DOCX package"
}
Command(
"issues",
about=summary,
positionals=[
PositionArg(
"file",
about="path to an .xlsx or .docx file",
num_args=@argparse.ValueRange::single(),
),
],
flags=[
FlagArg("json", long="json", about="print office.output/1 JSON"),
FlagArg("jsonl", long="jsonl", about="print one office.output/1 line"),
],
)
}