@nextcommerce/campaigns-os 1.47.0 → 1.48.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +30 -3
  3. package/agents/claude/CLAUDE.md +2 -0
  4. package/agents/codex/AGENTS.md +1 -0
  5. package/agents/copilot/copilot-instructions.md +1 -0
  6. package/agents/cursor/campaigns-os.mdc +1 -1
  7. package/compatibility.json +1 -1
  8. package/contracts/commerce-surface-catalog.json +17 -17
  9. package/contracts/release-ledger.json +465 -0
  10. package/contracts/supported-surface.json +1 -1
  11. package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
  12. package/docs/build-packet.md +1 -1
  13. package/docs/campaign-build-brief.md +25 -28
  14. package/docs/local-setup.md +7 -2
  15. package/docs/orientation-contract-reference.md +1 -1
  16. package/docs/qa-and-test-orders.md +49 -2
  17. package/docs/runtime-readiness.md +1 -1
  18. package/docs/skills-revision.md +10 -10
  19. package/package.json +1 -1
  20. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  21. package/skills/campaign-readback-classification/SKILL.md +3 -3
  22. package/skills/campaign-run-evidence/SKILL.md +9 -4
  23. package/skills/contribution-intake/SKILL.md +3 -3
  24. package/skills/next-campaigns-build/SKILL.md +4 -3
  25. package/skills/next-campaigns-os/SKILL.md +3 -3
  26. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  27. package/skills/next-campaigns-polish/SKILL.md +4 -4
  28. package/skills/next-campaigns-qa/SKILL.md +3 -3
  29. package/skills.json +10 -10
  30. package/src/cli.mjs +6 -3
  31. package/src/content-residue.mjs +18 -90
  32. package/src/doctor/checks.mjs +21 -29
  33. package/src/doctor/inspect.mjs +13 -3
  34. package/src/doctor/next-step.mjs +4 -0
  35. package/src/gate-actions.mjs +8 -0
  36. package/src/local-preview-policy.mjs +92 -0
  37. package/src/page-kit-sdk-version.mjs +8 -1
  38. package/src/polish-node.mjs +26 -2
  39. package/src/progress-node.mjs +5 -1
  40. package/src/qa-binding-evidence.mjs +1 -1
  41. package/src/qa-browser.mjs +62 -13
  42. package/src/qa-node.mjs +33 -4
  43. package/src/readback.mjs +19 -10
  44. package/src/source-prep.mjs +1 -1
package/src/readback.mjs CHANGED
@@ -1065,8 +1065,11 @@ function artifactData(views, key) {
1065
1065
  }
1066
1066
 
1067
1067
  /**
1068
- * Bucket the QA verdict's assertions by recorded status.
1068
+ * Bucket the QA verdict's assertions by recorded status: `{ fail, pass,
1069
+ * skipped, review, other }`.
1069
1070
  *
1071
+ * `review` collects the verdict's `warn` and `manual_review` statuses: evidence
1072
+ * that still needs reading, which the disposition counts as an exception.
1070
1073
  * `other` collects any status this readback does not project (it renders those
1071
1074
  * rows as written rather than reclassifying them). Non-object entries are
1072
1075
  * dropped: an assertion the readback cannot address by status is not an
@@ -1075,13 +1078,15 @@ function artifactData(views, key) {
1075
1078
  * branch.
1076
1079
  */
1077
1080
  export function partitionAssertions(views) {
1078
- const buckets = { fail: [], pass: [], skipped: [], other: [] };
1081
+ const buckets = { fail: [], pass: [], skipped: [], review: [], other: [] };
1079
1082
  const verdict = artifactData(views, "qa_verdict");
1080
1083
  if (verdict === null) return buckets;
1081
1084
  for (const assertion of verdict.assertions ?? []) {
1082
1085
  if (!isPlainObject(assertion)) continue;
1083
1086
  const status = assertion.status;
1084
- const bucket = status === "fail" || status === "pass" || status === "skipped" ? status : "other";
1087
+ const bucket = status === "fail" || status === "pass" || status === "skipped"
1088
+ ? status
1089
+ : status === "warn" || status === "manual_review" ? "review" : "other";
1085
1090
  buckets[bucket].push(assertion);
1086
1091
  }
1087
1092
  return buckets;
@@ -1414,15 +1419,17 @@ function renderVerdict(views, lines) {
1414
1419
  lines.push(
1415
1420
  `QA VERDICT [QA verdict; disposition: ${verdict.disposition} — Campaigns OS is the verdict authority]`,
1416
1421
  );
1417
- const { fail: failed, pass: passed, skipped, other } = partitionAssertions(views);
1422
+ const { fail: failed, pass: passed, skipped, review, other } = partitionAssertions(views);
1418
1423
  let countLine = ` assertions: ${failed.length} fail, ${passed.length} pass, ${skipped.length} skipped`;
1424
+ if (review.length) countLine += `, ${review.length} warn or manual review`;
1419
1425
  if (other.length) countLine += `, ${other.length} unrecognized status`;
1420
1426
  lines.push(countLine);
1421
- for (const assertion of failed) {
1422
- const severity = assertion.severity;
1423
- const severityText = severity ? `, severity ${severity}` : "";
1427
+ // fail, warn and manual_review rows print what Campaigns OS recorded: the
1428
+ // row's actual value and any evidence problems.
1429
+ const renderRecordedRow = (assertion) => {
1430
+ const severityText = assertion.severity ? `, severity ${assertion.severity}` : "";
1424
1431
  lines.push(
1425
- ` fail ${recorded(assertion.id, "(no id)")} ` +
1432
+ ` ${assertion.status} ${recorded(assertion.id, "(no id)")} ` +
1426
1433
  `(family ${recorded(assertion.family, "(no family)")}${severityText})`,
1427
1434
  );
1428
1435
  const actual = assertion.actual;
@@ -1433,7 +1440,9 @@ function renderVerdict(views, lines) {
1433
1440
  lines.push(` recorded problems (${problems.length}):`);
1434
1441
  for (const problem of problems) lines.push(` - ${problem}`);
1435
1442
  }
1436
- }
1443
+ };
1444
+ for (const assertion of failed) renderRecordedRow(assertion);
1445
+ for (const assertion of review) renderRecordedRow(assertion);
1437
1446
  for (const assertion of passed) {
1438
1447
  const family = assertion.family || assertion.id || "(no family)";
1439
1448
  lines.push(` pass ${recorded(assertion.id, "(no id)")} (family ${family})`);
@@ -1442,7 +1451,7 @@ function renderVerdict(views, lines) {
1442
1451
  lines.push(
1443
1452
  ` unrecognized status ${JSON.stringify(assertion.status ?? null)} ${recorded(assertion.id, "(no id)")} ` +
1444
1453
  `(family ${assertion.family || "(no family)"}) — shown as written; this readback ` +
1445
- "projects fail, pass, and skipped statuses",
1454
+ "projects fail, pass, skipped, warn and manual_review statuses",
1446
1455
  );
1447
1456
  }
1448
1457
 
@@ -201,7 +201,7 @@ function describeFinding(code, pages, { wrapperPolicy }) {
201
201
  const policyNote = wrapperPolicy === "preserve_document_wrappers"
202
202
  ? " The adapter contract records wrapper_policy \"preserve_document_wrappers\", so this is reported without blocking."
203
203
  : "";
204
- return `Mapped source HTML is a full browser document, not page-kit-ready source: ${listed}${more}. Strip <!doctype>, <html>, <head>, and <body> so the campaign layout can wrap the page, or record wrapper_policy "preserve_document_wrappers" as an explicit adapter decision.${policyNote} See ${docs}.`;
204
+ return `Mapped source HTML is a full browser document, not page-kit-ready source: ${listed}${more}. Strip <!doctype>, <html>, <head>, and <body> so the campaign layout can wrap the page, or, for a standalone page meant to stay whole, record wrapper_policy "preserve_document_wrappers" as an explicit adapter decision: re-run start or prepare-build with --wrapper-policy preserve_document_wrappers, or set "wrapper_policy" in the source-html manifest.${policyNote} See ${docs}.`;
205
205
  }
206
206
  if (code === SOURCE_PREP_FRONTMATTER_RESIDUE) {
207
207
  const listed = sample.map((page) => {