|
7 | 7 | "regexp" |
8 | 8 | "sort" |
9 | 9 | "strings" |
| 10 | + "sync" |
10 | 11 | "time" |
11 | 12 | "unicode" |
12 | 13 | ) |
@@ -60,6 +61,9 @@ var classifySectionsFn = func(cfg Config, items []WorkItem, options []sectionOpt |
60 | 61 | return CategorizeItemsToSections(cfg, items, options, existing, corrections, historicalItems) |
61 | 62 | } |
62 | 63 |
|
| 64 | +// GenerationMu prevents concurrent report generation from Slack and web UI. |
| 65 | +var GenerationMu sync.Mutex |
| 66 | + |
63 | 67 | type BuildResult struct { |
64 | 68 | Template *ReportTemplate |
65 | 69 | Usage LLMUsage |
@@ -88,6 +92,11 @@ func BuildReportsFromLast(cfg Config, items []WorkItem, reportDate time.Time, co |
88 | 92 | } |
89 | 93 | } |
90 | 94 |
|
| 95 | + // Apply authoritative corrections: override LLM decisions with manual corrections. |
| 96 | + // Corrections are hard overrides, not suggestions — if a manager moved an item |
| 97 | + // to a section, that decision sticks even if the LLM disagrees. |
| 98 | + applyAuthoritativeCorrections(decisions, corrections, items) |
| 99 | + |
91 | 100 | confidenceThreshold := cfg.LLMConfidence |
92 | 101 | if confidenceThreshold <= 0 || confidenceThreshold > 1 { |
93 | 102 | confidenceThreshold = 0.70 |
@@ -331,6 +340,35 @@ func trimDoneItems(t *ReportTemplate) { |
331 | 340 | } |
332 | 341 | } |
333 | 342 |
|
| 343 | +// applyAuthoritativeCorrections overrides LLM decisions with manual corrections. |
| 344 | +// For each item that has a correction, the LLM's section assignment is replaced |
| 345 | +// with the corrected section, and confidence is set to 1.0 to ensure it passes |
| 346 | +// the confidence threshold. |
| 347 | +func applyAuthoritativeCorrections(decisions map[int64]LLMSectionDecision, corrections []ClassificationCorrection, items []WorkItem) { |
| 348 | + if len(corrections) == 0 || len(decisions) == 0 { |
| 349 | + return |
| 350 | + } |
| 351 | + // Build a map from work_item_id to the most recent correction |
| 352 | + correctionByItem := make(map[int64]ClassificationCorrection) |
| 353 | + for _, c := range corrections { |
| 354 | + if existing, ok := correctionByItem[c.WorkItemID]; !ok || c.CorrectedAt.After(existing.CorrectedAt) { |
| 355 | + correctionByItem[c.WorkItemID] = c |
| 356 | + } |
| 357 | + } |
| 358 | + // Override LLM decisions for items that have corrections |
| 359 | + for _, item := range items { |
| 360 | + correction, ok := correctionByItem[item.ID] |
| 361 | + if !ok { |
| 362 | + continue |
| 363 | + } |
| 364 | + if d, exists := decisions[item.ID]; exists { |
| 365 | + d.SectionID = correction.CorrectedSectionID |
| 366 | + d.Confidence = 1.0 // Ensure it passes any confidence threshold |
| 367 | + decisions[item.ID] = d |
| 368 | + } |
| 369 | + } |
| 370 | +} |
| 371 | + |
334 | 372 | func mergeIncomingItems( |
335 | 373 | t *ReportTemplate, |
336 | 374 | items []WorkItem, |
@@ -598,6 +636,14 @@ func renderBossMarkdown(t *ReportTemplate) string { |
598 | 636 | ) |
599 | 637 | } |
600 | 638 |
|
| 639 | +// RenderMarkdownByMode renders a template as markdown in the given mode ("team" or "boss"). |
| 640 | +func RenderMarkdownByMode(t *ReportTemplate, mode string) string { |
| 641 | + if mode == "boss" { |
| 642 | + return renderBossMarkdown(t) |
| 643 | + } |
| 644 | + return renderTeamMarkdown(t) |
| 645 | +} |
| 646 | + |
601 | 647 | func renderMarkdown( |
602 | 648 | t *ReportTemplate, |
603 | 649 | categoryHeading func(cat TemplateCategory) string, |
|
0 commit comments