Skip to content

symfonic.diagnostics.inspection.model

model

What an offline inspection finds, and how it folds into the audit report.

Pure data. A finding names four things and nothing else: what was looked at (subject), what is true of it (detail), what to do about it (migration_action), and where it was seen (location). The split matters because a migration report and a CLI line want different halves of it, and a single pre-formatted string forces both to re-parse prose.

InspectionFinding dataclass

InspectionFinding(code: str, category: str, severity: Severity, subject: str, detail: str, migration_action: str, location: str | None = None)

One offline observation about an adopter's project.

sort_key property

sort_key: tuple[int, int, str, str, str]

Group by category, then most severe first, then stably by name.

as_check_result

as_check_result() -> CheckResult

Render as a :class:CheckResult so doctor treats it like any check.

Source code in src/symfonic/diagnostics/inspection/model.py
def as_check_result(self) -> CheckResult:
    """Render as a :class:`CheckResult` so ``doctor`` treats it like any check."""
    where = f" ({self.location})" if self.location else ""
    return CheckResult(
        name=f"{self.category}.{self.code}",
        severity=self.severity,
        message=f"{self.subject}: {self.detail}{where}",
        fix_hint=self.migration_action or None,
    )

MigrationInspection dataclass

MigrationInspection(findings: tuple[InspectionFinding, ...] = (), inspected: tuple[str, ...] = (), skipped: tuple[tuple[str, str], ...] = ())

Everything one offline pass established.

inspected and skipped together cover every category, always. An empty findings list is ambiguous on its own -- it means "clean" for an inspected category and "never looked" for a skipped one, and conflating those two is how a migration tool earns undeserved trust.

as_report

as_report() -> AuditReport

Fold into the shipped audit report, exit codes and all.

Source code in src/symfonic/diagnostics/inspection/model.py
def as_report(self) -> AuditReport:
    """Fold into the shipped audit report, exit codes and all."""
    return AuditReport(results=[f.as_check_result() for f in self.findings])

counts

counts() -> dict[str, int]

Findings per category, omitting categories with none.

Source code in src/symfonic/diagnostics/inspection/model.py
def counts(self) -> dict[str, int]:
    """Findings per category, omitting categories with none."""
    return dict(Counter(finding.category for finding in self.findings))