TA 导入检查不应边扫描边改报告:完整候选才能替换稳定结果

角色贴图刚被改成 4096,MipMap 也被关闭;同一批材质又把 Game/Lit 变体推到 48 个。导入检查器先发现贴图尺寸超限,面板立刻刷新成“一条问题”,随后扫描材质时异常退出。此时团队看到的不是旧报告,也不是本次完整报告,而是一份看似成功、实际只检查了一半的结果。

规则本身并不复杂。危险来自报告何时取得可见性:逐资产扫描可以持续产生局部发现,但这些发现只属于候选批次。只有扫描完整结束,并且完成结果仍对应最新一次检查请求,候选报告才有资格替换当前稳定报告。

贴图、材质和变体经过检查门后形成稳定报告的技术主视觉

问题列表不是可随时追加的共享容器

如果面板直接绑定一个全局 List<Issue>,扫描线程每发现一条就追加,UI 会观察到批次的中间状态。失败时清空列表同样不安全:读者无法区分“确实没有问题”和“扫描没有完成”。这里需要分开的两个对象:规则验证器负责从一组不可变描述生成问题;协调器拥有活动报告,并决定候选能否提交。

最小探针位于 $PROBE_ROOT/ImportValidation.csImportAsset 只保留当前纵向切片需要的描述字段;CompleteBatch 先拒绝陈旧、失败和空批次,再构造完整候选,确认条件成立后一次替换 ActiveReport

public enum AssetKind { Texture, Material }
public enum CommitResult { Committed, StaleBatch, ScanFailed, EmptyBatch }

public sealed record ImportAsset(
    string Path,
    AssetKind Kind,
    int MaxSize = 0,
    bool HasMipMaps = false,
    string Shader = "",
    int VariantCount = 0);

public sealed record ValidationIssue(string Path, string Rule, string Detail);
public sealed record ValidationReport(
    long BatchId,
    int AssetCount,
    IReadOnlyList<ValidationIssue> Issues);

public sealed class ImportRuleValidator
{
    public IReadOnlyList<ValidationIssue> Validate(IReadOnlyList<ImportAsset> assets)
    {
        var issues = new List<ValidationIssue>();
        foreach (var asset in assets)
        {
            if (asset.Kind == AssetKind.Texture && asset.MaxSize > 2048)
                issues.Add(new(asset.Path, "texture.max-size", $"{asset.MaxSize} > 2048"));

            if (asset.Kind == AssetKind.Texture && !asset.HasMipMaps)
                issues.Add(new(asset.Path, "texture.mipmaps", "disabled"));

            if (asset.Kind == AssetKind.Material &&
                asset.Shader == "Game/Lit" && asset.VariantCount > 32)
                issues.Add(new(asset.Path, "material.variant-budget", $"{asset.VariantCount} > 32"));
        }
        return issues;
    }
}

public sealed class ImportAuditCoordinator(ImportRuleValidator validator)
{
    private long _latestBatchId;
    public ValidationReport? ActiveReport { get; private set; }

    public long BeginBatch() => ++_latestBatchId;

    public CommitResult CompleteBatch(
        long batchId,
        IReadOnlyList<ImportAsset>? assets,
        bool scanSucceeded)
    {
        if (batchId != _latestBatchId)
            return CommitResult.StaleBatch;
        if (!scanSucceeded || assets is null)
            return CommitResult.ScanFailed;
        if (assets.Count == 0)
            return CommitResult.EmptyBatch;

        var candidate = new ValidationReport(
            batchId, assets.Count, validator.Validate(assets));
        ActiveReport = candidate;
        return CommitResult.Committed;
    }
}

这条边界还解决了工具中常见的重入。第一次检查尚未完成,文件监听器又触发第二次检查;旧任务晚到时,它携带的 batchId 已经小于 _latestBatchId。协调器不需要猜测两份结果谁“更完整”,只接受当前意图对应的批次。

三条规则必须共享同一次报告身份

固定违规输入包含三个资产:Boss 贴图为 4096 且关闭 MipMap,Boss 材质使用 Game/Lit 并产生 48 个变体,UI Atlas 满足当前规则。阈值分别是 2048 和 32,因此输出应当严格包含三条问题,而不是“扫描过三个资产”就算完成。

探针先提交两资产、零问题的干净报告;批次 2 提交三资产、三问题的违规报告。批次 3 模拟扫描异常,批次 4 在批次 5 已开始后迟到,批次 6 输入为空。这三种情况都没有生成一份可以解释的完整报告,因此活动引用保持原值。

导入描述经过三条规则形成候选报告,失败与陈旧批次保持稳定报告的状态图

这里把空批次视为失败边界,而不是“零问题”。这是由工具合同决定的:本次检查的目标集合本应来自导入事件,空集合更可能表示枚举或过滤链断裂。若产品确实支持用户主动检查空目录,可以增加显式的范围描述,并让“已确认的空范围”成为合法报告;不能仅凭 Count == 0 猜测意图。

运行结果证明失败没有擦除旧结论

回归入口位于 $PROBE_ROOT/Program.cs,同时断言结果枚举、问题数量、规则身份与活动批次。执行命令如下:

dotnet build $PROBE_ROOT/ImportValidationProbe.csproj -c Release --nologo
dotnet run --project $PROBE_ROOT/ImportValidationProbe.csproj -c Release --no-build

Release 构建退出码为 0,0 警告、0 错误;进程退出码为 0,17 项断言通过,失败 0、跳过 0,进程墙钟 1.73 秒。决定性输出保留了每次状态变化:

clean: batch=1 assets=2 issues=0 result=Committed
violations: batch=2 assets=3 issues=3 rules=texture.max-size,texture.mipmaps,material.variant-budget result=Committed
scan-failed: batch=3 result=ScanFailed active=2
stale: batch=4 latest=5 result=StaleBatch active=2
current: batch=5 assets=3 issues=3 result=Committed active=5
empty: batch=6 result=EmptyBatch active=5
tests: pass=17 fail=0 skip=0 duration_ms=6.31

这些数据证明的是报告提交语义,不是导入器性能。探针没有读取真实像素、调用引擎导入 API 或编译 Shader;MaxSize、MipMap 状态和变体数都是导入阶段已经取得的描述数据。生产工具仍应把描述提取错误映射为 ScanFailed,不能让默认值穿过验证器后被解释成合规。

何时需要从内存候选升级为持久快照

单进程编辑器内,构造完 ValidationReport 再替换引用已经足够。报告跨进程写入数据库或供构建农场读取时,提交单位要扩展为带批次身份的持久快照:问题明细、扫描范围、规则版本和完成标记应处于同一事务,读端只查询已完成版本。否则内存中解决的半份报告,会在存储层重新出现。

规则版本同样不能藏在进程配置里。阈值从 32 改为 64 后,旧报告与新报告的“零问题”含义不同;一旦报告要缓存、比较或作为构建门禁,ValidationReport 应增加明确的规则集版本。本文探针暂未实现持久化和版本迁移,因此结论只覆盖单进程、一次扫描形成一份完整内存报告的边界。

开篇那次中途异常不应留下“一条已发现问题”的假完整面板。扫描线程可以保留诊断日志,活动报告却继续显示上一次成功批次,并明确标记本次检查失败。把局部发现留在候选域,把可见性集中到一次提交,TA 才能判断眼前结果究竟代表完整资产范围,还是一次尚未取得发布资格的尝试。