@nextcommerce/campaigns-os 1.41.2 → 1.43.2

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 (83) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +629 -0
  3. package/README.md +8 -6
  4. package/agents/claude/CLAUDE.md +5 -1
  5. package/campaign-spec/dist/types.d.ts +2 -0
  6. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  7. package/contracts/effects.v1.json +118 -25
  8. package/contracts/migration-sidecar-bundle.v0.json +9 -0
  9. package/contracts/release-ledger.json +1424 -0
  10. package/contracts/supported-surface.json +12 -11
  11. package/docs/build-packet.md +120 -8
  12. package/docs/campaigns-os-build-flow.md +3 -2
  13. package/docs/design-source-package.md +89 -15
  14. package/docs/effects.md +83 -2
  15. package/docs/local-setup.md +51 -0
  16. package/docs/migration-sidecar-bundle.md +6 -1
  17. package/docs/orientation-contract-reference.md +1 -1
  18. package/docs/progress-snapshots.md +16 -6
  19. package/docs/qa-and-test-orders.md +157 -17
  20. package/docs/release-ledger-authoring-guide.md +6 -4
  21. package/docs/runtime-readiness.md +1 -1
  22. package/docs/skills-revision.md +10 -10
  23. package/package.json +3 -2
  24. package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
  25. package/schemas/campaign-runtime-build-packet.v0.schema.json +6 -1
  26. package/schemas/campaign-spec.v4.schema.json +4 -0
  27. package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
  28. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
  29. package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
  30. package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +13 -8
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +8 -6
  34. package/skills/contribution-intake/SKILL.md +3 -3
  35. package/skills/next-campaigns-build/SKILL.md +4 -4
  36. package/skills/next-campaigns-os/SKILL.md +17 -4
  37. package/skills/next-campaigns-os/references/session-intake.md +4 -4
  38. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  39. package/skills/next-campaigns-polish/SKILL.md +3 -3
  40. package/skills/next-campaigns-qa/SKILL.md +10 -9
  41. package/skills.json +11 -11
  42. package/src/build-brief.mjs +6 -4
  43. package/src/built-script-syntax.mjs +480 -0
  44. package/src/campaigns-api-key.mjs +99 -0
  45. package/src/cli-helpers.mjs +118 -0
  46. package/src/cli.mjs +796 -6963
  47. package/src/design-source-package.mjs +1 -1
  48. package/src/design-source-publication.mjs +898 -0
  49. package/src/diagnostic.mjs +2 -1
  50. package/src/directory-lock.mjs +270 -0
  51. package/src/doctor/checks.mjs +4415 -0
  52. package/src/doctor/inspect.mjs +636 -0
  53. package/src/doctor/next-step.mjs +731 -0
  54. package/src/finding-cause.mjs +14 -10
  55. package/src/install-invocation.mjs +29 -0
  56. package/src/invocation.mjs +179 -0
  57. package/src/lifecycle.mjs +5 -4
  58. package/src/polish-node.mjs +5 -2
  59. package/src/private-template-source.mjs +1 -1
  60. package/src/progress-node.mjs +9 -37
  61. package/src/progress.mjs +5 -3
  62. package/src/proof-policy.mjs +1 -1
  63. package/src/qa-analytics-correctness.mjs +3 -0
  64. package/src/qa-binding-evidence.mjs +76 -11
  65. package/src/qa-browser.mjs +778 -77
  66. package/src/qa-build-scope.mjs +47 -0
  67. package/src/qa-node.mjs +276 -39
  68. package/src/qa-publish.mjs +4 -0
  69. package/src/qa-sidecar.mjs +2 -0
  70. package/src/qa-verdict-discovery.mjs +11 -0
  71. package/src/qa-verdict-publish.mjs +1 -0
  72. package/src/qa-verdict.mjs +8 -1
  73. package/src/readback.mjs +2 -1
  74. package/src/run-record-closeout.mjs +3 -4
  75. package/src/run-record.mjs +4 -0
  76. package/src/sidecar-bundle.mjs +21 -0
  77. package/src/source-html-intake.mjs +1 -1
  78. package/src/source-html-manifest.mjs +9 -2
  79. package/src/spec-source-identity.mjs +44 -0
  80. package/src/stage-ledger.mjs +32 -1
  81. package/src/target-lock.mjs +54 -0
  82. package/src/template-brand-contract.mjs +17 -1
  83. package/src/tooling-setup.mjs +160 -0
@@ -1,6 +1,7 @@
1
1
  import { parse as parseHtml } from 'parse5';
2
2
  import { parse as parseJs } from 'acorn';
3
3
  import { createPageSourceLoader, resolveCommercialApiKey } from './qa-commercial-parity.mjs';
4
+ import { HTML_NAMESPACE, baseInEffect, documentBases, frozenBaseUrl, parseFailureDiagnostic, scriptKind } from './built-script-syntax.mjs';
4
5
 
5
6
  export const BINDING_SCHEMA = 'campaigns-os-page-binding/v0';
6
7
  export const BINDING_LIMITS = Object.freeze({ scripts_per_page: 6, scripts_per_run: 24, script_bytes: 262144, timeout_ms: 5000 });
@@ -55,12 +56,15 @@ function literalObject(node) {
55
56
  if (node?.type === 'ObjectExpression') return node.properties.every(p => p.type === 'Property' && !p.computed && p.kind === 'init' && !p.method && !p.shorthand && literalObject(p.value));
56
57
  return false;
57
58
  }
58
- function declarations(text) {
59
+ function declarations(text, { module = false } = {}) {
59
60
  // Only whole, unconditional literal assignments are accepted. No evaluation,
60
61
  // constant propagation, getters, spreads, callbacks, aliases, or branch guesses.
62
+ // A script that does not parse is not dynamic: the browser throws on it and
63
+ // nothing in it runs. It is its own state, reported with its position (#480).
61
64
  let ast;
62
- try { ast = parseJs(text, { ecmaVersion: 2022, sourceType: 'script' }); }
63
- catch { return { values: [], dynamic: true }; }
65
+ try { ast = parseJs(text, { ecmaVersion: 'latest', sourceType: module ? 'module' : 'script', locations: true }); }
66
+ // The message is a fixed category: acorn echoes source text into some.
67
+ catch (error) { return { values: [], dynamic: false, unparsable: parseFailureDiagnostic(error) }; }
64
68
  const values = [];
65
69
  let dynamic = false;
66
70
  for (const statement of ast.body) {
@@ -78,7 +82,23 @@ function declarations(text) {
78
82
  return { values, dynamic };
79
83
  }
80
84
 
81
- export async function observeBinding({ source, page, expected, scriptLoader }) {
85
+ // A path for a script, for findings: never a full URL, query or fragment. A
86
+ // script served from another origin keeps its host, so same-named files on two
87
+ // CDNs stay distinguishable.
88
+ function scriptPath(src, pageUrl) {
89
+ try {
90
+ const url = new URL(src, pageUrl);
91
+ let pageOrigin = null;
92
+ try { pageOrigin = new URL(pageUrl).origin; } catch {}
93
+ return pageOrigin && url.origin !== pageOrigin ? `${url.host}${url.pathname}` : url.pathname;
94
+ } catch { return null; }
95
+ }
96
+
97
+ // `parseFailures`, when given, receives one record per page script that does
98
+ // not parse ({ source_kind, script, line, column, message }). It is kept off
99
+ // the page-binding evidence, whose shape is a closed contract; the caller
100
+ // reports it as its own assertion.
101
+ export async function observeBinding({ source, page, expected, scriptLoader, parseFailures = null }) {
82
102
  const kinds = new Set();
83
103
  const result = (outcome, reason) => ({ schema_version: BINDING_SCHEMA, observation: 'static_declaration',
84
104
  outcome, reason, source_kinds: [...kinds].sort(), identity: 'not_verified' });
@@ -94,33 +114,68 @@ export async function observeBinding({ source, page, expected, scriptLoader }) {
94
114
  const values = [];
95
115
  const scripts = [];
96
116
  let dynamic = false;
117
+ let bases = [];
97
118
  const walk = node => {
98
119
  const attrs = Object.fromEntries((node.attrs || []).map(a => [a.name, a.value]));
99
120
  if (node.tagName === 'base' || Object.entries(attrs).some(([name, value]) => /^on/i.test(name) || /^\s*javascript:/i.test(value))) dynamic = true;
100
121
  if (node.tagName === 'meta' && attrs.name === 'next-api-key') { values.push(attrs.content ?? ''); kinds.add('meta'); }
101
- if (node.tagName === 'script') scripts.push({ attrs, text: (node.childNodes || []).map(n => n.value || '').join('') });
122
+ // Each script keeps the <base href> in effect when the parser prepares it
123
+ // at its end tag (see baseInEffect; #502).
124
+ // Only an HTML-namespace <script> loads `src`. An SVG script runs from
125
+ // href / xlink:href or its inline text, which this static read does not
126
+ // model: it is not fetched and leaves the binding dynamic.
127
+ if (node.tagName === 'script' && node.namespaceURI !== HTML_NAMESPACE) dynamic = true;
128
+ else if (node.tagName === 'script') scripts.push({ attrs, base: baseInEffect(bases, node), text: (node.childNodes || []).map(n => n.value || '').join('') });
102
129
  // parse5 keeps template content separate; it is inert, as is noscript at boot.
103
130
  if (node.tagName !== 'noscript') for (const child of node.childNodes || []) walk(child);
104
131
  };
105
- try { walk(parseHtml(source.html)); } catch { return result('unknown', 'dynamic_unresolved'); }
132
+ try {
133
+ const document = parseHtml(source.html, { sourceCodeLocationInfo: true });
134
+ bases = documentBases(document);
135
+ walk(document);
136
+ } catch { return result('unknown', 'dynamic_unresolved'); }
137
+ // A script src resolves against the base in effect when the script is
138
+ // prepared: the frozen base URL of that <base href>, where a
139
+ // data:, javascript: or unparsable base falls back to the page URL (HTML
140
+ // "set the frozen base URL"), else the page URL. The loader still scopes
141
+ // the result to the page origin, so a cross-origin base leaves those
142
+ // scripts unavailable.
143
+ const scriptRef = (src, base) => { try { return new URL(src, frozenBaseUrl(base, pageUrl)).href; } catch { return null; } };
106
144
  let count = 0, unavailable = false;
107
145
  for (const script of scripts) {
108
146
  const { attrs } = script;
147
+ // Classified as the browser does (type trimmed of ASCII whitespace,
148
+ // case-insensitive). A module script ignores nomodule; a classic nomodule
149
+ // script is never fetched or run by a module-capable browser, so it cannot
150
+ // fail on load there: not fetched, not parsed, still not static.
151
+ const scriptType = scriptKind(attrs);
152
+ const { nomodule: _nomodule, ...withoutNomodule } = attrs;
153
+ const classicNomodule = scriptType === null && 'nomodule' in attrs && scriptKind(withoutNomodule) === 'classic';
109
154
  // Data-block types (e.g. JSON-LD) are not fetched and consume no config-request budget.
110
- if (attrs.type && !['text/javascript', 'application/javascript', 'module'].includes(attrs.type.toLowerCase())) continue;
111
- if (attrs.src && SDK.test(attrs.src)) continue;
155
+ if (scriptType === null && !classicNomodule) continue;
156
+ // Matched on the URL as the parser reads it (tab and newline removed,
157
+ // host case-folded), against the base in effect for this script.
158
+ if (attrs.src && SDK.test(scriptRef(attrs.src, script.base) ?? attrs.src)) continue;
159
+ if (classicNomodule) { dynamic = true; continue; }
112
160
  let text = script.text;
113
161
  let kind = 'inline';
114
162
  if (attrs.src) {
115
163
  kind = 'config_script';
116
164
  if (++count > BINDING_LIMITS.scripts_per_page) { unavailable = true; continue; }
117
- const loaded = await scriptLoader(attrs.src, pageUrl);
165
+ const ref = script.base === null ? attrs.src : scriptRef(attrs.src, script.base);
166
+ const loaded = ref ? await scriptLoader(ref, pageUrl) : { ok: false };
118
167
  if (!loaded.ok) { unavailable = true; continue; }
119
168
  text = loaded.html;
120
169
  }
121
- const found = declarations(text);
170
+ const found = declarations(text, { module: scriptType === 'module' });
171
+ if (found.unparsable) {
172
+ // The declarations in an unparsable script are unavailable, not dynamic.
173
+ unavailable = true;
174
+ if (Array.isArray(parseFailures)) parseFailures.push({ source_kind: kind, script: attrs.src ? scriptPath(scriptRef(attrs.src, script.base) ?? attrs.src, pageUrl) : null, ...found.unparsable });
175
+ continue;
176
+ }
122
177
  if (found.values.length) { kinds.add(kind); values.push(...found.values); }
123
- if (found.dynamic || 'async' in attrs || 'nomodule' in attrs || attrs.type === 'module') dynamic = true;
178
+ if (found.dynamic || 'async' in attrs || scriptType === 'module') dynamic = true;
124
179
  }
125
180
  if (new Set(values).size > 1) return result('unknown', 'conflicting_declarations');
126
181
  if (expected?.conflict) return result('unknown', 'conflicting_expected');
@@ -138,3 +193,13 @@ export function bindingAssertion(page, evidence) {
138
193
  ...(evidence.outcome === 'match' ? {} : { severity: evidence.outcome === 'mismatch' ? 'blocker' : 'warn' }),
139
194
  expected: 'expected credential declaration', actual: evidence.outcome, evidence };
140
195
  }
196
+
197
+ // A page script that does not parse throws a SyntaxError on every load (#480).
198
+ // One blocker per page, naming each script and the parse position.
199
+ export function scriptParseAssertion(page, failures) {
200
+ if (!Array.isArray(failures) || failures.length === 0) return null;
201
+ const where = failures.map(f => `${f.script || 'inline script'}:${f.line}:${f.column}`).join(', ');
202
+ return { id: `script-parse:${page.page_id}`, family: 'api-metadata', page: page.page_id, status: 'fail', severity: 'blocker',
203
+ expected: 'every page script parses', actual: `unparsable: ${where}`,
204
+ evidence: { observation: 'static_parse', scripts: failures.map(f => ({ source_kind: f.source_kind, script: f.script, line: f.line, column: f.column, message: f.message })) } };
205
+ }