@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/qc-accept.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { isNamedHuman, validateWaiverAttribution } from "./checkpoint-waiver.mjs
|
|
|
14
14
|
import { DOCTOR_SIDECAR_SCHEMA } from "./doctor-sidecar.mjs";
|
|
15
15
|
import { cmd } from "./install-invocation.mjs";
|
|
16
16
|
import { canonicalJson } from "./polish-capture.mjs";
|
|
17
|
+
import { QC_CHECK_REGISTRY } from "./qc-check-registry.mjs";
|
|
17
18
|
import { QC_LEGS, QC_REASON, fingerprint12, qcResultRef } from "./qc-results.mjs";
|
|
18
19
|
import { shellToken } from "./shell-token.mjs";
|
|
19
20
|
|
|
@@ -22,7 +23,18 @@ export const QC_ACCEPT_SCOPE = "qc_accept";
|
|
|
22
23
|
export const QC_ACCEPT_RECORDER = "campaigns-os checkpoint accept";
|
|
23
24
|
export const QC_ACCEPT_STATUSES = Object.freeze(["active", "lapsed", "orphaned", "expired", "inert"]);
|
|
24
25
|
export const QC_DISPOSITION = Object.freeze({ OPEN: "open", OPERATOR_ACCEPTED: "operator_accepted" });
|
|
25
|
-
|
|
26
|
+
// The evidence an accept's measurement came from: the doctor sidecar, the
|
|
27
|
+
// full QA verdict, or, on the Polish leg, the package-owned record the
|
|
28
|
+
// accepted check is read from (QC_CHECK_REGISTRY `record`).
|
|
29
|
+
const QC_LEG_SOURCES = Object.freeze({ doctor: "doctor_sidecar", qa: "qa_full_verdict" });
|
|
30
|
+
export const QC_POLISH_RECORD_SOURCES = Object.freeze({ media_weight: "polish_media_weight", readability: "polish_readability" });
|
|
31
|
+
|
|
32
|
+
// Null for a Polish check with no record, or a leg with no source.
|
|
33
|
+
export function measuredSourceFor(leg, check) {
|
|
34
|
+
if (leg !== "polish") return Object.hasOwn(QC_LEG_SOURCES, leg) ? QC_LEG_SOURCES[leg] : null;
|
|
35
|
+
const entry = Object.hasOwn(QC_CHECK_REGISTRY, check) ? QC_CHECK_REGISTRY[check] : null;
|
|
36
|
+
return entry?.leg === "polish" && Object.hasOwn(QC_POLISH_RECORD_SOURCES, entry.record) ? QC_POLISH_RECORD_SOURCES[entry.record] : null;
|
|
37
|
+
}
|
|
26
38
|
|
|
27
39
|
// The command's closed refusal list.
|
|
28
40
|
export const QC_ACCEPT_REFUSALS = Object.freeze({
|
|
@@ -100,7 +112,7 @@ export function createQcAccept(result, { measuredAt, attribution }) {
|
|
|
100
112
|
state_fingerprint: result.state_fingerprint,
|
|
101
113
|
result_at_accept: "warning",
|
|
102
114
|
measured_at: measuredAt,
|
|
103
|
-
measured_source:
|
|
115
|
+
measured_source: measuredSourceFor(result.leg, result.check),
|
|
104
116
|
...attribution,
|
|
105
117
|
recorded_by: QC_ACCEPT_RECORDER,
|
|
106
118
|
};
|
|
@@ -119,7 +131,8 @@ function malformed(record) {
|
|
|
119
131
|
&& FINGERPRINT.test(record.state_fingerprint || "")
|
|
120
132
|
&& record.result_at_accept === "warning"
|
|
121
133
|
&& validTime(record.measured_at)
|
|
122
|
-
&& record.measured_source
|
|
134
|
+
&& isNonEmptyString(record.measured_source)
|
|
135
|
+
&& record.measured_source === measuredSourceFor(record.leg, record.check)
|
|
123
136
|
&& typeof record.reason === "string"
|
|
124
137
|
&& typeof record.accepted_by === "string"
|
|
125
138
|
&& typeof record.accepted_at === "string"
|
|
@@ -291,7 +304,29 @@ export function planQcAccepts({ refs, results, sidecarPath, now, attribution })
|
|
|
291
304
|
const LEG_ORDER = Object.freeze({ doctor: 0, polish: 1, qa: 2 });
|
|
292
305
|
const compareText = (a, b) => String(a ?? "").localeCompare(String(b ?? ""));
|
|
293
306
|
|
|
307
|
+
// A readability colour pair reads as its roles, colours, ratio and
|
|
308
|
+
// requirement: `<roles>: <fg8> on <bg8>, <ratio>:1, needs <required>:1
|
|
309
|
+
// (<size_class> text)`, from the row's observation (the lowest ratio of the
|
|
310
|
+
// rows given). Null for any other row.
|
|
311
|
+
function readabilityPairSummary(rows) {
|
|
312
|
+
const pairs = rows.map((row) => (row?.check === "readability.contrast" && String(row.subject?.key ?? "").startsWith("pair:") && isPlainObject(row.observation) ? row.observation : null));
|
|
313
|
+
if (!pairs.length || pairs.some((observation) => observation === null)) return null;
|
|
314
|
+
const roles = [...new Set(pairs.flatMap((observation) => (Array.isArray(observation.roles) ? observation.roles : [])))].sort(compareText);
|
|
315
|
+
const lowest = pairs.reduce((low, observation) => (observation.ratio < low.ratio ? observation : low));
|
|
316
|
+
return `${roles.join(", ")}: ${lowest.fg8} on ${lowest.bg8}, ${lowest.display_ratio}:1, needs ${lowest.required}:1 (${lowest.size_class} text)`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The crop path of a readability review row: its first member's crop.
|
|
320
|
+
// Undefined for a review row with no crop, and for any other row.
|
|
321
|
+
function readabilityReviewCrop(row) {
|
|
322
|
+
if (row?.check !== "readability.contrast" || row.result !== "review") return undefined;
|
|
323
|
+
const members = Array.isArray(row.observation?.members) ? row.observation.members : [];
|
|
324
|
+
const ref = members.find((member) => isPlainObject(member?.crop_ref))?.crop_ref;
|
|
325
|
+
return typeof ref?.path === "string" ? ref.path : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
294
328
|
function handoffEntry(row) {
|
|
329
|
+
const crop = readabilityReviewCrop(row);
|
|
295
330
|
return {
|
|
296
331
|
result_ref: qcResultRef(row),
|
|
297
332
|
check: row.check,
|
|
@@ -303,7 +338,8 @@ function handoffEntry(row) {
|
|
|
303
338
|
reason_code: row.reason_code,
|
|
304
339
|
accept_eligible: row.accept_eligible === true,
|
|
305
340
|
members: Array.isArray(row.members) ? row.members : [],
|
|
306
|
-
summary: `${row.check} ${row.result}${row.reason_code ? ` (${row.reason_code})` : ""} on ${row.subject?.page ?? "(no page)"}${row.subject?.key == null ? "" : `: ${row.subject.key}`}`,
|
|
341
|
+
summary: readabilityPairSummary([row]) ?? `${row.check} ${row.result}${row.reason_code ? ` (${row.reason_code})` : ""} on ${row.subject?.page ?? "(no page)"}${row.subject?.key == null ? "" : `: ${row.subject.key}`}`,
|
|
342
|
+
...(crop === undefined ? {} : { crop }),
|
|
307
343
|
};
|
|
308
344
|
}
|
|
309
345
|
|
|
@@ -315,7 +351,7 @@ const byCheckKeyPage = (a, b) => compareText(a.check, b.check) || compareText(a.
|
|
|
315
351
|
// first page's fields, `pages`, and `results`, which keeps every page's result
|
|
316
352
|
// with its own ref, accept eligibility and members, so every member stays
|
|
317
353
|
// visible. The entry is accept-eligible only when every result in it is.
|
|
318
|
-
function groupOpenWarnings(entries) {
|
|
354
|
+
function groupOpenWarnings(entries, rows = []) {
|
|
319
355
|
const groups = new Map();
|
|
320
356
|
for (const entry of [...entries].sort(byCheckKeyPage)) {
|
|
321
357
|
const key = JSON.stringify([entry.check, entry.key]);
|
|
@@ -330,7 +366,7 @@ function groupOpenWarnings(entries) {
|
|
|
330
366
|
pages,
|
|
331
367
|
accept_eligible: results.every((entry) => entry.accept_eligible),
|
|
332
368
|
summary: results.length > 1
|
|
333
|
-
? `${lead.check} ${lead.result}${lead.reason_code ? ` (${lead.reason_code})` : ""} on ${pages.join(", ")}${lead.key == null ? "" : `: ${lead.key}`}`
|
|
369
|
+
? readabilityPairSummary(results.map((entry) => rows.find((row) => qcResultRef(row) === entry.result_ref))) ?? `${lead.check} ${lead.result}${lead.reason_code ? ` (${lead.reason_code})` : ""} on ${pages.join(", ")}${lead.key == null ? "" : `: ${lead.key}`}`
|
|
334
370
|
: lead.summary,
|
|
335
371
|
results,
|
|
336
372
|
};
|
|
@@ -401,7 +437,7 @@ export function buildQcHandoff({ results, coverage = [], accepts, now = new Date
|
|
|
401
437
|
const coverageEntries = [...grouped.values(), ...coverage]
|
|
402
438
|
.map((entry) => ({ ...entry, pages: [...entry.pages].sort(compareText) }))
|
|
403
439
|
.sort((a, b) => (LEG_ORDER[a.leg] ?? 9) - (LEG_ORDER[b.leg] ?? 9) || compareText(a.check, b.check) || compareText(a.reason_code, b.reason_code) || compareText(a.result, b.result));
|
|
404
|
-
const openGroups = groupOpenWarnings(open);
|
|
440
|
+
const openGroups = groupOpenWarnings(open, results);
|
|
405
441
|
review.sort(byCheckKeyPage);
|
|
406
442
|
lapsed.sort(byCheckKeyPage);
|
|
407
443
|
accepted.sort(byCheckKeyPage);
|
|
@@ -435,7 +471,7 @@ export function qcHandoffTextLines(handoff) {
|
|
|
435
471
|
section("Open warnings", handoff.open, (entry) => (entry.results?.length > 1
|
|
436
472
|
? [entry.summary, ...entry.results.map((result) => ` ${openLine(result)}`)]
|
|
437
473
|
: openLine(entry)));
|
|
438
|
-
section("Review", handoff.review, (entry) => `${entry.result_ref} ${entry.summary}${members(entry)}`);
|
|
474
|
+
section("Review", handoff.review, (entry) => `${entry.result_ref} ${entry.summary}${entry.crop ? ` crop: ${entry.crop}` : ""}${members(entry)}`);
|
|
439
475
|
section("Lapsed accepts", handoff.lapsed, (entry) => `${entry.result_ref} ${entry.summary}; accepted by ${entry.accepted_by} at ${entry.accepted_at}; lapsed: ${entry.why}`);
|
|
440
476
|
section("Coverage", handoff.coverage, (entry) => `${entry.leg} ${entry.check}: ${entry.result} (${entry.reason_code})${entry.count ? ` x${entry.count}` : ""}${entry.pages.length ? ` on ${entry.pages.join(", ")}` : ""}`);
|
|
441
477
|
section("Accepted", handoff.accepted, (entry) => `${entry.result_ref} accepted by ${entry.accepted_by} at ${entry.accepted_at}: ${entry.reason}${members(entry)}`);
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// The one map from a QC check id to the check module that re-derives its
|
|
2
|
-
// results. The QC readers
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// results. The QC readers (src/qc-results.mjs) reach every check's rules
|
|
3
|
+
// through here. A QA check lands by adding its module; a Polish check reads
|
|
4
|
+
// one package-owned visual_review record, named by `record`, and each record
|
|
5
|
+
// has its own reader in the Polish dispatch (POLISH_RECORDS), so a check for
|
|
6
|
+
// a new record also adds that reader.
|
|
5
7
|
//
|
|
6
8
|
// Each entry names a module specifier, imported lazily and relative to this
|
|
7
9
|
// file, and the export the reader calls:
|
|
8
10
|
// - QA checks: `rederiveQcResult(observation)` returns a Derived result
|
|
9
11
|
// ({check, subject, result, reason_code, members, accept_eligible, coverage,
|
|
10
12
|
// state}) or null when the observation cannot be re-derived.
|
|
11
|
-
// - Polish checks:
|
|
12
|
-
// evaluate(cell, thresholds) => Derived[]}
|
|
13
|
+
// - Polish checks: the record's rules, {thresholds, vocabulary,
|
|
14
|
+
// evaluate(cell, thresholds) => Derived[]} (MEDIA_WEIGHT_QC_RULES for
|
|
15
|
+
// media_weight; READABILITY_QC_RULES for readability, which also carries
|
|
16
|
+
// the record's fixed producer values, limits and route set).
|
|
13
17
|
// - Doctor checks recompute from the built HTML on every read and are called
|
|
14
18
|
// by doctor itself; their entries name the evaluator for completeness.
|
|
15
19
|
//
|
|
@@ -23,11 +27,13 @@ export const QC_CHECK_REGISTRY = Object.freeze({
|
|
|
23
27
|
// Content params
|
|
24
28
|
content_param: Object.freeze({ leg: "qa", unit: "1.2", module: "./qa-content-params.mjs", rederive: "rederiveQcResult" }),
|
|
25
29
|
// Media weight and oversizing
|
|
26
|
-
"media.weight": Object.freeze({ leg: "polish", unit: "1.3", module: "./polish-media-weight.mjs", rederive: "MEDIA_WEIGHT_QC_RULES" }),
|
|
27
|
-
"media.oversize": Object.freeze({ leg: "polish", unit: "1.3", module: "./polish-media-weight.mjs", rederive: "MEDIA_WEIGHT_QC_RULES" }),
|
|
30
|
+
"media.weight": Object.freeze({ leg: "polish", unit: "1.3", module: "./polish-media-weight.mjs", rederive: "MEDIA_WEIGHT_QC_RULES", record: "media_weight" }),
|
|
31
|
+
"media.oversize": Object.freeze({ leg: "polish", unit: "1.3", module: "./polish-media-weight.mjs", rederive: "MEDIA_WEIGHT_QC_RULES", record: "media_weight" }),
|
|
28
32
|
// Policy links
|
|
29
33
|
"policy.presence": Object.freeze({ leg: "qa", unit: "1.4", module: "./qa-policy-links.mjs", rederive: "rederiveQcResult" }),
|
|
30
34
|
"policy.availability": Object.freeze({ leg: "qa", unit: "1.4", module: "./qa-policy-links.mjs", rederive: "rederiveQcResult" }),
|
|
35
|
+
// Text readability
|
|
36
|
+
"readability.contrast": Object.freeze({ leg: "polish", unit: "2.4", module: "./polish-readability.mjs", rederive: "READABILITY_QC_RULES", record: "readability" }),
|
|
31
37
|
// Cart placeholders
|
|
32
38
|
cart_placeholders: Object.freeze({ leg: "doctor", unit: "1.5", module: "./cart-placeholders.mjs", rederive: "evaluateCartPlaceholders" }),
|
|
33
39
|
// Built-output smoke checks
|
|
@@ -35,6 +41,8 @@ export const QC_CHECK_REGISTRY = Object.freeze({
|
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
export const qcChecksForLeg = (leg) => Object.keys(QC_CHECK_REGISTRY).filter((check) => QC_CHECK_REGISTRY[check].leg === leg);
|
|
44
|
+
// The Polish checks read from one visual_review record.
|
|
45
|
+
export const qcChecksForRecord = (record) => Object.keys(QC_CHECK_REGISTRY).filter((check) => QC_CHECK_REGISTRY[check].leg === "polish" && QC_CHECK_REGISTRY[check].record === record);
|
|
38
46
|
|
|
39
47
|
// A module "does not exist yet" only when Node reports ERR_MODULE_NOT_FOUND
|
|
40
48
|
// for that exact specifier. A missing transitive import names another URL,
|