@nextcommerce/campaigns-os 1.54.0 → 1.55.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 +84 -0
- package/CONTEXT.md +8 -5
- package/README.md +26 -12
- package/compatibility.json +1 -1
- package/contracts/release-ledger.json +441 -0
- package/contracts/supported-surface.json +2 -2
- package/docs/brand-theme-bridge.md +10 -0
- package/docs/build-packet.md +35 -19
- package/docs/campaigns-os-build-flow.md +1 -1
- package/docs/effects.md +2 -5
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/qa-and-test-orders.md +55 -11
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +1 -1
- package/docs/workflow-findings-sidecar.md +57 -2
- package/package.json +3 -1
- package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
- 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 +3 -3
- package/skills/next-campaigns-os/SKILL.md +3 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +3 -3
- package/skills/next-campaigns-qa/SKILL.md +4 -4
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +26 -12
- package/src/brand-theme.mjs +44 -8
- package/src/campaign-identity.mjs +33 -4
- package/src/cli.mjs +36 -59
- package/src/consent.mjs +3 -2
- package/src/design-source-publication.mjs +1 -1
- package/src/deviation.mjs +2 -1
- package/src/doctor/checks.mjs +16 -6
- package/src/doctor/next-step.mjs +16 -6
- package/src/invocation.mjs +4 -3
- package/src/lifecycle.mjs +88 -6
- package/src/local-preview-policy.mjs +6 -0
- package/src/login.mjs +2 -1
- package/src/polish-browser.mjs +8 -2
- package/src/polish-node.mjs +6 -1
- package/src/polish-readability.mjs +2 -2
- package/src/qa-browser.mjs +521 -92
- package/src/qa-cart-entry.mjs +29 -6
- package/src/qa-node.mjs +144 -39
- package/src/qa-verdict-publish.mjs +2 -1
- package/src/run-record.mjs +46 -34
- package/src/sdk-markup.mjs +41 -1
- package/src/stage-ledger.mjs +14 -2
- package/src/stage-record.mjs +50 -13
package/src/lifecycle.mjs
CHANGED
|
@@ -66,6 +66,7 @@ export const LIFECYCLE_JOURNAL_REL_PATH = ".campaign-runtime/command-lifecycle.j
|
|
|
66
66
|
// from anywhere.
|
|
67
67
|
export const REFUSED_INVOCATION = "refused_invocation";
|
|
68
68
|
const refusalScope = new AsyncLocalStorage();
|
|
69
|
+
const invocationScope = new AsyncLocalStorage();
|
|
69
70
|
|
|
70
71
|
/** Run `fn` with its own refusal verdict. Returns whatever `fn` returns. */
|
|
71
72
|
export function runWithRefusalScope(fn) {
|
|
@@ -96,6 +97,14 @@ export function refusalSeen() {
|
|
|
96
97
|
return refusalScope.getStore()?.seen === true;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
// Prompt sites share this hook. Nested prompts count only the outer blocked
|
|
101
|
+
// interval, so a prompt helper can call another without inflating wait time.
|
|
102
|
+
export async function timeOperatorWait(fn) {
|
|
103
|
+
const active = invocationScope.getStore();
|
|
104
|
+
if (!active) return fn();
|
|
105
|
+
return active.wait(fn);
|
|
106
|
+
}
|
|
107
|
+
|
|
99
108
|
/**
|
|
100
109
|
* Run `fn()` and re-throw anything it throws as a tagged refusal, message
|
|
101
110
|
* byte-identical. The contract it encodes: THE TAG IS APPLIED AT THE UP-FRONT
|
|
@@ -123,6 +132,10 @@ function isStringArray(value) {
|
|
|
123
132
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
function isFindingCodes(value) {
|
|
136
|
+
return isStringArray(value) && value.length <= 5 && new Set(value).size === value.length;
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
// Injectable so tests are deterministic. `now` is wall-clock (ISO source);
|
|
127
140
|
// `monotonic` is a steadily-increasing millisecond counter for durations.
|
|
128
141
|
const defaultClock = {
|
|
@@ -146,13 +159,22 @@ function defaultReadExitStatus() {
|
|
|
146
159
|
export function createLifecycleRecorder(clock = defaultClock) {
|
|
147
160
|
const stages = [];
|
|
148
161
|
let repairLoopCount = 0;
|
|
162
|
+
let waitMs = 0;
|
|
163
|
+
let waitDepth = 0;
|
|
164
|
+
const activeStages = [];
|
|
165
|
+
const findingCodes = [];
|
|
149
166
|
function stage(name) {
|
|
150
167
|
const t0 = clock.monotonic();
|
|
168
|
+
const phase = { name: String(name), wait_ms: 0 };
|
|
169
|
+
activeStages.push(phase);
|
|
151
170
|
let stopped = false;
|
|
152
171
|
return function stop() {
|
|
153
172
|
if (stopped) return;
|
|
154
173
|
stopped = true;
|
|
155
|
-
|
|
174
|
+
activeStages.splice(activeStages.indexOf(phase), 1);
|
|
175
|
+
// Wait accumulates unrounded and is rounded once here, so it never
|
|
176
|
+
// exceeds the stage's own rounded duration.
|
|
177
|
+
stages.push({ ...phase, wait_ms: Math.round(phase.wait_ms), duration_ms: Math.max(0, Math.round(clock.monotonic() - t0)) });
|
|
156
178
|
};
|
|
157
179
|
}
|
|
158
180
|
// Convenience: time `fn` as a named sub-phase. Records the stage even if fn
|
|
@@ -168,11 +190,38 @@ export function createLifecycleRecorder(clock = defaultClock) {
|
|
|
168
190
|
return {
|
|
169
191
|
stage,
|
|
170
192
|
time,
|
|
193
|
+
async wait(fn) {
|
|
194
|
+
if (waitDepth++) {
|
|
195
|
+
try { return await fn(); } finally { waitDepth -= 1; }
|
|
196
|
+
}
|
|
197
|
+
const t0 = clock.monotonic();
|
|
198
|
+
const phase = activeStages.at(-1);
|
|
199
|
+
try { return await fn(); }
|
|
200
|
+
finally {
|
|
201
|
+
const elapsed = Math.max(0, clock.monotonic() - t0);
|
|
202
|
+
waitMs += elapsed;
|
|
203
|
+
if (phase) phase.wait_ms += elapsed;
|
|
204
|
+
waitDepth -= 1;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
recordFindingCodes(codes) {
|
|
208
|
+
// Codes attach to the phase active when they are recorded; outside any
|
|
209
|
+
// phase they stay invocation-level only.
|
|
210
|
+
const phase = activeStages.at(-1);
|
|
211
|
+
for (const code of Array.isArray(codes) ? codes : []) {
|
|
212
|
+
if (typeof code !== "string") continue;
|
|
213
|
+
if (!findingCodes.includes(code) && findingCodes.length < 5) findingCodes.push(code);
|
|
214
|
+
if (phase) {
|
|
215
|
+
phase.finding_codes ??= [];
|
|
216
|
+
if (!phase.finding_codes.includes(code) && phase.finding_codes.length < 5) phase.finding_codes.push(code);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
171
220
|
recordRepairLoop() {
|
|
172
221
|
repairLoopCount += 1;
|
|
173
222
|
},
|
|
174
223
|
snapshot() {
|
|
175
|
-
return { stages: stages.slice(), repair_loop_count: repairLoopCount };
|
|
224
|
+
return { stages: stages.slice(), repair_loop_count: repairLoopCount, wait_ms: Math.round(waitMs), finding_codes: findingCodes.slice() };
|
|
176
225
|
},
|
|
177
226
|
};
|
|
178
227
|
}
|
|
@@ -183,7 +232,9 @@ export const NOOP_RECORDER = {
|
|
|
183
232
|
stage: () => () => {},
|
|
184
233
|
time: async (_name, fn) => fn(),
|
|
185
234
|
recordRepairLoop: () => {},
|
|
186
|
-
|
|
235
|
+
wait: (fn) => fn(),
|
|
236
|
+
recordFindingCodes: () => {},
|
|
237
|
+
snapshot: () => ({ stages: [], repair_loop_count: 0, wait_ms: 0, finding_codes: [] }),
|
|
187
238
|
};
|
|
188
239
|
|
|
189
240
|
function buildLifecycle({ command, argvShape, runId, exitStatus, startedAt, completedAt, durationMs, recorder }) {
|
|
@@ -197,7 +248,10 @@ function buildLifecycle({ command, argvShape, runId, exitStatus, startedAt, comp
|
|
|
197
248
|
started_at: startedAt,
|
|
198
249
|
completed_at: completedAt,
|
|
199
250
|
duration_ms: Number.isFinite(durationMs) ? Math.max(0, Math.round(durationMs)) : null,
|
|
200
|
-
|
|
251
|
+
wait_ms: recorded.wait_ms,
|
|
252
|
+
...(Number.isInteger(exitStatus) && exitStatus !== 0 && recorded.finding_codes.length ? { finding_codes: recorded.finding_codes } : {}),
|
|
253
|
+
stages: recorded.stages.map(({ finding_codes, ...stage }) =>
|
|
254
|
+
Number.isInteger(exitStatus) && exitStatus !== 0 && finding_codes?.length ? { ...stage, finding_codes } : stage),
|
|
201
255
|
repair_loop_count: recorded.repair_loop_count,
|
|
202
256
|
};
|
|
203
257
|
}
|
|
@@ -225,7 +279,7 @@ export async function withCommandLifecycle({
|
|
|
225
279
|
let thrown = null;
|
|
226
280
|
let exitStatus = 0;
|
|
227
281
|
try {
|
|
228
|
-
result = await fn(recorder);
|
|
282
|
+
result = await invocationScope.run(recorder, () => fn(recorder));
|
|
229
283
|
// A clean return is exit 0 unless the command set process.exitCode.
|
|
230
284
|
const raw = readExitStatus();
|
|
231
285
|
exitStatus = Number.isInteger(raw) ? raw : 0;
|
|
@@ -278,6 +332,8 @@ export function validateLifecycle(entry) {
|
|
|
278
332
|
if (entry.exit_status != null && !Number.isInteger(entry.exit_status)) add("lifecycle.exit_status", "exit_status must be an integer or null.");
|
|
279
333
|
if (entry.run_id != null && typeof entry.run_id !== "string") add("lifecycle.run_id", "run_id must be a string or null.");
|
|
280
334
|
if (entry.duration_ms != null && typeof entry.duration_ms !== "number") add("lifecycle.duration_ms", "duration_ms must be a number or null.");
|
|
335
|
+
if (entry.wait_ms != null && (!Number.isInteger(entry.wait_ms) || entry.wait_ms < 0)) add("lifecycle.wait_ms", "wait_ms must be a non-negative integer or null.");
|
|
336
|
+
if (entry.finding_codes !== undefined && (!isFindingCodes(entry.finding_codes) || !Number.isInteger(entry.exit_status) || entry.exit_status === 0)) add("lifecycle.finding_codes", "finding_codes must have at most five distinct strings and require a non-zero exit_status.");
|
|
281
337
|
if (entry.repair_loop_count != null && !Number.isInteger(entry.repair_loop_count)) add("lifecycle.repair_loop_count", "repair_loop_count must be an integer.");
|
|
282
338
|
if (entry.stages != null) {
|
|
283
339
|
if (!Array.isArray(entry.stages)) {
|
|
@@ -285,7 +341,11 @@ export function validateLifecycle(entry) {
|
|
|
285
341
|
} else {
|
|
286
342
|
entry.stages.forEach((stage, index) => {
|
|
287
343
|
if (!stage || typeof stage !== "object" || Array.isArray(stage)) add(`lifecycle.stages[${index}]`, "each stage must be an object.");
|
|
288
|
-
else
|
|
344
|
+
else {
|
|
345
|
+
if (!isNonEmptyString(stage.name)) add(`lifecycle.stages[${index}].name`, "stage name is required.");
|
|
346
|
+
if (stage.wait_ms != null && (!Number.isInteger(stage.wait_ms) || stage.wait_ms < 0)) add(`lifecycle.stages[${index}].wait_ms`, "wait_ms must be a non-negative integer or null.");
|
|
347
|
+
if (stage.finding_codes !== undefined && (!isFindingCodes(stage.finding_codes) || !Number.isInteger(entry.exit_status) || entry.exit_status === 0)) add(`lifecycle.stages[${index}].finding_codes`, "finding_codes must have at most five distinct strings and require a non-zero exit_status.");
|
|
348
|
+
}
|
|
289
349
|
});
|
|
290
350
|
}
|
|
291
351
|
}
|
|
@@ -366,6 +426,8 @@ export function aggregateLifecycleForRun(journal, runId, { excludeCommands = []
|
|
|
366
426
|
let latest = null;
|
|
367
427
|
let durationSum = 0;
|
|
368
428
|
let explicitRepairLoops = 0;
|
|
429
|
+
let needsInputCount = 0;
|
|
430
|
+
let failureCount = 0;
|
|
369
431
|
|
|
370
432
|
// Only finite, parseable ISO timestamps participate in span timing; a junk
|
|
371
433
|
// string (e.g. a hand-edited journal) is ignored rather than emitted as a
|
|
@@ -376,6 +438,11 @@ export function aggregateLifecycleForRun(journal, runId, { excludeCommands = []
|
|
|
376
438
|
const command = typeof entry.command === "string" ? entry.command : "(unknown)";
|
|
377
439
|
commandCounts.set(command, (commandCounts.get(command) || 0) + 1);
|
|
378
440
|
const exitStatus = Number.isInteger(entry.exit_status) ? entry.exit_status : null;
|
|
441
|
+
if (exitStatus === 2) needsInputCount += 1;
|
|
442
|
+
else if (exitStatus !== null && exitStatus !== 0) failureCount += 1;
|
|
443
|
+
const findings = exitStatus !== null && exitStatus !== 0 && isStringArray(entry.finding_codes)
|
|
444
|
+
? [...new Set(entry.finding_codes)].slice(0, 5) : [];
|
|
445
|
+
const findingFields = findings.length ? { finding_codes: findings } : {};
|
|
379
446
|
// A command may have recorded its own repair loops via recordRepairLoop().
|
|
380
447
|
if (Number.isInteger(entry.repair_loop_count)) explicitRepairLoops += entry.repair_loop_count;
|
|
381
448
|
|
|
@@ -384,12 +451,25 @@ export function aggregateLifecycleForRun(journal, runId, { excludeCommands = []
|
|
|
384
451
|
name: `${command}:${typeof stage?.name === "string" ? stage.name : "stage"}`,
|
|
385
452
|
duration_ms: typeof stage?.duration_ms === "number" ? stage.duration_ms : null,
|
|
386
453
|
exit_status: exitStatus,
|
|
454
|
+
...(Number.isInteger(stage?.wait_ms) && stage.wait_ms >= 0 ? { wait_ms: stage.wait_ms } : {}),
|
|
455
|
+
...(exitStatus !== null && exitStatus !== 0 && isFindingCodes(stage?.finding_codes) && stage.finding_codes.length
|
|
456
|
+
? { finding_codes: stage.finding_codes } : {}),
|
|
387
457
|
}))
|
|
388
458
|
: [{
|
|
389
459
|
name: command,
|
|
390
460
|
duration_ms: typeof entry.duration_ms === "number" ? entry.duration_ms : null,
|
|
391
461
|
exit_status: exitStatus,
|
|
462
|
+
...(Number.isInteger(entry.wait_ms) && entry.wait_ms >= 0 ? { wait_ms: entry.wait_ms } : {}),
|
|
463
|
+
...findingFields,
|
|
392
464
|
}];
|
|
465
|
+
if (subStages.length > 1 || (Array.isArray(entry.stages) && entry.stages.length)) {
|
|
466
|
+
const attributed = new Set(subStages.flatMap((stage) => stage.finding_codes ?? []));
|
|
467
|
+
const unknown = findings.filter((code) => !attributed.has(code));
|
|
468
|
+
if (unknown.length) {
|
|
469
|
+
const lastStage = subStages.at(-1);
|
|
470
|
+
lastStage.finding_codes = [...new Set([...(lastStage.finding_codes ?? []), ...unknown])].slice(0, 5);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
393
473
|
stages.push(...subStages);
|
|
394
474
|
|
|
395
475
|
if (typeof entry.duration_ms === "number") durationSum += entry.duration_ms;
|
|
@@ -430,5 +510,7 @@ export function aggregateLifecycleForRun(journal, runId, { excludeCommands = []
|
|
|
430
510
|
wall_clock_duration_ms: wallClockDurationMs == null ? null : Math.round(wallClockDurationMs),
|
|
431
511
|
stages,
|
|
432
512
|
repair_loop_count: repairLoopCount,
|
|
513
|
+
needs_input_count: needsInputCount,
|
|
514
|
+
failure_count: failureCount,
|
|
433
515
|
};
|
|
434
516
|
}
|
|
@@ -75,6 +75,12 @@ export function applyLocalPreviewToCheckpoint(gate, { packet, report, baseUrl =
|
|
|
75
75
|
|
|
76
76
|
// The polish gate folds the checkpoint in, so it is carried forward only when
|
|
77
77
|
// whatever it owns from the checkpoint was carried forward too.
|
|
78
|
+
export function polishCarriedForwardForLadder(report, gate) {
|
|
79
|
+
return gate?.status === CARRIED_FORWARD
|
|
80
|
+
&& !report?.theme?.waiver
|
|
81
|
+
&& !report?.stages?.polish?.evidence?.visual_review?.page_load;
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
export function applyLocalPreviewToPolishGate(gate, { packet, checkpointGate = null, baseUrl = null } = {}) {
|
|
79
85
|
if (gate?.status !== "blocked" || !isLocalPreview(packet, { baseUrl })) return gate;
|
|
80
86
|
const checkpointCarried = checkpointGate?.status === CARRIED_FORWARD;
|
package/src/login.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createCredentialStore } from './credential-store.mjs';
|
|
2
|
+
import { timeOperatorWait } from './lifecycle.mjs';
|
|
2
3
|
|
|
3
4
|
export const GATEWAY = 'https://mcp.nextcommerce.com';
|
|
4
5
|
export const CLIENT_ID = 'campaigns-os-owned-store-pilot';
|
|
@@ -27,7 +28,7 @@ export function authenticationArguments(argv) {
|
|
|
27
28
|
async function askStore() {
|
|
28
29
|
const { createInterface } = await import('node:readline/promises');
|
|
29
30
|
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
30
|
-
try { return await prompt.question('Store subdomain (or network domain): '); }
|
|
31
|
+
try { return await timeOperatorWait(() => prompt.question('Store subdomain (or network domain): ')); }
|
|
31
32
|
finally { prompt.close(); }
|
|
32
33
|
}
|
|
33
34
|
const bindingFor = store => ({ gateway: GATEWAY, client_id: CLIENT_ID, store });
|
package/src/polish-browser.mjs
CHANGED
|
@@ -1012,6 +1012,10 @@ function polishBrowserMissing(kind, error) {
|
|
|
1012
1012
|
].join(" "));
|
|
1013
1013
|
}
|
|
1014
1014
|
|
|
1015
|
+
// `networkIdleTimeoutMs`, like the deadlines, can only shorten its bound: the
|
|
1016
|
+
// wait for network idle after each navigation (NETWORKIDLE_TIMEOUT_MS). The
|
|
1017
|
+
// CLI never passes it; a test whose page holds a transfer open passes a
|
|
1018
|
+
// shorter window rather than wait out the production one on every cell.
|
|
1015
1019
|
export async function createPolishBrowserAdapter({
|
|
1016
1020
|
headed = false,
|
|
1017
1021
|
authCookie = null,
|
|
@@ -1019,6 +1023,7 @@ export async function createPolishBrowserAdapter({
|
|
|
1019
1023
|
cellDeadlineMs,
|
|
1020
1024
|
cleanupDeadlineMs,
|
|
1021
1025
|
startupDeadlineMs,
|
|
1026
|
+
networkIdleTimeoutMs,
|
|
1022
1027
|
} = {}) {
|
|
1023
1028
|
const authCookies = parseAuthCookie(authCookie);
|
|
1024
1029
|
const boundedCellDeadlineMs = boundedPolishDeadline(cellDeadlineMs, POLISH_BROWSER_CELL_DEADLINE_MS);
|
|
@@ -1030,6 +1035,7 @@ export async function createPolishBrowserAdapter({
|
|
|
1030
1035
|
startupDeadlineMs,
|
|
1031
1036
|
POLISH_BROWSER_STARTUP_DEADLINE_MS,
|
|
1032
1037
|
);
|
|
1038
|
+
const networkIdleMs = boundedPolishDeadline(networkIdleTimeoutMs, NETWORKIDLE_TIMEOUT_MS);
|
|
1033
1039
|
let browser;
|
|
1034
1040
|
let startupTimedOut = false;
|
|
1035
1041
|
const startupAbort = new AbortController();
|
|
@@ -1234,7 +1240,7 @@ export async function createPolishBrowserAdapter({
|
|
|
1234
1240
|
const initialMediaElements = await awaitActive(collectMediaElements(page));
|
|
1235
1241
|
let networkidle;
|
|
1236
1242
|
try {
|
|
1237
|
-
await awaitActive(page.waitForLoadState("networkidle", { timeout:
|
|
1243
|
+
await awaitActive(page.waitForLoadState("networkidle", { timeout: networkIdleMs }));
|
|
1238
1244
|
networkidle = { status: "settled", duration_ms: durationSince(navigationStartedAt) };
|
|
1239
1245
|
} catch (error) {
|
|
1240
1246
|
if (!timeoutError(error)) throw error;
|
|
@@ -1327,7 +1333,7 @@ export async function createPolishBrowserAdapter({
|
|
|
1327
1333
|
if (initialTree === BOUND_ENDED) return unmeasured(boundEnded());
|
|
1328
1334
|
if (initialTree.error) return unmeasured("document_changed");
|
|
1329
1335
|
const initialMainFrame = initialTree.value?.frameTree?.frame;
|
|
1330
|
-
const idleTimeout = Math.max(1, Math.min(
|
|
1336
|
+
const idleTimeout = Math.max(1, Math.min(networkIdleMs, Math.floor(addedLeft()), Math.floor(cellLeftMs())));
|
|
1331
1337
|
const idle = await awaitActive(steps.step(() => page.waitForLoadState("networkidle", { timeout: idleTimeout })));
|
|
1332
1338
|
if (idle === BOUND_ENDED) return unmeasured(boundEnded());
|
|
1333
1339
|
if (idle.error && !timeoutError(idle.error)) throw idle.error;
|
package/src/polish-node.mjs
CHANGED
|
@@ -669,6 +669,7 @@ async function runPolishCapture({
|
|
|
669
669
|
captureCellDeadlineMs,
|
|
670
670
|
adapterCloseDeadlineMs,
|
|
671
671
|
probeClock = null,
|
|
672
|
+
mediaProbeRunBudgetMs,
|
|
672
673
|
targetRepo = null,
|
|
673
674
|
now = new Date(),
|
|
674
675
|
} = {}) {
|
|
@@ -687,6 +688,10 @@ async function runPolishCapture({
|
|
|
687
688
|
adapterCloseDeadlineMs,
|
|
688
689
|
POLISH_CAPTURE_CLOSE_DEADLINE_MS,
|
|
689
690
|
);
|
|
691
|
+
// The 1.3 image probe's run budget. Like the deadlines it can only shorten:
|
|
692
|
+
// the CLI never passes it, and a test that spends the budget passes a
|
|
693
|
+
// smaller one so that fewer slow cells spend it.
|
|
694
|
+
const probeRunBudgetMs = boundedPolishDeadline(mediaProbeRunBudgetMs, MEDIA_PROBE_LIMITS.runBudgetMs);
|
|
690
695
|
|
|
691
696
|
let adapter = null;
|
|
692
697
|
// Failure is tracked apart from its reason: a rejection may carry any value,
|
|
@@ -806,7 +811,7 @@ async function runPolishCapture({
|
|
|
806
811
|
const abortController = new AbortController();
|
|
807
812
|
const imageProbe = {
|
|
808
813
|
...(isProbeClock(probeClock) ? { clock: probeClock } : {}),
|
|
809
|
-
remainingMs:
|
|
814
|
+
remainingMs: probeRunBudgetMs - probeSpentMs,
|
|
810
815
|
cellBoundMs: MEDIA_PROBE_LIMITS.cellBoundMs,
|
|
811
816
|
imageCap: MEDIA_PROBE_LIMITS.imageCap,
|
|
812
817
|
};
|
|
@@ -433,8 +433,8 @@ export function readabilityProbe(toolkit, limits, closedRoots, generatedText) {
|
|
|
433
433
|
// ancestor matches. checkout_label and checkout_hint are read separately.
|
|
434
434
|
const ROLES = [
|
|
435
435
|
["add_to_cart", "[data-next-action=\"add-to-cart\"]"],
|
|
436
|
-
["upsell_accept", "[data-next-upsell-action=\"add\"]"],
|
|
437
|
-
["upsell_decline", "[data-next-upsell-action=\"skip\"]"],
|
|
436
|
+
["upsell_accept", "[data-next-upsell-action=\"add\"], [data-next-upsell-action=\"accept\"]"],
|
|
437
|
+
["upsell_decline", "[data-next-upsell-action=\"skip\"], [data-next-upsell-action=\"decline\"]"],
|
|
438
438
|
["submit_control", "button.submit-button[os-checkout-payment=\"combo\"], button[os-checkout-payment=\"combo\"], button[type=\"submit\"], input[type=\"submit\"], input[type=\"button\"]"],
|
|
439
439
|
["sdk_action", "[data-next-action]"],
|
|
440
440
|
["bundle_card", "[data-next-bundle-card], [data-next-selector-card], [data-next-package-id], [data-next-bundle-id]"],
|