feat(scanner): complete localized artifact quality checkpoint
This commit is contained in:
@@ -1,18 +1,210 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { AppSnapshot } from "../../../types/domain";
|
||||
import type { BuildSuggestion } from "../../../types/domain";
|
||||
import type { StoredScanResultEntry } from "../../../types/storage";
|
||||
import {
|
||||
BUILD_FIT_PROFILE_CORPUS_VERSION,
|
||||
curatedBuildFitProfiles,
|
||||
} from "../../../data/buildFitProfiles";
|
||||
import {
|
||||
BUILD_FIT_COMBINATION_ASSESSMENT_VERSION,
|
||||
BUILD_FIT_CONTRACT_VERSION,
|
||||
BUILD_FIT_EVIDENCE_RANKING_VERSION,
|
||||
type BuildFitEvidenceRankingResultV1,
|
||||
} from "../../../types/buildFit";
|
||||
import {
|
||||
buildFitInputFromScanResult,
|
||||
rankBuildFitEvidence,
|
||||
} from "../../../lib/buildFitContract";
|
||||
import {
|
||||
aggregateContextKey,
|
||||
buildExplicitAggregateContexts,
|
||||
type BuildFitExplicitContextDraft,
|
||||
} from "../../../lib/buildFitExplicitContext";
|
||||
import { createRendererRepositories } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
||||
import type { AppLocale } from "../../../i18n";
|
||||
import { getBuildCopy } from "../buildsCopy";
|
||||
|
||||
const MAX_NATIVE_BUILD_FIT_RESULTS = 2400;
|
||||
|
||||
interface UseBuildsViewModelInput {
|
||||
snapshot: AppSnapshot;
|
||||
locale: AppLocale;
|
||||
}
|
||||
|
||||
export interface BuildsViewModel {
|
||||
visibleBuilds: Array<{
|
||||
build: BuildSuggestion;
|
||||
}>;
|
||||
artifactCount: number;
|
||||
hasScanData: boolean;
|
||||
foundationLabel: string;
|
||||
foundationDetail: string;
|
||||
contractLabel: string;
|
||||
contractDetail: string;
|
||||
profileLabel: string;
|
||||
profileDetail: string;
|
||||
nativeRunDir: string;
|
||||
nativeResultCount: number;
|
||||
isLoadingBuildFit: boolean;
|
||||
buildFitLoadError: string;
|
||||
ranking: BuildFitEvidenceRankingResultV1;
|
||||
aggregateContextFields: BuildFitAggregateContextField[];
|
||||
setAggregateContextDraft: (
|
||||
profileId: string,
|
||||
targetKey: string,
|
||||
update: Partial<BuildFitExplicitContextDraft>,
|
||||
) => void;
|
||||
refreshBuildFit: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useBuildsViewModel({ snapshot }: UseBuildsViewModelInput): BuildsViewModel {
|
||||
export interface BuildFitAggregateContextField {
|
||||
profileId: string;
|
||||
characterName: string;
|
||||
targetLabel: string;
|
||||
targetKey: string;
|
||||
stat: string;
|
||||
unit: "flat" | "percent";
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
reason: string;
|
||||
value: string;
|
||||
confirmed: boolean;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export function useBuildsViewModel({ snapshot, locale }: UseBuildsViewModelInput): BuildsViewModel {
|
||||
const copy = getBuildCopy(locale);
|
||||
const repositories = useMemo(() => createRendererRepositories(), []);
|
||||
const [nativeResults, setNativeResults] = useState<StoredScanResultEntry[]>([]);
|
||||
const [nativeRunDir, setNativeRunDir] = useState("");
|
||||
const [nativeResultCount, setNativeResultCount] = useState(0);
|
||||
const [isLoadingBuildFit, setIsLoadingBuildFit] = useState(Boolean(repositories));
|
||||
const [buildFitLoadError, setBuildFitLoadError] = useState("");
|
||||
const [aggregateContextDrafts, setAggregateContextDrafts] = useState<Record<string, Record<string, BuildFitExplicitContextDraft>>>({});
|
||||
|
||||
const refreshBuildFit = useCallback(async () => {
|
||||
if (!repositories?.automation) {
|
||||
setNativeResults([]);
|
||||
setNativeRunDir("");
|
||||
setNativeResultCount(0);
|
||||
setBuildFitLoadError(copy.noBridge);
|
||||
setIsLoadingBuildFit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingBuildFit(true);
|
||||
setBuildFitLoadError("");
|
||||
try {
|
||||
const result = await repositories.automation.nativeScannerLoadResults({
|
||||
limit: MAX_NATIVE_BUILD_FIT_RESULTS,
|
||||
});
|
||||
if (!result.ok) {
|
||||
setNativeResults([]);
|
||||
setNativeRunDir("");
|
||||
setNativeResultCount(0);
|
||||
setBuildFitLoadError(result.error ?? copy.noCompleteRun);
|
||||
return;
|
||||
}
|
||||
setNativeResults(result.results);
|
||||
setNativeRunDir(result.runDir);
|
||||
setNativeResultCount(result.total);
|
||||
} catch (error) {
|
||||
setNativeResults([]);
|
||||
setNativeRunDir("");
|
||||
setNativeResultCount(0);
|
||||
setBuildFitLoadError(error instanceof Error ? error.message : copy.loadFailed);
|
||||
} finally {
|
||||
setIsLoadingBuildFit(false);
|
||||
}
|
||||
}, [copy.loadFailed, copy.noBridge, copy.noCompleteRun, repositories]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshBuildFit();
|
||||
}, [refreshBuildFit]);
|
||||
|
||||
const aggregateContextResults = useMemo(() => Object.fromEntries(
|
||||
curatedBuildFitProfiles.map((profile) => [
|
||||
profile.id,
|
||||
buildExplicitAggregateContexts(profile, aggregateContextDrafts[profile.id] ?? {}),
|
||||
]),
|
||||
) as Record<string, ReturnType<typeof buildExplicitAggregateContexts>>, [aggregateContextDrafts]);
|
||||
const aggregateContextByProfile = useMemo(() => Object.fromEntries(
|
||||
Object.entries(aggregateContextResults)
|
||||
.filter(([, result]) => result.contexts.length > 0)
|
||||
.map(([profileId, result]) => [profileId, result.contexts]),
|
||||
), [aggregateContextResults]);
|
||||
const aggregateContextFields = useMemo(() => curatedBuildFitProfiles.flatMap((profile) => profile.aggregateTargets.map((target) => {
|
||||
const targetKey = aggregateContextKey(target);
|
||||
const draft = aggregateContextDrafts[profile.id]?.[targetKey] ?? { value: "", confirmed: false };
|
||||
return {
|
||||
profileId: profile.id,
|
||||
characterName: profile.character.name,
|
||||
targetLabel: profile.target.label,
|
||||
targetKey,
|
||||
stat: target.stat,
|
||||
unit: target.unit,
|
||||
...(target.minimum === undefined ? {} : { minimum: target.minimum }),
|
||||
...(target.maximum === undefined ? {} : { maximum: target.maximum }),
|
||||
reason: target.reason,
|
||||
value: draft.value,
|
||||
confirmed: draft.confirmed,
|
||||
error: aggregateContextResults[profile.id]?.issues[targetKey] ?? "",
|
||||
};
|
||||
})), [aggregateContextDrafts, aggregateContextResults]);
|
||||
const setAggregateContextDraft = useCallback((
|
||||
profileId: string,
|
||||
targetKey: string,
|
||||
update: Partial<BuildFitExplicitContextDraft>,
|
||||
) => {
|
||||
setAggregateContextDrafts((current) => ({
|
||||
...current,
|
||||
[profileId]: {
|
||||
...current[profileId],
|
||||
[targetKey]: {
|
||||
value: current[profileId]?.[targetKey]?.value ?? "",
|
||||
confirmed: current[profileId]?.[targetKey]?.confirmed ?? false,
|
||||
...update,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const ranking = useMemo(() => rankBuildFitEvidence({
|
||||
version: BUILD_FIT_EVIDENCE_RANKING_VERSION,
|
||||
profiles: curatedBuildFitProfiles,
|
||||
artifacts: nativeResults
|
||||
.map((result) => buildFitInputFromScanResult(result))
|
||||
.filter((result): result is NonNullable<typeof result> => result !== null),
|
||||
aggregateContextByProfile,
|
||||
}), [aggregateContextByProfile, nativeResults]);
|
||||
const artifactCount = Math.max(snapshot.artifacts.length, nativeResultCount);
|
||||
const hasScanData = artifactCount > 0;
|
||||
const profileCount = curatedBuildFitProfiles.length;
|
||||
const suggestionCount = ranking.suggestions.length;
|
||||
|
||||
return {
|
||||
visibleBuilds: snapshot.builds.slice(0, 9).map((build) => ({ build })),
|
||||
artifactCount,
|
||||
hasScanData,
|
||||
foundationLabel: hasScanData ? copy.foundationReady : copy.foundationEmpty,
|
||||
foundationDetail: hasScanData
|
||||
? copy.foundationReadyDetail(nativeResultCount || artifactCount)
|
||||
: copy.foundationEmptyDetail,
|
||||
contractLabel: copy.contractLabel,
|
||||
contractDetail: copy.contractDetail(
|
||||
BUILD_FIT_CONTRACT_VERSION,
|
||||
BUILD_FIT_COMBINATION_ASSESSMENT_VERSION,
|
||||
BUILD_FIT_EVIDENCE_RANKING_VERSION,
|
||||
),
|
||||
profileLabel: suggestionCount > 0
|
||||
? copy.profilesReady(suggestionCount)
|
||||
: copy.profilesChecked(profileCount),
|
||||
profileDetail: suggestionCount > 0
|
||||
? copy.profilesReadyDetail(BUILD_FIT_PROFILE_CORPUS_VERSION)
|
||||
: copy.profilesBlockedDetail(BUILD_FIT_PROFILE_CORPUS_VERSION),
|
||||
nativeRunDir,
|
||||
nativeResultCount,
|
||||
isLoadingBuildFit,
|
||||
buildFitLoadError,
|
||||
ranking,
|
||||
aggregateContextFields,
|
||||
setAggregateContextDraft,
|
||||
refreshBuildFit,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user