@nextcommerce/campaigns-os 1.48.0 → 1.52.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 +539 -0
- package/agents/claude/CLAUDE.md +6 -5
- package/agents/codex/AGENTS.md +6 -5
- package/agents/copilot/copilot-instructions.md +3 -3
- package/agents/cursor/campaigns-os.mdc +3 -3
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
- package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
- package/campaign-spec/dist/rules/design-source-shape.js +13 -3
- package/campaign-spec/dist/rules/sdk-version.js +2 -1
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +26 -46
- package/contracts/effects.v1.json +254 -2
- package/contracts/release-ledger.json +1239 -0
- package/contracts/supported-surface.json +4 -4
- package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
- package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
- package/docs/brand-theme-bridge.md +12 -6
- package/docs/build-packet.md +101 -12
- package/docs/campaign-build-brief.md +25 -1
- package/docs/effects.md +6 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +10 -0
- package/docs/qa-and-test-orders.md +66 -11
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +8 -3
- 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 +4 -4
- package/skills/next-campaigns-os/SKILL.md +4 -4
- package/skills/next-campaigns-os/references/session-intake.md +7 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +5 -4
- package/skills/next-campaigns-qa/SKILL.md +6 -5
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +1 -1
- package/src/brand-theme.mjs +25 -2
- package/src/build-brief.mjs +68 -21
- package/src/built-site-scope.mjs +39 -6
- package/src/built-smoke-qc.mjs +1117 -0
- package/src/campaign-identity.mjs +36 -2
- package/src/cart-placeholders.mjs +730 -0
- package/src/cli.mjs +320 -42
- package/src/commercial-journey.mjs +65 -4
- package/src/commercial-parity.mjs +6 -1
- package/src/doctor/checks.mjs +291 -24
- package/src/doctor/inspect.mjs +53 -2
- package/src/doctor/next-step.mjs +1 -1
- package/src/install-mode.mjs +0 -8
- package/src/invocation.mjs +5 -2
- package/src/local-preview-policy.mjs +1 -1
- package/src/local-proof.mjs +4 -1
- package/src/polish-browser.mjs +218 -1
- package/src/polish-capture.mjs +1 -1
- package/src/polish-media-weight.mjs +492 -0
- package/src/polish-node.mjs +96 -4
- package/src/progress-node.mjs +5 -1
- package/src/qa-binding-evidence.mjs +21 -0
- package/src/qa-browser.mjs +338 -97
- package/src/qa-content-params.mjs +889 -0
- package/src/qa-node.mjs +114 -14
- package/src/qa-order-bump.mjs +22 -1
- package/src/qa-policy-links.mjs +1019 -0
- package/src/qa-tracking-params.mjs +1389 -0
- package/src/qa-url-privacy.mjs +168 -0
- package/src/qc-accept.mjs +446 -0
- package/src/qc-check-registry.mjs +83 -0
- package/src/qc-results.mjs +1049 -0
- package/src/sdk-attribute-index.mjs +71 -0
- package/src/sdk-markup.mjs +2 -2
- package/src/sdk-storage-compatibility.mjs +63 -3
- package/src/source-prep.mjs +37 -7
- package/src/stage-record.mjs +356 -36
- package/src/theme-gate.mjs +3 -3
package/src/qa-node.mjs
CHANGED
|
@@ -33,11 +33,15 @@ function cmd(verb, rest = "") {
|
|
|
33
33
|
}
|
|
34
34
|
import { isSameAnalyticsCapturePage, runAnalyticsCorrectnessChecks, runAnalyticsParityChecks, runBrowserChecks, runBrowserTestOrders, testEmail, upsellActionCoverageWithoutOrders, validatedOrderCreationLimit } from "./qa-browser.mjs";
|
|
35
35
|
import { assessReceiptPurchase } from "./qa-analytics-correctness.mjs";
|
|
36
|
+
import { trackingQaAssertion, trackingRunScopeRows } from "./qa-tracking-params.mjs";
|
|
37
|
+
import { contentParamNotRequestedRows, contentParamQaAssertion } from "./qa-content-params.mjs";
|
|
38
|
+
import { policyLinkNotRequestedRows, policyLinkQaAssertion } from "./qa-policy-links.mjs";
|
|
36
39
|
import { createVerdict, isFindingAssertion, QA_ASSERTION_FAMILY_VOCABULARY, SESSION_ENDING_DISPOSITIONS, SEVERITY, STATUS, validateVerdict } from "./qa-verdict.mjs";
|
|
37
40
|
import { normalizeSdkMetaName, lookupSdkIgnoredMetaTag } from "./sdk-meta-tags.mjs";
|
|
38
41
|
import { annotateQaAssertionCauses, formatCauseReportLines, formatCauseTag } from "./finding-cause.mjs";
|
|
39
42
|
import { promoteQaVerdict, writeQaSidecar } from "./qa-sidecar.mjs";
|
|
40
43
|
import { publishQaVerdict, qaPortalUrl, qaVerdictPublishBlock, QA_VERDICT_PUBLISHERS, skippedQaVerdictPublish } from "./qa-verdict-publish.mjs";
|
|
44
|
+
import { redactPersisted } from "./qa-url-privacy.mjs";
|
|
41
45
|
import {
|
|
42
46
|
isLocalServePacket,
|
|
43
47
|
LOCAL_PROOF_BUILD_ENVIRONMENT,
|
|
@@ -203,7 +207,7 @@ Options:
|
|
|
203
207
|
block before browser launch. The default cap is 6; overflow names the exact raise.
|
|
204
208
|
"tiers" is spec-driven: one strict-selection order per selector tier the
|
|
205
209
|
CampaignSpec declares on the checkout page (order-bump rows marked
|
|
206
|
-
is_upsell are add-ons, never tiers), plus one coupon order per declared
|
|
210
|
+
is_order_bump or is_upsell are add-ons, never tiers), plus one coupon order per declared
|
|
207
211
|
offer code (checkout exit_intent / promo_code_input); "tiers:common" and
|
|
208
212
|
"tiers:full" cross every tier with those path shapes. --select-package
|
|
209
213
|
<ref[:qty],...> narrows a tiers run to the listed declared tiers;
|
|
@@ -2328,8 +2332,9 @@ async function runResolvedQa(args, resolved, { runSessionActive = false, liveCam
|
|
|
2328
2332
|
proxyBase: resolved.proxyBase,
|
|
2329
2333
|
});
|
|
2330
2334
|
assertions.push(...liveCampaignRefAssertions({ pages: [...livePages.values()], spec: liveSpec, liveCampaign: liveRead }));
|
|
2335
|
+
const qcResults = [];
|
|
2331
2336
|
if (args.browser === true) {
|
|
2332
|
-
|
|
2337
|
+
const browserAssertions = await runBrowserChecks(resolved.topologies, args, {
|
|
2333
2338
|
brandContract: resolved.brandContract,
|
|
2334
2339
|
// With no generatable brand theme on the local preview, the starter
|
|
2335
2340
|
// template is the design: its residue is a warning (local-preview-policy.mjs).
|
|
@@ -2337,10 +2342,31 @@ async function runResolvedQa(args, resolved, { runSessionActive = false, liveCam
|
|
|
2337
2342
|
? SEVERITY.WARN
|
|
2338
2343
|
: residueSeverityForThemeGate(gate.status),
|
|
2339
2344
|
supportedPaymentMethods: supportedPaymentMethodsFromSpec(resolved.spec),
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2345
|
+
bindingExpected,
|
|
2346
|
+
spec: resolved.spec,
|
|
2347
|
+
qcResults,
|
|
2348
|
+
});
|
|
2349
|
+
// A page-binding row from the browser is the key the SDK actually sent;
|
|
2350
|
+
// it takes the place of that page's static read.
|
|
2351
|
+
for (const observed of browserAssertions) {
|
|
2352
|
+
const at = observed.id.startsWith("page-binding:") ? assertions.findIndex((entry) => entry.id === observed.id) : -1;
|
|
2353
|
+
if (at >= 0) assertions[at] = observed;
|
|
2354
|
+
else assertions.push(observed);
|
|
2355
|
+
}
|
|
2356
|
+
} else {
|
|
2357
|
+
// The browser checks were not requested: each declared content parameter
|
|
2358
|
+
// (param, page) is listed as excluded, never left silent.
|
|
2359
|
+
const rows = contentParamNotRequestedRows(resolved.spec, resolved.topologies);
|
|
2360
|
+
qcResults.push(...rows);
|
|
2361
|
+
assertions.push(...rows.map(contentParamQaAssertion));
|
|
2362
|
+
// Each configured policy link field likewise lists its presence and
|
|
2363
|
+
// availability rows as excluded; no probe is sent.
|
|
2364
|
+
const policyRows = policyLinkNotRequestedRows(resolved.spec, resolved.topologies);
|
|
2365
|
+
qcResults.push(...policyRows);
|
|
2366
|
+
assertions.push(...policyRows.map(policyLinkQaAssertion));
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const testOrders = await runAnalyticsOrderSequence({ args, resolved, runId, assertions, qcResults });
|
|
2344
2370
|
const remainingAssertionBudget = Math.max(0, QA_VERDICT_ASSERTION_LIMIT - assertions.length);
|
|
2345
2371
|
let commercialResult;
|
|
2346
2372
|
try {
|
|
@@ -2367,6 +2393,7 @@ async function runResolvedQa(args, resolved, { runSessionActive = false, liveCam
|
|
|
2367
2393
|
testOrders,
|
|
2368
2394
|
commercial: commercialResult.commercial,
|
|
2369
2395
|
runSessionActive,
|
|
2396
|
+
qcResults,
|
|
2370
2397
|
});
|
|
2371
2398
|
}
|
|
2372
2399
|
|
|
@@ -2381,7 +2408,7 @@ function reportCommercialRunnerError(args, error, write = (message) => process.s
|
|
|
2381
2408
|
// one canonical typed-card run captures its settled terminal and only then do
|
|
2382
2409
|
// we finalize the stable purchase-fires assertion. There is no receipt replay
|
|
2383
2410
|
// and no second order.
|
|
2384
|
-
async function runAnalyticsOrderSequence({ args, resolved, runId, assertions }, overrides = {}) {
|
|
2411
|
+
async function runAnalyticsOrderSequence({ args, resolved, runId, assertions, qcResults = null }, overrides = {}) {
|
|
2385
2412
|
const operations = {
|
|
2386
2413
|
runInventory: runAnalyticsCorrectnessChecks,
|
|
2387
2414
|
runParity: runAnalyticsParityChecks,
|
|
@@ -2419,6 +2446,7 @@ async function runAnalyticsOrderSequence({ args, resolved, runId, assertions },
|
|
|
2419
2446
|
assertions,
|
|
2420
2447
|
captureAnalytics: analyticsLeg === "run",
|
|
2421
2448
|
});
|
|
2449
|
+
if (Array.isArray(qcResults) && Array.isArray(result.qc_results)) qcResults.push(...result.qc_results);
|
|
2422
2450
|
if (analyticsLeg === "run") {
|
|
2423
2451
|
assertions.push(operations.assessReceipt(result.receiptAnalytics, { waivers: resolved.qaWaivers }));
|
|
2424
2452
|
}
|
|
@@ -2546,7 +2574,7 @@ function applyLocalServeAnalyticsReview(assertions, localServe) {
|
|
|
2546
2574
|
}
|
|
2547
2575
|
}
|
|
2548
2576
|
|
|
2549
|
-
async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, testOrders, commercial = null, runSessionActive = false, browser = null }) {
|
|
2577
|
+
async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, testOrders, commercial = null, runSessionActive = false, browser = null, qcResults = [] }) {
|
|
2550
2578
|
const entryUrls = deriveEntryUrls(resolved.topologies);
|
|
2551
2579
|
const pageUrls = derivePageUrls(resolved.topologies);
|
|
2552
2580
|
const testedUrls = deriveTestedUrlsFromAssertions(assertions, pageUrls);
|
|
@@ -2567,7 +2595,10 @@ async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, tes
|
|
|
2567
2595
|
currentRunId: runId,
|
|
2568
2596
|
isFinding: isFindingAssertion,
|
|
2569
2597
|
});
|
|
2570
|
-
|
|
2598
|
+
// The verdict as it is persisted: the local file, the committed sidecar and
|
|
2599
|
+
// the published copy all read this one projection (no URL query in any
|
|
2600
|
+
// string or key), whichever runner, return path or field produced a value.
|
|
2601
|
+
const verdict = redactPersisted(createVerdict({
|
|
2571
2602
|
runId,
|
|
2572
2603
|
mapId: resolved.mapId,
|
|
2573
2604
|
localSpecId: resolved.localSpecId,
|
|
@@ -2588,7 +2619,7 @@ async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, tes
|
|
|
2588
2619
|
commercial,
|
|
2589
2620
|
causeSummary,
|
|
2590
2621
|
browser,
|
|
2591
|
-
});
|
|
2622
|
+
}));
|
|
2592
2623
|
|
|
2593
2624
|
const validationErrors = validateVerdict(verdict);
|
|
2594
2625
|
if (validationErrors.length) throw new Error(`QA verdict failed local validation:\n- ${validationErrors.join("\n- ")}`);
|
|
@@ -2670,6 +2701,9 @@ async function finalizeQaRun({ args, resolved, runId, startedAt, assertions, tes
|
|
|
2670
2701
|
browser: verdict.browser || null,
|
|
2671
2702
|
commercial: verdict.commercial || null,
|
|
2672
2703
|
next_actions: buildQaCloseoutActions({ packetPath: resolved.packetPath, localPath, runSessionActive, disposition: verdict.disposition }),
|
|
2704
|
+
// The QC rows the QA stage records; readers re-derive them from the qc.*
|
|
2705
|
+
// assertions of the verdict written above.
|
|
2706
|
+
qc_results: qcResults,
|
|
2673
2707
|
verdict,
|
|
2674
2708
|
};
|
|
2675
2709
|
}
|
|
@@ -2936,6 +2970,23 @@ async function runPageChecks(page, args, {
|
|
|
2936
2970
|
["decline", page.expected_decline_url],
|
|
2937
2971
|
]) {
|
|
2938
2972
|
if (!expectedUrl) continue;
|
|
2973
|
+
const deadProxy = findDeadUpsellProxy(html, kind, page);
|
|
2974
|
+
if (deadProxy) {
|
|
2975
|
+
assertions.push(assertion({
|
|
2976
|
+
id: `route-link:${page.page_id}:${kind}`,
|
|
2977
|
+
family: "funnel-flow",
|
|
2978
|
+
page,
|
|
2979
|
+
status: STATUS.FAIL,
|
|
2980
|
+
severity: SEVERITY.BLOCKER,
|
|
2981
|
+
expected: expectedUrl,
|
|
2982
|
+
actual: deadProxy,
|
|
2983
|
+
evidence: {
|
|
2984
|
+
expected: expectedUrl,
|
|
2985
|
+
note: `The page's ${kind} button only forwards its click to the SDK's data-next-upsell-action inside the offer, and the offer has none, so the shopper's click does nothing. Render the in-offer action (it may be hidden).`,
|
|
2986
|
+
},
|
|
2987
|
+
}));
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2939
2990
|
const staticFound = htmlIncludesRouteReference(html, expectedUrl);
|
|
2940
2991
|
const sdkAction = staticFound ? null : findSdkRouteAction(html, kind, page);
|
|
2941
2992
|
const found = staticFound || Boolean(sdkAction);
|
|
@@ -3042,21 +3093,32 @@ async function maybeRunTestOrders(
|
|
|
3042
3093
|
const coverage = upsellActionCoverageWithoutOrders(resolved.topologies);
|
|
3043
3094
|
if (coverage) assertions.push(coverage);
|
|
3044
3095
|
}
|
|
3096
|
+
// Tracking parameters ride browser test orders only: a run without one
|
|
3097
|
+
// says so with a run-scope excluded row per tracking check.
|
|
3098
|
+
const notRequestedRows = () => {
|
|
3099
|
+
const rows = trackingRunScopeRows({ runId });
|
|
3100
|
+
assertions.push(...rows.map(trackingQaAssertion));
|
|
3101
|
+
return rows;
|
|
3102
|
+
};
|
|
3045
3103
|
if ((!mode || mode === "off") && (!legacyMode || legacyMode === "off")) {
|
|
3046
|
-
return { orders: [], receiptAnalytics: emptyReceiptAnalytics() };
|
|
3104
|
+
return { orders: [], receiptAnalytics: emptyReceiptAnalytics(), qc_results: notRequestedRows() };
|
|
3047
3105
|
}
|
|
3048
3106
|
if (mode && mode !== "off") {
|
|
3049
3107
|
// Test Orders use global test cards: they bypass the payment gateway, create
|
|
3050
3108
|
// no transactions, and need no merchant setup or approval. `--test-order
|
|
3051
3109
|
// <mode>` is sufficient intent — no permission flags or packet policy gate.
|
|
3052
|
-
const result = await operations.runBrowser(resolved.topologies, args, runId, { captureAnalytics });
|
|
3110
|
+
const result = await operations.runBrowser(resolved.topologies, args, runId, { captureAnalytics, spec: resolved.spec });
|
|
3053
3111
|
assertions.push(...result.assertions);
|
|
3054
|
-
return {
|
|
3112
|
+
return {
|
|
3113
|
+
orders: result.orders,
|
|
3114
|
+
receiptAnalytics: result.receiptAnalytics || emptyReceiptAnalytics(),
|
|
3115
|
+
qc_results: Array.isArray(result.qc_results) ? result.qc_results : [],
|
|
3116
|
+
};
|
|
3055
3117
|
}
|
|
3056
3118
|
|
|
3057
3119
|
const orders = await operations.runLegacy({ args: { ...args, "test-order": legacyMode }, resolved, runId, assertions });
|
|
3058
3120
|
// Direct API diagnostics never qualify as canonical receipt proof.
|
|
3059
|
-
return { orders, receiptAnalytics: emptyReceiptAnalytics() };
|
|
3121
|
+
return { orders, receiptAnalytics: emptyReceiptAnalytics(), qc_results: notRequestedRows() };
|
|
3060
3122
|
}
|
|
3061
3123
|
|
|
3062
3124
|
async function maybeRunLegacyApiTestOrders({ args, resolved, runId, assertions }) {
|
|
@@ -3921,6 +3983,43 @@ function htmlIncludesRouteReference(html, expectedUrl) {
|
|
|
3921
3983
|
return html.includes(expectedUrl) || html.includes(path);
|
|
3922
3984
|
}
|
|
3923
3985
|
|
|
3986
|
+
// A data-upsell-proxy button forwards its click to the SDK action inside the
|
|
3987
|
+
// offer. With no such action, the route's URL can still sit in the page's meta
|
|
3988
|
+
// tags, so the reference match would pass a control that does nothing.
|
|
3989
|
+
function findDeadUpsellProxy(html, kind, page) {
|
|
3990
|
+
if (page.page_type !== "upsell" || kind === "next") return null;
|
|
3991
|
+
const action = kind === "accept" ? "add" : "skip";
|
|
3992
|
+
if (!new RegExp(`\\bdata-upsell-proxy\\s*=\\s*["']${action}["']`, "i").test(html)) return null;
|
|
3993
|
+
const target = new RegExp(`\\bdata-next-upsell-action\\s*=\\s*["']${action}["']`, "i");
|
|
3994
|
+
if (upsellOfferElements(html).some((offer) => target.test(offer))) return null;
|
|
3995
|
+
return `data-upsell-proxy="${action}" with no data-next-upsell-action="${action}" inside the offer to forward to`;
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
// The markup of each [data-next-upsell="offer"] element, read to its closing
|
|
3999
|
+
// tag by depth of that tag name: the proxy forwards only to actions inside it.
|
|
4000
|
+
function upsellOfferElements(html) {
|
|
4001
|
+
const offers = [];
|
|
4002
|
+
const attrRe = /\bdata-next-upsell\s*=\s*["']offer["']/gi;
|
|
4003
|
+
let attr;
|
|
4004
|
+
while ((attr = attrRe.exec(html))) {
|
|
4005
|
+
const start = html.lastIndexOf("<", attr.index);
|
|
4006
|
+
const name = /^<([a-z][\w-]*)/i.exec(html.slice(start))?.[1];
|
|
4007
|
+
if (!name) continue;
|
|
4008
|
+
const tagRe = new RegExp(`<(/?)${name}\\b[^>]*>`, "gi");
|
|
4009
|
+
tagRe.lastIndex = start;
|
|
4010
|
+
let depth = 0;
|
|
4011
|
+
let end = html.length;
|
|
4012
|
+
let match;
|
|
4013
|
+
while ((match = tagRe.exec(html))) {
|
|
4014
|
+
if (match[1]) depth -= 1;
|
|
4015
|
+
else if (!match[0].endsWith("/>")) depth += 1;
|
|
4016
|
+
if (depth === 0) { end = match.index; break; }
|
|
4017
|
+
}
|
|
4018
|
+
offers.push(html.slice(start, end));
|
|
4019
|
+
}
|
|
4020
|
+
return offers;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
3924
4023
|
function findSdkRouteAction(html, kind, page) {
|
|
3925
4024
|
if (page.page_type !== "upsell") return null;
|
|
3926
4025
|
if (kind === "accept" && /\bdata-next-upsell-action\s*=\s*["']add["']/i.test(html)) {
|
|
@@ -4001,6 +4100,7 @@ export const __qaNodeTestHooks = Object.freeze({
|
|
|
4001
4100
|
analyticsCorrectnessDisabledAssertion,
|
|
4002
4101
|
runAnalyticsOrderSequence,
|
|
4003
4102
|
maybeRunTestOrders,
|
|
4103
|
+
finalizeQaRun,
|
|
4004
4104
|
campaignOutputDir,
|
|
4005
4105
|
polishBlockedAssertions,
|
|
4006
4106
|
polishGateAssertion,
|
package/src/qa-order-bump.mjs
CHANGED
|
@@ -287,6 +287,25 @@ export function orderBumpEvidenceScript() {
|
|
|
287
287
|
};
|
|
288
288
|
|
|
289
289
|
|
|
290
|
+
// Text painted with a zero-alpha colour (`color: transparent`, or any
|
|
291
|
+
// colour function whose alpha is 0) occupies its box and draws nothing.
|
|
292
|
+
// Computed colours come back as a function: comma form, where a fourth
|
|
293
|
+
// argument is the alpha (`rgba(0, 0, 0, 0)`, `hsla(…, 0)`), or space form
|
|
294
|
+
// with the alpha after a slash (`color(srgb 0 0 0 / 0)`).
|
|
295
|
+
const transparentInk = (style) => {
|
|
296
|
+
const ink = String(style.webkitTextFillColor || style.color || "").trim().toLowerCase();
|
|
297
|
+
if (ink === "transparent") return true;
|
|
298
|
+
const fn = ink.match(/^[a-z-]+\((.*)\)$/);
|
|
299
|
+
if (!fn) return false;
|
|
300
|
+
const args = fn[1];
|
|
301
|
+
const alpha = args.includes("/")
|
|
302
|
+
? args.slice(args.lastIndexOf("/") + 1).trim()
|
|
303
|
+
: (args.split(",").length === 4 ? args.split(",")[3].trim() : null);
|
|
304
|
+
if (alpha === null) return false;
|
|
305
|
+
const value = alpha.endsWith("%") ? Number.parseFloat(alpha) / 100 : Number.parseFloat(alpha);
|
|
306
|
+
return Number.isFinite(value) && value === 0;
|
|
307
|
+
};
|
|
308
|
+
|
|
290
309
|
// The positive signals, in the order that settles which vocabulary the
|
|
291
310
|
// marker speaks. Returns null when a rendered marker carries none.
|
|
292
311
|
const checkedSignal = (marker) => {
|
|
@@ -296,7 +315,9 @@ export function orderBumpEvidenceScript() {
|
|
|
296
315
|
// rendering — never the box's.
|
|
297
316
|
const tick = pseudoTick(marker);
|
|
298
317
|
if (tick) return { signal: "pseudo", checked: tick.shown };
|
|
299
|
-
|
|
318
|
+
// A glyph tick that is always in the DOM can be shown and hidden by its
|
|
319
|
+
// colour alone, so the text is a tick only when it is painted.
|
|
320
|
+
if (/check|\u2713/.test(marker.textContent || "")) return { signal: "glyph", checked: !transparentInk(style) };
|
|
300
321
|
if (style.backgroundColor === acceptedFillColor) return { signal: "fill", checked: true };
|
|
301
322
|
if (hiddenByAMatchingRule(marker)) return { signal: "display_toggled", checked: true };
|
|
302
323
|
return null;
|