@nextcommerce/campaigns-os 1.52.0 → 1.54.0
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.
- package/CHANGELOG.md +109 -0
- package/README.md +1 -0
- package/agents/claude/CLAUDE.md +2 -0
- package/agents/codex/AGENTS.md +2 -0
- package/agents/copilot/copilot-instructions.md +2 -0
- package/agents/cursor/campaigns-os.mdc +2 -0
- package/compatibility.json +1 -1
- package/contracts/campaign-cart-checkout-field-contract.v0.json +10 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +27 -27
- package/contracts/effects.v1.json +295 -47
- package/contracts/release-ledger.json +660 -0
- package/contracts/supported-surface.json +3 -3
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/brand-theme-bridge.md +2 -0
- package/docs/build-packet.md +67 -19
- package/docs/campaign-build-brief.md +48 -17
- package/docs/campaign-standardization-report.md +32 -0
- package/docs/design-source-package.md +7 -3
- package/docs/effects.md +10 -1
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +2 -0
- package/docs/progress-snapshots.md +5 -5
- package/docs/qa-and-test-orders.md +30 -3
- package/docs/readback.md +19 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +15 -0
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +1 -0
- package/package.json +1 -1
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +5 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -3
- package/skills/next-campaigns-os/SKILL.md +10 -19
- package/skills/next-campaigns-os/references/session-intake.md +10 -6
- package/skills/next-campaigns-os-setup/SKILL.md +4 -3
- package/skills/next-campaigns-polish/SKILL.md +7 -5
- package/skills/next-campaigns-qa/SKILL.md +5 -3
- package/skills.json +10 -10
- package/src/brand-theme.mjs +21 -20
- package/src/brief-summary.mjs +275 -0
- package/src/build-brief.mjs +167 -15
- package/src/campaign-ecosystem.mjs +153 -31
- package/src/cli.mjs +182 -115
- package/src/contrast.mjs +649 -0
- package/src/design-source-publication.mjs +110 -3
- package/src/directory-lock.mjs +79 -7
- package/src/doctor/checks.mjs +141 -16
- package/src/doctor/inspect.mjs +13 -0
- package/src/doctor/next-step.mjs +15 -6
- package/src/input-currency.mjs +394 -0
- package/src/input-refresh.mjs +708 -0
- package/src/invocation.mjs +6 -2
- package/src/polish-browser.mjs +503 -201
- package/src/polish-deadline.mjs +7 -0
- package/src/polish-node.mjs +326 -46
- package/src/polish-readability.mjs +828 -0
- package/src/progress-node.mjs +18 -5
- package/src/qa-browser.mjs +211 -93
- package/src/qa-node.mjs +47 -7
- package/src/qc-accept.mjs +44 -8
- package/src/qc-check-registry.mjs +15 -7
- package/src/qc-results.mjs +326 -34
- package/src/readback.mjs +58 -10
- package/src/sdk-attribute-index.mjs +111 -23
- package/src/sdk-markup.mjs +49 -10
- package/src/sdk-repin.mjs +282 -0
- package/src/stage-ledger.mjs +235 -44
- package/src/stage-record.mjs +154 -30
- package/src/standardization-report.mjs +26 -1
- package/src/target-lock.mjs +9 -1
- package/src/tooling-setup.mjs +1 -1
package/src/polish-deadline.mjs
CHANGED
|
@@ -12,6 +12,13 @@ export const POLISH_BROWSER_STARTUP_DEADLINE_MS = 45_000;
|
|
|
12
12
|
export const POLISH_CAPTURE_CELL_DEADLINE_MS = 55_000;
|
|
13
13
|
export const POLISH_CAPTURE_CLOSE_DEADLINE_MS = 10_000;
|
|
14
14
|
export const POLISH_CAPTURE_STARTUP_DEADLINE_MS = 55_000;
|
|
15
|
+
// Readability bounds, beside the capture deadlines above: one cell's probe,
|
|
16
|
+
// every probe of a run, every crop of a run, and all readability work a run
|
|
17
|
+
// adds (readability-only navigation, probes and crops).
|
|
18
|
+
export const READABILITY_PROBE_CELL_MS = 1_500;
|
|
19
|
+
export const READABILITY_PROBE_RUN_MS = 120_000;
|
|
20
|
+
export const READABILITY_CROP_RUN_MS = 30_000;
|
|
21
|
+
export const READABILITY_ADDED_RUN_MS = 300_000;
|
|
15
22
|
|
|
16
23
|
export function boundedPolishDeadline(value, fallback) {
|
|
17
24
|
return Number.isSafeInteger(value) && value > 0 && value <= fallback ? value : fallback;
|
package/src/polish-node.mjs
CHANGED
|
@@ -35,8 +35,22 @@ import {
|
|
|
35
35
|
POLISH_PRODUCER_TIMEOUT_ERROR_CODE,
|
|
36
36
|
polishProducerCleanupError,
|
|
37
37
|
polishProducerTimeoutError,
|
|
38
|
+
READABILITY_ADDED_RUN_MS,
|
|
39
|
+
READABILITY_CROP_RUN_MS,
|
|
40
|
+
READABILITY_PROBE_CELL_MS,
|
|
41
|
+
READABILITY_PROBE_RUN_MS,
|
|
38
42
|
runWithPolishProducerDeadline,
|
|
39
43
|
} from "./polish-deadline.mjs";
|
|
44
|
+
import {
|
|
45
|
+
buildReadabilityCell,
|
|
46
|
+
buildReadabilityRecord,
|
|
47
|
+
readabilityLimits,
|
|
48
|
+
readabilityObservationOk,
|
|
49
|
+
readabilityRoutes,
|
|
50
|
+
READABILITY_PRODUCER,
|
|
51
|
+
READABILITY_SCHEMA,
|
|
52
|
+
writeReadabilityCrop,
|
|
53
|
+
} from "./polish-readability.mjs";
|
|
40
54
|
import {
|
|
41
55
|
assemblySourcePackageMaterialFingerprint,
|
|
42
56
|
currentSourcePackageMaterialFingerprint,
|
|
@@ -104,6 +118,9 @@ function mappedSpecRoute(value, pageId) {
|
|
|
104
118
|
|
|
105
119
|
const NO_CAPTURABLE_ROUTES_ERROR = "no_capturable_routes";
|
|
106
120
|
|
|
121
|
+
// Whether planPolishCapture refused because every mapping is template stock.
|
|
122
|
+
export const isNoCapturableRoutesError = (error) => error?.code === NO_CAPTURABLE_ROUTES_ERROR;
|
|
123
|
+
|
|
107
124
|
export function planPolishCapture({ packet, baseUrl } = {}) {
|
|
108
125
|
if (!isPlainObject(packet) || !Array.isArray(packet?.source_html?.pages) || packet.source_html.pages.length === 0) {
|
|
109
126
|
throw new Error("polish capture requires packet.source_html.pages mappings.");
|
|
@@ -293,15 +310,18 @@ function canonicalJson(value) {
|
|
|
293
310
|
return JSON.stringify(canonicalize(value));
|
|
294
311
|
}
|
|
295
312
|
|
|
296
|
-
// Covers
|
|
297
|
-
// changes during the browser pass refuses the
|
|
313
|
+
// Covers every package-owned capture key: a page_load, media_weight or
|
|
314
|
+
// readability record that changes during the browser pass refuses the
|
|
315
|
+
// attachment.
|
|
298
316
|
function conflictToken(visualReview) {
|
|
299
317
|
const hasPageLoad = Object.hasOwn(visualReview, "page_load");
|
|
300
318
|
const hasMediaWeight = Object.hasOwn(visualReview, "media_weight");
|
|
301
|
-
|
|
319
|
+
const hasReadability = Object.hasOwn(visualReview, "readability");
|
|
320
|
+
if (!hasPageLoad && !hasMediaWeight && !hasReadability) return "absent";
|
|
302
321
|
const value = {
|
|
303
322
|
...(hasPageLoad ? { value: visualReview.page_load } : {}),
|
|
304
323
|
...(hasMediaWeight ? { media_weight: visualReview.media_weight } : {}),
|
|
324
|
+
...(hasReadability ? { readability: visualReview.readability } : {}),
|
|
305
325
|
};
|
|
306
326
|
return `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
|
|
307
327
|
}
|
|
@@ -355,6 +375,8 @@ function bindingPlanProjection(plan) {
|
|
|
355
375
|
};
|
|
356
376
|
}
|
|
357
377
|
|
|
378
|
+
// `plan` is null for a campaign with no capturable page_load routes (every
|
|
379
|
+
// mapping is template stock), where the capture measures readability only.
|
|
358
380
|
export function createPolishCaptureBinding({ packet, report, plan, packetPath, targetRepo } = {}) {
|
|
359
381
|
if (!isPlainObject(packet) || packet.schema_version !== "campaign-runtime-build-packet/v0") {
|
|
360
382
|
throw new Error("polish capture requires a current campaign-runtime-build-packet/v0 packet.");
|
|
@@ -424,10 +446,23 @@ export function createPolishCaptureBinding({ packet, report, plan, packetPath, t
|
|
|
424
446
|
current_source_package_material_fingerprint: currentSourcePackageMaterialFingerprint(report),
|
|
425
447
|
page_load_conflict_token: conflictToken(visualReview),
|
|
426
448
|
},
|
|
427
|
-
plan: bindingPlanProjection(plan),
|
|
449
|
+
plan: plan === null ? null : bindingPlanProjection(plan),
|
|
450
|
+
readability_routes: readabilityRouteProjection(resolvedTargetRepo, slug),
|
|
428
451
|
});
|
|
429
452
|
}
|
|
430
453
|
|
|
454
|
+
// The built routes readability captures, enumerated again for each binding,
|
|
455
|
+
// so a route set that changes during the browser pass refuses the attachment.
|
|
456
|
+
function readabilityRouteProjection(targetRepo, slug) {
|
|
457
|
+
const built = readabilityRoutes(targetRepo, slug);
|
|
458
|
+
return {
|
|
459
|
+
ok: built.ok === true,
|
|
460
|
+
routes: built.routes,
|
|
461
|
+
uncaptured_routes: built.uncaptured_routes,
|
|
462
|
+
route_enumeration_capped: built.route_enumeration_capped,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
431
466
|
export function assertPolishCaptureBindingUnchanged(initial, current) {
|
|
432
467
|
if (canonicalJson(initial) !== canonicalJson(current)) {
|
|
433
468
|
throw new Error(
|
|
@@ -462,27 +497,66 @@ export function mergePolishPageLoadEvidence(report, pageLoad) {
|
|
|
462
497
|
};
|
|
463
498
|
}
|
|
464
499
|
|
|
465
|
-
// Attaches
|
|
466
|
-
// mergePolishPageLoadEvidence does,
|
|
467
|
-
//
|
|
468
|
-
// capture
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
500
|
+
// Attaches the records of one capture. page_load attaches as
|
|
501
|
+
// mergePolishPageLoadEvidence does, stamped with `captured_at` when the
|
|
502
|
+
// command passes its clock `now` (the time of the merge, outside every
|
|
503
|
+
// capture's integrity). Its sibling media_weight attaches under the same
|
|
504
|
+
// producer and schema checks, bound to the same page_load subject; a capture
|
|
505
|
+
// without media_weight (an adapter that ran no image probe) removes any
|
|
506
|
+
// earlier media_weight. With no pageLoad (a campaign with no capturable
|
|
507
|
+
// page_load routes), page_load and media_weight stay exactly as they are.
|
|
508
|
+
// readability attaches on its own: only a package-produced record bound to
|
|
509
|
+
// the current build, whose every shared cell carries the integrity of the
|
|
510
|
+
// matching page_load capture; any other value, or none, removes an earlier
|
|
511
|
+
// one.
|
|
512
|
+
export function mergePolishCaptureEvidence(report, { pageLoad = null, mediaWeight = null, readability = null, now = null } = {}) {
|
|
513
|
+
let merged;
|
|
514
|
+
if (pageLoad === null || pageLoad === undefined) {
|
|
515
|
+
const { visualReview } = captureReportAncestors(report);
|
|
516
|
+
merged = {
|
|
517
|
+
...report,
|
|
518
|
+
stages: {
|
|
519
|
+
...report.stages,
|
|
520
|
+
polish: {
|
|
521
|
+
...report.stages.polish,
|
|
522
|
+
evidence: { ...report.stages.polish.evidence, visual_review: { ...visualReview } },
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
} else {
|
|
527
|
+
merged = mergePolishPageLoadEvidence(report, now === null || now === undefined ? pageLoad : { ...pageLoad, captured_at: new Date(now).toISOString() });
|
|
528
|
+
delete merged.stages.polish.evidence.visual_review.media_weight;
|
|
529
|
+
if (mediaWeight !== null && mediaWeight !== undefined) {
|
|
530
|
+
if (!isPlainObject(mediaWeight)
|
|
531
|
+
|| mediaWeight.schema_version !== MEDIA_WEIGHT_SCHEMA
|
|
532
|
+
|| mediaWeight.performed_by !== MEDIA_WEIGHT_PRODUCER
|
|
533
|
+
|| canonicalJson(mediaWeight.subject) !== canonicalJson(pageLoad.subject)) {
|
|
534
|
+
throw new Error("polish capture can attach only package-produced media_weight evidence for the same page_load capture.");
|
|
535
|
+
}
|
|
536
|
+
merged.stages.polish.evidence.visual_review.media_weight = mediaWeight;
|
|
479
537
|
}
|
|
480
|
-
visualReview.media_weight = mediaWeight;
|
|
481
538
|
}
|
|
482
|
-
merged.stages.polish.evidence.visual_review
|
|
539
|
+
const visualReview = merged.stages.polish.evidence.visual_review;
|
|
540
|
+
delete visualReview.readability;
|
|
541
|
+
if (readabilityAttaches(readability, { buildFingerprint: currentBuildFingerprint(report), pageLoad: visualReview.page_load })) {
|
|
542
|
+
visualReview.readability = readability;
|
|
543
|
+
}
|
|
483
544
|
return merged;
|
|
484
545
|
}
|
|
485
546
|
|
|
547
|
+
function readabilityAttaches(record, { buildFingerprint, pageLoad }) {
|
|
548
|
+
if (!isPlainObject(record)
|
|
549
|
+
|| record.schema_version !== READABILITY_SCHEMA
|
|
550
|
+
|| record.performed_by !== READABILITY_PRODUCER
|
|
551
|
+
|| !buildFingerprint
|
|
552
|
+
|| record.subject?.build_fingerprint !== buildFingerprint
|
|
553
|
+
|| !Array.isArray(record.cells)) return false;
|
|
554
|
+
const captures = Array.isArray(pageLoad?.captures) ? pageLoad.captures : [];
|
|
555
|
+
return record.cells.every((cell) => cell?.page_load_integrity === null || captures.some((capture) => capture?.subject?.requested_route === cell.route
|
|
556
|
+
&& capture?.subject?.viewport === cell.viewport
|
|
557
|
+
&& capture?.integrity?.projection_fingerprint === cell.page_load_integrity));
|
|
558
|
+
}
|
|
559
|
+
|
|
486
560
|
// The spec pages the plan skips (no source mapping), for media_weight's
|
|
487
561
|
// page_not_captured results: the public route of each one that has a
|
|
488
562
|
// resolvable route, and the page id of each one that has none (a skipped
|
|
@@ -552,9 +626,41 @@ function captureCampaignSlug(packet, report) {
|
|
|
552
626
|
return packetSlug;
|
|
553
627
|
}
|
|
554
628
|
|
|
555
|
-
export async function capturePolishPageLoad({
|
|
629
|
+
export async function capturePolishPageLoad(options = {}) {
|
|
630
|
+
const plan = planPolishCapture({ packet: options.packet, baseUrl: options.baseUrl });
|
|
631
|
+
return runPolishCapture({ ...options, plan });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// The readability-only capture of a campaign with no capturable page_load
|
|
635
|
+
// routes (every mapping is template stock): every built route is a
|
|
636
|
+
// readability-only cell; page_load and media_weight are not produced. A
|
|
637
|
+
// browser that cannot start is recorded on every cell.
|
|
638
|
+
export async function capturePolishReadability(options = {}) {
|
|
639
|
+
captureBaseUrl(options.baseUrl);
|
|
640
|
+
return runPolishCapture({ ...options, plan: null });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Readability for one run: the built routes (readabilityRoutes) when the
|
|
644
|
+
// adapter has a readability probe or did not start, else null (no record is
|
|
645
|
+
// produced).
|
|
646
|
+
function readabilityScope({ adapter, startupFailed, targetRepo, slug, plan, baseUrl }) {
|
|
647
|
+
if (!startupFailed && typeof adapter?.probeReadabilityRoute !== "function") return null;
|
|
648
|
+
if (!nonemptyString(targetRepo)) return null;
|
|
649
|
+
const built = readabilityRoutes(resolve(targetRepo), slug);
|
|
650
|
+
if (!built.ok) return null;
|
|
651
|
+
const base = captureBaseUrl(baseUrl);
|
|
652
|
+
const shared = new Set((plan?.routes || []).map((route) => route.requested_route));
|
|
653
|
+
return {
|
|
654
|
+
...built,
|
|
655
|
+
shared,
|
|
656
|
+
only: built.routes.filter((route) => !shared.has(route)).map((route) => ({ route, url: new URL(route, base).href })),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function runPolishCapture({
|
|
556
661
|
packet,
|
|
557
662
|
report,
|
|
663
|
+
plan,
|
|
558
664
|
baseUrl,
|
|
559
665
|
headed = false,
|
|
560
666
|
authCookie = null,
|
|
@@ -563,8 +669,9 @@ export async function capturePolishPageLoad({
|
|
|
563
669
|
captureCellDeadlineMs,
|
|
564
670
|
adapterCloseDeadlineMs,
|
|
565
671
|
probeClock = null,
|
|
672
|
+
targetRepo = null,
|
|
673
|
+
now = new Date(),
|
|
566
674
|
} = {}) {
|
|
567
|
-
const plan = planPolishCapture({ packet, baseUrl });
|
|
568
675
|
const slug = captureCampaignSlug(packet, report);
|
|
569
676
|
const buildFingerprint = currentBuildFingerprint(report);
|
|
570
677
|
if (!buildFingerprint) throw new Error("polish capture requires a current Assembly Report build fingerprint.");
|
|
@@ -582,6 +689,9 @@ export async function capturePolishPageLoad({
|
|
|
582
689
|
);
|
|
583
690
|
|
|
584
691
|
let adapter = null;
|
|
692
|
+
// Failure is tracked apart from its reason: a rejection may carry any value,
|
|
693
|
+
// including null or undefined.
|
|
694
|
+
let adapterStartupFailed = false;
|
|
585
695
|
let adapterStartupError = null;
|
|
586
696
|
let startupTimedOut = false;
|
|
587
697
|
const adapterPromise = Promise.resolve().then(() => createBrowserAdapter({
|
|
@@ -604,19 +714,56 @@ export async function capturePolishPageLoad({
|
|
|
604
714
|
throw new Error("polish capture browser adapter must provide captureRoute() and close().");
|
|
605
715
|
}
|
|
606
716
|
} catch (error) {
|
|
607
|
-
|
|
717
|
+
adapterStartupFailed = true;
|
|
718
|
+
adapterStartupError = error ?? new Error("polish capture browser adapter failed to start.");
|
|
608
719
|
}
|
|
720
|
+
// A browser that did not start leaves every readability-only cell
|
|
721
|
+
// unmeasured: producer_timeout for a startup timeout, else navigation_failed
|
|
722
|
+
// (the status a shared cell reads for the same page_load failure).
|
|
723
|
+
const startupStatus = !adapterStartupFailed ? null
|
|
724
|
+
: adapterStartupError?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE ? "producer_timeout" : "navigation_failed";
|
|
725
|
+
|
|
726
|
+
const scope = readabilityScope({ adapter, startupFailed: adapterStartupFailed, targetRepo, slug, plan, baseUrl });
|
|
727
|
+
const clock = isProbeClock(probeClock) ? probeClock : { now: () => performance.now() };
|
|
728
|
+
// The readability budgets, charged on `clock`: probe time, crop time, and
|
|
729
|
+
// all readability work (shared-cell probes and crops, and readability-only
|
|
730
|
+
// cells end to end). Probe and crop time are subtotals of the added work, so
|
|
731
|
+
// a shared cell's probe and crop time is charged once to its own budget and
|
|
732
|
+
// once to the added budget.
|
|
733
|
+
const spentMs = { probe: 0, crop: 0, added: 0 };
|
|
734
|
+
const readabilityProbe = () => ({
|
|
735
|
+
...(isProbeClock(probeClock) ? { clock: probeClock } : {}),
|
|
736
|
+
cellBoundMs: READABILITY_PROBE_CELL_MS,
|
|
737
|
+
probeRemainingMs: READABILITY_PROBE_RUN_MS - spentMs.probe,
|
|
738
|
+
cropRemainingMs: READABILITY_CROP_RUN_MS - spentMs.crop,
|
|
739
|
+
addedRemainingMs: READABILITY_ADDED_RUN_MS - spentMs.added,
|
|
740
|
+
elementCap: readabilityLimits().elements_per_cell,
|
|
741
|
+
cropsPerCell: readabilityLimits().crops_per_cell,
|
|
742
|
+
});
|
|
743
|
+
const chargeReadability = (observation, addedMs) => {
|
|
744
|
+
if (Number.isFinite(observation?.probe_ms) && observation.probe_ms > 0) spentMs.probe += observation.probe_ms;
|
|
745
|
+
if (Number.isFinite(observation?.crop_ms) && observation.crop_ms > 0) spentMs.crop += observation.crop_ms;
|
|
746
|
+
if (Number.isFinite(addedMs) && addedMs > 0) spentMs.added += addedMs;
|
|
747
|
+
};
|
|
609
748
|
|
|
610
749
|
const captures = [];
|
|
611
750
|
// Per cell, beside its capture: the adapter's observation and image probe,
|
|
612
751
|
// for the media_weight record.
|
|
613
752
|
const cellInputs = [];
|
|
753
|
+
// Readability-only cells, each { route, viewport, status, observation }.
|
|
754
|
+
const readabilityOnly = [];
|
|
614
755
|
let probeSpentMs = 0;
|
|
615
756
|
let probed = false;
|
|
757
|
+
// As for startup, a close failure is tracked apart from its reason.
|
|
758
|
+
let adapterCloseFailed = false;
|
|
616
759
|
let adapterCloseError = null;
|
|
617
760
|
let adapterPoisonProblem = null;
|
|
618
761
|
let observedProducerTimeout = false;
|
|
619
762
|
let adapterClosePromise = null;
|
|
763
|
+
// Whether page_load's cells were all run when the adapter failed to close:
|
|
764
|
+
// a close failure after readability-only cells never changes page_load.
|
|
765
|
+
let closeFailedAfterGrid = false;
|
|
766
|
+
let gridDone = false;
|
|
620
767
|
const closeAdapter = async () => {
|
|
621
768
|
if (!adapter || typeof adapter.close !== "function") return;
|
|
622
769
|
if (!adapterClosePromise) {
|
|
@@ -626,13 +773,31 @@ export async function capturePolishPageLoad({
|
|
|
626
773
|
}
|
|
627
774
|
return adapterClosePromise;
|
|
628
775
|
};
|
|
776
|
+
// A producer timeout or cleanup failure poisons the adapter for every later
|
|
777
|
+
// cell, and closes it.
|
|
778
|
+
const poison = async (error) => {
|
|
779
|
+
const producerTimedOut = error?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
780
|
+
const producerCleanupFailed = error?.code === POLISH_PRODUCER_CLEANUP_ERROR_CODE;
|
|
781
|
+
if (producerTimedOut) observedProducerTimeout = true;
|
|
782
|
+
if ((producerTimedOut || producerCleanupFailed) && adapter && !adapterPoisonProblem) {
|
|
783
|
+
adapterPoisonProblem = producerTimedOut ? "producer_timeout" : "producer_failed";
|
|
784
|
+
try {
|
|
785
|
+
await closeAdapter();
|
|
786
|
+
} catch (closeError) {
|
|
787
|
+
adapterCloseFailed = true;
|
|
788
|
+
adapterCloseError = closeError;
|
|
789
|
+
closeFailedAfterGrid = gridDone;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
629
793
|
try {
|
|
630
|
-
for (const route of plan
|
|
794
|
+
for (const route of plan?.routes || []) {
|
|
631
795
|
for (const viewport of plan.viewports) {
|
|
632
796
|
let observation;
|
|
633
797
|
let cellFailed = false;
|
|
798
|
+
const readabilityShared = scope?.shared.has(route.requested_route) && scope.routes.includes(route.requested_route);
|
|
634
799
|
try {
|
|
635
|
-
if (
|
|
800
|
+
if (adapterStartupFailed) throw adapterStartupError;
|
|
636
801
|
if (adapterPoisonProblem) {
|
|
637
802
|
throw adapterPoisonProblem === "producer_timeout"
|
|
638
803
|
? polishProducerTimeoutError()
|
|
@@ -646,7 +811,13 @@ export async function capturePolishPageLoad({
|
|
|
646
811
|
imageCap: MEDIA_PROBE_LIMITS.imageCap,
|
|
647
812
|
};
|
|
648
813
|
observation = await runWithPolishProducerDeadline(
|
|
649
|
-
() => adapter.captureRoute({
|
|
814
|
+
() => adapter.captureRoute({
|
|
815
|
+
url: route.url,
|
|
816
|
+
viewport,
|
|
817
|
+
signal: abortController.signal,
|
|
818
|
+
imageProbe,
|
|
819
|
+
...(readabilityShared ? { readabilityProbe: readabilityProbe() } : {}),
|
|
820
|
+
}),
|
|
650
821
|
{
|
|
651
822
|
timeoutMs: cellDeadlineMs,
|
|
652
823
|
onTimeout() { abortController.abort(); },
|
|
@@ -656,16 +827,7 @@ export async function capturePolishPageLoad({
|
|
|
656
827
|
} catch (error) {
|
|
657
828
|
const browserUnavailable = error?.code === POLISH_BROWSER_UNAVAILABLE_ERROR_CODE;
|
|
658
829
|
const producerTimedOut = error?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE;
|
|
659
|
-
|
|
660
|
-
if (producerTimedOut) observedProducerTimeout = true;
|
|
661
|
-
if ((producerTimedOut || producerCleanupFailed) && adapter && !adapterPoisonProblem) {
|
|
662
|
-
adapterPoisonProblem = producerTimedOut ? "producer_timeout" : "producer_failed";
|
|
663
|
-
try {
|
|
664
|
-
await closeAdapter();
|
|
665
|
-
} catch (closeError) {
|
|
666
|
-
adapterCloseError = closeError;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
830
|
+
await poison(error);
|
|
669
831
|
cellFailed = true;
|
|
670
832
|
observation = {
|
|
671
833
|
finalDocumentUrl: route.url,
|
|
@@ -679,6 +841,7 @@ export async function capturePolishPageLoad({
|
|
|
679
841
|
const probe = !cellFailed && isPlainObject(observation.imageProbe) ? observation.imageProbe : null;
|
|
680
842
|
if (probe) probed = true;
|
|
681
843
|
if (Number.isFinite(probe?.spent_ms) && probe.spent_ms > 0) probeSpentMs += probe.spent_ms;
|
|
844
|
+
if (readabilityShared && !cellFailed) chargeReadability(observation.readability, (observation.readability?.probe_ms || 0) + (observation.readability?.crop_ms || 0));
|
|
682
845
|
cellInputs.push({ route: route.requested_route, viewport: viewport.key, observation: cellFailed ? null : observation, probe });
|
|
683
846
|
captures.push(buildPageLoadCapture({
|
|
684
847
|
buildFingerprint,
|
|
@@ -696,24 +859,77 @@ export async function capturePolishPageLoad({
|
|
|
696
859
|
}));
|
|
697
860
|
}
|
|
698
861
|
}
|
|
862
|
+
gridDone = true;
|
|
863
|
+
|
|
864
|
+
// Readability-only cells, after every page_load cell, each in a fresh
|
|
865
|
+
// context, under the producer cell deadline and the run budgets. A cell
|
|
866
|
+
// that starts once the added budget is spent reads run_budget_exhausted;
|
|
867
|
+
// after a producer timeout, every later cell reads producer_timeout and
|
|
868
|
+
// none is tried again.
|
|
869
|
+
for (const { route, url } of scope?.only || []) {
|
|
870
|
+
for (const viewport of POLISH_CAPTURE_VIEWPORTS) {
|
|
871
|
+
const cell = { route, viewport: viewport.key, status: null, observation: null };
|
|
872
|
+
readabilityOnly.push(cell);
|
|
873
|
+
if (startupStatus) {
|
|
874
|
+
cell.status = startupStatus;
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (adapterPoisonProblem) {
|
|
878
|
+
cell.status = adapterPoisonProblem === "producer_timeout" ? "producer_timeout" : "navigation_failed";
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (!(READABILITY_ADDED_RUN_MS - spentMs.added > 0)) {
|
|
882
|
+
cell.status = "run_budget_exhausted";
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const started = clock.now();
|
|
886
|
+
let observation = null;
|
|
887
|
+
try {
|
|
888
|
+
const abortController = new AbortController();
|
|
889
|
+
observation = await runWithPolishProducerDeadline(
|
|
890
|
+
() => adapter.probeReadabilityRoute({ route, url }, viewport, { signal: abortController.signal, probe: readabilityProbe() }),
|
|
891
|
+
{
|
|
892
|
+
timeoutMs: cellDeadlineMs,
|
|
893
|
+
onTimeout() { abortController.abort(); },
|
|
894
|
+
},
|
|
895
|
+
);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
await poison(error);
|
|
898
|
+
cell.status = error?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE ? "producer_timeout" : "navigation_failed";
|
|
899
|
+
}
|
|
900
|
+
const elapsedMs = Math.max(0, clock.now() - started);
|
|
901
|
+
chargeReadability(observation, elapsedMs);
|
|
902
|
+
if (cell.status) continue;
|
|
903
|
+
// An observation outside the record vocabulary is no measurement: its
|
|
904
|
+
// probe ran out its bound on the clock, or returned nothing.
|
|
905
|
+
if (readabilityObservationOk(observation)) {
|
|
906
|
+
cell.status = observation.status;
|
|
907
|
+
cell.observation = observation;
|
|
908
|
+
} else {
|
|
909
|
+
cell.status = elapsedMs >= READABILITY_PROBE_CELL_MS ? "probe_timeout" : "document_changed";
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
699
913
|
} finally {
|
|
700
|
-
if (adapter && !
|
|
914
|
+
if (adapter && !adapterCloseFailed) {
|
|
701
915
|
try {
|
|
702
916
|
await closeAdapter();
|
|
703
917
|
} catch (error) {
|
|
918
|
+
adapterCloseFailed = true;
|
|
704
919
|
adapterCloseError = error;
|
|
920
|
+
closeFailedAfterGrid = gridDone && readabilityOnly.length > 0;
|
|
705
921
|
}
|
|
706
922
|
}
|
|
707
923
|
}
|
|
708
924
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
925
|
+
const closeProblem = observedProducerTimeout
|
|
926
|
+
|| adapterCloseError?.code === POLISH_PRODUCER_TIMEOUT_ERROR_CODE
|
|
927
|
+
? "producer_timeout"
|
|
928
|
+
: "producer_failed";
|
|
929
|
+
if (adapterCloseFailed && !closeFailedAfterGrid) {
|
|
714
930
|
captures.length = 0;
|
|
715
931
|
for (const input of cellInputs) Object.assign(input, { observation: null, probe: null });
|
|
716
|
-
for (const route of plan
|
|
932
|
+
for (const route of plan?.routes || []) {
|
|
717
933
|
for (const viewport of plan.viewports) {
|
|
718
934
|
captures.push(buildPageLoadCapture({
|
|
719
935
|
buildFingerprint,
|
|
@@ -724,12 +940,29 @@ export async function capturePolishPageLoad({
|
|
|
724
940
|
finalDocumentUrl: route.url,
|
|
725
941
|
responseCollectionStatus: "failed",
|
|
726
942
|
networkidle: { status: "invalid", duration_ms: null },
|
|
727
|
-
producerProblem,
|
|
943
|
+
producerProblem: closeProblem,
|
|
728
944
|
}));
|
|
729
945
|
}
|
|
730
946
|
}
|
|
731
947
|
}
|
|
732
948
|
|
|
949
|
+
const readability = scope ? buildRunReadability({
|
|
950
|
+
scope,
|
|
951
|
+
cellInputs,
|
|
952
|
+
readabilityOnly,
|
|
953
|
+
captures,
|
|
954
|
+
// A browser that did not close cleanly leaves every readability cell
|
|
955
|
+
// unmeasured.
|
|
956
|
+
closeStatus: adapterCloseFailed ? (closeProblem === "producer_timeout" ? "producer_timeout" : "navigation_failed") : null,
|
|
957
|
+
report,
|
|
958
|
+
buildFingerprint,
|
|
959
|
+
slug,
|
|
960
|
+
targetRepo,
|
|
961
|
+
now,
|
|
962
|
+
}) : null;
|
|
963
|
+
const withReadability = (result) => (readability ? { ...result, readability } : result);
|
|
964
|
+
if (plan === null) return withReadability({ plan: null });
|
|
965
|
+
|
|
733
966
|
const routes = plan.routes.map((route) => route.requested_route);
|
|
734
967
|
const viewports = plan.viewports.map((viewport) => viewport.key);
|
|
735
968
|
const pageLoad = buildPolishPageLoadEvidence({
|
|
@@ -742,7 +975,7 @@ export async function capturePolishPageLoad({
|
|
|
742
975
|
});
|
|
743
976
|
// The image probe's sibling record, when the adapter ran the probe. Each
|
|
744
977
|
// cell is stamped with the integrity of its page_load capture.
|
|
745
|
-
if (!probed) return { plan, page_load: pageLoad };
|
|
978
|
+
if (!probed) return withReadability({ plan, page_load: pageLoad });
|
|
746
979
|
const uncaptured = uncapturedPages(packet, plan);
|
|
747
980
|
const mediaWeight = buildMediaWeightRecord({
|
|
748
981
|
pageLoad,
|
|
@@ -754,5 +987,52 @@ export async function capturePolishPageLoad({
|
|
|
754
987
|
uncapturedRoutes: uncaptured.routes,
|
|
755
988
|
uncapturedPageIds: uncaptured.pageIds,
|
|
756
989
|
});
|
|
757
|
-
return { plan, page_load: pageLoad, media_weight: mediaWeight };
|
|
990
|
+
return withReadability({ plan, page_load: pageLoad, media_weight: mediaWeight });
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// The readability record of one run: one cell per captured built route ×
|
|
994
|
+
// viewport. A shared cell (a route in page_load's grid) carries its probe from
|
|
995
|
+
// the page_load cell and that capture's integrity; it reads producer_timeout
|
|
996
|
+
// or navigation_failed when its page_load cell failed, and document_changed
|
|
997
|
+
// when the cell returned no readability read. Readability-only cells carry
|
|
998
|
+
// their own status.
|
|
999
|
+
function buildRunReadability({ scope, cellInputs, readabilityOnly, captures, closeStatus, report, buildFingerprint, slug, targetRepo, now }) {
|
|
1000
|
+
const writeCrop = (bytes) => writeReadabilityCrop(resolve(targetRepo), bytes);
|
|
1001
|
+
const cells = [];
|
|
1002
|
+
for (const route of scope.routes) {
|
|
1003
|
+
for (const viewport of POLISH_CAPTURE_VIEWPORTS) {
|
|
1004
|
+
const key = viewport.key;
|
|
1005
|
+
if (scope.shared.has(route)) {
|
|
1006
|
+
const input = cellInputs.find((entry) => entry.route === route && entry.viewport === key);
|
|
1007
|
+
const capture = captures.find((entry) => entry.subject.requested_route === route && entry.subject.viewport === key);
|
|
1008
|
+
const failed = (Array.isArray(capture?.problems) ? capture.problems : []).find((problem) => ["producer_timeout", "producer_failed", "browser_unavailable"].includes(problem?.code));
|
|
1009
|
+
const observation = input?.observation?.readability;
|
|
1010
|
+
const status = closeStatus
|
|
1011
|
+
?? (failed ? (failed.code === "producer_timeout" ? "producer_timeout" : "navigation_failed")
|
|
1012
|
+
: readabilityObservationOk(observation) ? observation.status : "document_changed");
|
|
1013
|
+
cells.push(buildReadabilityCell({
|
|
1014
|
+
route,
|
|
1015
|
+
viewport: key,
|
|
1016
|
+
pageLoadIntegrity: capture?.integrity?.projection_fingerprint ?? null,
|
|
1017
|
+
status,
|
|
1018
|
+
observation: status === "measured" ? observation : null,
|
|
1019
|
+
writeCrop,
|
|
1020
|
+
}));
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
const only = readabilityOnly.find((entry) => entry.route === route && entry.viewport === key);
|
|
1024
|
+
const status = closeStatus ?? only?.status ?? "navigation_failed";
|
|
1025
|
+
cells.push(buildReadabilityCell({ route, viewport: key, status, observation: status === "measured" ? only.observation : null, writeCrop }));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return buildReadabilityRecord({
|
|
1029
|
+
buildFingerprint,
|
|
1030
|
+
sourceFingerprint: currentSourcePackageMaterialFingerprint(report),
|
|
1031
|
+
slug,
|
|
1032
|
+
routes: scope.routes,
|
|
1033
|
+
uncapturedRoutes: scope.uncaptured_routes,
|
|
1034
|
+
routeEnumerationCapped: scope.route_enumeration_capped,
|
|
1035
|
+
cells,
|
|
1036
|
+
measuredAt: now,
|
|
1037
|
+
});
|
|
758
1038
|
}
|