using System.Security.Cryptography; using System.Text; namespace AIStudio.Assistants.BeyondFactsLab; /// /// Tells whether a confirmed work state of the BeyondFacts Lab still fits what it builds on. /// /// /// The answer is computed whenever it is needed and never stored, so that it cannot drift from the work states. A /// confirmed work state remembers the revisions of the earlier steps and a fingerprint of the form of its step; /// comparing both with the present ones gives the answer. A changed form weighs less than a changed earlier step: /// the person may have only corrected a typo. /// public static class BflStaleness { /// /// Separates the parts of the text a fingerprint is computed from; it cannot occur in a value. /// private const char SEPARATOR = '\0'; /// /// Tells where the work state of a step stands. /// /// The step. /// The work states of the preparation, by their steps; a step which is missing has none. /// The forms of the preparation, by their steps; a step which is missing has a form this version cannot open, whose changes then do not count. /// The status of the work state. public static BflWorkStateStatus Of(BflStep step, IReadOnlyDictionary workStates, IReadOnlyDictionary forms) { if (workStates.GetValueOrDefault(step)?.Confirmed is not { } confirmed) return BflWorkStateStatus.NOT_CONFIRMED; var upstreamRevisions = UpstreamRevisionsOf(step, workStates); if (upstreamRevisions.Count != confirmed.UpstreamRevisions.Count || upstreamRevisions.Any(revision => confirmed.UpstreamRevisions.GetValueOrDefault(revision.Key) != revision.Value)) return BflWorkStateStatus.OUTDATED; if (forms.TryGetValue(step, out var form) && FingerprintOf(form) != confirmed.DetailsFingerprint) return BflWorkStateStatus.DETAILS_CHANGED; return BflWorkStateStatus.CONFIRMED; } /// /// Gets the revisions of the confirmed work states of the steps before a step, which a confirmation of that step /// remembers. /// /// The step. /// The work states of the preparation, by their steps. /// The revision of each earlier step with a confirmed work state. public static IReadOnlyDictionary UpstreamRevisionsOf(BflStep step, IReadOnlyDictionary workStates) { var revisions = new Dictionary(); foreach (var earlier in BflStep.All.Where(candidate => candidate < step)) { if (workStates.GetValueOrDefault(earlier)?.Confirmed is { } confirmed) revisions[earlier] = confirmed.Revision; } return revisions; } /// /// Computes the fingerprint of a form, which tells whether the person changed it. /// /// /// Line endings and spaces around a value do not count, since they change nothing the model reads. /// /// The form. /// The fingerprint, in hexadecimal digits. public static string FingerprintOf(BflStepFormData form) { var text = new StringBuilder(); foreach (var (fieldId, value) in form.Values.OrderBy(entry => entry.Key, StringComparer.Ordinal)) { var normalized = value.Trim().ReplaceLineEndings("\n"); if (normalized.Length is 0) continue; text.Append(fieldId).Append(SEPARATOR).Append(normalized).Append(SEPARATOR); } return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text.ToString()))); } }