@pilio/gemini-watermark-remover 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,823 +1,823 @@
1
- import {
2
- assessCalibratedWatermarkResidualVisibility,
3
- assessReferenceTextureAlignment,
4
- assessRemovalDiffArtifacts,
5
- calculateNearBlackRatio,
6
- calculateNearWhiteRatio,
7
- scoreRegion
8
- } from './restorationMetrics.js';
9
- import { getEmbeddedAlphaMap } from './embeddedAlphaMaps.js';
10
- import {
11
- buildPreviewNeighborhoodPrior,
12
- measurePreviewBoundaryMetrics
13
- } from './previewAlphaCalibration.js';
14
- import { compareRankingKey } from './watermarkScoring.js';
15
- import { shouldPreferFullStrengthNewMarginVariant } from './candidateEvaluation.js';
16
-
17
- export const FINAL_EVIDENCE_WEIGHT = 0.35;
18
- export const FINAL_RESIDUAL_WEIGHT = 0.40;
19
- export const FINAL_DAMAGE_WEIGHT = 0.25;
20
- export const FINAL_DAMAGE_WARNING_PENALTY = 0.08;
21
-
22
- const DISCOVERY_ROLE_PENALTIES = {
23
- 'fixed-selected': 0,
24
- 'automatic-selected': 0.03,
25
- 'aggressive-fallback-alternative': 0.015,
26
- 'conservative-derived': 0.15,
27
- 'discovered-alternative': 0.25
28
- };
29
- // Keep these reporting thresholds aligned with assessWatermarkResidualVisibility.
30
- const IMPERFECTION_POSITIVE_HALO_LUM_THRESHOLD = 6;
31
- const IMPERFECTION_GRADIENT_THRESHOLD = 0.22;
32
- const IMPERFECTION_SPATIAL_THRESHOLD = 0.18;
33
- const IMPERFECTION_WARNING_RATIO = 0.5;
34
- const DARK_SUPPORT_RESOLUTION_MAX_ABS_SPATIAL = 0.18;
35
- const DARK_SUPPORT_RESOLUTION_MAX_GRADIENT = 0.18;
36
- const DARK_SUPPORT_RESOLUTION_MAX_NEWLY_CLIPPED_RATIO = 0.25;
37
- const SEVERE_DARK_FLAT_TEXTURE_WARNING_THRESHOLD = 1.5;
38
- const INDEPENDENT_DARK_SUPPORT_SIZE = 96;
39
- const INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD = 12;
40
- const INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_SPATIAL = 0.95;
41
- const INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_GRADIENT = 0.9;
42
- const INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_NEAR_BLACK = 0.1;
43
- const INDEPENDENT_DARK_SUPPORT_MIN_CANDIDATE_NEAR_BLACK = 0.25;
44
- const INDEPENDENT_DARK_SUPPORT_MAX_OPPOSITE_HALO_LUMINANCE = 1;
45
- const INDEPENDENT_DARK_SUPPORT_MAX_OUTSIDE_ALPHA_CLIP_RATIO = 0.03;
46
- const INDEPENDENT_DARK_SUPPORT_MIN_STRONG_ALPHA_CLIP_RATIO = 0.8;
47
- const INDEPENDENT_DARK_SUPPORT_ALPHA_MIN = 0.05;
48
- const INDEPENDENT_DARK_SUPPORT_ALPHA_STRONG_MIN = 0.2;
49
- const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_LUMINANCE = 8;
50
- const INDEPENDENT_DARK_SUPPORT_MAX_CANDIDATE_MEAN_LUMINANCE = 8;
51
- const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_DELTA = 1.5;
52
- const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MAE = 2.25;
53
- const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_WEIGHTED_MAE = 3.5;
54
- const INDEPENDENT_DARK_SUPPORT_MAX_BOUNDARY_SCORE = 0.35;
55
- const INDEPENDENT_DARK_SUPPORT_PRIOR_RADII = Object.freeze([4, 12]);
56
- const INDEPENDENT_DARK_SUPPORT_RELAXATION_PASSES = 0;
57
-
58
- function clamp01(value) {
59
- if (!Number.isFinite(value)) return 0;
60
- return Math.max(0, Math.min(1, value));
61
- }
62
-
63
- function haveEqualAlphaValues(left, right) {
64
- if (!left || !right || left.length !== right.length) return false;
65
- for (let index = 0; index < left.length; index += 1) {
66
- if (left[index] !== right[index]) return false;
67
- }
68
- return true;
69
- }
70
-
71
- function resolveReferenceAlphaMap(
72
- hypothesis,
73
- explicitReferenceAlphaMap,
74
- selectedAlphaMap
75
- ) {
76
- const config = hypothesis?.config ?? hypothesis?.trial?.config;
77
- const logoSize = config?.logoSize;
78
- const alphaVariant = config?.alphaVariant;
79
- const catalogKey = Number.isFinite(logoSize)
80
- ? typeof alphaVariant === 'string' && alphaVariant.length > 0
81
- ? `${logoSize}-${alphaVariant}`
82
- : logoSize
83
- : null;
84
- // Keep one catalog-derived observer independent from any alpha profile
85
- // mutated or injected by the candidate being evaluated.
86
- const referenceAlphaMap = explicitReferenceAlphaMap ??
87
- (catalogKey === null ? null : getEmbeddedAlphaMap(catalogKey));
88
- return haveEqualAlphaValues(referenceAlphaMap, selectedAlphaMap)
89
- ? null
90
- : referenceAlphaMap;
91
- }
92
-
93
- function extractCandidateQualityScratch(imageData, position) {
94
- const x = Math.max(0, position.x - INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD);
95
- const y = Math.max(0, position.y - INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD);
96
- const right = Math.min(
97
- imageData.width,
98
- position.x + position.width + INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD
99
- );
100
- const bottom = Math.min(
101
- imageData.height,
102
- position.y + position.height + INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD
103
- );
104
- const scratch = {
105
- width: right - x,
106
- height: bottom - y,
107
- data: new Uint8ClampedArray((right - x) * (bottom - y) * 4)
108
- };
109
- for (let row = 0; row < scratch.height; row++) {
110
- const sourceStart = ((y + row) * imageData.width + x) * 4;
111
- const sourceEnd = sourceStart + scratch.width * 4;
112
- scratch.data.set(
113
- imageData.data.subarray(sourceStart, sourceEnd),
114
- row * scratch.width * 4
115
- );
116
- }
117
- return {
118
- imageData: scratch,
119
- position: {
120
- x: position.x - x,
121
- y: position.y - y,
122
- width: position.width,
123
- height: position.height
124
- }
125
- };
126
- }
127
-
128
- function measureIndependentDarkSupportClipping({
129
- originalImageData,
130
- candidateImageData,
131
- alphaMap,
132
- position
133
- }) {
134
- let newlyClippedCount = 0;
135
- let outsideAlphaSupportCount = 0;
136
- let strongAlphaSupportCount = 0;
137
- for (let row = 0; row < position.height; row++) {
138
- for (let col = 0; col < position.width; col++) {
139
- const localIndex = row * position.width + col;
140
- const pixelIndex = (
141
- (position.y + row) * originalImageData.width +
142
- position.x +
143
- col
144
- ) * 4;
145
- const newlyClipped = [0, 1, 2].some((channel) => (
146
- candidateImageData.data[pixelIndex + channel] === 0 &&
147
- originalImageData.data[pixelIndex + channel] > 5
148
- ));
149
- if (!newlyClipped) continue;
150
-
151
- newlyClippedCount++;
152
- const alpha = Math.abs(alphaMap[localIndex]);
153
- if (alpha < INDEPENDENT_DARK_SUPPORT_ALPHA_MIN) {
154
- outsideAlphaSupportCount++;
155
- }
156
- if (alpha >= INDEPENDENT_DARK_SUPPORT_ALPHA_STRONG_MIN) {
157
- strongAlphaSupportCount++;
158
- }
159
- }
160
- }
161
- return {
162
- outsideAlphaSupportRatio: newlyClippedCount > 0
163
- ? outsideAlphaSupportCount / newlyClippedCount
164
- : 0,
165
- strongAlphaSupportRatio: newlyClippedCount > 0
166
- ? strongAlphaSupportCount / newlyClippedCount
167
- : 1
168
- };
169
- }
170
-
171
- function measureIndependentDarkSupportPrior({
172
- candidateImageData,
173
- priorImageData,
174
- alphaMap,
175
- position
176
- }) {
177
- let absoluteDelta = 0;
178
- let weightedAbsoluteDelta = 0;
179
- let alphaWeight = 0;
180
- let candidateLuminance = 0;
181
- let priorLuminance = 0;
182
- let channelCount = 0;
183
- let pixelCount = 0;
184
- for (let row = 0; row < position.height; row++) {
185
- for (let col = 0; col < position.width; col++) {
186
- const localIndex = row * position.width + col;
187
- const alpha = Math.abs(alphaMap[localIndex]);
188
- const pixelIndex = (
189
- (position.y + row) * candidateImageData.width +
190
- position.x +
191
- col
192
- ) * 4;
193
- for (let channel = 0; channel < 3; channel++) {
194
- const delta = Math.abs(
195
- candidateImageData.data[pixelIndex + channel] -
196
- priorImageData.data[pixelIndex + channel]
197
- );
198
- absoluteDelta += delta;
199
- weightedAbsoluteDelta += delta * alpha;
200
- channelCount++;
201
- }
202
- candidateLuminance +=
203
- 0.2126 * candidateImageData.data[pixelIndex] +
204
- 0.7152 * candidateImageData.data[pixelIndex + 1] +
205
- 0.0722 * candidateImageData.data[pixelIndex + 2];
206
- priorLuminance +=
207
- 0.2126 * priorImageData.data[pixelIndex] +
208
- 0.7152 * priorImageData.data[pixelIndex + 1] +
209
- 0.0722 * priorImageData.data[pixelIndex + 2];
210
- alphaWeight += alpha;
211
- pixelCount++;
212
- }
213
- }
214
- const candidateMeanLuminance = candidateLuminance / Math.max(1, pixelCount);
215
- const priorMeanLuminance = priorLuminance / Math.max(1, pixelCount);
216
- return {
217
- meanAbsoluteDelta: absoluteDelta / Math.max(1, channelCount),
218
- weightedMeanAbsoluteDelta:
219
- weightedAbsoluteDelta / Math.max(1e-8, alphaWeight * 3),
220
- candidateMeanLuminance,
221
- priorMeanLuminance,
222
- meanLuminanceDelta: Math.abs(candidateMeanLuminance - priorMeanLuminance)
223
- };
224
- }
225
-
226
- function assessIndependentDarkBackgroundSupportConvergence({
227
- originalImageData,
228
- candidateImageData,
229
- hypothesis,
230
- position,
231
- alphaMap,
232
- alphaGain,
233
- original,
234
- final,
235
- visibility,
236
- artifacts,
237
- originalNearBlackRatio,
238
- candidateNearBlackRatio,
239
- rawDamageWarning
240
- }) {
241
- const trial = hypothesis?.trial;
242
- const source = trial?.source;
243
- const catalogAlphaMap = getEmbeddedAlphaMap('96-20260520');
244
- if (
245
- rawDamageWarning !== true ||
246
- source !== 'standard' ||
247
- position.width !== INDEPENDENT_DARK_SUPPORT_SIZE ||
248
- position.height !== INDEPENDENT_DARK_SUPPORT_SIZE ||
249
- alphaGain !== 1 ||
250
- !haveEqualAlphaValues(alphaMap, catalogAlphaMap) ||
251
- original.spatialScore < INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_SPATIAL ||
252
- original.gradientScore < INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_GRADIENT ||
253
- visibility?.visible !== false ||
254
- Math.abs(final.spatialScore) > DARK_SUPPORT_RESOLUTION_MAX_ABS_SPATIAL ||
255
- final.gradientScore > DARK_SUPPORT_RESOLUTION_MAX_GRADIENT ||
256
- originalNearBlackRatio < INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_NEAR_BLACK ||
257
- candidateNearBlackRatio < INDEPENDENT_DARK_SUPPORT_MIN_CANDIDATE_NEAR_BLACK ||
258
- Number(artifacts?.newlyClippedRatio ?? Infinity) >
259
- DARK_SUPPORT_RESOLUTION_MAX_NEWLY_CLIPPED_RATIO ||
260
- Number(artifacts?.oppositeDirectionHaloLum ?? Infinity) >
261
- INDEPENDENT_DARK_SUPPORT_MAX_OPPOSITE_HALO_LUMINANCE
262
- ) {
263
- return null;
264
- }
265
-
266
- const originalScratch = extractCandidateQualityScratch(
267
- originalImageData,
268
- position
269
- );
270
- const candidateScratch = extractCandidateQualityScratch(
271
- candidateImageData,
272
- position
273
- );
274
- const priorMetrics = INDEPENDENT_DARK_SUPPORT_PRIOR_RADII.map((radius) => {
275
- const priorImageData = buildPreviewNeighborhoodPrior({
276
- previewImageData: originalScratch.imageData,
277
- position: originalScratch.position,
278
- radius,
279
- relaxationPasses: INDEPENDENT_DARK_SUPPORT_RELAXATION_PASSES
280
- });
281
- return {
282
- radius,
283
- ...measureIndependentDarkSupportPrior({
284
- candidateImageData: candidateScratch.imageData,
285
- priorImageData,
286
- alphaMap,
287
- position: candidateScratch.position
288
- })
289
- };
290
- });
291
- const clipping = measureIndependentDarkSupportClipping({
292
- originalImageData: originalScratch.imageData,
293
- candidateImageData: candidateScratch.imageData,
294
- alphaMap,
295
- position: originalScratch.position
296
- });
297
- const boundary = measurePreviewBoundaryMetrics(
298
- candidateScratch.imageData,
299
- originalScratch.imageData,
300
- originalScratch.position
301
- );
302
- const maximumPriorMeanLuminance = Math.max(
303
- ...priorMetrics.map((item) => item.priorMeanLuminance)
304
- );
305
- const maximumCandidateMeanLuminance = Math.max(
306
- ...priorMetrics.map((item) => item.candidateMeanLuminance)
307
- );
308
- const maximumPriorMeanDelta = Math.max(
309
- ...priorMetrics.map((item) => item.meanLuminanceDelta)
310
- );
311
- const maximumPriorMae = Math.max(
312
- ...priorMetrics.map((item) => item.meanAbsoluteDelta)
313
- );
314
- const maximumPriorWeightedMae = Math.max(
315
- ...priorMetrics.map((item) => item.weightedMeanAbsoluteDelta)
316
- );
317
- const accepted = Boolean(
318
- clipping.outsideAlphaSupportRatio <=
319
- INDEPENDENT_DARK_SUPPORT_MAX_OUTSIDE_ALPHA_CLIP_RATIO &&
320
- clipping.strongAlphaSupportRatio >=
321
- INDEPENDENT_DARK_SUPPORT_MIN_STRONG_ALPHA_CLIP_RATIO &&
322
- maximumPriorMeanLuminance <=
323
- INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_LUMINANCE &&
324
- maximumCandidateMeanLuminance <=
325
- INDEPENDENT_DARK_SUPPORT_MAX_CANDIDATE_MEAN_LUMINANCE &&
326
- maximumPriorMeanDelta <= INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_DELTA &&
327
- maximumPriorMae <= INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MAE &&
328
- maximumPriorWeightedMae <=
329
- INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_WEIGHTED_MAE &&
330
- boundary.normalizedScore <= INDEPENDENT_DARK_SUPPORT_MAX_BOUNDARY_SCORE
331
- );
332
- return {
333
- accepted,
334
- source: 'independent-standard-96-neighborhood-prior',
335
- textureHardRejectResolved: accepted,
336
- priorRadii: [...INDEPENDENT_DARK_SUPPORT_PRIOR_RADII],
337
- maximumPriorMeanLuminance,
338
- maximumCandidateMeanLuminance,
339
- maximumPriorMeanDelta,
340
- maximumPriorMae,
341
- maximumPriorWeightedMae,
342
- boundaryScore: boundary.normalizedScore,
343
- clipping
344
- };
345
- }
346
-
347
- export function classifyCandidateQuality({
348
- residualVisible = false,
349
- damageWarning = false
350
- } = {}) {
351
- if (residualVisible && damageWarning) return 'mixed';
352
- if (residualVisible) return 'visible-residual';
353
- if (damageWarning) return 'possible-content-damage';
354
- return 'clean';
355
- }
356
-
357
- export function createCandidateImperfectionSignals(visibility = {}) {
358
- const darkPolarityHaloThreshold = Number.isFinite(visibility.darkPolarityHaloThreshold)
359
- ? visibility.darkPolarityHaloThreshold
360
- : IMPERFECTION_POSITIVE_HALO_LUM_THRESHOLD;
361
- const definitions = [
362
- ['spatial-residual', visibility.spatialResidual, IMPERFECTION_SPATIAL_THRESHOLD],
363
- ['gradient-residual', visibility.gradientResidual, IMPERFECTION_GRADIENT_THRESHOLD],
364
- ['positive-halo', visibility.positiveHaloLum, IMPERFECTION_POSITIVE_HALO_LUM_THRESHOLD],
365
- ['dark-polarity-halo', visibility.darkPolarityHaloLum, darkPolarityHaloThreshold]
366
- ];
367
- const components = Object.fromEntries(definitions.map(([type, rawValue, threshold]) => {
368
- const value = Number.isFinite(rawValue) ? Math.max(0, rawValue) : 0;
369
- return [type, {
370
- value,
371
- threshold,
372
- ratio: value / threshold
373
- }];
374
- }));
375
- const score = Math.max(0, ...Object.values(components).map((component) => component.ratio));
376
- const severity = score >= 1
377
- ? 'high'
378
- : score >= IMPERFECTION_WARNING_RATIO
379
- ? 'moderate'
380
- : score > 0
381
- ? 'low'
382
- : 'none';
383
- const types = definitions
384
- .map(([type]) => type)
385
- .filter((type) => components[type].ratio >= IMPERFECTION_WARNING_RATIO);
386
-
387
- return {
388
- detected: score >= IMPERFECTION_WARNING_RATIO,
389
- severity,
390
- score,
391
- types,
392
- components
393
- };
394
- }
395
-
396
- export function createCandidateQualitySignals({
397
- originalImageData,
398
- candidateImageData,
399
- hypothesis,
400
- finalCandidate = null,
401
- referenceAlphaMap = null
402
- } = {}) {
403
- const trial = hypothesis?.trial;
404
- const position = finalCandidate?.position ?? trial?.position ?? hypothesis?.position;
405
- const alphaMap = finalCandidate?.alphaMap ?? trial?.alphaMap;
406
- const alphaGain = finalCandidate?.alphaGain ?? trial?.alphaGain ?? 1;
407
- if (!originalImageData || !candidateImageData || !position || !alphaMap) {
408
- throw new Error('Candidate quality measurement requires original pixels, candidate pixels, position, and alpha map');
409
- }
410
-
411
- const original = scoreRegion(originalImageData, alphaMap, position);
412
- const final = scoreRegion(candidateImageData, alphaMap, position);
413
- const visibility = assessCalibratedWatermarkResidualVisibility({
414
- imageData: candidateImageData,
415
- originalImageData,
416
- position,
417
- alphaMap,
418
- alphaGain
419
- });
420
- const resolvedReferenceAlphaMap = resolveReferenceAlphaMap(
421
- hypothesis,
422
- referenceAlphaMap,
423
- alphaMap
424
- );
425
- const referenceVisibility = resolvedReferenceAlphaMap
426
- ? assessCalibratedWatermarkResidualVisibility({
427
- imageData: candidateImageData,
428
- originalImageData,
429
- position,
430
- alphaMap: resolvedReferenceAlphaMap,
431
- alphaGain
432
- })
433
- : null;
434
- const imperfections = createCandidateImperfectionSignals(visibility);
435
- const artifacts = assessRemovalDiffArtifacts({
436
- originalImageData,
437
- candidateImageData,
438
- alphaMap,
439
- position,
440
- alphaGain
441
- });
442
- const texture = assessReferenceTextureAlignment({
443
- originalImageData,
444
- referenceImageData: originalImageData,
445
- candidateImageData,
446
- position
447
- });
448
- const originalNearBlackRatio = calculateNearBlackRatio(originalImageData, position);
449
- const candidateNearBlackRatio = calculateNearBlackRatio(candidateImageData, position);
450
- const originalNearWhiteRatio = calculateNearWhiteRatio(originalImageData, position);
451
- const candidateNearWhiteRatio = calculateNearWhiteRatio(candidateImageData, position);
452
- const nearBlackIncrease = candidateNearBlackRatio - originalNearBlackRatio;
453
- const nearWhiteIncrease = candidateNearWhiteRatio - originalNearWhiteRatio;
454
-
455
- const evidence = clamp01((
456
- Math.max(0, original.spatialScore) +
457
- Math.max(0, original.gradientScore)
458
- ) / 2);
459
- const residualComponents = {
460
- spatial: clamp01(Math.abs(final.spatialScore) / 0.4),
461
- gradient: clamp01(Math.max(0, final.gradientScore) / 0.35),
462
- halo: clamp01(Math.max(
463
- visibility?.positiveHaloLum ?? 0,
464
- visibility?.darkPolarityHaloLum ?? 0
465
- ) / 8)
466
- };
467
- const residualLoss =
468
- residualComponents.spatial * 0.45 +
469
- residualComponents.gradient * 0.35 +
470
- residualComponents.halo * 0.20;
471
- const damageComponents = {
472
- nearBlack: clamp01(Math.max(0, nearBlackIncrease) / 0.05),
473
- nearWhite: clamp01(Math.max(0, nearWhiteIncrease) / 0.05),
474
- texture: clamp01((texture?.texturePenalty ?? 0) / 1),
475
- clipped: clamp01((artifacts?.newlyClippedRatio ?? 0) / 0.02)
476
- };
477
- const rawDamageLoss =
478
- damageComponents.nearBlack * 0.25 +
479
- damageComponents.nearWhite * 0.25 +
480
- damageComponents.texture * 0.25 +
481
- damageComponents.clipped * 0.25;
482
- const residualVisible = visibility?.visible === true ||
483
- referenceVisibility?.visible === true;
484
- const textureWarningCorroborated = (
485
- texture?.visibleDarkHole === true ||
486
- texture?.hardReject === true
487
- ) && (
488
- damageComponents.nearBlack >= 0.4 ||
489
- damageComponents.nearWhite >= 0.4 ||
490
- damageComponents.clipped >= 0.4
491
- );
492
- const severeDarkFlatTextureWarning =
493
- texture?.tooDark === true &&
494
- texture?.tooFlat === true &&
495
- Number(texture?.texturePenalty ?? 0) >=
496
- SEVERE_DARK_FLAT_TEXTURE_WARNING_THRESHOLD;
497
- const rawDamageWarning = damageComponents.nearBlack >= 1 ||
498
- damageComponents.nearWhite >= 1 ||
499
- damageComponents.clipped >= 1 ||
500
- textureWarningCorroborated ||
501
- severeDarkFlatTextureWarning;
502
- const explicitDarkBackgroundSupportConvergence =
503
- finalCandidate?.darkBackgroundSupportConvergence?.accepted === true
504
- ? finalCandidate.darkBackgroundSupportConvergence
505
- : null;
506
- const independentDarkBackgroundSupportConvergence =
507
- explicitDarkBackgroundSupportConvergence
508
- ? null
509
- : assessIndependentDarkBackgroundSupportConvergence({
510
- originalImageData,
511
- candidateImageData,
512
- hypothesis,
513
- position,
514
- alphaMap,
515
- alphaGain,
516
- original,
517
- final,
518
- visibility,
519
- artifacts,
520
- originalNearBlackRatio,
521
- candidateNearBlackRatio,
522
- rawDamageWarning
523
- });
524
- const darkBackgroundSupportConvergence =
525
- explicitDarkBackgroundSupportConvergence ??
526
- independentDarkBackgroundSupportConvergence;
527
- const textureHardRejectResolved =
528
- darkBackgroundSupportConvergence?.textureHardRejectResolved === true;
529
- const darkBackgroundSupportResolved = Boolean(
530
- rawDamageWarning &&
531
- darkBackgroundSupportConvergence &&
532
- !residualVisible &&
533
- (
534
- texture?.hardReject !== true ||
535
- textureHardRejectResolved
536
- ) &&
537
- Math.abs(final.spatialScore) <=
538
- DARK_SUPPORT_RESOLUTION_MAX_ABS_SPATIAL &&
539
- final.gradientScore <= DARK_SUPPORT_RESOLUTION_MAX_GRADIENT &&
540
- Number(artifacts?.newlyClippedRatio ?? Infinity) <=
541
- DARK_SUPPORT_RESOLUTION_MAX_NEWLY_CLIPPED_RATIO
542
- );
543
- const damageWarning = rawDamageWarning &&
544
- !darkBackgroundSupportResolved;
545
- const damageLoss = darkBackgroundSupportResolved ? 0 : rawDamageLoss;
546
- const damageRiskResolution = darkBackgroundSupportResolved
547
- ? 'dark-background-support-converged'
548
- : null;
549
- const qualityStatus = classifyCandidateQuality({
550
- residualVisible,
551
- damageWarning
552
- });
553
-
554
- return {
555
- evidenceLoss: 1 - evidence,
556
- residualLoss,
557
- damageLoss,
558
- rawDamageLoss,
559
- residualVisible,
560
- damageWarning,
561
- rawDamageWarning,
562
- damageRiskResolution,
563
- darkBackgroundSupportConvergence,
564
- qualityStatus,
565
- imperfections,
566
- original,
567
- final,
568
- visibility,
569
- referenceVisibility,
570
- artifacts,
571
- texture,
572
- originalNearBlackRatio,
573
- candidateNearBlackRatio,
574
- nearBlackIncrease,
575
- originalNearWhiteRatio,
576
- candidateNearWhiteRatio,
577
- nearWhiteIncrease,
578
- residualComponents,
579
- damageComponents
580
- };
581
- }
582
-
583
- function getFinalScore(signals = {}) {
584
- const weightedLoss = (signals.evidenceLoss ?? 1) * FINAL_EVIDENCE_WEIGHT +
585
- (signals.residualLoss ?? 1) * FINAL_RESIDUAL_WEIGHT +
586
- (signals.damageLoss ?? 1) * FINAL_DAMAGE_WEIGHT;
587
- return weightedLoss + (signals.damageWarning === true ? FINAL_DAMAGE_WARNING_PENALTY : 0);
588
- }
589
-
590
- function getDiscoveryPenalty(hypothesis = {}) {
591
- return DISCOVERY_ROLE_PENALTIES[hypothesis.discoveryRole] ?? 0;
592
- }
593
-
594
- function dominates(left, right) {
595
- const keys = ['evidenceLoss', 'residualLoss', 'damageLoss'];
596
- const noWorse = keys.every((key) => (
597
- (left.qualitySignals?.[key] ?? Infinity) <= (right.qualitySignals?.[key] ?? Infinity)
598
- )) && left.discoveryPenalty <= right.discoveryPenalty;
599
- const strictlyBetter = keys.some((key) => (
600
- (left.qualitySignals?.[key] ?? Infinity) < (right.qualitySignals?.[key] ?? Infinity)
601
- )) || left.discoveryPenalty < right.discoveryPenalty;
602
- return noWorse && strictlyBetter;
603
- }
604
-
605
- function hasCatastrophicBlock(signals = {}) {
606
- if (
607
- signals.damageWarning === false &&
608
- signals.darkBackgroundSupportConvergence?.accepted === true
609
- ) {
610
- return false;
611
- }
612
- const damage = signals.damageComponents ?? {};
613
- return signals.texture?.hardReject === true &&
614
- damage.clipped >= 1 &&
615
- (damage.nearBlack >= 1 || damage.nearWhite >= 1);
616
- }
617
-
618
- function getCandidatePosition(candidate = {}) {
619
- return candidate.hypothesis?.trial?.position ?? candidate.hypothesis?.position ?? null;
620
- }
621
-
622
- function hasSameCandidateAnchor(left, right) {
623
- const leftPosition = getCandidatePosition(left);
624
- const rightPosition = getCandidatePosition(right);
625
- if (!leftPosition || !rightPosition) return false;
626
- return leftPosition.x === rightPosition.x &&
627
- leftPosition.y === rightPosition.y &&
628
- leftPosition.width === rightPosition.width &&
629
- leftPosition.height === rightPosition.height;
630
- }
631
-
632
- const SAME_ANCHOR_96_SIZE = 96;
633
- const SAME_ANCHOR_96_IMPERFECTION_IMPROVEMENT = 0.15;
634
- const SAME_ANCHOR_96_EVIDENCE_TOLERANCE = 0.05;
635
- const SAME_ANCHOR_96_DAMAGE_TOLERANCE = 0.05;
636
-
637
- function finiteOr(value, fallback = Infinity) {
638
- return Number.isFinite(value) ? value : fallback;
639
- }
640
-
641
- function isEligibleSameAnchor96Alternative(incumbent, alternative) {
642
- const incumbentSignals = incumbent?.qualitySignals ?? {};
643
- const alternativeSignals = alternative?.qualitySignals ?? {};
644
- const incumbentScore = incumbentSignals.imperfections?.score;
645
- const alternativeScore = alternativeSignals.imperfections?.score;
646
- return hasSameCandidateAnchor(incumbent, alternative) &&
647
- !hasCatastrophicBlock(alternativeSignals) &&
648
- Number.isFinite(alternativeScore) &&
649
- alternativeScore <=
650
- incumbentScore - SAME_ANCHOR_96_IMPERFECTION_IMPROVEMENT &&
651
- Number.isFinite(alternativeSignals.evidenceLoss) &&
652
- alternativeSignals.evidenceLoss <=
653
- incumbentSignals.evidenceLoss + SAME_ANCHOR_96_EVIDENCE_TOLERANCE &&
654
- Number.isFinite(alternativeSignals.damageLoss) &&
655
- alternativeSignals.damageLoss <=
656
- incumbentSignals.damageLoss + SAME_ANCHOR_96_DAMAGE_TOLERANCE;
657
- }
658
-
659
- export function applySameAnchor96ImperfectionPreference(baseRanked = []) {
660
- const incumbent = baseRanked[0];
661
- const position = getCandidatePosition(incumbent);
662
- const signals = incumbent?.qualitySignals ?? {};
663
- if (
664
- !position ||
665
- position.width !== SAME_ANCHOR_96_SIZE ||
666
- position.height !== SAME_ANCHOR_96_SIZE ||
667
- signals.imperfections?.severity !== 'high' ||
668
- !Number.isFinite(signals.imperfections?.score) ||
669
- !Number.isFinite(signals.evidenceLoss) ||
670
- !Number.isFinite(signals.damageLoss)
671
- ) {
672
- return baseRanked;
673
- }
674
-
675
- const eligible = baseRanked
676
- .slice(1)
677
- .map((candidate, offset) => ({ candidate, baseIndex: offset + 1 }))
678
- .filter(({ candidate }) =>
679
- isEligibleSameAnchor96Alternative(incumbent, candidate)
680
- )
681
- .sort((left, right) =>
682
- left.candidate.qualitySignals.imperfections.score -
683
- right.candidate.qualitySignals.imperfections.score ||
684
- finiteOr(left.candidate.qualitySignals.residualLoss) -
685
- finiteOr(right.candidate.qualitySignals.residualLoss) ||
686
- left.baseIndex - right.baseIndex ||
687
- String(left.candidate.hypothesis?.id ?? '').localeCompare(
688
- String(right.candidate.hypothesis?.id ?? '')
689
- )
690
- );
691
- if (eligible.length === 0) return baseRanked;
692
-
693
- const promoted = eligible[0].candidate;
694
- return [promoted, ...baseRanked.filter((candidate) => candidate !== promoted)];
695
- }
696
-
697
- function applyFullStrengthNewMarginPreference(baseRanked = []) {
698
- const incumbent = baseRanked[0];
699
- if (!incumbent) return baseRanked;
700
-
701
- const getFinalTrialView = (candidate) => {
702
- const trial = candidate?.hypothesis?.trial ?? {};
703
- const meta = candidate?.result?.meta ?? {};
704
- const detection = meta.detection ?? {};
705
- return {
706
- ...trial,
707
- alphaGain: Number.isFinite(Number(meta.alphaGain))
708
- ? Number(meta.alphaGain)
709
- : trial.alphaGain,
710
- config: meta.config ?? trial.config,
711
- position: meta.position ?? trial.position,
712
- originalSpatialScore:
713
- detection.originalSpatialScore ?? trial.originalSpatialScore,
714
- originalGradientScore:
715
- detection.originalGradientScore ?? trial.originalGradientScore,
716
- processedGradientScore:
717
- detection.processedGradientScore ?? trial.processedGradientScore
718
- };
719
- };
720
- const incumbentTrial = getFinalTrialView(incumbent);
721
-
722
- const promoted = baseRanked.slice(1).find((candidate) => (
723
- !hasCatastrophicBlock(candidate.qualitySignals) &&
724
- shouldPreferFullStrengthNewMarginVariant(
725
- getFinalTrialView(candidate),
726
- incumbentTrial
727
- )
728
- ));
729
- if (!promoted) return baseRanked;
730
-
731
- return [promoted, ...baseRanked.filter((candidate) => candidate !== promoted)];
732
- }
733
-
734
- function strictlyDominatesQuality(leftSignals = {}, rightSignals = {}) {
735
- const keys = ['evidenceLoss', 'residualLoss', 'damageLoss'];
736
- const noWorse = keys.every((key) => (
737
- (leftSignals[key] ?? Infinity) <= (rightSignals[key] ?? Infinity)
738
- ));
739
- const strictlyBetter = keys.some((key) => (
740
- (leftSignals[key] ?? Infinity) < (rightSignals[key] ?? Infinity)
741
- ));
742
- return noWorse && strictlyBetter;
743
- }
744
-
745
- function shouldPreferSameAnchorCleanCandidate(left, right) {
746
- return left.qualitySignals?.qualityStatus === 'clean' &&
747
- right.qualitySignals?.qualityStatus !== 'clean' &&
748
- hasSameCandidateAnchor(left, right) &&
749
- strictlyDominatesQuality(left.qualitySignals, right.qualitySignals);
750
- }
751
-
752
- function compareRankedCandidates(left, right) {
753
- const leftCatastrophic = hasCatastrophicBlock(left.qualitySignals);
754
- const rightCatastrophic = hasCatastrophicBlock(right.qualitySignals);
755
- if (leftCatastrophic !== rightCatastrophic) return leftCatastrophic ? 1 : -1;
756
- const leftCleanDominates = shouldPreferSameAnchorCleanCandidate(left, right);
757
- const rightCleanDominates = shouldPreferSameAnchorCleanCandidate(right, left);
758
- if (leftCleanDominates !== rightCleanDominates) return leftCleanDominates ? -1 : 1;
759
- if (left.dominated !== right.dominated) return left.dominated ? 1 : -1;
760
- if (left.finalScore !== right.finalScore) return left.finalScore - right.finalScore;
761
- const rankingComparison = compareRankingKey(
762
- left.hypothesis?.rankingKey,
763
- right.hypothesis?.rankingKey
764
- );
765
- if (rankingComparison !== 0) return rankingComparison;
766
- return String(left.hypothesis?.id ?? '').localeCompare(String(right.hypothesis?.id ?? ''));
767
- }
768
-
769
- export function rankCompletedCandidates(completed = []) {
770
- const scored = completed.map((item) => ({
771
- ...item,
772
- discoveryPenalty: getDiscoveryPenalty(item.hypothesis),
773
- finalScore: getFinalScore(item.qualitySignals) + getDiscoveryPenalty(item.hypothesis)
774
- }));
775
- for (const candidate of scored) {
776
- candidate.dominated = scored.some((other) => (
777
- other !== candidate && dominates(other, candidate)
778
- ));
779
- }
780
- scored.sort(compareRankedCandidates);
781
- const imperfectionPreferred = applySameAnchor96ImperfectionPreference(scored);
782
- const preferred = applyFullStrengthNewMarginPreference(imperfectionPreferred);
783
-
784
- const first = preferred[0];
785
- const second = preferred[1];
786
- const selectionConfidence = !first
787
- ? 0
788
- : !second
789
- ? 1
790
- : clamp01((second.finalScore - first.finalScore) / Math.max(0.05, second.finalScore));
791
-
792
- return preferred.map((item, index) => ({
793
- ...item,
794
- rank: index + 1,
795
- selectionConfidence: index === 0 ? selectionConfidence : 0
796
- }));
797
- }
798
-
799
- export function createCandidateSummaries(ranked = [], failures = []) {
800
- const valid = ranked.map((item) => ({
801
- id: item.hypothesis?.id ?? null,
802
- family: item.hypothesis?.family ?? null,
803
- rank: item.rank,
804
- valid: true,
805
- finalScore: item.finalScore,
806
- qualityStatus: item.qualitySignals?.qualityStatus ?? null,
807
- qualitySignals: item.qualitySignals ?? null,
808
- error: null
809
- }));
810
- const invalid = failures.map((item) => ({
811
- id: item.hypothesis?.id ?? null,
812
- family: item.hypothesis?.family ?? null,
813
- rank: null,
814
- valid: false,
815
- finalScore: null,
816
- qualityStatus: null,
817
- qualitySignals: null,
818
- error: item.error instanceof Error
819
- ? item.error.message
820
- : String(item.error ?? 'Candidate execution failed')
821
- }));
822
- return [...valid, ...invalid];
823
- }
1
+ import {
2
+ assessCalibratedWatermarkResidualVisibility,
3
+ assessReferenceTextureAlignment,
4
+ assessRemovalDiffArtifacts,
5
+ calculateNearBlackRatio,
6
+ calculateNearWhiteRatio,
7
+ scoreRegion
8
+ } from './restorationMetrics.js';
9
+ import { getEmbeddedAlphaMap } from './embeddedAlphaMaps.js';
10
+ import {
11
+ buildPreviewNeighborhoodPrior,
12
+ measurePreviewBoundaryMetrics
13
+ } from './previewAlphaCalibration.js';
14
+ import { compareRankingKey } from './watermarkScoring.js';
15
+ import { shouldPreferFullStrengthNewMarginVariant } from './candidateEvaluation.js';
16
+
17
+ export const FINAL_EVIDENCE_WEIGHT = 0.35;
18
+ export const FINAL_RESIDUAL_WEIGHT = 0.40;
19
+ export const FINAL_DAMAGE_WEIGHT = 0.25;
20
+ export const FINAL_DAMAGE_WARNING_PENALTY = 0.08;
21
+
22
+ const DISCOVERY_ROLE_PENALTIES = {
23
+ 'fixed-selected': 0,
24
+ 'automatic-selected': 0.03,
25
+ 'aggressive-fallback-alternative': 0.015,
26
+ 'conservative-derived': 0.15,
27
+ 'discovered-alternative': 0.25
28
+ };
29
+ // Keep these reporting thresholds aligned with assessWatermarkResidualVisibility.
30
+ const IMPERFECTION_POSITIVE_HALO_LUM_THRESHOLD = 6;
31
+ const IMPERFECTION_GRADIENT_THRESHOLD = 0.22;
32
+ const IMPERFECTION_SPATIAL_THRESHOLD = 0.18;
33
+ const IMPERFECTION_WARNING_RATIO = 0.5;
34
+ const DARK_SUPPORT_RESOLUTION_MAX_ABS_SPATIAL = 0.18;
35
+ const DARK_SUPPORT_RESOLUTION_MAX_GRADIENT = 0.18;
36
+ const DARK_SUPPORT_RESOLUTION_MAX_NEWLY_CLIPPED_RATIO = 0.25;
37
+ const SEVERE_DARK_FLAT_TEXTURE_WARNING_THRESHOLD = 1.5;
38
+ const INDEPENDENT_DARK_SUPPORT_SIZE = 96;
39
+ const INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD = 12;
40
+ const INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_SPATIAL = 0.95;
41
+ const INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_GRADIENT = 0.9;
42
+ const INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_NEAR_BLACK = 0.1;
43
+ const INDEPENDENT_DARK_SUPPORT_MIN_CANDIDATE_NEAR_BLACK = 0.25;
44
+ const INDEPENDENT_DARK_SUPPORT_MAX_OPPOSITE_HALO_LUMINANCE = 1;
45
+ const INDEPENDENT_DARK_SUPPORT_MAX_OUTSIDE_ALPHA_CLIP_RATIO = 0.03;
46
+ const INDEPENDENT_DARK_SUPPORT_MIN_STRONG_ALPHA_CLIP_RATIO = 0.8;
47
+ const INDEPENDENT_DARK_SUPPORT_ALPHA_MIN = 0.05;
48
+ const INDEPENDENT_DARK_SUPPORT_ALPHA_STRONG_MIN = 0.2;
49
+ const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_LUMINANCE = 8;
50
+ const INDEPENDENT_DARK_SUPPORT_MAX_CANDIDATE_MEAN_LUMINANCE = 8;
51
+ const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_DELTA = 1.5;
52
+ const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MAE = 2.25;
53
+ const INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_WEIGHTED_MAE = 3.5;
54
+ const INDEPENDENT_DARK_SUPPORT_MAX_BOUNDARY_SCORE = 0.35;
55
+ const INDEPENDENT_DARK_SUPPORT_PRIOR_RADII = Object.freeze([4, 12]);
56
+ const INDEPENDENT_DARK_SUPPORT_RELAXATION_PASSES = 0;
57
+
58
+ function clamp01(value) {
59
+ if (!Number.isFinite(value)) return 0;
60
+ return Math.max(0, Math.min(1, value));
61
+ }
62
+
63
+ function haveEqualAlphaValues(left, right) {
64
+ if (!left || !right || left.length !== right.length) return false;
65
+ for (let index = 0; index < left.length; index += 1) {
66
+ if (left[index] !== right[index]) return false;
67
+ }
68
+ return true;
69
+ }
70
+
71
+ function resolveReferenceAlphaMap(
72
+ hypothesis,
73
+ explicitReferenceAlphaMap,
74
+ selectedAlphaMap
75
+ ) {
76
+ const config = hypothesis?.config ?? hypothesis?.trial?.config;
77
+ const logoSize = config?.logoSize;
78
+ const alphaVariant = config?.alphaVariant;
79
+ const catalogKey = Number.isFinite(logoSize)
80
+ ? typeof alphaVariant === 'string' && alphaVariant.length > 0
81
+ ? `${logoSize}-${alphaVariant}`
82
+ : logoSize
83
+ : null;
84
+ // Keep one catalog-derived observer independent from any alpha profile
85
+ // mutated or injected by the candidate being evaluated.
86
+ const referenceAlphaMap = explicitReferenceAlphaMap ??
87
+ (catalogKey === null ? null : getEmbeddedAlphaMap(catalogKey));
88
+ return haveEqualAlphaValues(referenceAlphaMap, selectedAlphaMap)
89
+ ? null
90
+ : referenceAlphaMap;
91
+ }
92
+
93
+ function extractCandidateQualityScratch(imageData, position) {
94
+ const x = Math.max(0, position.x - INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD);
95
+ const y = Math.max(0, position.y - INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD);
96
+ const right = Math.min(
97
+ imageData.width,
98
+ position.x + position.width + INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD
99
+ );
100
+ const bottom = Math.min(
101
+ imageData.height,
102
+ position.y + position.height + INDEPENDENT_DARK_SUPPORT_SCRATCH_PAD
103
+ );
104
+ const scratch = {
105
+ width: right - x,
106
+ height: bottom - y,
107
+ data: new Uint8ClampedArray((right - x) * (bottom - y) * 4)
108
+ };
109
+ for (let row = 0; row < scratch.height; row++) {
110
+ const sourceStart = ((y + row) * imageData.width + x) * 4;
111
+ const sourceEnd = sourceStart + scratch.width * 4;
112
+ scratch.data.set(
113
+ imageData.data.subarray(sourceStart, sourceEnd),
114
+ row * scratch.width * 4
115
+ );
116
+ }
117
+ return {
118
+ imageData: scratch,
119
+ position: {
120
+ x: position.x - x,
121
+ y: position.y - y,
122
+ width: position.width,
123
+ height: position.height
124
+ }
125
+ };
126
+ }
127
+
128
+ function measureIndependentDarkSupportClipping({
129
+ originalImageData,
130
+ candidateImageData,
131
+ alphaMap,
132
+ position
133
+ }) {
134
+ let newlyClippedCount = 0;
135
+ let outsideAlphaSupportCount = 0;
136
+ let strongAlphaSupportCount = 0;
137
+ for (let row = 0; row < position.height; row++) {
138
+ for (let col = 0; col < position.width; col++) {
139
+ const localIndex = row * position.width + col;
140
+ const pixelIndex = (
141
+ (position.y + row) * originalImageData.width +
142
+ position.x +
143
+ col
144
+ ) * 4;
145
+ const newlyClipped = [0, 1, 2].some((channel) => (
146
+ candidateImageData.data[pixelIndex + channel] === 0 &&
147
+ originalImageData.data[pixelIndex + channel] > 5
148
+ ));
149
+ if (!newlyClipped) continue;
150
+
151
+ newlyClippedCount++;
152
+ const alpha = Math.abs(alphaMap[localIndex]);
153
+ if (alpha < INDEPENDENT_DARK_SUPPORT_ALPHA_MIN) {
154
+ outsideAlphaSupportCount++;
155
+ }
156
+ if (alpha >= INDEPENDENT_DARK_SUPPORT_ALPHA_STRONG_MIN) {
157
+ strongAlphaSupportCount++;
158
+ }
159
+ }
160
+ }
161
+ return {
162
+ outsideAlphaSupportRatio: newlyClippedCount > 0
163
+ ? outsideAlphaSupportCount / newlyClippedCount
164
+ : 0,
165
+ strongAlphaSupportRatio: newlyClippedCount > 0
166
+ ? strongAlphaSupportCount / newlyClippedCount
167
+ : 1
168
+ };
169
+ }
170
+
171
+ function measureIndependentDarkSupportPrior({
172
+ candidateImageData,
173
+ priorImageData,
174
+ alphaMap,
175
+ position
176
+ }) {
177
+ let absoluteDelta = 0;
178
+ let weightedAbsoluteDelta = 0;
179
+ let alphaWeight = 0;
180
+ let candidateLuminance = 0;
181
+ let priorLuminance = 0;
182
+ let channelCount = 0;
183
+ let pixelCount = 0;
184
+ for (let row = 0; row < position.height; row++) {
185
+ for (let col = 0; col < position.width; col++) {
186
+ const localIndex = row * position.width + col;
187
+ const alpha = Math.abs(alphaMap[localIndex]);
188
+ const pixelIndex = (
189
+ (position.y + row) * candidateImageData.width +
190
+ position.x +
191
+ col
192
+ ) * 4;
193
+ for (let channel = 0; channel < 3; channel++) {
194
+ const delta = Math.abs(
195
+ candidateImageData.data[pixelIndex + channel] -
196
+ priorImageData.data[pixelIndex + channel]
197
+ );
198
+ absoluteDelta += delta;
199
+ weightedAbsoluteDelta += delta * alpha;
200
+ channelCount++;
201
+ }
202
+ candidateLuminance +=
203
+ 0.2126 * candidateImageData.data[pixelIndex] +
204
+ 0.7152 * candidateImageData.data[pixelIndex + 1] +
205
+ 0.0722 * candidateImageData.data[pixelIndex + 2];
206
+ priorLuminance +=
207
+ 0.2126 * priorImageData.data[pixelIndex] +
208
+ 0.7152 * priorImageData.data[pixelIndex + 1] +
209
+ 0.0722 * priorImageData.data[pixelIndex + 2];
210
+ alphaWeight += alpha;
211
+ pixelCount++;
212
+ }
213
+ }
214
+ const candidateMeanLuminance = candidateLuminance / Math.max(1, pixelCount);
215
+ const priorMeanLuminance = priorLuminance / Math.max(1, pixelCount);
216
+ return {
217
+ meanAbsoluteDelta: absoluteDelta / Math.max(1, channelCount),
218
+ weightedMeanAbsoluteDelta:
219
+ weightedAbsoluteDelta / Math.max(1e-8, alphaWeight * 3),
220
+ candidateMeanLuminance,
221
+ priorMeanLuminance,
222
+ meanLuminanceDelta: Math.abs(candidateMeanLuminance - priorMeanLuminance)
223
+ };
224
+ }
225
+
226
+ function assessIndependentDarkBackgroundSupportConvergence({
227
+ originalImageData,
228
+ candidateImageData,
229
+ hypothesis,
230
+ position,
231
+ alphaMap,
232
+ alphaGain,
233
+ original,
234
+ final,
235
+ visibility,
236
+ artifacts,
237
+ originalNearBlackRatio,
238
+ candidateNearBlackRatio,
239
+ rawDamageWarning
240
+ }) {
241
+ const trial = hypothesis?.trial;
242
+ const source = trial?.source;
243
+ const catalogAlphaMap = getEmbeddedAlphaMap('96-20260520');
244
+ if (
245
+ rawDamageWarning !== true ||
246
+ source !== 'standard' ||
247
+ position.width !== INDEPENDENT_DARK_SUPPORT_SIZE ||
248
+ position.height !== INDEPENDENT_DARK_SUPPORT_SIZE ||
249
+ alphaGain !== 1 ||
250
+ !haveEqualAlphaValues(alphaMap, catalogAlphaMap) ||
251
+ original.spatialScore < INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_SPATIAL ||
252
+ original.gradientScore < INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_GRADIENT ||
253
+ visibility?.visible !== false ||
254
+ Math.abs(final.spatialScore) > DARK_SUPPORT_RESOLUTION_MAX_ABS_SPATIAL ||
255
+ final.gradientScore > DARK_SUPPORT_RESOLUTION_MAX_GRADIENT ||
256
+ originalNearBlackRatio < INDEPENDENT_DARK_SUPPORT_MIN_ORIGINAL_NEAR_BLACK ||
257
+ candidateNearBlackRatio < INDEPENDENT_DARK_SUPPORT_MIN_CANDIDATE_NEAR_BLACK ||
258
+ Number(artifacts?.newlyClippedRatio ?? Infinity) >
259
+ DARK_SUPPORT_RESOLUTION_MAX_NEWLY_CLIPPED_RATIO ||
260
+ Number(artifacts?.oppositeDirectionHaloLum ?? Infinity) >
261
+ INDEPENDENT_DARK_SUPPORT_MAX_OPPOSITE_HALO_LUMINANCE
262
+ ) {
263
+ return null;
264
+ }
265
+
266
+ const originalScratch = extractCandidateQualityScratch(
267
+ originalImageData,
268
+ position
269
+ );
270
+ const candidateScratch = extractCandidateQualityScratch(
271
+ candidateImageData,
272
+ position
273
+ );
274
+ const priorMetrics = INDEPENDENT_DARK_SUPPORT_PRIOR_RADII.map((radius) => {
275
+ const priorImageData = buildPreviewNeighborhoodPrior({
276
+ previewImageData: originalScratch.imageData,
277
+ position: originalScratch.position,
278
+ radius,
279
+ relaxationPasses: INDEPENDENT_DARK_SUPPORT_RELAXATION_PASSES
280
+ });
281
+ return {
282
+ radius,
283
+ ...measureIndependentDarkSupportPrior({
284
+ candidateImageData: candidateScratch.imageData,
285
+ priorImageData,
286
+ alphaMap,
287
+ position: candidateScratch.position
288
+ })
289
+ };
290
+ });
291
+ const clipping = measureIndependentDarkSupportClipping({
292
+ originalImageData: originalScratch.imageData,
293
+ candidateImageData: candidateScratch.imageData,
294
+ alphaMap,
295
+ position: originalScratch.position
296
+ });
297
+ const boundary = measurePreviewBoundaryMetrics(
298
+ candidateScratch.imageData,
299
+ originalScratch.imageData,
300
+ originalScratch.position
301
+ );
302
+ const maximumPriorMeanLuminance = Math.max(
303
+ ...priorMetrics.map((item) => item.priorMeanLuminance)
304
+ );
305
+ const maximumCandidateMeanLuminance = Math.max(
306
+ ...priorMetrics.map((item) => item.candidateMeanLuminance)
307
+ );
308
+ const maximumPriorMeanDelta = Math.max(
309
+ ...priorMetrics.map((item) => item.meanLuminanceDelta)
310
+ );
311
+ const maximumPriorMae = Math.max(
312
+ ...priorMetrics.map((item) => item.meanAbsoluteDelta)
313
+ );
314
+ const maximumPriorWeightedMae = Math.max(
315
+ ...priorMetrics.map((item) => item.weightedMeanAbsoluteDelta)
316
+ );
317
+ const accepted = Boolean(
318
+ clipping.outsideAlphaSupportRatio <=
319
+ INDEPENDENT_DARK_SUPPORT_MAX_OUTSIDE_ALPHA_CLIP_RATIO &&
320
+ clipping.strongAlphaSupportRatio >=
321
+ INDEPENDENT_DARK_SUPPORT_MIN_STRONG_ALPHA_CLIP_RATIO &&
322
+ maximumPriorMeanLuminance <=
323
+ INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_LUMINANCE &&
324
+ maximumCandidateMeanLuminance <=
325
+ INDEPENDENT_DARK_SUPPORT_MAX_CANDIDATE_MEAN_LUMINANCE &&
326
+ maximumPriorMeanDelta <= INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MEAN_DELTA &&
327
+ maximumPriorMae <= INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_MAE &&
328
+ maximumPriorWeightedMae <=
329
+ INDEPENDENT_DARK_SUPPORT_MAX_PRIOR_WEIGHTED_MAE &&
330
+ boundary.normalizedScore <= INDEPENDENT_DARK_SUPPORT_MAX_BOUNDARY_SCORE
331
+ );
332
+ return {
333
+ accepted,
334
+ source: 'independent-standard-96-neighborhood-prior',
335
+ textureHardRejectResolved: accepted,
336
+ priorRadii: [...INDEPENDENT_DARK_SUPPORT_PRIOR_RADII],
337
+ maximumPriorMeanLuminance,
338
+ maximumCandidateMeanLuminance,
339
+ maximumPriorMeanDelta,
340
+ maximumPriorMae,
341
+ maximumPriorWeightedMae,
342
+ boundaryScore: boundary.normalizedScore,
343
+ clipping
344
+ };
345
+ }
346
+
347
+ export function classifyCandidateQuality({
348
+ residualVisible = false,
349
+ damageWarning = false
350
+ } = {}) {
351
+ if (residualVisible && damageWarning) return 'mixed';
352
+ if (residualVisible) return 'visible-residual';
353
+ if (damageWarning) return 'possible-content-damage';
354
+ return 'clean';
355
+ }
356
+
357
+ export function createCandidateImperfectionSignals(visibility = {}) {
358
+ const darkPolarityHaloThreshold = Number.isFinite(visibility.darkPolarityHaloThreshold)
359
+ ? visibility.darkPolarityHaloThreshold
360
+ : IMPERFECTION_POSITIVE_HALO_LUM_THRESHOLD;
361
+ const definitions = [
362
+ ['spatial-residual', visibility.spatialResidual, IMPERFECTION_SPATIAL_THRESHOLD],
363
+ ['gradient-residual', visibility.gradientResidual, IMPERFECTION_GRADIENT_THRESHOLD],
364
+ ['positive-halo', visibility.positiveHaloLum, IMPERFECTION_POSITIVE_HALO_LUM_THRESHOLD],
365
+ ['dark-polarity-halo', visibility.darkPolarityHaloLum, darkPolarityHaloThreshold]
366
+ ];
367
+ const components = Object.fromEntries(definitions.map(([type, rawValue, threshold]) => {
368
+ const value = Number.isFinite(rawValue) ? Math.max(0, rawValue) : 0;
369
+ return [type, {
370
+ value,
371
+ threshold,
372
+ ratio: value / threshold
373
+ }];
374
+ }));
375
+ const score = Math.max(0, ...Object.values(components).map((component) => component.ratio));
376
+ const severity = score >= 1
377
+ ? 'high'
378
+ : score >= IMPERFECTION_WARNING_RATIO
379
+ ? 'moderate'
380
+ : score > 0
381
+ ? 'low'
382
+ : 'none';
383
+ const types = definitions
384
+ .map(([type]) => type)
385
+ .filter((type) => components[type].ratio >= IMPERFECTION_WARNING_RATIO);
386
+
387
+ return {
388
+ detected: score >= IMPERFECTION_WARNING_RATIO,
389
+ severity,
390
+ score,
391
+ types,
392
+ components
393
+ };
394
+ }
395
+
396
+ export function createCandidateQualitySignals({
397
+ originalImageData,
398
+ candidateImageData,
399
+ hypothesis,
400
+ finalCandidate = null,
401
+ referenceAlphaMap = null
402
+ } = {}) {
403
+ const trial = hypothesis?.trial;
404
+ const position = finalCandidate?.position ?? trial?.position ?? hypothesis?.position;
405
+ const alphaMap = finalCandidate?.alphaMap ?? trial?.alphaMap;
406
+ const alphaGain = finalCandidate?.alphaGain ?? trial?.alphaGain ?? 1;
407
+ if (!originalImageData || !candidateImageData || !position || !alphaMap) {
408
+ throw new Error('Candidate quality measurement requires original pixels, candidate pixels, position, and alpha map');
409
+ }
410
+
411
+ const original = scoreRegion(originalImageData, alphaMap, position);
412
+ const final = scoreRegion(candidateImageData, alphaMap, position);
413
+ const visibility = assessCalibratedWatermarkResidualVisibility({
414
+ imageData: candidateImageData,
415
+ originalImageData,
416
+ position,
417
+ alphaMap,
418
+ alphaGain
419
+ });
420
+ const resolvedReferenceAlphaMap = resolveReferenceAlphaMap(
421
+ hypothesis,
422
+ referenceAlphaMap,
423
+ alphaMap
424
+ );
425
+ const referenceVisibility = resolvedReferenceAlphaMap
426
+ ? assessCalibratedWatermarkResidualVisibility({
427
+ imageData: candidateImageData,
428
+ originalImageData,
429
+ position,
430
+ alphaMap: resolvedReferenceAlphaMap,
431
+ alphaGain
432
+ })
433
+ : null;
434
+ const imperfections = createCandidateImperfectionSignals(visibility);
435
+ const artifacts = assessRemovalDiffArtifacts({
436
+ originalImageData,
437
+ candidateImageData,
438
+ alphaMap,
439
+ position,
440
+ alphaGain
441
+ });
442
+ const texture = assessReferenceTextureAlignment({
443
+ originalImageData,
444
+ referenceImageData: originalImageData,
445
+ candidateImageData,
446
+ position
447
+ });
448
+ const originalNearBlackRatio = calculateNearBlackRatio(originalImageData, position);
449
+ const candidateNearBlackRatio = calculateNearBlackRatio(candidateImageData, position);
450
+ const originalNearWhiteRatio = calculateNearWhiteRatio(originalImageData, position);
451
+ const candidateNearWhiteRatio = calculateNearWhiteRatio(candidateImageData, position);
452
+ const nearBlackIncrease = candidateNearBlackRatio - originalNearBlackRatio;
453
+ const nearWhiteIncrease = candidateNearWhiteRatio - originalNearWhiteRatio;
454
+
455
+ const evidence = clamp01((
456
+ Math.max(0, original.spatialScore) +
457
+ Math.max(0, original.gradientScore)
458
+ ) / 2);
459
+ const residualComponents = {
460
+ spatial: clamp01(Math.abs(final.spatialScore) / 0.4),
461
+ gradient: clamp01(Math.max(0, final.gradientScore) / 0.35),
462
+ halo: clamp01(Math.max(
463
+ visibility?.positiveHaloLum ?? 0,
464
+ visibility?.darkPolarityHaloLum ?? 0
465
+ ) / 8)
466
+ };
467
+ const residualLoss =
468
+ residualComponents.spatial * 0.45 +
469
+ residualComponents.gradient * 0.35 +
470
+ residualComponents.halo * 0.20;
471
+ const damageComponents = {
472
+ nearBlack: clamp01(Math.max(0, nearBlackIncrease) / 0.05),
473
+ nearWhite: clamp01(Math.max(0, nearWhiteIncrease) / 0.05),
474
+ texture: clamp01((texture?.texturePenalty ?? 0) / 1),
475
+ clipped: clamp01((artifacts?.newlyClippedRatio ?? 0) / 0.02)
476
+ };
477
+ const rawDamageLoss =
478
+ damageComponents.nearBlack * 0.25 +
479
+ damageComponents.nearWhite * 0.25 +
480
+ damageComponents.texture * 0.25 +
481
+ damageComponents.clipped * 0.25;
482
+ const residualVisible = visibility?.visible === true ||
483
+ referenceVisibility?.visible === true;
484
+ const textureWarningCorroborated = (
485
+ texture?.visibleDarkHole === true ||
486
+ texture?.hardReject === true
487
+ ) && (
488
+ damageComponents.nearBlack >= 0.4 ||
489
+ damageComponents.nearWhite >= 0.4 ||
490
+ damageComponents.clipped >= 0.4
491
+ );
492
+ const severeDarkFlatTextureWarning =
493
+ texture?.tooDark === true &&
494
+ texture?.tooFlat === true &&
495
+ Number(texture?.texturePenalty ?? 0) >=
496
+ SEVERE_DARK_FLAT_TEXTURE_WARNING_THRESHOLD;
497
+ const rawDamageWarning = damageComponents.nearBlack >= 1 ||
498
+ damageComponents.nearWhite >= 1 ||
499
+ damageComponents.clipped >= 1 ||
500
+ textureWarningCorroborated ||
501
+ severeDarkFlatTextureWarning;
502
+ const explicitDarkBackgroundSupportConvergence =
503
+ finalCandidate?.darkBackgroundSupportConvergence?.accepted === true
504
+ ? finalCandidate.darkBackgroundSupportConvergence
505
+ : null;
506
+ const independentDarkBackgroundSupportConvergence =
507
+ explicitDarkBackgroundSupportConvergence
508
+ ? null
509
+ : assessIndependentDarkBackgroundSupportConvergence({
510
+ originalImageData,
511
+ candidateImageData,
512
+ hypothesis,
513
+ position,
514
+ alphaMap,
515
+ alphaGain,
516
+ original,
517
+ final,
518
+ visibility,
519
+ artifacts,
520
+ originalNearBlackRatio,
521
+ candidateNearBlackRatio,
522
+ rawDamageWarning
523
+ });
524
+ const darkBackgroundSupportConvergence =
525
+ explicitDarkBackgroundSupportConvergence ??
526
+ independentDarkBackgroundSupportConvergence;
527
+ const textureHardRejectResolved =
528
+ darkBackgroundSupportConvergence?.textureHardRejectResolved === true;
529
+ const darkBackgroundSupportResolved = Boolean(
530
+ rawDamageWarning &&
531
+ darkBackgroundSupportConvergence &&
532
+ !residualVisible &&
533
+ (
534
+ texture?.hardReject !== true ||
535
+ textureHardRejectResolved
536
+ ) &&
537
+ Math.abs(final.spatialScore) <=
538
+ DARK_SUPPORT_RESOLUTION_MAX_ABS_SPATIAL &&
539
+ final.gradientScore <= DARK_SUPPORT_RESOLUTION_MAX_GRADIENT &&
540
+ Number(artifacts?.newlyClippedRatio ?? Infinity) <=
541
+ DARK_SUPPORT_RESOLUTION_MAX_NEWLY_CLIPPED_RATIO
542
+ );
543
+ const damageWarning = rawDamageWarning &&
544
+ !darkBackgroundSupportResolved;
545
+ const damageLoss = darkBackgroundSupportResolved ? 0 : rawDamageLoss;
546
+ const damageRiskResolution = darkBackgroundSupportResolved
547
+ ? 'dark-background-support-converged'
548
+ : null;
549
+ const qualityStatus = classifyCandidateQuality({
550
+ residualVisible,
551
+ damageWarning
552
+ });
553
+
554
+ return {
555
+ evidenceLoss: 1 - evidence,
556
+ residualLoss,
557
+ damageLoss,
558
+ rawDamageLoss,
559
+ residualVisible,
560
+ damageWarning,
561
+ rawDamageWarning,
562
+ damageRiskResolution,
563
+ darkBackgroundSupportConvergence,
564
+ qualityStatus,
565
+ imperfections,
566
+ original,
567
+ final,
568
+ visibility,
569
+ referenceVisibility,
570
+ artifacts,
571
+ texture,
572
+ originalNearBlackRatio,
573
+ candidateNearBlackRatio,
574
+ nearBlackIncrease,
575
+ originalNearWhiteRatio,
576
+ candidateNearWhiteRatio,
577
+ nearWhiteIncrease,
578
+ residualComponents,
579
+ damageComponents
580
+ };
581
+ }
582
+
583
+ function getFinalScore(signals = {}) {
584
+ const weightedLoss = (signals.evidenceLoss ?? 1) * FINAL_EVIDENCE_WEIGHT +
585
+ (signals.residualLoss ?? 1) * FINAL_RESIDUAL_WEIGHT +
586
+ (signals.damageLoss ?? 1) * FINAL_DAMAGE_WEIGHT;
587
+ return weightedLoss + (signals.damageWarning === true ? FINAL_DAMAGE_WARNING_PENALTY : 0);
588
+ }
589
+
590
+ function getDiscoveryPenalty(hypothesis = {}) {
591
+ return DISCOVERY_ROLE_PENALTIES[hypothesis.discoveryRole] ?? 0;
592
+ }
593
+
594
+ function dominates(left, right) {
595
+ const keys = ['evidenceLoss', 'residualLoss', 'damageLoss'];
596
+ const noWorse = keys.every((key) => (
597
+ (left.qualitySignals?.[key] ?? Infinity) <= (right.qualitySignals?.[key] ?? Infinity)
598
+ )) && left.discoveryPenalty <= right.discoveryPenalty;
599
+ const strictlyBetter = keys.some((key) => (
600
+ (left.qualitySignals?.[key] ?? Infinity) < (right.qualitySignals?.[key] ?? Infinity)
601
+ )) || left.discoveryPenalty < right.discoveryPenalty;
602
+ return noWorse && strictlyBetter;
603
+ }
604
+
605
+ function hasCatastrophicBlock(signals = {}) {
606
+ if (
607
+ signals.damageWarning === false &&
608
+ signals.darkBackgroundSupportConvergence?.accepted === true
609
+ ) {
610
+ return false;
611
+ }
612
+ const damage = signals.damageComponents ?? {};
613
+ return signals.texture?.hardReject === true &&
614
+ damage.clipped >= 1 &&
615
+ (damage.nearBlack >= 1 || damage.nearWhite >= 1);
616
+ }
617
+
618
+ function getCandidatePosition(candidate = {}) {
619
+ return candidate.hypothesis?.trial?.position ?? candidate.hypothesis?.position ?? null;
620
+ }
621
+
622
+ function hasSameCandidateAnchor(left, right) {
623
+ const leftPosition = getCandidatePosition(left);
624
+ const rightPosition = getCandidatePosition(right);
625
+ if (!leftPosition || !rightPosition) return false;
626
+ return leftPosition.x === rightPosition.x &&
627
+ leftPosition.y === rightPosition.y &&
628
+ leftPosition.width === rightPosition.width &&
629
+ leftPosition.height === rightPosition.height;
630
+ }
631
+
632
+ const SAME_ANCHOR_96_SIZE = 96;
633
+ const SAME_ANCHOR_96_IMPERFECTION_IMPROVEMENT = 0.15;
634
+ const SAME_ANCHOR_96_EVIDENCE_TOLERANCE = 0.05;
635
+ const SAME_ANCHOR_96_DAMAGE_TOLERANCE = 0.05;
636
+
637
+ function finiteOr(value, fallback = Infinity) {
638
+ return Number.isFinite(value) ? value : fallback;
639
+ }
640
+
641
+ function isEligibleSameAnchor96Alternative(incumbent, alternative) {
642
+ const incumbentSignals = incumbent?.qualitySignals ?? {};
643
+ const alternativeSignals = alternative?.qualitySignals ?? {};
644
+ const incumbentScore = incumbentSignals.imperfections?.score;
645
+ const alternativeScore = alternativeSignals.imperfections?.score;
646
+ return hasSameCandidateAnchor(incumbent, alternative) &&
647
+ !hasCatastrophicBlock(alternativeSignals) &&
648
+ Number.isFinite(alternativeScore) &&
649
+ alternativeScore <=
650
+ incumbentScore - SAME_ANCHOR_96_IMPERFECTION_IMPROVEMENT &&
651
+ Number.isFinite(alternativeSignals.evidenceLoss) &&
652
+ alternativeSignals.evidenceLoss <=
653
+ incumbentSignals.evidenceLoss + SAME_ANCHOR_96_EVIDENCE_TOLERANCE &&
654
+ Number.isFinite(alternativeSignals.damageLoss) &&
655
+ alternativeSignals.damageLoss <=
656
+ incumbentSignals.damageLoss + SAME_ANCHOR_96_DAMAGE_TOLERANCE;
657
+ }
658
+
659
+ export function applySameAnchor96ImperfectionPreference(baseRanked = []) {
660
+ const incumbent = baseRanked[0];
661
+ const position = getCandidatePosition(incumbent);
662
+ const signals = incumbent?.qualitySignals ?? {};
663
+ if (
664
+ !position ||
665
+ position.width !== SAME_ANCHOR_96_SIZE ||
666
+ position.height !== SAME_ANCHOR_96_SIZE ||
667
+ signals.imperfections?.severity !== 'high' ||
668
+ !Number.isFinite(signals.imperfections?.score) ||
669
+ !Number.isFinite(signals.evidenceLoss) ||
670
+ !Number.isFinite(signals.damageLoss)
671
+ ) {
672
+ return baseRanked;
673
+ }
674
+
675
+ const eligible = baseRanked
676
+ .slice(1)
677
+ .map((candidate, offset) => ({ candidate, baseIndex: offset + 1 }))
678
+ .filter(({ candidate }) =>
679
+ isEligibleSameAnchor96Alternative(incumbent, candidate)
680
+ )
681
+ .sort((left, right) =>
682
+ left.candidate.qualitySignals.imperfections.score -
683
+ right.candidate.qualitySignals.imperfections.score ||
684
+ finiteOr(left.candidate.qualitySignals.residualLoss) -
685
+ finiteOr(right.candidate.qualitySignals.residualLoss) ||
686
+ left.baseIndex - right.baseIndex ||
687
+ String(left.candidate.hypothesis?.id ?? '').localeCompare(
688
+ String(right.candidate.hypothesis?.id ?? '')
689
+ )
690
+ );
691
+ if (eligible.length === 0) return baseRanked;
692
+
693
+ const promoted = eligible[0].candidate;
694
+ return [promoted, ...baseRanked.filter((candidate) => candidate !== promoted)];
695
+ }
696
+
697
+ function applyFullStrengthNewMarginPreference(baseRanked = []) {
698
+ const incumbent = baseRanked[0];
699
+ if (!incumbent) return baseRanked;
700
+
701
+ const getFinalTrialView = (candidate) => {
702
+ const trial = candidate?.hypothesis?.trial ?? {};
703
+ const meta = candidate?.result?.meta ?? {};
704
+ const detection = meta.detection ?? {};
705
+ return {
706
+ ...trial,
707
+ alphaGain: Number.isFinite(Number(meta.alphaGain))
708
+ ? Number(meta.alphaGain)
709
+ : trial.alphaGain,
710
+ config: meta.config ?? trial.config,
711
+ position: meta.position ?? trial.position,
712
+ originalSpatialScore:
713
+ detection.originalSpatialScore ?? trial.originalSpatialScore,
714
+ originalGradientScore:
715
+ detection.originalGradientScore ?? trial.originalGradientScore,
716
+ processedGradientScore:
717
+ detection.processedGradientScore ?? trial.processedGradientScore
718
+ };
719
+ };
720
+ const incumbentTrial = getFinalTrialView(incumbent);
721
+
722
+ const promoted = baseRanked.slice(1).find((candidate) => (
723
+ !hasCatastrophicBlock(candidate.qualitySignals) &&
724
+ shouldPreferFullStrengthNewMarginVariant(
725
+ getFinalTrialView(candidate),
726
+ incumbentTrial
727
+ )
728
+ ));
729
+ if (!promoted) return baseRanked;
730
+
731
+ return [promoted, ...baseRanked.filter((candidate) => candidate !== promoted)];
732
+ }
733
+
734
+ function strictlyDominatesQuality(leftSignals = {}, rightSignals = {}) {
735
+ const keys = ['evidenceLoss', 'residualLoss', 'damageLoss'];
736
+ const noWorse = keys.every((key) => (
737
+ (leftSignals[key] ?? Infinity) <= (rightSignals[key] ?? Infinity)
738
+ ));
739
+ const strictlyBetter = keys.some((key) => (
740
+ (leftSignals[key] ?? Infinity) < (rightSignals[key] ?? Infinity)
741
+ ));
742
+ return noWorse && strictlyBetter;
743
+ }
744
+
745
+ function shouldPreferSameAnchorCleanCandidate(left, right) {
746
+ return left.qualitySignals?.qualityStatus === 'clean' &&
747
+ right.qualitySignals?.qualityStatus !== 'clean' &&
748
+ hasSameCandidateAnchor(left, right) &&
749
+ strictlyDominatesQuality(left.qualitySignals, right.qualitySignals);
750
+ }
751
+
752
+ function compareRankedCandidates(left, right) {
753
+ const leftCatastrophic = hasCatastrophicBlock(left.qualitySignals);
754
+ const rightCatastrophic = hasCatastrophicBlock(right.qualitySignals);
755
+ if (leftCatastrophic !== rightCatastrophic) return leftCatastrophic ? 1 : -1;
756
+ const leftCleanDominates = shouldPreferSameAnchorCleanCandidate(left, right);
757
+ const rightCleanDominates = shouldPreferSameAnchorCleanCandidate(right, left);
758
+ if (leftCleanDominates !== rightCleanDominates) return leftCleanDominates ? -1 : 1;
759
+ if (left.dominated !== right.dominated) return left.dominated ? 1 : -1;
760
+ if (left.finalScore !== right.finalScore) return left.finalScore - right.finalScore;
761
+ const rankingComparison = compareRankingKey(
762
+ left.hypothesis?.rankingKey,
763
+ right.hypothesis?.rankingKey
764
+ );
765
+ if (rankingComparison !== 0) return rankingComparison;
766
+ return String(left.hypothesis?.id ?? '').localeCompare(String(right.hypothesis?.id ?? ''));
767
+ }
768
+
769
+ export function rankCompletedCandidates(completed = []) {
770
+ const scored = completed.map((item) => ({
771
+ ...item,
772
+ discoveryPenalty: getDiscoveryPenalty(item.hypothesis),
773
+ finalScore: getFinalScore(item.qualitySignals) + getDiscoveryPenalty(item.hypothesis)
774
+ }));
775
+ for (const candidate of scored) {
776
+ candidate.dominated = scored.some((other) => (
777
+ other !== candidate && dominates(other, candidate)
778
+ ));
779
+ }
780
+ scored.sort(compareRankedCandidates);
781
+ const imperfectionPreferred = applySameAnchor96ImperfectionPreference(scored);
782
+ const preferred = applyFullStrengthNewMarginPreference(imperfectionPreferred);
783
+
784
+ const first = preferred[0];
785
+ const second = preferred[1];
786
+ const selectionConfidence = !first
787
+ ? 0
788
+ : !second
789
+ ? 1
790
+ : clamp01((second.finalScore - first.finalScore) / Math.max(0.05, second.finalScore));
791
+
792
+ return preferred.map((item, index) => ({
793
+ ...item,
794
+ rank: index + 1,
795
+ selectionConfidence: index === 0 ? selectionConfidence : 0
796
+ }));
797
+ }
798
+
799
+ export function createCandidateSummaries(ranked = [], failures = []) {
800
+ const valid = ranked.map((item) => ({
801
+ id: item.hypothesis?.id ?? null,
802
+ family: item.hypothesis?.family ?? null,
803
+ rank: item.rank,
804
+ valid: true,
805
+ finalScore: item.finalScore,
806
+ qualityStatus: item.qualitySignals?.qualityStatus ?? null,
807
+ qualitySignals: item.qualitySignals ?? null,
808
+ error: null
809
+ }));
810
+ const invalid = failures.map((item) => ({
811
+ id: item.hypothesis?.id ?? null,
812
+ family: item.hypothesis?.family ?? null,
813
+ rank: null,
814
+ valid: false,
815
+ finalScore: null,
816
+ qualityStatus: null,
817
+ qualitySignals: null,
818
+ error: item.error instanceof Error
819
+ ? item.error.message
820
+ : String(item.error ?? 'Candidate execution failed')
821
+ }));
822
+ return [...valid, ...invalid];
823
+ }