Moved report/ledger assembly to a new processreport module
This commit is contained in:
@@ -1,151 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
||||
validatormetadata "gitea.maximumdirect.net/eric/audita/internal/validators/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
correctionDispositionApplied = "applied"
|
||||
correctionDispositionRejected = "rejected"
|
||||
correctionDispositionSkipped = "skipped"
|
||||
correctionDispositionFailed = "failed"
|
||||
)
|
||||
|
||||
type correctionLedgerEntry struct {
|
||||
RunID string `json:"run_id,omitempty"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
ModuleInstance string `json:"module_instance"`
|
||||
ProposalIndex int `json:"proposal_index"`
|
||||
SegmentID int `json:"segment_id,omitempty"`
|
||||
OriginalText string `json:"original_text,omitempty"`
|
||||
ProposedCorrectedText string `json:"proposed_corrected_text,omitempty"`
|
||||
AppliedCorrectedText string `json:"applied_corrected_text,omitempty"`
|
||||
ReplacementPolicy string `json:"replacement_policy,omitempty"`
|
||||
Disposition string `json:"disposition"`
|
||||
DispositionReasonCode string `json:"disposition_reason_code,omitempty"`
|
||||
DispositionMessage string `json:"disposition_message,omitempty"`
|
||||
DeterministicValidatorResults []ledgerValidatorDecisionRecord `json:"deterministic_validator_decisions,omitempty"`
|
||||
LLMValidatorResults []ledgerValidatorDecisionRecord `json:"llm_validator_decisions,omitempty"`
|
||||
}
|
||||
|
||||
type ledgerValidatorDecisionRecord struct {
|
||||
ValidatorKey string `json:"validator_key"`
|
||||
Approved bool `json:"approved"`
|
||||
ReasonCode string `json:"reason_code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func buildCorrectionLedger(runDirPath string, runOutput *runner.RunOutput) []correctionLedgerEntry {
|
||||
if runOutput == nil || len(runOutput.ModuleResults) == 0 {
|
||||
return nil
|
||||
}
|
||||
runID := ""
|
||||
if runDirPath != "" {
|
||||
runID = filepath.Base(runDirPath)
|
||||
}
|
||||
|
||||
entries := make([]correctionLedgerEntry, 0)
|
||||
for _, module := range runOutput.ModuleResults {
|
||||
decisionsByProposal := make(map[int][]runner.ValidatorDecisionRecord)
|
||||
for _, decision := range module.ValidatorDecisions {
|
||||
decisionsByProposal[decision.ProposalIndex] = append(decisionsByProposal[decision.ProposalIndex], decision)
|
||||
}
|
||||
|
||||
for _, change := range module.AppliedChanges {
|
||||
entries = append(entries, correctionLedgerEntry{
|
||||
RunID: runID,
|
||||
ModuleKey: module.ModuleKey,
|
||||
ModuleInstance: module.ModuleInstance,
|
||||
ProposalIndex: change.ProposalIndex,
|
||||
SegmentID: change.TargetSegmentID,
|
||||
OriginalText: change.OriginalText,
|
||||
ProposedCorrectedText: change.CorrectedText,
|
||||
AppliedCorrectedText: change.CorrectedText,
|
||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||
Disposition: correctionDispositionApplied,
|
||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
|
||||
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], true),
|
||||
})
|
||||
}
|
||||
for _, change := range module.SkippedChanges {
|
||||
entries = append(entries, correctionLedgerEntry{
|
||||
RunID: runID,
|
||||
ModuleKey: module.ModuleKey,
|
||||
ModuleInstance: module.ModuleInstance,
|
||||
ProposalIndex: change.ProposalIndex,
|
||||
SegmentID: change.TargetSegmentID,
|
||||
OriginalText: change.OriginalText,
|
||||
ProposedCorrectedText: change.CorrectedText,
|
||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||
Disposition: correctionDispositionSkipped,
|
||||
DispositionReasonCode: string(change.SkipReason),
|
||||
DispositionMessage: change.Message,
|
||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], false),
|
||||
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[change.ProposalIndex], true),
|
||||
})
|
||||
}
|
||||
for _, rejection := range module.ValidatorRejected {
|
||||
entries = append(entries, correctionLedgerEntry{
|
||||
RunID: runID,
|
||||
ModuleKey: module.ModuleKey,
|
||||
ModuleInstance: module.ModuleInstance,
|
||||
ProposalIndex: rejection.ProposalIndex,
|
||||
SegmentID: rejection.TargetSegmentID,
|
||||
OriginalText: rejection.OriginalText,
|
||||
ProposedCorrectedText: rejection.CorrectedText,
|
||||
ReplacementPolicy: string(module.ReplacementPolicy),
|
||||
Disposition: correctionDispositionRejected,
|
||||
DispositionReasonCode: rejection.ReasonCode,
|
||||
DispositionMessage: rejection.Message,
|
||||
DeterministicValidatorResults: filterLedgerDecisions(decisionsByProposal[rejection.ProposalIndex], false),
|
||||
LLMValidatorResults: filterLedgerDecisions(decisionsByProposal[rejection.ProposalIndex], true),
|
||||
})
|
||||
}
|
||||
if module.Status == runner.ModuleStatusFailed {
|
||||
entries = append(entries, correctionLedgerEntry{
|
||||
RunID: runID,
|
||||
ModuleKey: module.ModuleKey,
|
||||
ModuleInstance: module.ModuleInstance,
|
||||
Disposition: correctionDispositionFailed,
|
||||
DispositionReasonCode: "module_failed",
|
||||
DispositionMessage: module.ErrorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(entries, func(i, j int) bool {
|
||||
if entries[i].ModuleInstance != entries[j].ModuleInstance {
|
||||
return entries[i].ModuleInstance < entries[j].ModuleInstance
|
||||
}
|
||||
if entries[i].ProposalIndex != entries[j].ProposalIndex {
|
||||
return entries[i].ProposalIndex < entries[j].ProposalIndex
|
||||
}
|
||||
return entries[i].Disposition < entries[j].Disposition
|
||||
})
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func filterLedgerDecisions(in []runner.ValidatorDecisionRecord, wantLLM bool) []ledgerValidatorDecisionRecord {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]ledgerValidatorDecisionRecord, 0, len(in))
|
||||
for _, decision := range in {
|
||||
isLLMBacked := validatormetadata.ClassForKey(decision.ValidatorName) == validatormetadata.ExecutionClassLLMBacked
|
||||
if isLLMBacked != wantLLM {
|
||||
continue
|
||||
}
|
||||
out = append(out, ledgerValidatorDecisionRecord{
|
||||
ValidatorKey: decision.ValidatorName,
|
||||
Approved: decision.Approved,
|
||||
ReasonCode: decision.ReasonCode,
|
||||
Message: decision.Message,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
||||
)
|
||||
|
||||
func TestBuildCorrectionLedgerClassifiesValidatorDecisionsFromCanonicalMetadata(t *testing.T) {
|
||||
output := &runner.RunOutput{
|
||||
ModuleResults: []runner.ModuleResult{
|
||||
{
|
||||
ModuleKey: "glossary",
|
||||
ModuleInstance: "glossary",
|
||||
ReplacementPolicy: proposals.ReplacementPolicyReplaceAll,
|
||||
ValidatorDecisions: []runner.ValidatorDecisionRecord{
|
||||
{ValidatorName: "proposal_shape", ProposalIndex: 3, Approved: true, ReasonCode: "approved"},
|
||||
{ValidatorName: "spoken_form_plausibility", ProposalIndex: 3, Approved: true, ReasonCode: "approved"},
|
||||
},
|
||||
AppliedChanges: []proposals.AppliedChange{
|
||||
{
|
||||
ProposalIndex: 3,
|
||||
ModuleKey: "glossary",
|
||||
ModuleInstance: "glossary",
|
||||
TargetSegmentID: 1,
|
||||
OriginalText: "gestures",
|
||||
CorrectedText: "Jesters",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ledger := buildCorrectionLedger("/tmp/audita-run-id", output)
|
||||
if len(ledger) != 1 {
|
||||
t.Fatalf("expected one ledger entry, got %d", len(ledger))
|
||||
}
|
||||
entry := ledger[0]
|
||||
if len(entry.DeterministicValidatorResults) != 1 || entry.DeterministicValidatorResults[0].ValidatorKey != "proposal_shape" {
|
||||
t.Fatalf("unexpected deterministic decision split: %+v", entry.DeterministicValidatorResults)
|
||||
}
|
||||
if len(entry.LLMValidatorResults) != 1 || entry.LLMValidatorResults[0].ValidatorKey != "spoken_form_plausibility" {
|
||||
t.Fatalf("unexpected llm-backed decision split: %+v", entry.LLMValidatorResults)
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/modules"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/processreport"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/validators"
|
||||
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
||||
)
|
||||
|
||||
type noOpStructuredLLMClient struct{}
|
||||
@@ -531,10 +531,13 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
if runOutput.Utilization != nil {
|
||||
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactUtilizationSummary, runOutput.Utilization)
|
||||
}
|
||||
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactCorrectionLedger, buildCorrectionLedger(runDir.Path(), runOutput))
|
||||
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactCorrectionLedger, processreport.BuildCorrectionLedger(processreport.CorrectionLedgerInput{
|
||||
RunDirectoryPath: runDir.Path(),
|
||||
RunOutput: runOutput,
|
||||
}))
|
||||
}
|
||||
errorPhase, errorMessage := extractErrorPhase(runErr)
|
||||
report := buildProcessReport("failed", inv, runDir, startedAt, completedAt, errorMessage, errorPhase, nil, nil, runOutput)
|
||||
report := processreport.Build(processReportInput("failed", inv, runDir, startedAt, completedAt, errorMessage, errorPhase, nil, nil, runOutput))
|
||||
|
||||
if strings.TrimSpace(inv.ReportJSONPath) != "" {
|
||||
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
||||
@@ -560,10 +563,13 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
if runOutput.Utilization != nil {
|
||||
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactUtilizationSummary, runOutput.Utilization)
|
||||
}
|
||||
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactCorrectionLedger, buildCorrectionLedger(runDir.Path(), runOutput))
|
||||
_ = runDir.WriteJSONArtifact(diagnostics.ArtifactCorrectionLedger, processreport.BuildCorrectionLedger(processreport.CorrectionLedgerInput{
|
||||
RunDirectoryPath: runDir.Path(),
|
||||
RunOutput: runOutput,
|
||||
}))
|
||||
}
|
||||
|
||||
report := buildProcessReport("success", inv, runDir, startedAt, completedAt, "", "", normSummary, chunkSummary, runOutput)
|
||||
report := processreport.Build(processReportInput("success", inv, runDir, startedAt, completedAt, "", "", normSummary, chunkSummary, runOutput))
|
||||
|
||||
if strings.TrimSpace(inv.ReportJSONPath) != "" {
|
||||
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
||||
@@ -578,21 +584,11 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
}
|
||||
|
||||
hasSkippedCorrections := false
|
||||
if runOutput != nil {
|
||||
for _, mr := range runOutput.ModuleResults {
|
||||
if len(mr.SkippedChanges) > 0 || len(mr.ValidatorRejected) > 0 {
|
||||
hasSkippedCorrections = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if runDir != nil {
|
||||
_ = runDir.WriteReport(report)
|
||||
if err := runDir.ApplyRetention(diagnostics.RetentionDecisionInput{
|
||||
RunSucceeded: true,
|
||||
HasSkippedCorrections: hasSkippedCorrections,
|
||||
HasSkippedCorrections: processreport.HasSkippedCorrections(runOutput),
|
||||
}); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: failed to apply work-dir retention: %v\n", err)
|
||||
return 1
|
||||
@@ -710,130 +706,28 @@ func extractErrorPhase(err error) (phase string, message string) {
|
||||
return "", msg
|
||||
}
|
||||
|
||||
func buildProcessReport(status string, inv processInvocation, runDir *diagnostics.RunDirectory, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary, chunkingSummary *chunking.Summary, runOutput *runner.RunOutput) reporting.ProcessReport {
|
||||
report := reporting.ProcessReport{
|
||||
ReportMetadata: reporting.ReportMetadata{
|
||||
ReportSchemaName: reporting.DefaultProcessReportSchemaName,
|
||||
ReportSchemaVersion: reporting.DefaultProcessReportSchemaVersion,
|
||||
OutputSchema: inv.Config.OutputSchema,
|
||||
ConfigVersion: inv.ConfigVersion,
|
||||
},
|
||||
Phase: "default_pipeline",
|
||||
Status: status,
|
||||
Operation: "process",
|
||||
TranscriptPath: inv.TranscriptPath,
|
||||
GlossaryPath: inv.GlossaryPath,
|
||||
OutputPath: inv.OutputPath,
|
||||
Modules: append([]string(nil), inv.Config.Modules...),
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: &completedAt,
|
||||
ErrorPhase: errorPhase,
|
||||
}
|
||||
func processReportInput(status string, inv processInvocation, runDir *diagnostics.RunDirectory, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary, chunkingSummary *chunking.Summary, runOutput *runner.RunOutput) processreport.BuildInput {
|
||||
runDirectoryPath := ""
|
||||
if runDir != nil {
|
||||
runSucceeded := status == "success"
|
||||
metadata := diagnostics.BuildDiagnosticsMetadata(runDir.Path(), runSucceeded)
|
||||
report.Diagnostics = &metadata
|
||||
runDirectoryPath = runDir.Path()
|
||||
}
|
||||
if errorMessage != "" {
|
||||
report.ErrorMessage = errorMessage
|
||||
return processreport.BuildInput{
|
||||
Status: status,
|
||||
TranscriptPath: inv.TranscriptPath,
|
||||
GlossaryPath: inv.GlossaryPath,
|
||||
OutputPath: inv.OutputPath,
|
||||
Modules: inv.Config.Modules,
|
||||
OutputSchema: inv.Config.OutputSchema,
|
||||
ConfigVersion: inv.ConfigVersion,
|
||||
StartedAt: startedAt,
|
||||
CompletedAt: completedAt,
|
||||
ErrorMessage: errorMessage,
|
||||
ErrorPhase: errorPhase,
|
||||
RunDirectoryPath: runDirectoryPath,
|
||||
NormalizationSummary: normalizationSummary,
|
||||
ChunkingSummary: chunkingSummary,
|
||||
RunOutput: runOutput,
|
||||
}
|
||||
if normalizationSummary != nil {
|
||||
report.InputSegmentCount = &normalizationSummary.InputSegmentCount
|
||||
report.NormalizedSegmentCount = &normalizationSummary.OutputSegmentCount
|
||||
report.NormalizationMerges = &normalizationSummary.MergesPerformed
|
||||
report.NormalizationIDReassignments = &normalizationSummary.IDsReassigned
|
||||
report.NormalizationSkipped.DifferentSpeakers = &normalizationSummary.SkippedMerges.DifferentSpeakers
|
||||
report.NormalizationSkipped.GapTooLarge = &normalizationSummary.SkippedMerges.GapTooLarge
|
||||
report.NormalizationSkipped.DurationExceeded = &normalizationSummary.SkippedMerges.DurationExceeded
|
||||
report.NormalizationSkipped.TokenLimitExceeded = &normalizationSummary.SkippedMerges.TokenLimitExceeded
|
||||
}
|
||||
if chunkingSummary != nil {
|
||||
report.Chunking = &reporting.ChunkingSummary{
|
||||
ChunkCount: chunkingSummary.ChunkCount,
|
||||
MinEstimatedTokens: chunkingSummary.MinEstimatedTokens,
|
||||
MaxEstimatedTokens: chunkingSummary.MaxEstimatedTokens,
|
||||
TotalEstimatedTokens: chunkingSummary.TotalEstimatedTokens,
|
||||
TargetSections: chunkingSummary.TargetSections,
|
||||
MaxSectionTokens: chunkingSummary.MaxSectionTokens,
|
||||
MinSectionTokens: chunkingSummary.MinSectionTokens,
|
||||
}
|
||||
}
|
||||
report.ModulesSummary, report.ModuleResults = buildModuleReporting(runOutput)
|
||||
return report
|
||||
}
|
||||
|
||||
func buildModuleReporting(runOutput *runner.RunOutput) (*reporting.ModulesSummary, []reporting.ModuleReport) {
|
||||
if runOutput == nil || len(runOutput.ModuleResults) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
moduleReports := make([]reporting.ModuleReport, 0, len(runOutput.ModuleResults))
|
||||
summary := &reporting.ModulesSummary{ModuleCount: len(runOutput.ModuleResults)}
|
||||
for _, r := range runOutput.ModuleResults {
|
||||
startedAt := r.StartedAt
|
||||
completedAt := r.CompletedAt
|
||||
moduleReports = append(moduleReports, reporting.ModuleReport{
|
||||
ModuleKey: r.ModuleKey,
|
||||
ModuleInstance: r.ModuleInstance,
|
||||
ReplacementPolicy: string(r.ReplacementPolicy),
|
||||
Status: r.Status,
|
||||
ProposalCount: r.ProposalCount,
|
||||
Warnings: append([]stagewarnings.StageWarning(nil), r.Warnings...),
|
||||
ValidatorDecisions: mapValidatorDecisions(r.ValidatorDecisions),
|
||||
ValidatorRejected: mapValidatorRejected(r.ValidatorRejected),
|
||||
AppliedChanges: r.AppliedChanges,
|
||||
SkippedChanges: r.SkippedChanges,
|
||||
ErrorMessage: r.ErrorMessage,
|
||||
StartedAt: &startedAt,
|
||||
CompletedAt: &completedAt,
|
||||
})
|
||||
summary.TotalAppliedChanges += len(r.AppliedChanges)
|
||||
summary.TotalSkippedChanges += len(r.SkippedChanges) + len(r.ValidatorRejected)
|
||||
if r.Status == runner.ModuleStatusFailed && summary.FailedModuleInstance == "" {
|
||||
summary.FailedModuleInstance = r.ModuleInstance
|
||||
}
|
||||
}
|
||||
|
||||
return summary, moduleReports
|
||||
}
|
||||
|
||||
func mapValidatorDecisions(in []runner.ValidatorDecisionRecord) []reporting.ValidatorDecisionReport {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]reporting.ValidatorDecisionReport, len(in))
|
||||
for i, d := range in {
|
||||
out[i] = reporting.ValidatorDecisionReport{
|
||||
ValidatorName: d.ValidatorName,
|
||||
ProposalIndex: d.ProposalIndex,
|
||||
Approved: d.Approved,
|
||||
ReasonCode: d.ReasonCode,
|
||||
Message: d.Message,
|
||||
DiagnosticArtifactPath: d.DiagnosticArtifactPath,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mapValidatorRejected(in []runner.ValidatorRejectedChange) []reporting.ValidatorRejectedReport {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]reporting.ValidatorRejectedReport, len(in))
|
||||
for i, d := range in {
|
||||
out[i] = reporting.ValidatorRejectedReport{
|
||||
ValidatorName: d.ValidatorName,
|
||||
ProposalIndex: d.ProposalIndex,
|
||||
ModuleKey: d.ModuleKey,
|
||||
ModuleInstance: d.ModuleInstance,
|
||||
TargetSegmentID: d.TargetSegmentID,
|
||||
OriginalText: d.OriginalText,
|
||||
CorrectedText: d.CorrectedText,
|
||||
ReasonCode: d.ReasonCode,
|
||||
Message: d.Message,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type processFlags struct {
|
||||
|
||||
Reference in New Issue
Block a user