mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
642 lines
20 KiB
TypeScript
642 lines
20 KiB
TypeScript
import type {
|
|
ExecutionOrigin,
|
|
RunAttemptStatus,
|
|
RunStatus,
|
|
} from '../domain/run';
|
|
import { EXECUTION_ORIGINS } from '../domain/run';
|
|
import {
|
|
MAX_LEGACY_SHADOW_TERMINAL_EVIDENCE_PER_PAGE,
|
|
MAX_LEGACY_SHADOW_TERMINAL_PAGE_SIZE,
|
|
type LegacyShadowTerminalAttemptEvidence,
|
|
type LegacyShadowTerminalCandidate,
|
|
type LegacyShadowTerminalCursor,
|
|
type LegacyShadowTerminalDifferenceSource,
|
|
type LegacyShadowTerminalPage,
|
|
type LegacyTerminalEvidence,
|
|
type LegacyTerminalOutcome,
|
|
} from '../ports/legacyShadowTerminalDifference';
|
|
|
|
export type LegacyShadowTerminalAuditProfile = 'edge' | 'standalone';
|
|
|
|
export const LEGACY_SHADOW_TERMINAL_CATEGORIES = Object.freeze([
|
|
'matched',
|
|
'shadow_not_terminal',
|
|
'shadow_attempt_missing',
|
|
'shadow_attempt_ambiguous',
|
|
'legacy_evidence_missing',
|
|
'legacy_evidence_ambiguous',
|
|
'status_mismatch',
|
|
'field_mismatch',
|
|
] as const);
|
|
|
|
export type LegacyShadowTerminalCategory =
|
|
(typeof LEGACY_SHADOW_TERMINAL_CATEGORIES)[number];
|
|
|
|
export type LegacyShadowTerminalCategoryCounts = Record<
|
|
LegacyShadowTerminalCategory,
|
|
number
|
|
>;
|
|
|
|
export interface LegacyShadowTerminalDimensionCounts {
|
|
compared: number;
|
|
matched: number;
|
|
mismatched: number;
|
|
unavailable: number;
|
|
}
|
|
|
|
export interface LegacyShadowTerminalDimensions {
|
|
status: LegacyShadowTerminalDimensionCounts;
|
|
exitCode: LegacyShadowTerminalDimensionCounts;
|
|
startedAt: LegacyShadowTerminalDimensionCounts;
|
|
finishedAt: LegacyShadowTerminalDimensionCounts;
|
|
logArtifact: LegacyShadowTerminalDimensionCounts;
|
|
}
|
|
|
|
export interface LegacyShadowTerminalOriginSummary
|
|
extends LegacyShadowTerminalCategoryCounts {
|
|
origin: ExecutionOrigin;
|
|
scanned: number;
|
|
}
|
|
|
|
export type LegacyShadowTerminalAuditAssessment =
|
|
| 'matched'
|
|
| 'empty'
|
|
| 'differences_found'
|
|
| 'window_open'
|
|
| 'incomplete';
|
|
|
|
export type LegacyShadowTerminalAuditStopReason =
|
|
| 'complete'
|
|
| 'page_limit'
|
|
| 'cursor_stalled';
|
|
|
|
export interface LegacyShadowTerminalDifferenceReport {
|
|
schema: 'qinglong/legacy-shadow-terminal-difference-report@v1';
|
|
profile: LegacyShadowTerminalAuditProfile;
|
|
observedAtMs: number;
|
|
window: {
|
|
basis: 'shadow_run_created_at';
|
|
startInclusiveMs: number;
|
|
endExclusiveMs: number;
|
|
minimumSettlingAgeMs: number;
|
|
closed: boolean;
|
|
};
|
|
coverage: {
|
|
direction: 'shadow_to_legacy';
|
|
cohort: 'legacy_owned_shadow_runs';
|
|
legacyWithoutShadow: 'not_measured';
|
|
};
|
|
budget: { pageSize: number; maxPages: number; maxCandidates: number };
|
|
pages: number;
|
|
scanned: number;
|
|
stopReason: LegacyShadowTerminalAuditStopReason;
|
|
remaining: boolean;
|
|
evidenceComplete: boolean;
|
|
evidenceOverflowPages: number;
|
|
assessment: LegacyShadowTerminalAuditAssessment;
|
|
counts: LegacyShadowTerminalCategoryCounts;
|
|
byOrigin: readonly LegacyShadowTerminalOriginSummary[];
|
|
dimensions: LegacyShadowTerminalDimensions;
|
|
terminalAgreementPermille?: number;
|
|
fullyComparablePermille?: number;
|
|
}
|
|
|
|
export interface LegacyShadowTerminalAuditorOptions {
|
|
profile: LegacyShadowTerminalAuditProfile;
|
|
projectId?: string;
|
|
origins: readonly ExecutionOrigin[];
|
|
windowStartMs: number;
|
|
windowEndMs: number;
|
|
observedAtMs?: number;
|
|
minimumSettlingAgeMs?: number;
|
|
correlationToleranceMs?: number;
|
|
clock?: { now(): number };
|
|
}
|
|
|
|
interface Selection {
|
|
status: 'matched' | 'missing' | 'ambiguous';
|
|
evidence?: LegacyTerminalEvidence;
|
|
}
|
|
|
|
interface Classification {
|
|
category: LegacyShadowTerminalCategory;
|
|
fullyComparable: boolean;
|
|
}
|
|
|
|
const BUDGETS: Readonly<
|
|
Record<
|
|
LegacyShadowTerminalAuditProfile,
|
|
{ pageSize: number; maxPages: number }
|
|
>
|
|
> = {
|
|
edge: { pageSize: 8, maxPages: 1 },
|
|
standalone: { pageSize: 32, maxPages: 4 },
|
|
};
|
|
|
|
const MAX_CONFIGURED_ORIGINS = 7;
|
|
const MAX_CORRELATION_TOLERANCE_MS = 60_000;
|
|
const EXECUTION_ORIGIN_SET = new Set<ExecutionOrigin>(EXECUTION_ORIGINS);
|
|
|
|
function assertSafeInteger(name: string, value: number): void {
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
}
|
|
}
|
|
|
|
function emptyCounts(): LegacyShadowTerminalCategoryCounts {
|
|
return Object.fromEntries(
|
|
LEGACY_SHADOW_TERMINAL_CATEGORIES.map((category) => [category, 0]),
|
|
) as LegacyShadowTerminalCategoryCounts;
|
|
}
|
|
|
|
function emptyDimension(): LegacyShadowTerminalDimensionCounts {
|
|
return { compared: 0, matched: 0, mismatched: 0, unavailable: 0 };
|
|
}
|
|
|
|
function emptyDimensions(): LegacyShadowTerminalDimensions {
|
|
return {
|
|
status: emptyDimension(),
|
|
exitCode: emptyDimension(),
|
|
startedAt: emptyDimension(),
|
|
finishedAt: emptyDimension(),
|
|
logArtifact: emptyDimension(),
|
|
};
|
|
}
|
|
|
|
function originSummary(
|
|
origin: ExecutionOrigin,
|
|
): LegacyShadowTerminalOriginSummary {
|
|
return { origin, scanned: 0, ...emptyCounts() };
|
|
}
|
|
|
|
function compareDimension(
|
|
target: LegacyShadowTerminalDimensionCounts,
|
|
left: unknown,
|
|
right: unknown,
|
|
matches: (left: unknown, right: unknown) => boolean = (a, b) => a === b,
|
|
): boolean | undefined {
|
|
if (left === undefined || right === undefined) {
|
|
target.unavailable += 1;
|
|
return undefined;
|
|
}
|
|
target.compared += 1;
|
|
if (matches(left, right)) {
|
|
target.matched += 1;
|
|
return true;
|
|
}
|
|
target.mismatched += 1;
|
|
return false;
|
|
}
|
|
|
|
function markUnavailable(dimensions: LegacyShadowTerminalDimensions): void {
|
|
for (const dimension of Object.values(dimensions)) {
|
|
dimension.unavailable += 1;
|
|
}
|
|
}
|
|
|
|
function expectedOutcome(
|
|
status: RunStatus | RunAttemptStatus,
|
|
): LegacyTerminalOutcome | undefined {
|
|
if (status === 'succeeded') return 'succeeded';
|
|
if (status === 'failed') return 'failed';
|
|
if (status === 'cancelled') return 'stopped';
|
|
return undefined;
|
|
}
|
|
|
|
function isTerminalShadowStatus(status: RunStatus | RunAttemptStatus): boolean {
|
|
return ['succeeded', 'failed', 'cancelled', 'timed_out', 'lost'].includes(
|
|
status,
|
|
);
|
|
}
|
|
|
|
function selectEvidence(
|
|
candidate: LegacyShadowTerminalCandidate,
|
|
attempt: LegacyShadowTerminalAttemptEvidence,
|
|
evidence: readonly LegacyTerminalEvidence[],
|
|
toleranceMs: number,
|
|
): Selection {
|
|
const direct = evidence.filter(
|
|
(item) =>
|
|
item.attemptId === attempt.attemptId || item.runId === candidate.runId,
|
|
);
|
|
if (
|
|
direct.some(
|
|
(item) =>
|
|
(item.attemptId !== undefined &&
|
|
item.attemptId !== attempt.attemptId) ||
|
|
(item.runId !== undefined && item.runId !== candidate.runId),
|
|
)
|
|
) {
|
|
return { status: 'ambiguous' };
|
|
}
|
|
if (direct.length === 1) return { status: 'matched', evidence: direct[0] };
|
|
if (direct.length > 1) return { status: 'ambiguous' };
|
|
if (candidate.legacyCronId === undefined) return { status: 'missing' };
|
|
|
|
const sameCron = evidence.filter(
|
|
(item) => item.legacyCronId === candidate.legacyCronId,
|
|
);
|
|
const byLog =
|
|
attempt.logArtifactId === undefined
|
|
? []
|
|
: sameCron.filter((item) => item.logArtifactId === attempt.logArtifactId);
|
|
const byPid =
|
|
attempt.pid === undefined
|
|
? []
|
|
: sameCron.filter((item) => item.pid === attempt.pid);
|
|
|
|
if (attempt.logArtifactId !== undefined && attempt.pid !== undefined) {
|
|
const byPidSet = new Set(byPid);
|
|
const intersection = byLog.filter((item) => byPidSet.has(item));
|
|
if (intersection.length === 1) {
|
|
return { status: 'matched', evidence: intersection[0] };
|
|
}
|
|
if (intersection.length > 1 || byLog.length > 1 || byPid.length > 1) {
|
|
return { status: 'ambiguous' };
|
|
}
|
|
if (byLog.length === 1 && byPid.length === 0) {
|
|
return { status: 'matched', evidence: byLog[0] };
|
|
}
|
|
if (byPid.length === 1 && byLog.length === 0) {
|
|
return { status: 'matched', evidence: byPid[0] };
|
|
}
|
|
if (byLog.length === 1 && byPid.length === 1) {
|
|
return { status: 'ambiguous' };
|
|
}
|
|
} else if (byLog.length === 1) {
|
|
return { status: 'matched', evidence: byLog[0] };
|
|
} else if (byLog.length > 1) {
|
|
return { status: 'ambiguous' };
|
|
} else if (byPid.length === 1) {
|
|
return { status: 'matched', evidence: byPid[0] };
|
|
} else if (byPid.length > 1) {
|
|
return { status: 'ambiguous' };
|
|
}
|
|
|
|
const shadowStartedAtMs = attempt.startedAtMs ?? candidate.startedAtMs;
|
|
if (shadowStartedAtMs === undefined) return { status: 'missing' };
|
|
const byStart = sameCron.filter(
|
|
(item) => Math.abs(item.startedAtMs - shadowStartedAtMs) <= toleranceMs,
|
|
);
|
|
if (byStart.length === 1) {
|
|
return { status: 'matched', evidence: byStart[0] };
|
|
}
|
|
return { status: byStart.length > 1 ? 'ambiguous' : 'missing' };
|
|
}
|
|
|
|
function classify(
|
|
candidate: LegacyShadowTerminalCandidate,
|
|
evidence: readonly LegacyTerminalEvidence[],
|
|
evidenceTruncated: boolean,
|
|
toleranceMs: number,
|
|
dimensions: LegacyShadowTerminalDimensions,
|
|
): Classification {
|
|
if (
|
|
!isTerminalShadowStatus(candidate.runStatus) ||
|
|
candidate.finishedAtMs === undefined
|
|
) {
|
|
markUnavailable(dimensions);
|
|
return { category: 'shadow_not_terminal', fullyComparable: false };
|
|
}
|
|
if (candidate.attemptCount === 0 || candidate.attempt === undefined) {
|
|
markUnavailable(dimensions);
|
|
return { category: 'shadow_attempt_missing', fullyComparable: false };
|
|
}
|
|
if (candidate.attemptCount !== 1) {
|
|
markUnavailable(dimensions);
|
|
return { category: 'shadow_attempt_ambiguous', fullyComparable: false };
|
|
}
|
|
if (
|
|
!isTerminalShadowStatus(candidate.attempt.status) ||
|
|
candidate.attempt.finishedAtMs === undefined
|
|
) {
|
|
markUnavailable(dimensions);
|
|
return { category: 'shadow_not_terminal', fullyComparable: false };
|
|
}
|
|
if (evidenceTruncated) {
|
|
markUnavailable(dimensions);
|
|
return { category: 'legacy_evidence_ambiguous', fullyComparable: false };
|
|
}
|
|
|
|
const selection = selectEvidence(
|
|
candidate,
|
|
candidate.attempt,
|
|
evidence,
|
|
toleranceMs,
|
|
);
|
|
if (selection.status === 'ambiguous') {
|
|
markUnavailable(dimensions);
|
|
return { category: 'legacy_evidence_ambiguous', fullyComparable: false };
|
|
}
|
|
if (selection.status === 'missing' || selection.evidence === undefined) {
|
|
markUnavailable(dimensions);
|
|
return { category: 'legacy_evidence_missing', fullyComparable: false };
|
|
}
|
|
|
|
const expectedRunOutcome = expectedOutcome(candidate.runStatus);
|
|
const expectedAttemptOutcome = expectedOutcome(candidate.attempt.status);
|
|
let statusMatches: boolean | undefined;
|
|
if (
|
|
expectedRunOutcome === undefined ||
|
|
expectedAttemptOutcome === undefined
|
|
) {
|
|
dimensions.status.unavailable += 1;
|
|
} else if (expectedRunOutcome !== expectedAttemptOutcome) {
|
|
dimensions.status.compared += 1;
|
|
dimensions.status.mismatched += 1;
|
|
statusMatches = false;
|
|
} else {
|
|
statusMatches = compareDimension(
|
|
dimensions.status,
|
|
expectedRunOutcome,
|
|
selection.evidence.outcome,
|
|
);
|
|
}
|
|
if (
|
|
expectedRunOutcome === undefined ||
|
|
expectedAttemptOutcome === undefined ||
|
|
expectedRunOutcome !== expectedAttemptOutcome ||
|
|
statusMatches !== true
|
|
) {
|
|
for (const dimension of [
|
|
dimensions.exitCode,
|
|
dimensions.startedAt,
|
|
dimensions.finishedAt,
|
|
dimensions.logArtifact,
|
|
]) {
|
|
dimension.unavailable += 1;
|
|
}
|
|
return { category: 'status_mismatch', fullyComparable: false };
|
|
}
|
|
|
|
const exitMatches = compareDimension(
|
|
dimensions.exitCode,
|
|
candidate.attempt.exitCode,
|
|
selection.evidence.exitCode,
|
|
);
|
|
const startMatches = compareDimension(
|
|
dimensions.startedAt,
|
|
candidate.attempt.startedAtMs ?? candidate.startedAtMs,
|
|
selection.evidence.startedAtMs,
|
|
(left, right) => Math.abs(Number(left) - Number(right)) <= toleranceMs,
|
|
);
|
|
const finishMatches = compareDimension(
|
|
dimensions.finishedAt,
|
|
candidate.attempt.finishedAtMs ?? candidate.finishedAtMs,
|
|
selection.evidence.finishedAtMs,
|
|
(left, right) => Math.abs(Number(left) - Number(right)) <= toleranceMs,
|
|
);
|
|
const logMatches = compareDimension(
|
|
dimensions.logArtifact,
|
|
candidate.attempt.logArtifactId,
|
|
selection.evidence.logArtifactId,
|
|
);
|
|
const compared = [true, exitMatches, startMatches, finishMatches, logMatches];
|
|
const hasMismatch = compared.includes(false);
|
|
return {
|
|
category: hasMismatch ? 'field_mismatch' : 'matched',
|
|
fullyComparable: compared.every((value) => value === true),
|
|
};
|
|
}
|
|
|
|
function sameCursor(
|
|
left: LegacyShadowTerminalCursor | undefined,
|
|
right: LegacyShadowTerminalCursor,
|
|
): boolean {
|
|
return (
|
|
left !== undefined &&
|
|
left.createdAtMs === right.createdAtMs &&
|
|
left.runId === right.runId
|
|
);
|
|
}
|
|
|
|
function validatePage(
|
|
page: LegacyShadowTerminalPage,
|
|
cursor: LegacyShadowTerminalCursor | undefined,
|
|
limit: number,
|
|
allowedOrigins: ReadonlySet<ExecutionOrigin>,
|
|
windowStartMs: number,
|
|
windowEndMs: number,
|
|
): void {
|
|
if (page.candidates.length > limit) {
|
|
throw new Error('Legacy Shadow terminal source exceeded candidate limit');
|
|
}
|
|
if (
|
|
page.evidence.length >
|
|
Math.min(
|
|
MAX_LEGACY_SHADOW_TERMINAL_EVIDENCE_PER_PAGE,
|
|
page.candidates.length * 8,
|
|
)
|
|
) {
|
|
throw new Error('Legacy Shadow terminal source exceeded evidence limit');
|
|
}
|
|
let previous = cursor;
|
|
const runIds = new Set<string>();
|
|
for (const candidate of page.candidates) {
|
|
if (
|
|
candidate.runId.length === 0 ||
|
|
runIds.has(candidate.runId) ||
|
|
!allowedOrigins.has(candidate.origin) ||
|
|
!Number.isSafeInteger(candidate.createdAtMs) ||
|
|
candidate.createdAtMs < windowStartMs ||
|
|
candidate.createdAtMs >= windowEndMs ||
|
|
(previous !== undefined &&
|
|
(candidate.createdAtMs < previous.createdAtMs ||
|
|
(candidate.createdAtMs === previous.createdAtMs &&
|
|
candidate.runId <= previous.runId)))
|
|
) {
|
|
throw new Error('Legacy Shadow terminal source returned an invalid page');
|
|
}
|
|
runIds.add(candidate.runId);
|
|
previous = { createdAtMs: candidate.createdAtMs, runId: candidate.runId };
|
|
}
|
|
if (page.truncated) {
|
|
if (
|
|
page.candidates.length !== limit ||
|
|
page.nextCursor === undefined ||
|
|
previous === undefined ||
|
|
page.nextCursor.createdAtMs !== previous.createdAtMs ||
|
|
page.nextCursor.runId !== previous.runId
|
|
) {
|
|
throw new Error(
|
|
'Legacy Shadow terminal source returned an invalid cursor',
|
|
);
|
|
}
|
|
} else if (page.nextCursor !== undefined) {
|
|
throw new Error('Completed Legacy Shadow terminal page returned a cursor');
|
|
}
|
|
}
|
|
|
|
function permille(numerator: number, denominator: number): number {
|
|
return Math.floor((numerator * 1_000) / denominator);
|
|
}
|
|
|
|
/**
|
|
* Explicit, timer-free audit for a closed Shadow cohort. The report deliberately
|
|
* does not claim Legacy-to-Shadow capture coverage because RunningInstances have
|
|
* no trustworthy execution-origin field in the 2.x schema.
|
|
*/
|
|
export class LegacyShadowTerminalDifferenceAuditor {
|
|
constructor(private readonly source: LegacyShadowTerminalDifferenceSource) {}
|
|
|
|
async run(
|
|
options: LegacyShadowTerminalAuditorOptions,
|
|
): Promise<LegacyShadowTerminalDifferenceReport> {
|
|
if (!['edge', 'standalone'].includes(options.profile)) {
|
|
throw new RangeError('profile must be edge or standalone');
|
|
}
|
|
const projectId = options.projectId ?? 'default';
|
|
if (projectId.length < 1 || projectId.length > 128) {
|
|
throw new RangeError('projectId length is invalid');
|
|
}
|
|
const origins = [...new Set(options.origins)];
|
|
if (origins.length < 1 || origins.length > MAX_CONFIGURED_ORIGINS) {
|
|
throw new RangeError('origin count is outside its audit budget');
|
|
}
|
|
if (origins.some((origin) => !EXECUTION_ORIGIN_SET.has(origin))) {
|
|
throw new RangeError('terminal audit origin is invalid');
|
|
}
|
|
assertSafeInteger('windowStartMs', options.windowStartMs);
|
|
assertSafeInteger('windowEndMs', options.windowEndMs);
|
|
if (options.windowStartMs >= options.windowEndMs) {
|
|
throw new RangeError('terminal audit window must be non-empty');
|
|
}
|
|
const observedAtMs =
|
|
options.observedAtMs ?? options.clock?.now() ?? Date.now();
|
|
const minimumSettlingAgeMs = options.minimumSettlingAgeMs ?? 5 * 60_000;
|
|
const correlationToleranceMs = options.correlationToleranceMs ?? 2_000;
|
|
assertSafeInteger('observedAtMs', observedAtMs);
|
|
assertSafeInteger('minimumSettlingAgeMs', minimumSettlingAgeMs);
|
|
assertSafeInteger('correlationToleranceMs', correlationToleranceMs);
|
|
if (correlationToleranceMs > MAX_CORRELATION_TOLERANCE_MS) {
|
|
throw new RangeError('correlationToleranceMs exceeds its hard limit');
|
|
}
|
|
const budget = BUDGETS[options.profile];
|
|
if (budget.pageSize > MAX_LEGACY_SHADOW_TERMINAL_PAGE_SIZE) {
|
|
throw new RangeError('terminal audit Profile exceeds the source limit');
|
|
}
|
|
|
|
const counts = emptyCounts();
|
|
const dimensions = emptyDimensions();
|
|
const originSummaries = new Map(
|
|
origins.map((origin) => [origin, originSummary(origin)]),
|
|
);
|
|
const allowedOrigins = new Set(origins);
|
|
let cursor: LegacyShadowTerminalCursor | undefined;
|
|
let pages = 0;
|
|
let scanned = 0;
|
|
let fullyComparable = 0;
|
|
let evidenceOverflowPages = 0;
|
|
let remaining = false;
|
|
let stopReason: LegacyShadowTerminalAuditStopReason = 'complete';
|
|
|
|
while (pages < budget.maxPages) {
|
|
const page = await this.source.listCandidates({
|
|
projectId,
|
|
origins,
|
|
windowStartMs: options.windowStartMs,
|
|
windowEndMs: options.windowEndMs,
|
|
observedAtMs,
|
|
correlationToleranceMs,
|
|
...(cursor === undefined ? {} : { cursor }),
|
|
limit: budget.pageSize,
|
|
});
|
|
pages += 1;
|
|
validatePage(
|
|
page,
|
|
cursor,
|
|
budget.pageSize,
|
|
allowedOrigins,
|
|
options.windowStartMs,
|
|
options.windowEndMs,
|
|
);
|
|
if (page.evidenceTruncated) evidenceOverflowPages += 1;
|
|
|
|
for (const candidate of page.candidates) {
|
|
const result = classify(
|
|
candidate,
|
|
page.evidence,
|
|
page.evidenceTruncated,
|
|
correlationToleranceMs,
|
|
dimensions,
|
|
);
|
|
counts[result.category] += 1;
|
|
const perOrigin = originSummaries.get(candidate.origin)!;
|
|
perOrigin.scanned += 1;
|
|
perOrigin[result.category] += 1;
|
|
scanned += 1;
|
|
if (result.fullyComparable) fullyComparable += 1;
|
|
}
|
|
|
|
if (!page.truncated) break;
|
|
if (
|
|
page.nextCursor === undefined ||
|
|
sameCursor(cursor, page.nextCursor)
|
|
) {
|
|
remaining = true;
|
|
stopReason = 'cursor_stalled';
|
|
break;
|
|
}
|
|
cursor = page.nextCursor;
|
|
if (pages === budget.maxPages) {
|
|
remaining = true;
|
|
stopReason = 'page_limit';
|
|
}
|
|
}
|
|
|
|
const windowClosed =
|
|
options.windowEndMs <= observedAtMs - minimumSettlingAgeMs;
|
|
const evidenceComplete = evidenceOverflowPages === 0;
|
|
const assessment: LegacyShadowTerminalAuditAssessment = !windowClosed
|
|
? 'window_open'
|
|
: remaining || !evidenceComplete
|
|
? 'incomplete'
|
|
: scanned === 0
|
|
? 'empty'
|
|
: counts.matched === scanned
|
|
? 'matched'
|
|
: 'differences_found';
|
|
const ratiosAvailable =
|
|
windowClosed && !remaining && evidenceComplete && scanned > 0;
|
|
|
|
return {
|
|
schema: 'qinglong/legacy-shadow-terminal-difference-report@v1',
|
|
profile: options.profile,
|
|
observedAtMs,
|
|
window: {
|
|
basis: 'shadow_run_created_at',
|
|
startInclusiveMs: options.windowStartMs,
|
|
endExclusiveMs: options.windowEndMs,
|
|
minimumSettlingAgeMs,
|
|
closed: windowClosed,
|
|
},
|
|
coverage: {
|
|
direction: 'shadow_to_legacy',
|
|
cohort: 'legacy_owned_shadow_runs',
|
|
legacyWithoutShadow: 'not_measured',
|
|
},
|
|
budget: {
|
|
pageSize: budget.pageSize,
|
|
maxPages: budget.maxPages,
|
|
maxCandidates: budget.pageSize * budget.maxPages,
|
|
},
|
|
pages,
|
|
scanned,
|
|
stopReason,
|
|
remaining,
|
|
evidenceComplete,
|
|
evidenceOverflowPages,
|
|
assessment,
|
|
counts,
|
|
byOrigin: [...originSummaries.values()],
|
|
dimensions,
|
|
...(ratiosAvailable
|
|
? {
|
|
terminalAgreementPermille: permille(counts.matched, scanned),
|
|
fullyComparablePermille: permille(fullyComparable, scanned),
|
|
}
|
|
: {}),
|
|
};
|
|
}
|
|
}
|