Skip to content

Structured build diagnostics - #1012

Open
Enkidu93 wants to merge 9 commits into
mainfrom
structured-build-diagnostics
Open

Structured build diagnostics#1012
Enkidu93 wants to merge 9 commits into
mainfrom
structured-build-diagnostics

Conversation

@Enkidu93

@Enkidu93 Enkidu93 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #965.

See https://docs.google.com/document/d/1719NBOAx0NRvk8WpcciTqvFPPZDRSK-oZXbrIKUgL20/edit?tab=t.0#heading=h.dsj2hidolq5 for details.

This work so far does not include:

  • Adding tests
  • Updating documentation - I'm figuring we'll want this in the wiki?
  • The correct confidence threshold

This change is Reviewable

@Enkidu93
Enkidu93 requested a review from ddaspit July 28, 2026 20:30
@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 39.44444% with 327 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.89%. Comparing base (8ddabc5) to head (28753aa).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...rval.Machine.Shared/Services/PreprocessBuildJob.cs 34.92% 121 Missing and 2 partials ⚠️
...src/Serval.Translation/Services/PlatformService.cs 26.92% 54 Missing and 3 partials ⚠️
...l.Machine.Shared/Services/NmtPreprocessBuildJob.cs 77.55% 18 Missing and 4 partials ⚠️
...Shared/Services/WordAlignmentPreprocessBuildJob.cs 0.00% 18 Missing ⚠️
...red/Services/ServalWordAlignmentPlatformService.cs 0.00% 12 Missing ⚠️
...ed/Services/EchoWordAlignmentPreprocessBuildJob.cs 44.44% 8 Missing and 2 partials ⚠️
...hared/Services/ServalTranslationPlatformService.cs 0.00% 10 Missing ⚠️
...e.Shared/Services/TranslationPreprocessBuildJob.cs 44.44% 8 Missing and 2 partials ⚠️
src/Serval/src/Serval.Client/Client.g.cs 16.66% 10 Missing ⚠️
...erval/src/Serval.Translation/Services/DtoMapper.cs 9.09% 10 Missing ⚠️
... and 9 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1012      +/-   ##
==========================================
- Coverage   71.72%   70.89%   -0.84%     
==========================================
  Files         370      377       +7     
  Lines       20287    20776     +489     
  Branches     2686     2711      +25     
==========================================
+ Hits        14551    14729     +178     
- Misses       4670     4975     +305     
- Partials     1066     1072       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ddaspit ddaspit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ddaspit reviewed 31 files and all commit messages, and made 19 comments.
Reviewable status: all files reviewed, 19 unresolved discussions (waiting on Enkidu93).


a discussion (no related file):
Are you planning on adding unit tests? And yes, we will want to add a page for the diagnostics to the wiki page. It would probably be good to have a link to the wiki page in the Swagger docs.


src/Serval/src/Serval.Translation/Dtos/ExecutionDataDto.cs line 14 at r1 (raw file):

    [Obsolete]
    public IReadOnlyList<string> Warnings { get; init; } = [];
    public IReadOnlyList<DiagnosticDto> Diagnostics { get; init; } = [];

This should be optional.


src/Machine/src/Serval.Machine.Shared/Services/EchoPreprocessBuildJob.cs line 59 at r1 (raw file):

            sourceLanguageTag,
            targetLanguageTag,
            true,

Make the parameter name explicit.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 475 at r1 (raw file):

            .Select(b => new Diagnostic
            {
                Code = "MODEL-004",

I think this is supposed to be MODEL-003.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 479 at r1 (raw file):

                Severity = Models.DiagnosticSeverity.Warn,
                Message =
                    $"The average pretranslation model confidence {b.averageConfidence} in book {b.bookId} is unusually low for the base model {ModelName}",

This confidence should be formatted.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 488 at r1 (raw file):

            });

        if (badBookConfidences.Any())

This deferred LINQ query is getting run multiple times.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 492 at r1 (raw file):

            Build? currentBuild = await _builds.GetAsync(b => b.Id == buildId, cancellationToken);

            await _builds.UpdateAsync(

Can we perform this update and the next in a single Mongo call?


src/Machine/src/Serval.Machine.Shared/Services/EchoWordAlignmentPreprocessBuildJob.cs line 119 at r1 (raw file):

            sourceLanguageTag,
            targetLanguageTag,
            true,

Make the parameter name explicit.


src/Serval/src/Serval.Translation/Dtos/DiagnosticDto.cs line 3 at r1 (raw file):

namespace Serval.Translation.Dtos;

public record DiagnosticDto

We should call this TranslationDiagnosticDto, so the name doesn't collide with the word alignment diagnostic in Serval.Client. ExecutionDataDto should really be renamed too, but that would be a breaking change at this point.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 203 at r1 (raw file):

    }

    protected virtual IReadOnlyList<BuildDiagnostic> GetDiagnostics(

We have a MaxWarnings. We should probably have a MaxDiagnostics.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 225 at r1 (raw file):

                string versificationName,
                IReadOnlyList<UsfmVersificationDiagnosticContract> usfmDiagnostics
            ) in ParallelCorpusService.AnalyzeUsfmVersification(parallelCorpora)

I don't love that we are running AnalyzeUsfmVersification twice for a build. Could we do this once and pass it to GetWarnings and GetDiagnostics or derive the warnings from the diagnostics?


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 243 at r1 (raw file):

                        Serval.Shared.Contracts.UsfmVersificationDiagnosticType.InvalidChapter => new BuildDiagnostic
                        {
                            Code = "USFM-001",

This is probably overkill, but I would feel a bit less anxious if the number part of the code was 4 digits.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 406 at r1 (raw file):

        }

        if (projectVersifications.Count > 1)

I don't think this is correct. It is checking the number of projects, not the number of versifications.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 415 at r1 (raw file):

                    Severity = BuildDiagnosticSeverity.Info,
                    Message =
                        $"There are multiple versifications represented among Paratext projects selected for training or inferencing: {string.Join(", ", projectVersifications)}.",

Are you intending to print out the dictionary entries?


src/Serval/src/Serval.WordAlignment/Dtos/WordAlignmentExecutionDataDto.cs line 14 at r1 (raw file):

    [Obsolete]
    public IReadOnlyList<string> Warnings { get; init; } = [];
    public IReadOnlyList<DiagnosticDto> Diagnostics { get; init; } = [];

This should be optional.


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 27 at r1 (raw file):

    private readonly ILanguageTagService _languageTagService = languageTagService;
    private const string ModelName = "NLLB";
    private const string MinimumTrainCount = "600"; //TODO move to options?

This should be an int and probably be an option.


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 234 at r1 (raw file):

                    Data = new Dictionary<string, object>
                    {
                        { "resolvedCode", sourceLanguageTag },

Is this supposed to be resolvedCode instead of sourceLanguageTag?


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 253 at r1 (raw file):

                    Data = new Dictionary<string, object>
                    {
                        { "resolvedCode", targetLanguageTag },

Is this supposed to be resolvedCode instead of targetLanguageTag?


src/Serval/src/Serval.WordAlignment/Dtos/DiagnosticDto.cs line 3 at r1 (raw file):

namespace Serval.WordAlignment.Dtos;

public record DiagnosticDto

This should be called WordAlignmentDiagnosticDto.

@Enkidu93 Enkidu93 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Enkidu93 made 19 comments.
Reviewable status: all files reviewed, 19 unresolved discussions (waiting on ddaspit).


a discussion (no related file):

Previously, ddaspit (Damien Daspit) wrote…

Are you planning on adding unit tests? And yes, we will want to add a page for the diagnostics to the wiki page. It would probably be good to have a link to the wiki page in the Swagger docs.

Yep, I don't know if you saw the high-level comment I left when I opened the PR. Those are among the TODOs. (I know sometimes Reviewable seems to bury those top-level initial comments which is frustrating.) I wasn't sure exactly how we want to handle the wiki: I figured once the code and document have both been reviewed, I'll basically copy the doc into the wiki and link it in the StartBuild Swagger docs since there isn't a way, I don't think, to make the wiki update part of this PR per se. I will work on tests though - although I expect those will be a little hairy.


src/Machine/src/Serval.Machine.Shared/Services/EchoPreprocessBuildJob.cs line 59 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

Make the parameter name explicit.

Done


src/Machine/src/Serval.Machine.Shared/Services/EchoWordAlignmentPreprocessBuildJob.cs line 119 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

Make the parameter name explicit.

Done.


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 27 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This should be an int and probably be an option.

OK, I made it an int. In general, how do we want to handle these options? I don't like having the model name in two classes for example. I suppose we could have some kind of diagnostics factory that has these options configured. It could have a method like CreateDiagnostic(string code, Dictionary<string, object> data) and we could validate all the data parameters there. Then we could also more easily test the factory with unit tests and then just confirm that the factory method is being called correctly with a mock in the build job classes. I think it also would make it easier to add new diagnostics. The only tricky part would be that this service would need to be called from both PlatformService (or at least that's the natural place) and the build job classes and potentially other places in the future. I'm not sure where a class like this would live. What do you think?


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 234 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

Is this supposed to be resolvedCode instead of sourceLanguageTag?

Yes, good catch. Something unit tests will catch hopefully 🤪.


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 253 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

Is this supposed to be resolvedCode instead of targetLanguageTag?

Yes, done.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 203 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

We have a MaxWarnings. We should probably have a MaxDiagnostics.

OK, as long as it's a pretty high value, I think that's fine. I figure we'll want to sort by severity before limiting. The only thing that's a little weird is adding a diagnostic that means there were too many diagnostics 🤔 - I'm not sure I like that. What do you think? Maybe it's fine.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 225 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

I don't love that we are running AnalyzeUsfmVersification twice for a build. Could we do this once and pass it to GetWarnings and GetDiagnostics or derive the warnings from the diagnostics?

We could. The only reason I did it this way was so that the warnings and diagnostics were completely separate. That way we could strip out the warnings easily when the time comes without affecting the diagnostics and adding the diagnostics wouldn't affect the warnings. But I could just make the warnings a list of all the diagnostic messages.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 243 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This is probably overkill, but I would feel a bit less anxious if the number part of the code was 4 digits.

OK 😆, I can do that.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 406 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

I don't think this is correct. It is checking the number of projects, not the number of versifications.

Yes, sorry, I switched to a map since Nathaniel asked but didn't update this properly. Done.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 415 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

Are you intending to print out the dictionary entries?

I think we probably ought to. What do you think? I can format them another way explicitly if you prefer.


src/Serval/src/Serval.Translation/Dtos/DiagnosticDto.cs line 3 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

We should call this TranslationDiagnosticDto, so the name doesn't collide with the word alignment diagnostic in Serval.Client. ExecutionDataDto should really be renamed too, but that would be a breaking change at this point.

Done. Yeah, I noticed that :/.


src/Serval/src/Serval.Translation/Dtos/ExecutionDataDto.cs line 14 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This should be optional.

Done.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 475 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

I think this is supposed to be MODEL-003.

Yes, done.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 479 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This confidence should be formatted.

Done.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 488 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This deferred LINQ query is getting run multiple times.

Done. Good catch. Just ToList()'d it. Do we have a preference for the new spread syntax where we can just use ToList()? I totally understand using it for concatenation, but I feel like it's a little less readable than, e.g., ToList() it situations like these.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 492 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

Can we perform this update and the next in a single Mongo call?

Yep, done.


src/Serval/src/Serval.WordAlignment/Dtos/DiagnosticDto.cs line 3 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This should be called WordAlignmentDiagnosticDto.

Done.


src/Serval/src/Serval.WordAlignment/Dtos/WordAlignmentExecutionDataDto.cs line 14 at r1 (raw file):

Previously, ddaspit (Damien Daspit) wrote…

This should be optional.

Done.

@ddaspit ddaspit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ddaspit partially reviewed 15 files and all commit messages, made 7 comments, and resolved 15 discussions.
Reviewable status: all files reviewed, 5 unresolved discussions (waiting on Enkidu93).


a discussion (no related file):

Previously, Enkidu93 (Eli C. Lowry) wrote…

Yep, I don't know if you saw the high-level comment I left when I opened the PR. Those are among the TODOs. (I know sometimes Reviewable seems to bury those top-level initial comments which is frustrating.) I wasn't sure exactly how we want to handle the wiki: I figured once the code and document have both been reviewed, I'll basically copy the doc into the wiki and link it in the StartBuild Swagger docs since there isn't a way, I don't think, to make the wiki update part of this PR per se. I will work on tests though - although I expect those will be a little hairy.

Yes, that sounds right.


src/Machine/src/Serval.Machine.Shared/Services/NmtPreprocessBuildJob.cs line 27 at r1 (raw file):

Previously, Enkidu93 (Eli C. Lowry) wrote…

OK, I made it an int. In general, how do we want to handle these options? I don't like having the model name in two classes for example. I suppose we could have some kind of diagnostics factory that has these options configured. It could have a method like CreateDiagnostic(string code, Dictionary<string, object> data) and we could validate all the data parameters there. Then we could also more easily test the factory with unit tests and then just confirm that the factory method is being called correctly with a mock in the build job classes. I think it also would make it easier to add new diagnostics. The only tricky part would be that this service would need to be called from both PlatformService (or at least that's the natural place) and the build job classes and potentially other places in the future. I'm not sure where a class like this would live. What do you think?

When you create the diagnostic factory in Serval, make sure that it isn't coupled to the Machine engine implementation in any way.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 203 at r1 (raw file):

Previously, Enkidu93 (Eli C. Lowry) wrote…

OK, as long as it's a pretty high value, I think that's fine. I figure we'll want to sort by severity before limiting. The only thing that's a little weird is adding a diagnostic that means there were too many diagnostics 🤔 - I'm not sure I like that. What do you think? Maybe it's fine.

I think it is fine. We need some way to communicate that we hit the max. We certainly don't want to create a huge Build that we store in the DB or pass from the API endpoint.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 225 at r1 (raw file):

Previously, Enkidu93 (Eli C. Lowry) wrote…

We could. The only reason I did it this way was so that the warnings and diagnostics were completely separate. That way we could strip out the warnings easily when the time comes without affecting the diagnostics and adding the diagnostics wouldn't affect the warnings. But I could just make the warnings a list of all the diagnostic messages.

If we generate the warnings from the diagnostics, it should be easy to strip out.


src/Machine/src/Serval.Machine.Shared/Services/PreprocessBuildJob.cs line 415 at r1 (raw file):

Previously, Enkidu93 (Eli C. Lowry) wrote…

I think we probably ought to. What do you think? I can format them another way explicitly if you prefer.

I would prefer that we explicitly formatted them even if it is basically the same as ToString generates for a dictionary entry.


src/Serval/src/Serval.Translation/Services/PlatformService.cs line 488 at r1 (raw file):

Previously, Enkidu93 (Eli C. Lowry) wrote…

Done. Good catch. Just ToList()'d it. Do we have a preference for the new spread syntax where we can just use ToList()? I totally understand using it for concatenation, but I feel like it's a little less readable than, e.g., ToList() it situations like these.

I prefer ToList, but I also don't like fighting with the suggestions from VS Code. I'm fine with either.


src/Serval/src/Serval.WordAlignment/Dtos/WordAlignmentDiagnosticDto.cs line 3 at r2 (raw file):

namespace Serval.WordAlignment.Dtos;

public record WordAlignmentDiagnosticDto

Thinking about this a bit more. I think this should be a shared DTO in Serval.Shared. The corresponding model can also be shared.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose structured build warnings through API

3 participants