@nextcommerce/campaigns-os 1.60.0 → 1.62.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 (161) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +244 -0
  3. package/README.md +86 -438
  4. package/agents/claude/CLAUDE.md +20 -0
  5. package/agents/codex/AGENTS.md +19 -0
  6. package/agents/copilot/copilot-instructions.md +19 -0
  7. package/agents/cursor/campaigns-os.mdc +21 -0
  8. package/campaign-spec/README.md +7 -0
  9. package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
  10. package/campaign-spec/dist/analytics-vendors.js +44 -0
  11. package/campaign-spec/dist/checkout-flow.d.ts +18 -0
  12. package/campaign-spec/dist/checkout-flow.js +28 -0
  13. package/campaign-spec/dist/index.d.ts +4 -2
  14. package/campaign-spec/dist/index.js +5 -1
  15. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
  16. package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
  17. package/campaign-spec/dist/rules/campaign-currencies.d.ts +29 -0
  18. package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
  19. package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
  20. package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
  21. package/campaign-spec/dist/rules/index.d.ts +2 -0
  22. package/campaign-spec/dist/rules/index.js +6 -0
  23. package/campaign-spec/dist/types.d.ts +43 -9
  24. package/compatibility.json +1 -1
  25. package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
  26. package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
  27. package/contracts/commerce-surface-catalog.json +68 -61
  28. package/contracts/effects.v1.json +549 -165
  29. package/contracts/fixtures/runtime-recipe/accept/current.json +3 -0
  30. package/contracts/fixtures/runtime-recipe/accept/minimal.json +3 -0
  31. package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
  32. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +3 -0
  33. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +3 -0
  34. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +3 -0
  35. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +3 -0
  36. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +3 -0
  37. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +3 -0
  38. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +3 -0
  39. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +3 -0
  40. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +3 -0
  41. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +3 -0
  42. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +3 -0
  43. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +3 -0
  44. package/contracts/release-ledger.json +1767 -0
  45. package/contracts/runtime-recipe.campaigns-os-node-v1.json +3 -0
  46. package/contracts/supported-surface.json +10 -10
  47. package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
  48. package/docs/access-model.md +20 -0
  49. package/docs/brand-theme-bridge.md +62 -3
  50. package/docs/build-packet.md +456 -36
  51. package/docs/campaign-build-brief.md +16 -0
  52. package/docs/campaign-standardization-report.md +4 -4
  53. package/docs/campaigns-os-build-flow.md +112 -2
  54. package/docs/campaignspec-authoring-examples.md +141 -0
  55. package/docs/design-source-package.md +64 -8
  56. package/docs/effects.md +17 -9
  57. package/docs/entry-points.md +188 -0
  58. package/docs/local-setup.md +1 -1
  59. package/docs/orientation-contract-reference.md +1 -1
  60. package/docs/polish-evidence.md +42 -2
  61. package/docs/progress-snapshots.md +7 -4
  62. package/docs/qa-and-test-orders.md +197 -3
  63. package/docs/quickstart.md +438 -0
  64. package/docs/readback.md +154 -6
  65. package/docs/runtime-readiness.md +6 -3
  66. package/docs/sdk-storage-compatibility.md +4 -2
  67. package/docs/skills-revision.md +10 -10
  68. package/docs/source-adapters.md +213 -0
  69. package/package.json +10 -3
  70. package/schemas/campaign-design-source-package.v0.schema.json +8 -0
  71. package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
  72. package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
  73. package/schemas/campaign-spec.v4.schema.json +19 -3
  74. package/schemas/campaign-spec.v5.schema.json +19 -3
  75. package/schemas/campaigns-os-readback.v2.schema.json +238 -1
  76. package/schemas/source-html-manifest.v0.schema.json +3 -0
  77. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  78. package/skills/campaign-readback-classification/SKILL.md +3 -3
  79. package/skills/campaign-run-evidence/SKILL.md +3 -3
  80. package/skills/contribution-intake/SKILL.md +3 -3
  81. package/skills/next-campaigns-build/SKILL.md +5 -4
  82. package/skills/next-campaigns-os/SKILL.md +7 -4
  83. package/skills/next-campaigns-os-setup/SKILL.md +5 -3
  84. package/skills/next-campaigns-polish/SKILL.md +8 -10
  85. package/skills/next-campaigns-qa/SKILL.md +7 -4
  86. package/skills/next-campaigns-sdk-bump/SKILL.md +151 -25
  87. package/skills.json +11 -11
  88. package/src/brand-theme.mjs +415 -9
  89. package/src/brief-summary.mjs +6 -2
  90. package/src/browser-launch.mjs +2 -1
  91. package/src/build-brief.mjs +13 -3
  92. package/src/built-site-scope.mjs +2 -2
  93. package/src/campaign-workspace.mjs +288 -15
  94. package/src/change-scope.mjs +284 -0
  95. package/src/cli.mjs +358 -97
  96. package/src/commercial-journey.mjs +16 -3
  97. package/src/commercial-parity.mjs +15 -7
  98. package/src/content-residue.mjs +4 -2
  99. package/src/design-file-reference.mjs +93 -0
  100. package/src/design-source-package.mjs +91 -1
  101. package/src/design-source-publication.mjs +9 -6
  102. package/src/directory-lock.mjs +73 -19
  103. package/src/doctor/checks.mjs +87 -81
  104. package/src/doctor/ci.mjs +177 -0
  105. package/src/doctor/inspect.mjs +41 -6
  106. package/src/doctor/next-step.mjs +7 -0
  107. package/src/doctor/source-provenance.mjs +74 -0
  108. package/src/doctor-sidecar.mjs +3 -2
  109. package/src/evidence-debt.mjs +159 -0
  110. package/src/finding-cause.mjs +12 -11
  111. package/src/input-refresh.mjs +339 -32
  112. package/src/invocation.mjs +7 -1
  113. package/src/live-campaign-refs.mjs +7 -2
  114. package/src/local-preview-policy.mjs +35 -2
  115. package/src/page-kit-analytics-vendors.mjs +132 -0
  116. package/src/parity/compare.mjs +316 -0
  117. package/src/parity/design-access.mjs +54 -0
  118. package/src/parity/element-parity-record.mjs +569 -0
  119. package/src/parity/probe.mjs +205 -0
  120. package/src/parity/snapshot.mjs +110 -0
  121. package/src/png-header.mjs +87 -0
  122. package/src/polish-browser.mjs +189 -10
  123. package/src/polish-captures-record.mjs +52 -0
  124. package/src/polish-deadline.mjs +14 -0
  125. package/src/polish-gate.mjs +25 -6
  126. package/src/polish-node.mjs +81 -7
  127. package/src/polish-readability.mjs +221 -9
  128. package/src/polish-visual-fixtures.mjs +74 -0
  129. package/src/polish-visual.mjs +613 -0
  130. package/src/progress-node.mjs +3 -2
  131. package/src/qa-analytics-correctness.mjs +14 -2
  132. package/src/qa-analytics-parity.mjs +111 -6
  133. package/src/qa-browser.mjs +40 -4
  134. package/src/qa-commercial-parity.mjs +50 -3
  135. package/src/qa-node.mjs +235 -22
  136. package/src/qa-publish.mjs +9 -8
  137. package/src/qa-sidecar.mjs +16 -4
  138. package/src/qa-vendor-beacons.mjs +232 -0
  139. package/src/qa-vendor-endpoints.mjs +306 -0
  140. package/src/qc-accept.mjs +176 -6
  141. package/src/qc-check-registry.mjs +42 -2
  142. package/src/qc-results.mjs +477 -42
  143. package/src/readback.mjs +113 -21
  144. package/src/route-identity.mjs +10 -0
  145. package/src/run-record.mjs +18 -10
  146. package/src/runtime-state-ignore.mjs +6 -2
  147. package/src/sdk-attribute-index.mjs +11 -11
  148. package/src/sdk-markup.mjs +77 -2
  149. package/src/sdk-storage-compatibility.mjs +115 -6
  150. package/src/source-html-intake.mjs +15 -1
  151. package/src/source-html-manifest.mjs +7 -0
  152. package/src/source-html-server.mjs +129 -0
  153. package/src/spec-derive-campaign.mjs +216 -0
  154. package/src/spec-derive.mjs +473 -53
  155. package/src/spec-identity.mjs +14 -0
  156. package/src/stage-ledger.mjs +40 -1
  157. package/src/stage-record.mjs +100 -17
  158. package/src/target-lock.mjs +11 -1
  159. package/src/theme-gate.mjs +2 -2
  160. package/src/visual-diff.mjs +296 -0
  161. package/src/visual-reference.mjs +126 -0
@@ -73,8 +73,9 @@ export function writeDoctorSidecar(path, doctor, { command, targetRepo = null }
73
73
  // qa policy set) stamp the retained snapshot stale instead of leaving a green
74
74
  // lie on disk, while commands that DO recompute (doctor, prepare-build/start,
75
75
  // next) rewrite the sidecar wholesale — which clears any stale stamp.
76
- export function markDoctorSidecarStale(targetBaseDir, { command = null, reason = null } = {}) {
77
- const path = doctorSidecarPath(targetBaseDir);
76
+ // `path` is the funnel's own sidecar (campaign-workspace.mjs doctorOutPath);
77
+ // without it, the project-level one under `targetBaseDir`.
78
+ export function markDoctorSidecarStale(targetBaseDir, { command = null, reason = null, path = doctorSidecarPath(targetBaseDir) } = {}) {
78
79
  if (!existsSync(path)) return null;
79
80
  let sidecar;
80
81
  try {
@@ -0,0 +1,159 @@
1
+ // Read-only build binding projection, shared by readback and other readers.
2
+ // No stage verdicts, clocks, Git metadata, processes, writes or network calls.
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+
5
+ import {
6
+ computeBuildFingerprint,
7
+ describeBuildOutputDrift,
8
+ pageIdForRoute,
9
+ resolveBuiltSiteScope,
10
+ routeForFile,
11
+ } from "./built-site-scope.mjs";
12
+ import { changeSince, classifyDrift } from "./change-scope.mjs";
13
+ import { normalizePublicRouteSlug } from "./route-identity.mjs";
14
+ import { isFileReadFailure } from "./cart-placeholders.mjs";
15
+ import { polishReviewedBuild, qaExtendedBuildFingerprint, qaRecordedBuildFingerprint, qaRecordedForCurrentBuild } from "./stage-ledger.mjs";
16
+
17
+ export const EVIDENCE_DEBT_STATUSES = Object.freeze(["current", "owed", "unknown"]);
18
+
19
+ const fingerprint = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
20
+ const portable = (value) => value.split(sep).join("/");
21
+
22
+ /**
23
+ * Compare recorded Polish/QA build bindings with bytes under _site/<slug>.
24
+ * `changes` is explicitly against the assembly manifest, which need not be
25
+ * the build an older stage reviewed. We cannot reconstruct an older manifest.
26
+ * Missing build inputs remain unknown; lifecycle-required stages are owed
27
+ * even without a stage binding. A matching binding alone is not a verdict.
28
+ *
29
+ * Each owed stage also carries `change`: what changed since the build that
30
+ * stage reviewed (change-scope.mjs), read from the rebuilds `record build`
31
+ * kept plus the drift on disk. A page-only change makes owed Polish
32
+ * `advisory` and owed QA `scope: "pages"` on `owed_pages`; anything else
33
+ * owes the full stage.
34
+ */
35
+ export function assessEvidenceDebt({ packet = null, packetPath = null, report = null } = {}) {
36
+ const assembly = report?.stages?.assembly;
37
+ const recordedBuild = fingerprint(assembly?.build_fingerprint);
38
+ // Match polish-gate's sourceBuildFingerprint precedence without reading the
39
+ // capture package (often absent in a fresh checkout).
40
+ const polishBuild = fingerprint(report?.stages?.polish?.source_build_fingerprint)
41
+ || fingerprint(report?.stages?.polish?.evidence?.source_build_fingerprint);
42
+ const qaBuild = qaRecordedBuildFingerprint(report);
43
+ let current = null;
44
+ let scope = null;
45
+ let outputRoot = null;
46
+ let reason = null;
47
+ let scopeReason = null;
48
+ let targetRepo = null;
49
+ const target = fingerprint(packet?.assembly?.target_repo);
50
+ const slug = normalizePublicRouteSlug(packet?.campaign?.public_route_slug);
51
+ if (!packet || !packetPath) reason = "packet_unavailable";
52
+ else if (!target || !slug) reason = "build_output_location_unknown";
53
+ else {
54
+ targetRepo = resolve(dirname(resolve(packetPath)), target);
55
+ outputRoot = join(targetRepo, "_site", slug);
56
+ try {
57
+ current = computeBuildFingerprint(outputRoot);
58
+ if (!current.ok) reason = "build_output_missing";
59
+ // Resolve only the chosen built directory: never discover another
60
+ // campaign, or fall back to source HTML when _site is absent.
61
+ else {
62
+ scope = resolveBuiltSiteScope(outputRoot);
63
+ if (!scope.ok) scopeReason = "build_output_pages_unresolved";
64
+ }
65
+ } catch (error) {
66
+ if (!isFileReadFailure(error)) throw error;
67
+ reason = "build_output_unreadable";
68
+ current = null;
69
+ }
70
+ }
71
+ const currentBuild = current?.ok ? current.fingerprint : null;
72
+ const drift = currentBuild ? describeBuildOutputDrift(assembly, current) : null;
73
+ const manifestKnown = drift?.manifest === "recorded";
74
+ // A missing manifest hides page changes, never a known stage binding.
75
+ const bindingReason = reason || (!recordedBuild ? "assembly_build_fingerprint_missing" : null);
76
+ const changesReason = bindingReason || scopeReason || (!manifestKnown ? "build_manifest_unavailable" : null);
77
+ const stage = (stageRecord, recorded, matches) => {
78
+ // src/stage-record.mjs writes required_by: build; the dependency map in
79
+ // src/input-currency.mjs supplies brief/spec for input-change demotions.
80
+ // This is lifecycle debt even when retained evidence matches the output.
81
+ const required = stageRecord?.status === "required";
82
+ const requiredReason = ["build", "brief", "spec"].includes(stageRecord?.required_by)
83
+ ? `stage_required_by_${stageRecord.required_by}` : "stage_required";
84
+ return {
85
+ status: bindingReason ? "unknown" : required ? "owed" : !recorded ? "unknown" : matches ? "current" : "owed",
86
+ matches_current_build: recorded && currentBuild ? matches : null,
87
+ recorded_build_fingerprint: recorded,
88
+ current_build_fingerprint: currentBuild,
89
+ reason: bindingReason || (required ? requiredReason : !recorded ? "stage_build_fingerprint_missing" : matches ? null : "build_fingerprint_changed"),
90
+ };
91
+ };
92
+
93
+ const changes = {
94
+ status: changesReason ? "unknown" : "known",
95
+ reason: changesReason,
96
+ recorded_build_fingerprint: recordedBuild,
97
+ current_build_fingerprint: currentBuild,
98
+ changed_pages: [],
99
+ unmapped_files: [],
100
+ extra: manifestKnown ? drift.extra : [],
101
+ missing: manifestKnown ? drift.missing : [],
102
+ changed: manifestKnown ? drift.changed : [],
103
+ };
104
+ if (manifestKnown && scope?.ok) {
105
+ const pages = new Map((scope?.pages || []).map((page) => [
106
+ portable(relative(outputRoot, page.built_path)),
107
+ { page_id: page.page_id, built_path: portable(relative(outputRoot, page.built_path)) },
108
+ ]));
109
+ // Deleted HTML no longer appears in the disk map. Reuse its route/id
110
+ // projection for those manifest entries, without reading nonexistent files.
111
+ for (const path of drift.missing) {
112
+ if (!path.toLowerCase().endsWith(".html") || path.split("/").some((part) => part.startsWith(".") || part.startsWith("_") || part === "node_modules")) continue;
113
+ pages.set(path, { page_id: pageIdForRoute(routeForFile(outputRoot, join(outputRoot, path))), built_path: path });
114
+ }
115
+ const paths = [...new Set([...drift.extra, ...drift.missing, ...drift.changed])].sort();
116
+ for (const path of paths) {
117
+ const page = pages.get(path);
118
+ if (page) changes.changed_pages.push(page);
119
+ else changes.unmapped_files.push(path);
120
+ }
121
+ }
122
+ const driftClassification = classifyDrift({ changes, baseline: assembly, outputRoot, targetRepo, publicRouteSlug: slug });
123
+ changes.kind = driftClassification.kind;
124
+ changes.reasons = driftClassification.reasons;
125
+ const changeFor = (anchor) => changeSince(anchor, { assembly, drift: changes, driftClassification });
126
+
127
+ // Polish reviewed the build a terminal record is bound to; once `record
128
+ // build` has made it required, the build it reviewed is kept as
129
+ // owed_since_build_fingerprint (polishReviewedBuild). Only a Polish that
130
+ // reviewed a build can be advisory: a blocked or pending one, or one a
131
+ // brief or spec change made required, owes the full stage, before and
132
+ // after record build alike.
133
+ const polishRecord = report?.stages?.polish;
134
+ const polish = stage(polishRecord, polishBuild, Boolean(polishBuild && currentBuild && polishBuild === currentBuild));
135
+ if (polish.status === "owed") {
136
+ const reviewed = polishReviewedBuild(polishRecord);
137
+ polish.change = changeFor(reviewed || polishBuild || fingerprint(polishRecord?.owed_since_build_fingerprint));
138
+ polish.advisory = Boolean(reviewed) && polish.change.kind === "page_only";
139
+ } else {
140
+ polish.change = null;
141
+ polish.advisory = false;
142
+ }
143
+ // QA on the changed pages only extends a completed QA: the change is read
144
+ // from the build that QA checked (qaExtendedBuildFingerprint). A blocked,
145
+ // pending or skipped full run extends nothing and owes the full stage.
146
+ const qaRecord = report?.stages?.qa;
147
+ const qa = stage(qaRecord, qaBuild, qaRecordedForCurrentBuild(report, currentBuild));
148
+ if (qa.status === "owed") {
149
+ const extended = qaExtendedBuildFingerprint(report);
150
+ qa.change = changeFor(extended || qaBuild);
151
+ qa.scope = extended && qaRecord?.status !== "required" && qa.change.kind === "page_only" ? "pages" : "full";
152
+ qa.owed_pages = qa.scope === "pages" ? qa.change.changed_pages : [];
153
+ } else {
154
+ qa.change = null;
155
+ qa.scope = null;
156
+ qa.owed_pages = [];
157
+ }
158
+ return { polish, qa, changes };
159
+ }
@@ -330,7 +330,8 @@ import { iterateQaVerdicts } from "./qa-verdict-discovery.mjs";
330
330
  import { readRunRecordsForTarget } from "./run-record.mjs";
331
331
 
332
332
  /**
333
- * The most recent Run Record for the same campaign identity under `baseDir`,
333
+ * The most recent Run Record for the same campaign identity under `baseDir`
334
+ * (in the packet's runtime directory, `runtimeRelPath`, when one is given),
334
335
  * or null. Records are discovered newest-first, and the current run's record
335
336
  * does not exist yet at classification time (both `qa run` and `doctor`
336
337
  * complete before `run-record` assembles anything), so the first identity
@@ -341,9 +342,9 @@ import { readRunRecordsForTarget } from "./run-record.mjs";
341
342
  * against a non-adjacent one and report findings introduced in between as
342
343
  * pre-existing.
343
344
  */
344
- export function findPriorRunRecord({ baseDir, mapId = null, localSpecId = null, currentRunId = null } = {}) {
345
+ export function findPriorRunRecord({ baseDir, runtimeRelPath = undefined, mapId = null, localSpecId = null, currentRunId = null } = {}) {
345
346
  if (!text(baseDir)) return null;
346
- for (const entry of readRunRecordsForTarget(baseDir)) {
347
+ for (const entry of readRunRecordsForTarget(baseDir, runtimeRelPath)) {
347
348
  const record = entry?.record;
348
349
  if (!record || typeof record !== "object" || Array.isArray(record)) continue;
349
350
  if (currentRunId && record.run_id === currentRunId) continue;
@@ -448,8 +449,8 @@ function locateExternalPriorVerdict({ targetRepo, record, ref }) {
448
449
  * The single-record boundary is unchanged: this still reads the final attempt
449
450
  * of exactly one earlier run, never a merged view across runs.
450
451
  */
451
- export function loadPriorQaVerdict({ baseDir, targetRepo = null, mapId = null, localSpecId = null, currentRunId = null } = {}) {
452
- const record = findPriorRunRecord({ baseDir, mapId, localSpecId, currentRunId });
452
+ export function loadPriorQaVerdict({ baseDir, runtimeRelPath = undefined, targetRepo = null, mapId = null, localSpecId = null, currentRunId = null } = {}) {
453
+ const record = findPriorRunRecord({ baseDir, runtimeRelPath, mapId, localSpecId, currentRunId });
453
454
  if (!record) return { verdict: null, record: null, path: null, reason: "no_prior_run" };
454
455
  const ref = (Array.isArray(record.artifacts) ? record.artifacts : []).findLast((artifact) => artifact?.kind === "qa_verdict");
455
456
  const refPath = text(ref?.path);
@@ -472,8 +473,8 @@ export function loadPriorQaVerdict({ baseDir, targetRepo = null, mapId = null, l
472
473
  * carries these, so doctor cause labels work against existing history with no
473
474
  * upgrade window. Returns `{ prior, record, reason }`.
474
475
  */
475
- function loadPriorDoctorFindings({ baseDir, mapId = null, localSpecId = null, currentRunId = null } = {}) {
476
- const record = findPriorRunRecord({ baseDir, mapId, localSpecId, currentRunId });
476
+ function loadPriorDoctorFindings({ baseDir, runtimeRelPath = undefined, mapId = null, localSpecId = null, currentRunId = null } = {}) {
477
+ const record = findPriorRunRecord({ baseDir, runtimeRelPath, mapId, localSpecId, currentRunId });
477
478
  if (!record) return { prior: null, record: null, reason: "no_prior_run" };
478
479
  const prior = priorSetFromRunRecordDoctor(record);
479
480
  if (!prior) return { prior: null, record, reason: "prior_run_without_doctor_observations" };
@@ -496,9 +497,9 @@ function loadPriorDoctorFindings({ baseDir, mapId = null, localSpecId = null, cu
496
497
  * raw map id) have no Run Record home, so they get `unknown` / `no_prior_run`
497
498
  * throughout — which is the truth, not a silence.
498
499
  */
499
- export function annotateQaAssertionCauses(assertions, { baseDir = null, targetRepo = null, mapId = null, localSpecId = null, currentRunId = null, isFinding } = {}) {
500
+ export function annotateQaAssertionCauses(assertions, { baseDir = null, runtimeRelPath = undefined, targetRepo = null, mapId = null, localSpecId = null, currentRunId = null, isFinding } = {}) {
500
501
  const lookup = baseDir
501
- ? loadPriorQaVerdict({ baseDir, targetRepo, mapId, localSpecId, currentRunId })
502
+ ? loadPriorQaVerdict({ baseDir, runtimeRelPath, targetRepo, mapId, localSpecId, currentRunId })
502
503
  : { verdict: null, record: null, path: null, reason: "no_prior_run" };
503
504
  const prior = lookup.verdict ? priorSetFromVerdict(lookup.verdict, { isFinding }) : null;
504
505
  const noPriorReason = lookup.reason || "no_prior_run";
@@ -532,9 +533,9 @@ const CAUSE_SUMMARY_SCHEMA = "campaigns-os-finding-cause/v0";
532
533
  * The doctor twin. `errors` and `warnings` are the doctor output's own arrays;
533
534
  * both are stamped in place. Returns the same summary shape.
534
535
  */
535
- export function annotateDoctorIssueCauses({ errors = [], warnings = [], baseDir = null, mapId = null, localSpecId = null, currentRunId = null } = {}) {
536
+ export function annotateDoctorIssueCauses({ errors = [], warnings = [], baseDir = null, runtimeRelPath = undefined, mapId = null, localSpecId = null, currentRunId = null } = {}) {
536
537
  const lookup = baseDir
537
- ? loadPriorDoctorFindings({ baseDir, mapId, localSpecId, currentRunId })
538
+ ? loadPriorDoctorFindings({ baseDir, runtimeRelPath, mapId, localSpecId, currentRunId })
538
539
  : { prior: null, record: null, reason: "no_prior_run" };
539
540
  const noPriorReason = lookup.reason || "no_prior_run";
540
541
  const findings = [];