feat(ql3): audit legacy shadow terminal windows

This commit is contained in:
whyour
2026-08-19 00:07:04 +08:00
parent d38f624262
commit b739d92347
10 changed files with 1991 additions and 5 deletions
@@ -0,0 +1,431 @@
import { QueryTypes, type Sequelize } from 'sequelize';
import {
RUN_ATTEMPT_TABLE,
RUN_TABLE,
} from '../../../migrations/0002-run-schema';
import { RUNNING_INSTANCE_TABLE } from '../../../migrations/0003-running-instance-run-reference';
import {
EXECUTION_ORIGINS,
RUN_ATTEMPT_STATUSES,
RUN_STATUSES,
type ExecutionOrigin,
type RunAttemptStatus,
type RunStatus,
} from '../../domain/run';
import {
MAX_LEGACY_SHADOW_TERMINAL_EVIDENCE_PER_PAGE,
MAX_LEGACY_SHADOW_TERMINAL_PAGE_SIZE,
type LegacyShadowTerminalCandidate,
type LegacyShadowTerminalCursor,
type LegacyShadowTerminalDifferenceSource,
type LegacyShadowTerminalPage,
type LegacyTerminalEvidence,
type LegacyTerminalOutcome,
} from '../../ports/legacyShadowTerminalDifference';
interface CandidateRow {
runId: string;
legacyCronId: number | null;
origin: string;
runStatus: string;
createdAtMs: number | string;
startedAtMs: number | string | null;
finishedAtMs: number | string | null;
attemptCount: number | string;
attemptId: string | null;
attemptStatus: string | null;
attemptPid: number | null;
attemptLogArtifactId: string | null;
attemptCreatedAtMs: number | string | null;
attemptStartedAtMs: number | string | null;
attemptFinishedAtMs: number | string | null;
attemptExitCode: number | null;
}
interface EvidenceRow {
instanceId: number | string;
legacyCronId: number | string;
runId: string | null;
attemptId: string | null;
pid: number | null;
logPath: string | null;
startedAt: number | string;
finishedAt: number | string;
status: number | string;
exitCode: number | null;
}
export type LegacyTerminalLogArtifactIdFactory = (logPath: string) => string;
const ORIGINS = new Set<string>(EXECUTION_ORIGINS);
const RUN_STATUS_SET = new Set<string>(RUN_STATUSES);
const ATTEMPT_STATUS_SET = new Set<string>(RUN_ATTEMPT_STATUSES);
function safeInteger(value: number | string, name: string): number {
const parsed = typeof value === 'number' ? value : Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new TypeError(`${name} is invalid`);
}
return parsed;
}
function optionalPositiveInteger(
value: number | null,
name: string,
): number | undefined {
if (value === null) return undefined;
const parsed = safeInteger(value, name);
if (parsed < 1) throw new TypeError(`${name} is invalid`);
return parsed;
}
function secondsToMilliseconds(value: number | string, name: string): number {
const milliseconds = safeInteger(value, name) * 1_000;
if (!Number.isSafeInteger(milliseconds)) {
throw new TypeError(`${name} is invalid`);
}
return milliseconds;
}
function assertLimit(limit: number): void {
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_LEGACY_SHADOW_TERMINAL_PAGE_SIZE
) {
throw new RangeError(
`limit must be between 1 and ${MAX_LEGACY_SHADOW_TERMINAL_PAGE_SIZE}`,
);
}
}
function runStatus(value: string): RunStatus {
if (!RUN_STATUS_SET.has(value)) {
throw new TypeError('Run status is invalid');
}
return value as RunStatus;
}
function attemptStatus(value: string): RunAttemptStatus {
if (!ATTEMPT_STATUS_SET.has(value)) {
throw new TypeError('Run Attempt status is invalid');
}
return value as RunAttemptStatus;
}
function origin(value: string): ExecutionOrigin {
if (!ORIGINS.has(value)) {
throw new TypeError('Run execution origin is invalid');
}
return value as ExecutionOrigin;
}
function terminalOutcome(value: number): LegacyTerminalOutcome {
if (value === 1) return 'succeeded';
if (value === 2) return 'stopped';
if (value === 3) return 'failed';
throw new TypeError('RunningInstance terminal status is invalid');
}
function candidateFromRow(row: CandidateRow): LegacyShadowTerminalCandidate {
const attemptCount = safeInteger(row.attemptCount, 'attemptCount');
if (
(attemptCount === 0 && row.attemptId !== null) ||
(attemptCount > 0 &&
(row.attemptId === null ||
row.attemptStatus === null ||
row.attemptCreatedAtMs === null))
) {
throw new TypeError('Run Attempt projection is invalid');
}
return {
runId: row.runId,
...(row.legacyCronId === null
? {}
: {
legacyCronId: optionalPositiveInteger(
row.legacyCronId,
'legacyCronId',
),
}),
origin: origin(row.origin),
runStatus: runStatus(row.runStatus),
createdAtMs: safeInteger(row.createdAtMs, 'createdAtMs'),
...(row.startedAtMs === null
? {}
: { startedAtMs: safeInteger(row.startedAtMs, 'startedAtMs') }),
...(row.finishedAtMs === null
? {}
: { finishedAtMs: safeInteger(row.finishedAtMs, 'finishedAtMs') }),
attemptCount,
...(row.attemptId === null
? {}
: {
attempt: {
attemptId: row.attemptId,
status: attemptStatus(row.attemptStatus!),
...(row.attemptPid === null
? {}
: { pid: optionalPositiveInteger(row.attemptPid, 'attemptPid') }),
...(row.attemptLogArtifactId === null
? {}
: { logArtifactId: row.attemptLogArtifactId }),
createdAtMs: safeInteger(
row.attemptCreatedAtMs!,
'attemptCreatedAtMs',
),
...(row.attemptStartedAtMs === null
? {}
: {
startedAtMs: safeInteger(
row.attemptStartedAtMs,
'attemptStartedAtMs',
),
}),
...(row.attemptFinishedAtMs === null
? {}
: {
finishedAtMs: safeInteger(
row.attemptFinishedAtMs,
'attemptFinishedAtMs',
),
}),
...(row.attemptExitCode === null
? {}
: {
exitCode: safeInteger(row.attemptExitCode, 'attemptExitCode'),
}),
},
}),
};
}
/** SQLite/Sequelize adapter for the explicit, read-only local audit. */
export class LegacySequelizeShadowTerminalDifferenceSource
implements LegacyShadowTerminalDifferenceSource
{
constructor(
private readonly database: Sequelize,
private readonly createLogArtifactId: LegacyTerminalLogArtifactIdFactory,
) {}
async listCandidates({
projectId,
origins,
windowStartMs,
windowEndMs,
observedAtMs,
correlationToleranceMs,
cursor,
limit,
}: {
projectId: string;
origins: readonly ExecutionOrigin[];
windowStartMs: number;
windowEndMs: number;
observedAtMs: number;
correlationToleranceMs: number;
cursor?: LegacyShadowTerminalCursor;
limit: number;
}): Promise<LegacyShadowTerminalPage> {
assertLimit(limit);
if (origins.length === 0) {
return {
candidates: [],
evidence: [],
evidenceTruncated: false,
truncated: false,
};
}
const rows = await this.database.query<CandidateRow>(
`SELECT
r.id AS "runId",
r.legacy_cron_id AS "legacyCronId",
r.execution_origin AS origin,
r.status AS "runStatus",
r.created_at_ms AS "createdAtMs",
r.started_at_ms AS "startedAtMs",
r.finished_at_ms AS "finishedAtMs",
(SELECT COUNT(*)
FROM ${RUN_ATTEMPT_TABLE} counted
WHERE counted.run_id = r.id) AS "attemptCount",
attempt.id AS "attemptId",
attempt.status AS "attemptStatus",
attempt.pid AS "attemptPid",
attempt.log_artifact_id AS "attemptLogArtifactId",
attempt.created_at_ms AS "attemptCreatedAtMs",
attempt.started_at_ms AS "attemptStartedAtMs",
attempt.finished_at_ms AS "attemptFinishedAtMs",
attempt.exit_code AS "attemptExitCode"
FROM ${RUN_TABLE} r
LEFT JOIN ${RUN_ATTEMPT_TABLE} attempt
ON attempt.id = (
SELECT latest.id
FROM ${RUN_ATTEMPT_TABLE} latest
WHERE latest.run_id = r.id
ORDER BY latest.attempt DESC, latest.id DESC
LIMIT 1
)
WHERE r.project_id = :projectId
AND r.execution_owner = 'legacy'
AND r.execution_origin IN (:origins)
AND r.created_at_ms >= :windowStartMs
AND r.created_at_ms < :windowEndMs
${
cursor === undefined
? ''
: `AND (
r.created_at_ms > :cursorCreatedAtMs OR
(r.created_at_ms = :cursorCreatedAtMs AND r.id > :cursorRunId)
)`
}
ORDER BY r.created_at_ms ASC, r.id ASC
LIMIT :fetchLimit`,
{
replacements: {
projectId,
origins: [...new Set(origins)],
windowStartMs,
windowEndMs,
...(cursor === undefined
? {}
: {
cursorCreatedAtMs: cursor.createdAtMs,
cursorRunId: cursor.runId,
}),
fetchLimit: limit + 1,
},
type: QueryTypes.SELECT,
},
);
const truncated = rows.length > limit;
const candidates = rows.slice(0, limit).map(candidateFromRow);
if (candidates.length === 0) {
return {
candidates,
evidence: [],
evidenceTruncated: false,
truncated: false,
};
}
const evidence = await this.listEvidence({
candidates,
windowStartMs,
observedAtMs,
correlationToleranceMs,
});
const last = candidates[candidates.length - 1];
return {
candidates,
...evidence,
truncated,
...(truncated
? {
nextCursor: {
createdAtMs: last.createdAtMs,
runId: last.runId,
},
}
: {}),
};
}
private async listEvidence({
candidates,
windowStartMs,
observedAtMs,
correlationToleranceMs,
}: {
candidates: readonly LegacyShadowTerminalCandidate[];
windowStartMs: number;
observedAtMs: number;
correlationToleranceMs: number;
}): Promise<{
evidence: readonly LegacyTerminalEvidence[];
evidenceTruncated: boolean;
}> {
const runIds = candidates.map((candidate) => candidate.runId);
const attemptIds = candidates.flatMap((candidate) =>
candidate.attempt ? [candidate.attempt.attemptId] : [],
);
const legacyCronIds = [
...new Set(
candidates.flatMap((candidate) =>
candidate.legacyCronId === undefined ? [] : [candidate.legacyCronId],
),
),
];
const clauses = ['run_id IN (:runIds)'];
if (attemptIds.length > 0) clauses.push('attempt_id IN (:attemptIds)');
if (legacyCronIds.length > 0) {
clauses.push(`(
cron_id IN (:legacyCronIds)
AND started_at >= :startedAfterSeconds
AND started_at <= :startedBeforeSeconds
)`);
}
const evidenceLimit = Math.min(
MAX_LEGACY_SHADOW_TERMINAL_EVIDENCE_PER_PAGE,
candidates.length * 8,
);
const rows = await this.database.query<EvidenceRow>(
`SELECT
id AS "instanceId",
cron_id AS "legacyCronId",
run_id AS "runId",
attempt_id AS "attemptId",
pid,
log_path AS "logPath",
started_at AS "startedAt",
finished_at AS "finishedAt",
status,
exit_code AS "exitCode"
FROM ${RUNNING_INSTANCE_TABLE}
WHERE status IN (1, 2, 3)
AND finished_at IS NOT NULL
AND (${clauses.join(' OR ')})
ORDER BY started_at ASC, id ASC
LIMIT :fetchLimit`,
{
replacements: {
runIds,
...(attemptIds.length === 0 ? {} : { attemptIds }),
...(legacyCronIds.length === 0
? {}
: {
legacyCronIds,
startedAfterSeconds: Math.floor(
Math.max(0, windowStartMs - correlationToleranceMs) / 1_000,
),
startedBeforeSeconds: Math.ceil(observedAtMs / 1_000),
}),
fetchLimit: evidenceLimit + 1,
},
type: QueryTypes.SELECT,
},
);
const evidenceTruncated = rows.length > evidenceLimit;
return {
evidence: rows.slice(0, evidenceLimit).map((row) => ({
instanceId: safeInteger(row.instanceId, 'instanceId'),
legacyCronId: safeInteger(row.legacyCronId, 'legacyCronId'),
...(row.runId === null ? {} : { runId: row.runId }),
...(row.attemptId === null ? {} : { attemptId: row.attemptId }),
...(row.pid === null
? {}
: { pid: optionalPositiveInteger(row.pid, 'pid') }),
...(row.logPath === null
? {}
: { logArtifactId: this.createLogArtifactId(row.logPath) }),
startedAtMs: secondsToMilliseconds(row.startedAt, 'startedAt'),
finishedAtMs: secondsToMilliseconds(row.finishedAt, 'finishedAt'),
outcome: terminalOutcome(safeInteger(row.status, 'status')),
...(row.exitCode === null
? {}
: { exitCode: safeInteger(row.exitCode, 'exitCode') }),
})),
evidenceTruncated,
};
}
}
@@ -0,0 +1,641 @@
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),
}
: {}),
};
}
}
@@ -0,0 +1,76 @@
import type {
ExecutionOrigin,
RunAttemptStatus,
RunStatus,
} from '../domain/run';
export const MAX_LEGACY_SHADOW_TERMINAL_PAGE_SIZE = 64;
export const MAX_LEGACY_SHADOW_TERMINAL_EVIDENCE_PER_PAGE = 512;
export interface LegacyShadowTerminalCursor {
createdAtMs: number;
runId: string;
}
export interface LegacyShadowTerminalAttemptEvidence {
attemptId: string;
status: RunAttemptStatus;
pid?: number;
logArtifactId?: string;
createdAtMs: number;
startedAtMs?: number;
finishedAtMs?: number;
exitCode?: number;
}
export interface LegacyShadowTerminalCandidate {
runId: string;
legacyCronId?: number;
origin: ExecutionOrigin;
runStatus: RunStatus;
createdAtMs: number;
startedAtMs?: number;
finishedAtMs?: number;
attemptCount: number;
attempt?: LegacyShadowTerminalAttemptEvidence;
}
export type LegacyTerminalOutcome = 'succeeded' | 'failed' | 'stopped';
export interface LegacyTerminalEvidence {
instanceId: number;
legacyCronId: number;
runId?: string;
attemptId?: string;
pid?: number;
logArtifactId?: string;
startedAtMs: number;
finishedAtMs: number;
outcome: LegacyTerminalOutcome;
exitCode?: number;
}
export interface LegacyShadowTerminalPage {
candidates: readonly LegacyShadowTerminalCandidate[];
evidence: readonly LegacyTerminalEvidence[];
evidenceTruncated: boolean;
truncated: boolean;
nextCursor?: LegacyShadowTerminalCursor;
}
/**
* Read-only local Legacy authority used by the explicit terminal-difference
* audit. Implementations must bound both candidate and evidence result sets.
*/
export interface LegacyShadowTerminalDifferenceSource {
listCandidates(options: {
projectId: string;
origins: readonly ExecutionOrigin[];
windowStartMs: number;
windowEndMs: number;
observedAtMs: number;
correlationToleranceMs: number;
cursor?: LegacyShadowTerminalCursor;
limit: number;
}): Promise<LegacyShadowTerminalPage>;
}