mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-15 21:28:27 +00:00
164 lines
8.3 KiB
C#
164 lines
8.3 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace AIStudio.Assistants.BeyondFactsLab;
|
|
|
|
/// <summary>
|
|
/// Tells whether a confirmed work state or a result of the BeyondFacts Lab still fits what it builds on.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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. A result remembers the revisions of the work states of all steps.
|
|
/// </remarks>
|
|
public static class BflStaleness
|
|
{
|
|
/// <summary>
|
|
/// Separates the parts of the text a fingerprint is computed from; it cannot occur in a value.
|
|
/// </summary>
|
|
private const char SEPARATOR = '\0';
|
|
|
|
/// <summary>
|
|
/// Tells where the work state of a step stands.
|
|
/// </summary>
|
|
/// <param name="step">The step.</param>
|
|
/// <param name="workStates">The work states of the preparation, by their steps; a step which is missing has none.</param>
|
|
/// <param name="forms">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.</param>
|
|
/// <returns>The status of the work state.</returns>
|
|
public static BflWorkStateStatus Of(BflStep step, IReadOnlyDictionary<BflStep, BflWorkState> workStates, IReadOnlyDictionary<BflStep, BflStepFormData> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the revisions of the confirmed work states of the steps before a step, which a confirmation of that step
|
|
/// remembers.
|
|
/// </summary>
|
|
/// <param name="step">The step.</param>
|
|
/// <param name="workStates">The work states of the preparation, by their steps.</param>
|
|
/// <returns>The revision of each earlier step with a confirmed work state.</returns>
|
|
public static IReadOnlyDictionary<BflStep, int> UpstreamRevisionsOf(BflStep step, IReadOnlyDictionary<BflStep, BflWorkState> workStates)
|
|
{
|
|
var revisions = new Dictionary<BflStep, int>();
|
|
foreach (var earlier in BflStep.All.Where(candidate => candidate < step))
|
|
{
|
|
if (workStates.GetValueOrDefault(earlier)?.Confirmed is { } confirmed)
|
|
revisions[earlier] = confirmed.Revision;
|
|
}
|
|
|
|
return revisions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the revisions of the confirmed work states of all steps, which a result remembers when the lab writes it.
|
|
/// </summary>
|
|
/// <param name="workStates">The work states of the preparation, by their steps.</param>
|
|
/// <returns>The revision of each step with a confirmed work state.</returns>
|
|
public static IReadOnlyDictionary<BflStep, int> ConfirmedRevisionsOf(IReadOnlyDictionary<BflStep, BflWorkState> workStates)
|
|
{
|
|
var revisions = new Dictionary<BflStep, int>();
|
|
foreach (var step in BflStep.All)
|
|
{
|
|
if (workStates.GetValueOrDefault(step)?.Confirmed is { } confirmed)
|
|
revisions[step] = confirmed.Revision;
|
|
}
|
|
|
|
return revisions;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tells whether a version of a result no longer fits the work states: a step was confirmed for the first time,
|
|
/// confirmed anew, or withdrawn since the lab wrote it.
|
|
/// </summary>
|
|
/// <param name="version">The version of the result.</param>
|
|
/// <param name="workStates">The work states of the preparation, by their steps.</param>
|
|
/// <returns>True, when the result is outdated.</returns>
|
|
public static bool IsOutdated(BflResultVersion version, IReadOnlyDictionary<BflStep, BflWorkState> workStates)
|
|
{
|
|
var revisions = ConfirmedRevisionsOf(workStates);
|
|
return revisions.Count != version.WorkStateRevisions.Count || revisions.Any(revision => version.WorkStateRevisions.GetValueOrDefault(revision.Key) != revision.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the fingerprint of a form, which tells whether the person changed it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Line endings and spaces around a value do not count, since they change nothing the model reads. The card sort
|
|
/// counts as a detail of its step; without one, or with an empty one, the fingerprint is the same as before the lab
|
|
/// had a card sort, so that no confirmation of an older preparation turns into "Details changed". The same holds for
|
|
/// the simulation of the step "Test situation" with generic counterparts.
|
|
/// </remarks>
|
|
/// <param name="form">The form.</param>
|
|
/// <returns>The fingerprint, in hexadecimal digits.</returns>
|
|
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);
|
|
}
|
|
|
|
if (form.CardSort is { IsEmpty: false } cardSort)
|
|
AppendCardSort(text, cardSort);
|
|
|
|
if (form.Simulation is { Mode.HasPeople: true } simulation)
|
|
AppendSimulation(text, simulation);
|
|
|
|
return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text.ToString())));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends a simulation with people to the text a fingerprint is computed from.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Generic counterparts add nothing, so that the fingerprint stays the same as before the lab simulated people. The
|
|
/// people count as a set, by the keys of their names: the order the person chose them in means nothing.
|
|
/// </remarks>
|
|
/// <param name="text">The text so far.</param>
|
|
/// <param name="simulation">The simulation.</param>
|
|
private static void AppendSimulation(StringBuilder text, BflSimulation simulation)
|
|
{
|
|
text.Append(SEPARATOR).Append("simulation").Append(SEPARATOR).Append(simulation.Mode.ToString()).Append(SEPARATOR);
|
|
foreach (var key in simulation.People.Select(BflParticipant.NameKey).Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal))
|
|
text.Append(key).Append(SEPARATOR);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends a card sort to the text a fingerprint is computed from.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Each part starts with a separator and its name, which no ID of a field begins with. The values to look at more
|
|
/// closely count as a set: the order the person chose them in is no ranking.
|
|
/// </remarks>
|
|
/// <param name="text">The text so far.</param>
|
|
/// <param name="cardSort">The card sort.</param>
|
|
private static void AppendCardSort(StringBuilder text, BflCardSort cardSort)
|
|
{
|
|
text.Append(SEPARATOR).Append("categories").Append(SEPARATOR);
|
|
foreach (var (valueId, category) in cardSort.Categories.Where(entry => entry.Value is not BflValueCategory.NONE).OrderBy(entry => entry.Key, StringComparer.Ordinal))
|
|
text.Append(valueId).Append(SEPARATOR).Append(category.ToString()).Append(SEPARATOR);
|
|
|
|
text.Append(SEPARATOR).Append("own values").Append(SEPARATOR);
|
|
foreach (var value in cardSort.OwnValues)
|
|
text.Append(value.Id).Append(SEPARATOR).Append(value.Name.Trim()).Append(SEPARATOR).Append(value.Definition.Trim()).Append(SEPARATOR);
|
|
|
|
text.Append(SEPARATOR).Append("focus").Append(SEPARATOR);
|
|
foreach (var valueId in cardSort.Focus.Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal))
|
|
text.Append(valueId).Append(SEPARATOR);
|
|
}
|
|
} |