Complete Phase 2 Go normalization foundation
This commit is contained in:
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/io"
|
||||
coreio "gitea.maximumdirect.net/eric/audita/internal/core/io"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
@@ -24,63 +24,84 @@ type processInvocation struct {
|
||||
Config config.Config
|
||||
}
|
||||
|
||||
var processRunner = func(inv processInvocation, stdout io.Writer) (*normalization.NormalizationSummary, error) {
|
||||
// Create run directory early to capture artifacts
|
||||
var processRunner = func(inv processInvocation, stdout io.Writer) (*normalization.NormalizationSummary, *diagnostics.RunDirectory, error) {
|
||||
runDir, err := diagnostics.NewRunDirectory(inv.Config.WorkDir, string(inv.Config.WorkDirRetention))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run_dir_creation: %w", err)
|
||||
return nil, nil, fmt.Errorf("run_dir_creation: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
// Apply retention policy - failed runs are always kept
|
||||
// For now, we consider any run that saved source as successful
|
||||
if runDir != nil {
|
||||
_ = runDir.ApplyRetention()
|
||||
}
|
||||
}()
|
||||
|
||||
transcriptBytes, err := io.ReadRequiredFile(inv.TranscriptPath, "transcript")
|
||||
fail := func(phase string, err error) (*normalization.NormalizationSummary, *diagnostics.RunDirectory, error) {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("%s: %v", phase, err))
|
||||
return nil, runDir, fmt.Errorf("%s: %w", phase, err)
|
||||
}
|
||||
|
||||
transcriptBytes, err := coreio.ReadRequiredFile(inv.TranscriptPath, "transcript")
|
||||
if err != nil {
|
||||
// Best effort to write error log on failure
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("transcript_read: %v", err))
|
||||
return nil, fmt.Errorf("transcript_read: %w", err)
|
||||
return fail("transcript_read", err)
|
||||
}
|
||||
|
||||
glossaryBytes, err := io.ReadRequiredFile(inv.GlossaryPath, "glossary")
|
||||
glossaryBytes, err := coreio.ReadRequiredFile(inv.GlossaryPath, "glossary")
|
||||
if err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("glossary_read: %v", err))
|
||||
return nil, fmt.Errorf("glossary_read: %w", err)
|
||||
return fail("glossary_read", err)
|
||||
}
|
||||
|
||||
// Parse and validate transcript using typed schema
|
||||
transcript, err := schema.ParseSourceTranscriptJSON(transcriptBytes)
|
||||
sourceTranscript, err := schema.ParseSourceTranscriptJSON(transcriptBytes)
|
||||
if err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("transcript_schema: %v", err))
|
||||
return nil, fmt.Errorf("transcript_schema: %w", err)
|
||||
return fail("transcript_schema", err)
|
||||
}
|
||||
|
||||
// Write source transcript artifacts
|
||||
if err := runDir.WriteSourceTranscript(transcript, transcriptBytes); err != nil {
|
||||
if _, err := schema.ParseGlossaryYAML(glossaryBytes); err != nil {
|
||||
return fail("glossary_schema", err)
|
||||
}
|
||||
|
||||
if err := runDir.WriteSourceTranscript(sourceTranscript, transcriptBytes); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("source_artifact: %v", err))
|
||||
// Continue without failing the whole process
|
||||
}
|
||||
|
||||
// Parse and validate glossary using typed schema
|
||||
glossary, err := schema.ParseGlossaryYAML(glossaryBytes)
|
||||
canonical := sourceToCanonicalTranscript(sourceTranscript)
|
||||
normalizer := normalization.NewNormalizer(normalization.NormalizationConfig{
|
||||
MaxSegmentGap: inv.Config.Normalization.MaxSegmentGap,
|
||||
EllipsisGap: inv.Config.Normalization.EllipsisGap,
|
||||
MaxSegmentDuration: inv.Config.Normalization.MaxSegmentDuration,
|
||||
MaxSegmentTokens: inv.Config.Normalization.MaxSegmentTokens,
|
||||
})
|
||||
|
||||
normalizedTranscript, summary := normalizer.Normalize(canonical)
|
||||
|
||||
if err := runDir.WriteNormalizedTranscript(normalizedTranscript); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("normalized_artifact: %v", err))
|
||||
}
|
||||
if err := runDir.WriteNormalizationSummary(summary); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("normalization_summary: %v", err))
|
||||
}
|
||||
|
||||
outputBytes, err := schema.TranscriptToJSON(normalizedTranscript)
|
||||
if err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("glossary_schema: %v", err))
|
||||
return nil, fmt.Errorf("glossary_schema: %w", err)
|
||||
return fail("serialization", err)
|
||||
}
|
||||
|
||||
// Convert to canonical transcript format for normalization
|
||||
normalizedTranscript := &schema.Transcript{
|
||||
Segments: make([]schema.Segment, len(transcript.Segments)),
|
||||
if strings.TrimSpace(inv.OutputPath) != "" {
|
||||
if err := coreio.WriteFile(inv.OutputPath, outputBytes); err != nil {
|
||||
return fail("output_write", err)
|
||||
}
|
||||
return summary, runDir, nil
|
||||
}
|
||||
for i, s := range transcript.Segments {
|
||||
|
||||
if _, err := stdout.Write(outputBytes); err != nil {
|
||||
return fail("stdout_write", err)
|
||||
}
|
||||
|
||||
return summary, runDir, nil
|
||||
}
|
||||
|
||||
func sourceToCanonicalTranscript(source *schema.SourceTranscript) *schema.Transcript {
|
||||
segments := make([]schema.Segment, len(source.Segments))
|
||||
for i, s := range source.Segments {
|
||||
id := i + 1
|
||||
if s.ID != nil {
|
||||
id = *s.ID
|
||||
}
|
||||
normalizedTranscript.Segments[i] = schema.Segment{
|
||||
segments[i] = schema.Segment{
|
||||
ID: id,
|
||||
Speaker: s.Speaker,
|
||||
Start: s.Start,
|
||||
@@ -89,49 +110,7 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
|
||||
Categories: s.Categories,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply deterministic normalization
|
||||
normalizer := normalization.NewNormalizer(normalization.NormalizationConfig{
|
||||
MaxSegmentGap: inv.Config.Normalization.MaxSegmentGap,
|
||||
EllipsisGap: inv.Config.Normalization.EllipsisGap,
|
||||
MaxSegmentDuration: inv.Config.Normalization.MaxSegmentDuration,
|
||||
MaxSegmentTokens: inv.Config.Normalization.MaxSegmentTokens,
|
||||
})
|
||||
|
||||
normalizedTranscript, normalizationSummary := normalizer.Normalize(normalizedTranscript)
|
||||
|
||||
// Write normalized transcript artifact
|
||||
if err := runDir.WriteNormalizedTranscript(normalizedTranscript); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("normalized_artifact: %v", err))
|
||||
// Continue without failing the whole process
|
||||
}
|
||||
|
||||
// Write normalization summary artifact
|
||||
if err := runDir.WriteNormalizationSummary(normalizationSummary); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("normalization_summary: %v", err))
|
||||
// Continue without failing the whole process
|
||||
}
|
||||
|
||||
// Serialize normalized transcript to JSON
|
||||
outputBytes, err := schema.TranscriptToJSON(normalizedTranscript)
|
||||
if err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("serialization: %v", err))
|
||||
return nil, fmt.Errorf("serialization: %w", err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(inv.OutputPath) != "" {
|
||||
if err := io.WriteFile(inv.OutputPath, outputBytes); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("output_write: %v", err))
|
||||
return nil, err
|
||||
}
|
||||
return normalizationSummary, nil
|
||||
}
|
||||
|
||||
if _, err := stdout.Write(outputBytes); err != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("stdout_write: %v", err))
|
||||
return nil, fmt.Errorf("stdout_write: %w", err)
|
||||
}
|
||||
return normalizationSummary, nil
|
||||
return &schema.Transcript{Segments: segments}
|
||||
}
|
||||
|
||||
// Run executes the Audita CLI with the provided arguments and streams.
|
||||
@@ -273,53 +252,45 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
if normalizationSummary, err := processRunner(inv, stdout); err != nil {
|
||||
completedAt := time.Now().UTC()
|
||||
|
||||
// Extract error phase from error message if present
|
||||
errorPhase := ""
|
||||
errorMsg := err.Error()
|
||||
if strings.Contains(errorMsg, ": ") {
|
||||
parts := strings.SplitN(errorMsg, ": ", 2)
|
||||
if len(parts) == 2 {
|
||||
errorPhase = parts[0]
|
||||
errorMsg = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
summary, runDir, runErr := processRunner(inv, stdout)
|
||||
completedAt := time.Now().UTC()
|
||||
|
||||
if runErr != nil {
|
||||
errorPhase, errorMessage := extractErrorPhase(runErr)
|
||||
report := buildProcessReport("failed", inv, startedAt, completedAt, errorMessage, errorPhase, nil)
|
||||
|
||||
if strings.TrimSpace(inv.ReportJSONPath) != "" {
|
||||
report := buildProcessReport("failed", inv, startedAt, completedAt, errorMsg, errorPhase, nil)
|
||||
if reportErr := reporting.WriteProcessReport(inv.ReportJSONPath, report); reportErr != nil {
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", reportErr)
|
||||
return 1
|
||||
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if runDir != nil {
|
||||
_ = runDir.WriteReport(report)
|
||||
_ = runDir.ApplyRetention(false)
|
||||
}
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", runErr)
|
||||
return 1
|
||||
}
|
||||
|
||||
completedAt := time.Now().UTC()
|
||||
report := buildProcessReport("success", inv, startedAt, completedAt, "", "", summary)
|
||||
|
||||
if strings.TrimSpace(inv.ReportJSONPath) != "" {
|
||||
report := buildProcessReport("success", inv, startedAt, completedAt, "", "", normalizationSummary)
|
||||
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
||||
if runDir != nil {
|
||||
_ = runDir.WriteErrorLog(fmt.Sprintf("report_write: %v", err))
|
||||
_ = runDir.ApplyRetention(false)
|
||||
}
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if strings.TrimSpace(inv.ReportJSONPath) != "" {
|
||||
completedAt := time.Now().UTC()
|
||||
report := buildProcessReport("success", inv, startedAt, completedAt, "")
|
||||
if err := reporting.WriteProcessReport(inv.ReportJSONPath, report); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: %v\n", err)
|
||||
if runDir != nil {
|
||||
_ = runDir.WriteReport(report)
|
||||
if err := runDir.ApplyRetention(true); err != nil {
|
||||
fmt.Fprintf(stderr, "audita process: failed to apply work-dir retention: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -327,9 +298,20 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func extractErrorPhase(err error) (phase string, message string) {
|
||||
msg := err.Error()
|
||||
if strings.Contains(msg, ": ") {
|
||||
parts := strings.SplitN(msg, ": ", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0], parts[1]
|
||||
}
|
||||
}
|
||||
return "", msg
|
||||
}
|
||||
|
||||
func buildProcessReport(status string, inv processInvocation, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary) reporting.ProcessReport {
|
||||
report := reporting.ProcessReport{
|
||||
Phase: "phase2-normalization",
|
||||
Phase: "phase2-foundation",
|
||||
Status: status,
|
||||
Operation: "process",
|
||||
TranscriptPath: inv.TranscriptPath,
|
||||
|
||||
Reference in New Issue
Block a user