@nextcommerce/campaigns-os 1.41.2 → 1.43.1
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 +178 -0
- package/README.md +6 -4
- package/agents/claude/CLAUDE.md +5 -1
- package/campaign-spec/dist/types.d.ts +2 -0
- package/contracts/effects.v1.json +111 -18
- package/contracts/migration-sidecar-bundle.v0.json +9 -0
- package/contracts/release-ledger.json +752 -0
- package/contracts/supported-surface.json +12 -11
- package/docs/build-packet.md +56 -6
- package/docs/campaigns-os-build-flow.md +2 -2
- package/docs/effects.md +70 -1
- package/docs/local-setup.md +51 -0
- package/docs/migration-sidecar-bundle.md +6 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/progress-snapshots.md +6 -0
- package/docs/qa-and-test-orders.md +26 -10
- package/docs/runtime-readiness.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +3 -2
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +6 -1
- package/schemas/campaign-spec.v4.schema.json +4 -0
- package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +13 -8
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +8 -6
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +3 -3
- package/skills/next-campaigns-os/SKILL.md +17 -4
- package/skills/next-campaigns-os/references/session-intake.md +4 -4
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +3 -3
- package/skills/next-campaigns-qa/SKILL.md +10 -9
- package/skills.json +11 -11
- package/src/cli.mjs +187 -70
- package/src/finding-cause.mjs +14 -10
- package/src/lifecycle.mjs +5 -4
- package/src/polish-node.mjs +5 -2
- package/src/progress-node.mjs +3 -2
- package/src/progress.mjs +5 -3
- package/src/qa-node.mjs +58 -26
- package/src/qa-publish.mjs +4 -0
- package/src/qa-sidecar.mjs +2 -0
- package/src/qa-verdict-discovery.mjs +11 -0
- package/src/qa-verdict-publish.mjs +1 -0
- package/src/qa-verdict.mjs +8 -1
- package/src/readback.mjs +2 -1
- package/src/run-record-closeout.mjs +3 -4
- package/src/run-record.mjs +4 -0
- package/src/sidecar-bundle.mjs +21 -0
- package/src/spec-source-identity.mjs +44 -0
- package/src/stage-ledger.mjs +4 -1
- package/src/tooling-setup.mjs +160 -0
package/src/cli.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { campaignSpecIdentity, resolveCampaignIdentity, campaignIdentitiesMatch, localSpecIdentityFields } from "./spec-source-identity.mjs";
|
|
1
2
|
import { withHtmlScanSnapshot, readHtmlScanText, htmlScanDigest } from "./html-scan.mjs";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { createDemo, demoArguments } from "./demo.mjs";
|
|
@@ -28,7 +29,7 @@ import { observeProgress, PROGRESS_OBSERVATION } from "./progress-node.mjs";
|
|
|
28
29
|
import { describeSdkIgnoredMetaTags, isSdkIgnoredMetaTag } from "./sdk-meta-tags.mjs";
|
|
29
30
|
import { HIDDEN_EAGER_MEDIA_ACTIONS, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
30
31
|
import { ORDER_PATH_DEPTH_DRIFT_CODE, orderPathDepthDriftText, orderPathDepthReconcileAction, orderPathDepthsDisagree, parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
31
|
-
import { specMaterialHash } from "./spec-identity.mjs";
|
|
32
|
+
import { specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
32
33
|
// The same predicate stage-ledger.mjs judges a mutator's result with, imported
|
|
33
34
|
// rather than re-stated so the waive preview and the commit agree by identity.
|
|
34
35
|
import { isPlainObject } from "./repo-scan.mjs";
|
|
@@ -98,7 +99,7 @@ import { campaignSidecarPaths, resolveCampaignWorkspace, targetRepoFor } from ".
|
|
|
98
99
|
import { canonicalPath, sameFile } from "./fs-identity.mjs";
|
|
99
100
|
import { DEFAULT_PROXY_BASE, fetchSpecByMapId } from "./spec-fetch.mjs";
|
|
100
101
|
import { writeMapSdkPin } from "./map-pin-writeback.mjs";
|
|
101
|
-
import { discoverQaVerdicts, iterateQaVerdicts, qaVerdictCandidateScore, qaVerdictCandidateTime, qaVerdictPathHints } from "./qa-verdict-discovery.mjs";
|
|
102
|
+
import { qaVerdictIdentityMatch, discoverQaVerdicts, iterateQaVerdicts, qaVerdictCandidateScore, qaVerdictCandidateTime, qaVerdictPathHints } from "./qa-verdict-discovery.mjs";
|
|
102
103
|
import { assertFetchAvailable, assertSecureProxyBase, boundedResponseText, DEFAULT_RUNS_ENDPOINT, describeRemitBaseKind, isLoopbackHostname, REMIT_RESULTS, remitRunRecord } from "./remit.mjs";
|
|
103
104
|
import {
|
|
104
105
|
aggregateLifecycleForRun,
|
|
@@ -485,12 +486,13 @@ Usage:
|
|
|
485
486
|
campaigns-os readback --example [--json] # project the bundled synthetic sample; freshness is not computable for it by design
|
|
486
487
|
campaigns-os validate-assembly-report --report <json> [--json]
|
|
487
488
|
campaigns-os install-skills [--platform <claude|codex|agents|all>] [--target <skills-dir>] [--dry-run] [--json]
|
|
489
|
+
campaigns-os tooling setup --target <campaign-directory> [--platform claude] [--dry-run] [--json] # after installing the pinned project dependencies, install skills, connect Claude's context and install the QA browser; preserve existing pages and instructions, then restart the agent
|
|
488
490
|
campaigns-os login [--store <subdomain>]
|
|
489
491
|
campaigns-os logout [--store <subdomain>]
|
|
490
492
|
campaigns-os tooling status [--platform <claude|codex|agents|all>] [--target <skills-dir>] [--skills-revision <bundle-revision|skill-id@version>] [--packet <campaign-runtime.build.json>] [--force] [--json] # install-mode, git, skill freshness, and local gateway login/store/expiry/reported version. --skills-revision checks the bundle revision the skill you loaded states on its first body line (or that one skill's <skill-id>@<version>) against the bundle THIS CLI ships: revision_check is match, mismatch or unchecked, and a mismatch prints the full status and exits 2 because skill text already in context cannot be refreshed by re-running — start a fresh session. The pin check reports one executable per project: the project pin first — the first exact spec for this package (x.y.z, =x.y.z or vx.y.z) on the walk up from the nearest package.json, devDependencies then dependencies in each, entering a workspace root and stopping there, never peerDependencies or optionalDependencies — then the Build Packet's campaigns_os_version (the project's campaign-runtime.build.json, or --packet <path>); the Pin: line names the key and manifest (or packet) each version came from; pin.status is match, stale_pin (the pin is not the running version), conflicting_pin (the two sources disagree) or unpinned (neither, or only a range; exit 0). stale_pin and conflicting_pin exit 2 with the file to change; --force (bare) overrides them, is reported as pin.forced and lands on the lifecycle journal entry. See docs/skills-revision.md
|
|
491
493
|
campaigns-os tooling diagnose [--packet <packet>] [--platform <claude|codex|agents|all>] [--json] # read-only redacted support summary
|
|
492
494
|
campaigns-os install-agent-context --target <page-kit-dir> [--dry-run]
|
|
493
|
-
campaigns-os next --packet <json> [--no-write] [--no-remit] [--proxy-base <url>] [--json]
|
|
495
|
+
campaigns-os next [${NEXT_STAGE_ORDER.join("|")}] --packet <json> [--no-write] [--no-remit] [--proxy-base <url>] [--json] # no stage self-decides; returns gates[] + next_actions[] alongside the prompt
|
|
494
496
|
campaigns-os next setup --packet <json> [--context <json>] [--report <json>] [--json]
|
|
495
497
|
campaigns-os next build --packet <json> [--context <json>] [--report <json>] [--json]
|
|
496
498
|
campaigns-os next polish --packet <json> --report <json> [--json]
|
|
@@ -654,6 +656,18 @@ export async function main(argv, { authentication } = {}) {
|
|
|
654
656
|
return;
|
|
655
657
|
}
|
|
656
658
|
|
|
659
|
+
// Project setup must not recover campaign sessions, read gateway bindings,
|
|
660
|
+
// or emit lifecycle/telemetry evidence before a campaign is selected.
|
|
661
|
+
if (command === "tooling" && args._[1] === "setup") {
|
|
662
|
+
const { setupArguments, setupTooling, setupTextLines } = await import("./tooling-setup.mjs");
|
|
663
|
+
setupArguments(args, argv);
|
|
664
|
+
const { installQaBrowser } = await import("./qa-node.mjs");
|
|
665
|
+
const result = setupTooling(args, { packageRoot: ROOT, installSkills, installAgentContext, installBrowser: installQaBrowser });
|
|
666
|
+
console.log(args.json ? JSON.stringify(result, null, 2) : setupTextLines(result).join("\n"));
|
|
667
|
+
if (!result.ok) process.exitCode = 2;
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
657
671
|
// Ambient run session (Tier 3): when `run start` is active, every command
|
|
658
672
|
// shares its run_id WITHOUT --run-id. Explicit --run-id still wins. Resolved
|
|
659
673
|
// ONCE here and threaded through dispatch + persistence so the run_id a
|
|
@@ -959,31 +973,38 @@ export function recordQaStageOutcome(args, result) {
|
|
|
959
973
|
if (!existsSync(reportPath)) return false;
|
|
960
974
|
|
|
961
975
|
const verdict = result.verdict;
|
|
976
|
+
const hasLocalIdentity = packet.spec?.local_spec_id != null || verdict?.local_spec_id != null;
|
|
977
|
+
if (hasLocalIdentity && !qaVerdictIdentityMatch(verdict, packet)) return false;
|
|
962
978
|
const failed = (Array.isArray(verdict.assertions) ? verdict.assertions : [])
|
|
963
979
|
.filter((assertion) => assertion?.status === "fail")
|
|
964
980
|
.map((assertion) => `${assertion.id}: ${assertion.actual || "assertion failed"}`);
|
|
965
|
-
const committed = commitAssemblyReport(workspace, (report) =>
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
: [],
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
981
|
+
const committed = commitAssemblyReport(workspace, (report) => {
|
|
982
|
+
if (hasLocalIdentity && !specHashesMatch(verdict.spec_hash, report.identity?.spec_material_hash)) {
|
|
983
|
+
throw new Error("Local-spec QA verdict belongs to a different material revision; report evidence was not changed.");
|
|
984
|
+
}
|
|
985
|
+
return recordProducerStageOutcome(report, {
|
|
986
|
+
stage: "qa",
|
|
987
|
+
disposition: verdict.disposition,
|
|
988
|
+
timestamp: verdict.completed_at,
|
|
989
|
+
command: `campaigns-os ${QA_RUN_PRODUCER}`,
|
|
990
|
+
outputs: [result.local_path, result.qa_sidecar?.path].filter(isNonEmptyString),
|
|
991
|
+
blockers: verdict.disposition === "blocked" ? failed : [],
|
|
992
|
+
warnings: verdict.disposition === "ready_with_exceptions"
|
|
993
|
+
? ["QA completed with explicitly attributed exceptions; inspect the verdict artifact."]
|
|
994
|
+
: [],
|
|
995
|
+
// The producer knows its own run id and must restate it, or the stage
|
|
996
|
+
// keeps a previous run's identity beside this run's status and outputs.
|
|
997
|
+
identity: { verdict_run_id: optionalString(verdict.run_id) },
|
|
998
|
+
// Which build this verdict judged, and the gates whose browser outcome
|
|
999
|
+
// the doctor's static scan defers to (qaGatePassedForCurrentBuild). A
|
|
1000
|
+
// gate that never ran is left out, so silence never reads as a pass.
|
|
1001
|
+
evidence: qaStageGateEvidence(verdict, report),
|
|
1002
|
+
// Counts-only: never order ids, refs, emails or URLs (see
|
|
1003
|
+
// summarizePurchaseProof). This is what lets `next` tell a real purchase
|
|
1004
|
+
// path from a `--test-order off` diagnostic.
|
|
1005
|
+
proof: summarizePurchaseProof({ verdict, proofPolicy: packet.qa?.proof_policy }),
|
|
1006
|
+
});
|
|
1007
|
+
}, {
|
|
987
1008
|
stage: "qa",
|
|
988
1009
|
// The sidecar names this refresh as its producer (generated_by, #312).
|
|
989
1010
|
// This function is the `qa run` stage record, whichever token dispatch
|
|
@@ -1167,6 +1188,23 @@ async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null,
|
|
|
1167
1188
|
// them from this function's source.
|
|
1168
1189
|
const mode = PREPARE_MODES[command];
|
|
1169
1190
|
if (!mode) throw new Error(`No intake mode registered for "${command}"; add it to PREPARE_MODES.`);
|
|
1191
|
+
// Validate argv before --spec is inspected or --map-id fetches and caches.
|
|
1192
|
+
// Preserve resolveSpecPath's missing-input and map-id/target diagnostics.
|
|
1193
|
+
for (const flag of ["spec", "map-id", "source", "target", "source-kind", "proxy-base"]) {
|
|
1194
|
+
if (Object.hasOwn(args, flag)) requireArg(args, flag);
|
|
1195
|
+
}
|
|
1196
|
+
if (args.spec || (args["map-id"] && args.target)) requireArg(args, "source");
|
|
1197
|
+
if (args.spec) requireArg(args, "target");
|
|
1198
|
+
const sourceKind = optionalString(args["source-kind"], "html_funnel");
|
|
1199
|
+
if (sourceKind !== "html_funnel") {
|
|
1200
|
+
throw refused(`Unsupported source adapter "${sourceKind}". Use html_funnel for the current prepared-HTML flow.`);
|
|
1201
|
+
}
|
|
1202
|
+
if (Object.hasOwn(args, "wrapper-policy") && !isNonEmptyString(args["wrapper-policy"])) {
|
|
1203
|
+
throw refused(`--wrapper-policy needs a value. Accepted values: ${ADAPTER_WRAPPER_POLICIES.join(", ")}.`);
|
|
1204
|
+
}
|
|
1205
|
+
const wrapperPolicyFlag = refusing(() => parseWrapperPolicyFlag(args));
|
|
1206
|
+
refusing(() => requireDesignManifestValue(args));
|
|
1207
|
+
const orderPathDepthFlag = refusing(() => parseOrderPathDepthFlag(args, { command: "prepare-build" }));
|
|
1170
1208
|
// Tier 2: mark sub-phases so the lifecycle journal entry carries per-phase
|
|
1171
1209
|
// timings (spec resolve vs the prepare+doctor+install build), which Tier 1
|
|
1172
1210
|
// aggregates into `start:resolve-spec` / `start:prepare-build` stages.
|
|
@@ -1178,7 +1216,7 @@ async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null,
|
|
|
1178
1216
|
args.spec = resolved.specPath;
|
|
1179
1217
|
// `command` rides along for the doctor sidecar's generated_by stamp when
|
|
1180
1218
|
// the mode runs doctor (#312): threaded from here, not re-read from argv.
|
|
1181
|
-
const result = await recorder.time("prepare-build", () => prepareBuild(args, { ...mode, command, specInput }));
|
|
1219
|
+
const result = await recorder.time("prepare-build", () => prepareBuild(args, { ...mode, command, specInput, sourceKind, wrapperPolicyFlag, orderPathDepthFlag }));
|
|
1182
1220
|
result.spec_source = resolved;
|
|
1183
1221
|
autoStartRunSession(result, args, ambient, sessionHolder);
|
|
1184
1222
|
printPrepareResult(result, args);
|
|
@@ -1346,7 +1384,9 @@ async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null,
|
|
|
1346
1384
|
// command a QA run prints has to agree with the run_id this session will
|
|
1347
1385
|
// later close and remit under.
|
|
1348
1386
|
const result = await runQaCli(args, { ambient });
|
|
1349
|
-
|
|
1387
|
+
// nextStage requires a packet. Guard its optional, swallowed progress
|
|
1388
|
+
// probe explicitly so it cannot construct a refusal in that try block.
|
|
1389
|
+
if (args._[1] === "run" && result?.verdict && isNonEmptyString(args.packet) && recordQaStageOutcome(args, result)) {
|
|
1350
1390
|
// Observe committed QA before the existing closeout; this cannot close
|
|
1351
1391
|
// a run or change the QA disposition. Reuse the canonical picker.
|
|
1352
1392
|
try {
|
|
@@ -1364,6 +1404,9 @@ async function dispatch(command, args, recorder = NOOP_RECORDER, ambient = null,
|
|
|
1364
1404
|
}
|
|
1365
1405
|
|
|
1366
1406
|
if (command === "run-record") {
|
|
1407
|
+
// These are the operator's own argv. Internal closeouts call
|
|
1408
|
+
// runRecordCommand directly and retain their pre-1.43.1 flag handling.
|
|
1409
|
+
validateRunRecordArgv(args);
|
|
1367
1410
|
await runRecordCommand(args, ambient);
|
|
1368
1411
|
return;
|
|
1369
1412
|
}
|
|
@@ -1675,7 +1718,7 @@ function campaignIdentity(spec, args) {
|
|
|
1675
1718
|
|| optionalString(spec.spec_identity?.public_route_slug)
|
|
1676
1719
|
|| optionalString(spec.campaign?.slug)
|
|
1677
1720
|
|| optionalString(spec.campaign?.id);
|
|
1678
|
-
return { mapId, publicRouteSlug };
|
|
1721
|
+
return { mapId, publicRouteSlug, localSpecId: spec.spec_identity?.local_spec_id ?? null };
|
|
1679
1722
|
}
|
|
1680
1723
|
|
|
1681
1724
|
function preferredTemplateFamily(spec) {
|
|
@@ -2288,11 +2331,8 @@ function parseWrapperPolicyFlag(args) {
|
|
|
2288
2331
|
// directory are errors: the operator named the file, so silently falling back
|
|
2289
2332
|
// to filesystem matching would discard the declaration they made.
|
|
2290
2333
|
function parseDesignManifestFlag(args) {
|
|
2291
|
-
const raw = args
|
|
2334
|
+
const raw = requireDesignManifestValue(args);
|
|
2292
2335
|
if (raw == null) return null;
|
|
2293
|
-
if (raw === true || !isNonEmptyString(raw)) {
|
|
2294
|
-
throw new Error("--design-manifest needs a value: the path of a source-html-manifest/v0 JSON file.");
|
|
2295
|
-
}
|
|
2296
2336
|
const path = resolve(raw);
|
|
2297
2337
|
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
2298
2338
|
throw new Error(`Design manifest does not exist or is not a file: ${path}`);
|
|
@@ -2300,6 +2340,14 @@ function parseDesignManifestFlag(args) {
|
|
|
2300
2340
|
return path;
|
|
2301
2341
|
}
|
|
2302
2342
|
|
|
2343
|
+
function requireDesignManifestValue(args) {
|
|
2344
|
+
const raw = args["design-manifest"];
|
|
2345
|
+
if (raw != null && (raw === true || !isNonEmptyString(raw))) {
|
|
2346
|
+
throw new Error("--design-manifest needs a value: the path of a source-html-manifest/v0 JSON file.");
|
|
2347
|
+
}
|
|
2348
|
+
return raw;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2303
2351
|
// Same precedence the template family uses (docs/build-packet.md
|
|
2304
2352
|
// "Authoring-Time Hints"): an explicit CLI flag beats a declared file hint,
|
|
2305
2353
|
// and with neither the default stands. The vocabulary is the adapter
|
|
@@ -2350,21 +2398,18 @@ function prepareBuild(args, options = {}) {
|
|
|
2350
2398
|
assertDistinctPrepareBuildOutputPaths(prepareBuildCollisionPaths);
|
|
2351
2399
|
guardAssemblyReportOverwrite(reportPath, args);
|
|
2352
2400
|
const spec = readJson(specPath);
|
|
2353
|
-
const { mapId, publicRouteSlug } = campaignIdentity(spec, args);
|
|
2354
|
-
if (!
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
// partway through, so a flag that throws later would leave persistent state
|
|
2364
|
-
// behind for a bad argument.
|
|
2365
|
-
const wrapperPolicyFlag = parseWrapperPolicyFlag(args);
|
|
2401
|
+
const { mapId, publicRouteSlug, localSpecId } = campaignIdentity(spec, args);
|
|
2402
|
+
if (!resolveCampaignIdentity({ map_id: mapId, local_spec_id: localSpecId })) {
|
|
2403
|
+
throw new Error("CampaignSpec requires exactly one identity: a saved spec_identity.map_id, or an agent-authored spec_identity.local_spec_id (1–64 letters, digits, underscores or hyphens). Keep the local ID stable across revisions; do not invent a Map ID.");
|
|
2404
|
+
}
|
|
2405
|
+
if (!publicRouteSlug) throw new Error("CampaignSpec has no public route slug. Set spec_identity.public_route_slug or campaign.slug.");
|
|
2406
|
+
|
|
2407
|
+
// Dispatch validated the argv-only flags before spec resolution. Only the
|
|
2408
|
+
// manifest's filesystem check remains here, before preparation writes.
|
|
2409
|
+
const sourceKind = options.sourceKind;
|
|
2410
|
+
const wrapperPolicyFlag = options.wrapperPolicyFlag;
|
|
2366
2411
|
const designManifestPath = parseDesignManifestFlag(args);
|
|
2367
|
-
const orderPathDepthFlag =
|
|
2412
|
+
const orderPathDepthFlag = options.orderPathDepthFlag;
|
|
2368
2413
|
|
|
2369
2414
|
const activePages = activeSpecPages(spec);
|
|
2370
2415
|
const htmlFiles = collectHtmlFiles(sourceRoot);
|
|
@@ -2600,7 +2645,8 @@ function prepareBuild(args, options = {}) {
|
|
|
2600
2645
|
},
|
|
2601
2646
|
spec: {
|
|
2602
2647
|
map_id: mapId,
|
|
2603
|
-
|
|
2648
|
+
...(localSpecId ? { local_spec_id: localSpecId } : {}),
|
|
2649
|
+
spec_url: localSpecId ? null : spec.spec_identity?.spec_url || null,
|
|
2604
2650
|
local_path: relFromFile(packetPath, specPath),
|
|
2605
2651
|
},
|
|
2606
2652
|
design_source_package: designSourcePackage.referenceFor(packetPath),
|
|
@@ -2965,6 +3011,7 @@ function createAssemblyReport({
|
|
|
2965
3011
|
status: "prepared",
|
|
2966
3012
|
identity: {
|
|
2967
3013
|
map_id: packet.spec.map_id,
|
|
3014
|
+
...localSpecIdentityFields(packet.spec),
|
|
2968
3015
|
public_route_slug: packet.campaign.public_route_slug,
|
|
2969
3016
|
campaign_directory: packet.campaign.campaign_directory,
|
|
2970
3017
|
live_url_path: packet.campaign.live_url_path,
|
|
@@ -3860,11 +3907,17 @@ export function doctorPacket(packetPath, options = {}) {
|
|
|
3860
3907
|
// to go by first. Code granularity, because that is the granularity the
|
|
3861
3908
|
// record stores. baseDir is the packet directory, the same root the Run
|
|
3862
3909
|
// Record writes under.
|
|
3910
|
+
// An invalid identity cannot select history. In particular, withholding a
|
|
3911
|
+
// malformed local ID from derived must not turn it into an unfiltered or
|
|
3912
|
+
// Map-only lookup of another campaign's findings.
|
|
3913
|
+
const comparableIdentity = resolveCampaignIdentity(result.derived)
|
|
3914
|
+
&& !result.errors.some(issue => issue.code === "spec.local_identity" || issue.code === "spec.map_id");
|
|
3863
3915
|
result.cause_summary = annotateDoctorIssueCauses({
|
|
3864
3916
|
errors: result.errors,
|
|
3865
3917
|
warnings: result.warnings,
|
|
3866
|
-
baseDir: dirname(resolve(packetPath)),
|
|
3918
|
+
baseDir: comparableIdentity ? dirname(resolve(packetPath)) : null,
|
|
3867
3919
|
mapId: result.derived?.map_id || null,
|
|
3920
|
+
localSpecId: result.derived?.local_spec_id || null,
|
|
3868
3921
|
});
|
|
3869
3922
|
return result;
|
|
3870
3923
|
}
|
|
@@ -3884,6 +3937,7 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
3884
3937
|
const errors = [];
|
|
3885
3938
|
const warnings = [];
|
|
3886
3939
|
const ready = [];
|
|
3940
|
+
const packetIdentity = resolveCampaignIdentity(packet?.spec);
|
|
3887
3941
|
const derived = {
|
|
3888
3942
|
packet_path: packetPath,
|
|
3889
3943
|
// The report this inspection read (null when the caller switched the
|
|
@@ -3891,6 +3945,7 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
3891
3945
|
// different file.
|
|
3892
3946
|
assembly_report_path: typeof resolvedReportPath === "string" ? resolvedReportPath : null,
|
|
3893
3947
|
map_id: packet?.spec?.map_id || null,
|
|
3948
|
+
...(packetIdentity?.kind === "local_spec" ? { local_spec_id: packetIdentity.id } : {}),
|
|
3894
3949
|
public_route_slug: packet?.campaign?.public_route_slug || null,
|
|
3895
3950
|
template_family: packet?.assembly?.template_family || null,
|
|
3896
3951
|
source_root: null,
|
|
@@ -4324,7 +4379,10 @@ function validatePacket(packet, packetPath, errors, warnings, ready, derived, bu
|
|
|
4324
4379
|
|
|
4325
4380
|
requireString(packet, errors, "campaign.public_route_slug");
|
|
4326
4381
|
requireBoolean(packet, errors, "campaign.allowed_domains_confirmed");
|
|
4327
|
-
|
|
4382
|
+
if (!resolveCampaignIdentity(packet.spec)) addIssue(errors, packet.spec?.local_spec_id != null ? "spec.local_identity" : "spec.map_id", "Packet spec requires exactly one valid map_id or local_spec_id.", { kind: packet.spec?.local_spec_id != null ? "local_spec" : "saved_map" });
|
|
4383
|
+
if (packet.spec?.local_spec_id != null && (packet.spec.spec_url != null || !isNonEmptyString(packet.spec.local_path))) {
|
|
4384
|
+
addIssue(errors, "spec.local_identity", "Local-spec packets require a local_path and no saved-Map spec_url.");
|
|
4385
|
+
}
|
|
4328
4386
|
if (!synthesizedBuiltSite) {
|
|
4329
4387
|
requireString(packet, errors, "source_html.root");
|
|
4330
4388
|
requireArray(packet, errors, "source_html.pages");
|
|
@@ -4510,8 +4568,11 @@ function validatePacket(packet, packetPath, errors, warnings, ready, derived, bu
|
|
|
4510
4568
|
buildState.specStatus = specStatus;
|
|
4511
4569
|
if (specStatus === "ok") {
|
|
4512
4570
|
const specMapId = spec.spec_identity?.map_id || spec.map_id;
|
|
4513
|
-
if (specMapId && specMapId !== packet.spec.map_id)
|
|
4514
|
-
|
|
4571
|
+
if ((specMapId && specMapId !== packet.spec.map_id)
|
|
4572
|
+
|| ((spec.spec_identity?.local_spec_id != null || packet.spec?.local_spec_id != null)
|
|
4573
|
+
&& !campaignIdentitiesMatch(campaignSpecIdentity(spec), packet.spec))) {
|
|
4574
|
+
const localIdentity = spec.spec_identity?.local_spec_id != null || packet.spec?.local_spec_id != null;
|
|
4575
|
+
addIssue(errors, localIdentity ? "spec.local_identity" : "spec.map_id", "Packet identity does not match the CampaignSpec map_id/local_spec_id.", { kind: localIdentity ? "local_spec" : "saved_map" });
|
|
4515
4576
|
}
|
|
4516
4577
|
ready.push("Local CampaignSpec parsed");
|
|
4517
4578
|
runDoctorChecks(SPEC_DOCTOR_CHECKS, { packet, packetPath, spec, targetRepo, errors, warnings, ready, derived, buildState });
|
|
@@ -5006,6 +5067,9 @@ export function pageKitSyncCommand(args) {
|
|
|
5006
5067
|
addIssue(result.errors, "page_kit.sync.spec_identity_mismatch", `CampaignSpec identifies route "${singleLineField(specSlug)}" but the packet's campaign.public_route_slug is "${publicRouteSlug}". Point spec.local_path at this campaign's export (or re-run prepare-build from it); nothing was written.`);
|
|
5007
5068
|
} else if (specMapId && packetMapId && specMapId !== packetMapId) {
|
|
5008
5069
|
addIssue(result.errors, "page_kit.sync.spec_identity_mismatch", `CampaignSpec spec_identity.map_id "${singleLineField(specMapId)}" does not match the packet's spec.map_id "${singleLineField(packetMapId)}". Point spec.local_path at this campaign's export (or re-run prepare-build from it); nothing was written.`);
|
|
5070
|
+
} else if ((spec.spec_identity?.local_spec_id != null || packet.spec?.local_spec_id != null)
|
|
5071
|
+
&& !campaignIdentitiesMatch(campaignSpecIdentity(spec), packet.spec)) {
|
|
5072
|
+
addIssue(result.errors, "page_kit.sync.spec_identity_mismatch", "CampaignSpec identity (spec_identity.map_id/local_spec_id) does not match the packet identity. Point spec.local_path at this campaign's spec (or re-run prepare-build from it); nothing was written.");
|
|
5009
5073
|
}
|
|
5010
5074
|
}
|
|
5011
5075
|
|
|
@@ -5349,6 +5413,9 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
5349
5413
|
addIssue(result.errors, "spec.derive.spec_identity_mismatch", `CampaignSpec identifies route "${singleLineField(specSlug)}" but the packet's campaign.public_route_slug is "${publicRouteSlug}". Point spec.local_path at this campaign's export (or re-run prepare-build from it); nothing was written.`);
|
|
5350
5414
|
} else if (specMapId && packetMapId && specMapId !== packetMapId) {
|
|
5351
5415
|
addIssue(result.errors, "spec.derive.spec_identity_mismatch", `CampaignSpec spec_identity.map_id "${singleLineField(specMapId)}" does not match the packet's spec.map_id "${singleLineField(packetMapId)}". Point spec.local_path at this campaign's export (or re-run prepare-build from it); nothing was written.`);
|
|
5416
|
+
} else if ((spec.spec_identity?.local_spec_id != null || packet.spec?.local_spec_id != null)
|
|
5417
|
+
&& !campaignIdentitiesMatch(campaignSpecIdentity(spec), packet.spec)) {
|
|
5418
|
+
addIssue(result.errors, "spec.derive.spec_identity_mismatch", "CampaignSpec identity (spec_identity.map_id/local_spec_id) does not match the packet identity. Point spec.local_path at this campaign's spec (or re-run prepare-build from it); nothing was written.");
|
|
5352
5419
|
}
|
|
5353
5420
|
}
|
|
5354
5421
|
|
|
@@ -6133,15 +6200,15 @@ function validateSpecPackageAvailability(spec, warnings, ready) {
|
|
|
6133
6200
|
|
|
6134
6201
|
function validateSpecIdentityExport(spec, warnings, ready) {
|
|
6135
6202
|
const identity = spec?.spec_identity;
|
|
6136
|
-
if (isObject(identity) &&
|
|
6137
|
-
ready.push(
|
|
6203
|
+
if (isObject(identity) && resolveCampaignIdentity(identity) && isNonEmptyString(identity.public_route_slug)) {
|
|
6204
|
+
ready.push(`CampaignSpec spec_identity includes ${identity.local_spec_id ? "local_spec_id" : "map_id"} and public_route_slug`);
|
|
6138
6205
|
return;
|
|
6139
6206
|
}
|
|
6140
6207
|
|
|
6141
6208
|
addIssue(
|
|
6142
6209
|
warnings,
|
|
6143
6210
|
"spec_identity.export",
|
|
6144
|
-
"CampaignSpec is missing complete spec_identity
|
|
6211
|
+
"CampaignSpec is missing complete spec_identity: declare map_id for a saved Map or local_spec_id for a local spec, plus public_route_slug. CLI identity overrides should stay diagnostic-only."
|
|
6145
6212
|
);
|
|
6146
6213
|
}
|
|
6147
6214
|
|
|
@@ -9024,7 +9091,12 @@ function validateAssemblyReport(report, { checkSourcePackageFreshness = true } =
|
|
|
9024
9091
|
if (report.schema_version !== REPORT_SCHEMA) addIssue(errors, "schema_version", `Expected ${REPORT_SCHEMA}.`);
|
|
9025
9092
|
else ready.push(`Assembly report schema ${REPORT_SCHEMA}`);
|
|
9026
9093
|
for (const path of ["run_id", "generated_at", "status", "identity.map_id", "identity.public_route_slug", "inputs.packet_path", "template_family.value"]) {
|
|
9027
|
-
|
|
9094
|
+
if (path === "identity.map_id") {
|
|
9095
|
+
if (!resolveCampaignIdentity(report.identity)) addIssue(errors, "identity.map_id", "Assembly Report requires exactly one valid map_id or local_spec_id.");
|
|
9096
|
+
} else requireString(report, errors, path);
|
|
9097
|
+
}
|
|
9098
|
+
if (report.identity?.local_spec_id != null && !/^sha256:[0-9a-f]{64}$/.test(report.identity.spec_material_hash || "")) {
|
|
9099
|
+
addIssue(errors, "identity.spec_material_hash", "Local-spec reports require a current SHA-256 material spec hash.");
|
|
9028
9100
|
}
|
|
9029
9101
|
const stages = report.stages;
|
|
9030
9102
|
if (!isObject(stages)) {
|
|
@@ -9274,13 +9346,14 @@ function nextPrepareBuildBindingIssues({
|
|
|
9274
9346
|
const expectedSlug = optionalString(packet.campaign?.public_route_slug);
|
|
9275
9347
|
const recordedMapId = optionalString(report.identity?.map_id);
|
|
9276
9348
|
const recordedSlug = optionalString(report.identity?.public_route_slug);
|
|
9277
|
-
if (
|
|
9349
|
+
if (!campaignIdentitiesMatch(packet.spec, report.identity) || recordedSlug !== expectedSlug) {
|
|
9278
9350
|
push(
|
|
9279
9351
|
"next.prepare_build.report_campaign_mismatch",
|
|
9280
9352
|
"Assembly Report campaign identity does not match the current Build Packet.",
|
|
9281
9353
|
{
|
|
9282
|
-
|
|
9283
|
-
|
|
9354
|
+
// Failed identity fields are diagnostic data, never adopted evidence.
|
|
9355
|
+
expected: { map_id: expectedMapId, local_spec_id: packet.spec?.local_spec_id ?? null, public_route_slug: expectedSlug },
|
|
9356
|
+
recorded: { map_id: recordedMapId, local_spec_id: report.identity?.local_spec_id ?? null, public_route_slug: recordedSlug },
|
|
9284
9357
|
},
|
|
9285
9358
|
);
|
|
9286
9359
|
}
|
|
@@ -9689,6 +9762,9 @@ function pickNextStage(report, { errors = [], derived = null }, prepareBuildGate
|
|
|
9689
9762
|
}
|
|
9690
9763
|
|
|
9691
9764
|
export function nextStage(stage, args, ambient = null) {
|
|
9765
|
+
if (stage !== null && !NEXT_STAGE_ORDER.includes(stage)) {
|
|
9766
|
+
throw refused(`Unknown next stage: ${stage}. Accepted stages: ${NEXT_STAGE_ORDER.join(", ")}.`);
|
|
9767
|
+
}
|
|
9692
9768
|
const packetPath = resolve(requireArg(args, "packet"));
|
|
9693
9769
|
// A custom prepare-build report is recorded on the Build Context relative
|
|
9694
9770
|
// to the target repo. Packet-only `next` must follow that durable pointer;
|
|
@@ -9894,8 +9970,6 @@ export function nextStage(stage, args, ambient = null) {
|
|
|
9894
9970
|
addPolishCheckpointGateErrors(errors, polishCheckpointGate, "qa");
|
|
9895
9971
|
addThemeGateErrors(errors, themeGate, "qa");
|
|
9896
9972
|
prompt = qaPrompt(packetPath, reportPath, packet);
|
|
9897
|
-
} else {
|
|
9898
|
-
throw new Error(`Unknown next stage: ${stage}`);
|
|
9899
9973
|
}
|
|
9900
9974
|
const status = errors.length
|
|
9901
9975
|
? "blocked"
|
|
@@ -10602,7 +10676,7 @@ function qaPrompt(packetPath, reportPath, packet) {
|
|
|
10602
10676
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
10603
10677
|
return `Use next-campaigns-qa for this deployed campaign.
|
|
10604
10678
|
|
|
10605
|
-
Map ID: ${packet.spec.map_id}
|
|
10679
|
+
${packet.spec.local_spec_id ? "Local spec ID" : "Map ID"}: ${packet.spec.local_spec_id || packet.spec.map_id}
|
|
10606
10680
|
Base URL: ${url}
|
|
10607
10681
|
Build Packet: ${packetPath}
|
|
10608
10682
|
Assembly Report: ${reportPath}
|
|
@@ -12609,6 +12683,12 @@ function runSessionProgress(found) {
|
|
|
12609
12683
|
}
|
|
12610
12684
|
|
|
12611
12685
|
async function runSessionEnd(args, ambient = null, sessionHolder = null) {
|
|
12686
|
+
// The closer reads a session, but its own argv refusals still belong to the
|
|
12687
|
+
// invoking command. Check them before entering closeRunSession's nested scope.
|
|
12688
|
+
if (Object.hasOwn(args, "new-run") || Object.hasOwn(args, "run-id")) {
|
|
12689
|
+
throw refused("run end uses the saved session's run ID; --new-run and --run-id are not accepted.");
|
|
12690
|
+
}
|
|
12691
|
+
validateRunRecordArgv(args);
|
|
12612
12692
|
// Use the session resolved once in main() (single source of truth).
|
|
12613
12693
|
const found = ambient;
|
|
12614
12694
|
if (!found) {
|
|
@@ -12643,11 +12723,15 @@ async function runSessionEnd(args, ambient = null, sessionHolder = null) {
|
|
|
12643
12723
|
// invoking command implements the flag; a closer invoked by a command that
|
|
12644
12724
|
// does not (the QA auto-end) drops it from extraArgs before calling — see
|
|
12645
12725
|
// DRY_RUN_COMMANDS and autoEndRunSessionAfterTerminalQa.
|
|
12646
|
-
const RUN_RECORD_INHERITABLE_FLAGS = Object.freeze([
|
|
12726
|
+
export const RUN_RECORD_INHERITABLE_FLAGS = Object.freeze([
|
|
12647
12727
|
"context", "report", "qa-verdict", "journal", "surfaces", "primary-surface", "surface-confidence",
|
|
12648
12728
|
"agent-input-tokens", "agent-output-tokens", "agent-tool-output-tokens", "agent-total-tokens", "agent-elapsed-ms", "agent-model", "agent-usage-source",
|
|
12649
12729
|
"no-remit", "no-write", "proxy-base", "dry-run", "json",
|
|
12650
12730
|
]);
|
|
12731
|
+
const RUN_RECORD_BOOLEAN_INHERITABLE_FLAGS = new Set(["no-remit", "no-write", "dry-run", "json"]);
|
|
12732
|
+
const RUN_RECORD_INTEGER_INHERITABLE_FLAGS = new Set([
|
|
12733
|
+
"agent-input-tokens", "agent-output-tokens", "agent-tool-output-tokens", "agent-total-tokens", "agent-elapsed-ms",
|
|
12734
|
+
]);
|
|
12651
12735
|
|
|
12652
12736
|
// The run-record argv that closes `session`: its run_id and journal, the
|
|
12653
12737
|
// packet the closer resolved, and only the inheritable flags of `extraArgs`.
|
|
@@ -12674,15 +12758,23 @@ export function runSessionEndArgs(session, packet, extraArgs = {}) {
|
|
|
12674
12758
|
async function closeRunSession(found, { packet, extraArgs = {}, silent = false, promptForConsent = true, onError = null } = {}) {
|
|
12675
12759
|
const endArgs = runSessionEndArgs(found.session, packet, extraArgs);
|
|
12676
12760
|
try {
|
|
12677
|
-
|
|
12761
|
+
// No internal closeout currently constructs a refusal before its invoking
|
|
12762
|
+
// command journals: the sweep forwards only remit controls tolerated by
|
|
12763
|
+
// run-record, run end validates its own argv first, and QA auto-end runs
|
|
12764
|
+
// after QA persistence. Keep the scope at this boundary so a future
|
|
12765
|
+
// closeout refusal swallowed by onError cannot mark the outer invocation.
|
|
12766
|
+
const summary = await runWithRefusalScope(() => runRecordCommand(endArgs, found, { silent, promptForConsent }));
|
|
12678
12767
|
// Clearing the session is a write like any other, so a closer carrying
|
|
12679
12768
|
// --dry-run leaves it open: the operator sees the record the close would
|
|
12680
12769
|
// assemble and can still close for real afterwards.
|
|
12681
12770
|
if (endArgs["dry-run"] !== true) clearRunSession(found.path);
|
|
12682
12771
|
return summary;
|
|
12683
12772
|
} catch (error) {
|
|
12684
|
-
|
|
12685
|
-
|
|
12773
|
+
// A nested run-record refusal is a failure of the invoking closer, which
|
|
12774
|
+
// has already read session state. Do not pass its tag to outer persistence.
|
|
12775
|
+
const failure = error?.code === REFUSED_INVOCATION ? new Error(error.message, { cause: error }) : error;
|
|
12776
|
+
if (!onError) throw failure;
|
|
12777
|
+
onError(failure);
|
|
12686
12778
|
return null;
|
|
12687
12779
|
}
|
|
12688
12780
|
}
|
|
@@ -12892,6 +12984,10 @@ export function describeCampaignKeyRejection(rejected) {
|
|
|
12892
12984
|
// docs/workflow-findings-sidecar.md.
|
|
12893
12985
|
async function runRecordCommand(args, ambient = null, { silent = false, promptForConsent = true } = {}) {
|
|
12894
12986
|
const packetPath = resolve(requireArg(args, "packet"));
|
|
12987
|
+
if (args["new-run"] === true && optionalString(args["run-id"])) {
|
|
12988
|
+
throw refused("run-record: --new-run and --run-id are exclusive; --run-id names the run to re-emit, --new-run mints a fresh one.");
|
|
12989
|
+
}
|
|
12990
|
+
const agentUsage = refusing(() => parseAgentUsageArgs(args));
|
|
12895
12991
|
// --dry-run assembles the record and shows it, then writes and sends
|
|
12896
12992
|
// nothing. It differs from --no-write, which skips the assembly's reads
|
|
12897
12993
|
// as well; combining the two is allowed and still writes nothing.
|
|
@@ -12919,10 +13015,12 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
12919
13015
|
: inferQaVerdictPath({ packet, report, reportPath: reportExists ? reportPath : null, targetRepo, baseDir });
|
|
12920
13016
|
const qaVerdictExists = qaVerdictPath != null && existsSync(qaVerdictPath);
|
|
12921
13017
|
const qaVerdict = qaVerdictExists ? readJson(qaVerdictPath) : null;
|
|
13018
|
+
if (qaVerdict && (packet.spec?.local_spec_id != null || qaVerdict.local_spec_id != null) && !qaVerdictIdentityMatch(qaVerdict, packet)) {
|
|
13019
|
+
throw new Error("run-record: QA verdict does not match this packet's local_spec_id; foreign evidence cannot be recorded as this local campaign.");
|
|
13020
|
+
}
|
|
12922
13021
|
|
|
12923
13022
|
const journalPath = resolveJournalPath(args);
|
|
12924
13023
|
const journal = readJournal(journalPath);
|
|
12925
|
-
const agentUsage = parseAgentUsageArgs(args);
|
|
12926
13024
|
|
|
12927
13025
|
// run_id: explicit flag > --new-run (mint) > active run session > the most
|
|
12928
13026
|
// recent Run Record on disk for this packet's campaign > freshly minted.
|
|
@@ -12935,9 +13033,6 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
12935
13033
|
// record for this campaign exists, or when --new-run asks for it. The
|
|
12936
13034
|
// source travels on the stdout envelope only: the record's schema is
|
|
12937
13035
|
// hashed surface and does not carry it.
|
|
12938
|
-
if (args["new-run"] === true && optionalString(args["run-id"])) {
|
|
12939
|
-
throw new Error("run-record: --new-run and --run-id are exclusive; --run-id names the run to re-emit, --new-run mints a fresh one.");
|
|
12940
|
-
}
|
|
12941
13036
|
const listOnly = args.list === true;
|
|
12942
13037
|
// The directory is scanned only when something reads it: --list, or an id
|
|
12943
13038
|
// that nothing else names. An explicit --run-id, --new-run or an open
|
|
@@ -13134,6 +13229,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
13134
13229
|
consent: { state: consent.state, source: consent.source },
|
|
13135
13230
|
identity: {
|
|
13136
13231
|
map_id: optionalString(packet.spec?.map_id),
|
|
13232
|
+
...localSpecIdentityFields(packet.spec),
|
|
13137
13233
|
campaign_slug: optionalString(packet.campaign?.public_route_slug),
|
|
13138
13234
|
template_family: optionalString(packet.assembly?.template_family),
|
|
13139
13235
|
entry_point_shape: "packet",
|
|
@@ -13467,7 +13563,8 @@ function inferQaVerdictPath({ packet, report, reportPath = null, targetRepo = nu
|
|
|
13467
13563
|
report,
|
|
13468
13564
|
reportPath: reportPath || (targetRepo ? campaignSidecarPaths(targetRepo).reportPath : null),
|
|
13469
13565
|
roots: [targetRepo, baseDir],
|
|
13470
|
-
}).filter((candidate) => candidate.verdict && candidate.trusted
|
|
13566
|
+
}).filter((candidate) => candidate.verdict && candidate.trusted
|
|
13567
|
+
&& ((packet?.spec?.local_spec_id == null && candidate.verdict.local_spec_id == null) || candidate.identityMatch));
|
|
13471
13568
|
eligible.sort((a, b) => {
|
|
13472
13569
|
const scoreDelta = qaVerdictCandidateScore(b, packet) - qaVerdictCandidateScore(a, packet);
|
|
13473
13570
|
if (scoreDelta !== 0) return scoreDelta;
|
|
@@ -13508,6 +13605,26 @@ function parseRunRecordSurfaces(value) {
|
|
|
13508
13605
|
return surfaces;
|
|
13509
13606
|
}
|
|
13510
13607
|
|
|
13608
|
+
function validateRunRecordArgv(args) {
|
|
13609
|
+
for (const flag of RUN_RECORD_INHERITABLE_FLAGS) {
|
|
13610
|
+
// The integer flags are checked below by parseAgentUsageArgs, which keeps
|
|
13611
|
+
// their non-negative-integer diagnostics for bare and blank values.
|
|
13612
|
+
if (!RUN_RECORD_BOOLEAN_INHERITABLE_FLAGS.has(flag) && !RUN_RECORD_INTEGER_INHERITABLE_FLAGS.has(flag) && Object.hasOwn(args, flag)) requireArg(args, flag);
|
|
13613
|
+
}
|
|
13614
|
+
if (Object.hasOwn(args, "run-id")) requireArg(args, "run-id");
|
|
13615
|
+
if (Object.hasOwn(args, "new-run") && args["new-run"] !== true) {
|
|
13616
|
+
throw refused("--new-run takes no value.");
|
|
13617
|
+
}
|
|
13618
|
+
if (args["new-run"] === true && optionalString(args["run-id"])) {
|
|
13619
|
+
throw refused("run-record: --new-run and --run-id are exclusive; --run-id names the run to re-emit, --new-run mints a fresh one.");
|
|
13620
|
+
}
|
|
13621
|
+
return {
|
|
13622
|
+
agentUsage: refusing(() => parseAgentUsageArgs(args)),
|
|
13623
|
+
dryRun: isDryRun(args),
|
|
13624
|
+
parsedSurfaces: parseRunRecordSurfaces(args.surfaces),
|
|
13625
|
+
};
|
|
13626
|
+
}
|
|
13627
|
+
|
|
13511
13628
|
function parseAgentUsageArgs(args) {
|
|
13512
13629
|
const fields = {
|
|
13513
13630
|
"agent-input-tokens": "input_tokens",
|
package/src/finding-cause.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { campaignIdentitiesMatch } from "./spec-source-identity.mjs";
|
|
1
2
|
// Per-finding cause class — "did the change under test cause this?"
|
|
2
3
|
//
|
|
3
4
|
// A run that surfaces eleven findings, none of them caused by the change being
|
|
@@ -340,12 +341,13 @@ import { readRunRecordsForTarget } from "./run-record.mjs";
|
|
|
340
341
|
* against a non-adjacent one and report findings introduced in between as
|
|
341
342
|
* pre-existing.
|
|
342
343
|
*/
|
|
343
|
-
export function findPriorRunRecord({ baseDir, mapId = null, currentRunId = null } = {}) {
|
|
344
|
+
export function findPriorRunRecord({ baseDir, mapId = null, localSpecId = null, currentRunId = null } = {}) {
|
|
344
345
|
if (!text(baseDir)) return null;
|
|
345
346
|
for (const entry of readRunRecordsForTarget(baseDir)) {
|
|
346
347
|
const record = entry?.record;
|
|
347
348
|
if (!record || typeof record !== "object" || Array.isArray(record)) continue;
|
|
348
349
|
if (currentRunId && record.run_id === currentRunId) continue;
|
|
350
|
+
if (localSpecId && !campaignIdentitiesMatch(record.identity, { local_spec_id: localSpecId })) continue;
|
|
349
351
|
if (text(mapId) && text(record.identity?.map_id) !== text(mapId)) continue;
|
|
350
352
|
return record;
|
|
351
353
|
}
|
|
@@ -376,7 +378,9 @@ function readPriorVerdictFile(path) {
|
|
|
376
378
|
// verdicts are filed and matched under the names the record itself stores.
|
|
377
379
|
function recordIdentityForDiscovery(record) {
|
|
378
380
|
return {
|
|
379
|
-
|
|
381
|
+
// This is a read-side comparison, not an artifact writer. Preserve even an
|
|
382
|
+
// invalid local marker so discovery rejects it instead of using its Map ID.
|
|
383
|
+
spec: { map_id: text(record?.identity?.map_id) || null, local_spec_id: record?.identity?.local_spec_id ?? null },
|
|
380
384
|
campaign: { public_route_slug: text(record?.identity?.campaign_slug) || null },
|
|
381
385
|
};
|
|
382
386
|
}
|
|
@@ -444,8 +448,8 @@ function locateExternalPriorVerdict({ targetRepo, record, ref }) {
|
|
|
444
448
|
* The single-record boundary is unchanged: this still reads the final attempt
|
|
445
449
|
* of exactly one earlier run, never a merged view across runs.
|
|
446
450
|
*/
|
|
447
|
-
export function loadPriorQaVerdict({ baseDir, targetRepo = null, mapId = null, currentRunId = null } = {}) {
|
|
448
|
-
const record = findPriorRunRecord({ baseDir, mapId, currentRunId });
|
|
451
|
+
export function loadPriorQaVerdict({ baseDir, targetRepo = null, mapId = null, localSpecId = null, currentRunId = null } = {}) {
|
|
452
|
+
const record = findPriorRunRecord({ baseDir, mapId, localSpecId, currentRunId });
|
|
449
453
|
if (!record) return { verdict: null, record: null, path: null, reason: "no_prior_run" };
|
|
450
454
|
const ref = (Array.isArray(record.artifacts) ? record.artifacts : []).findLast((artifact) => artifact?.kind === "qa_verdict");
|
|
451
455
|
const refPath = text(ref?.path);
|
|
@@ -468,8 +472,8 @@ export function loadPriorQaVerdict({ baseDir, targetRepo = null, mapId = null, c
|
|
|
468
472
|
* carries these, so doctor cause labels work against existing history with no
|
|
469
473
|
* upgrade window. Returns `{ prior, record, reason }`.
|
|
470
474
|
*/
|
|
471
|
-
function loadPriorDoctorFindings({ baseDir, mapId = null, currentRunId = null } = {}) {
|
|
472
|
-
const record = findPriorRunRecord({ baseDir, mapId, currentRunId });
|
|
475
|
+
function loadPriorDoctorFindings({ baseDir, mapId = null, localSpecId = null, currentRunId = null } = {}) {
|
|
476
|
+
const record = findPriorRunRecord({ baseDir, mapId, localSpecId, currentRunId });
|
|
473
477
|
if (!record) return { prior: null, record: null, reason: "no_prior_run" };
|
|
474
478
|
const prior = priorSetFromRunRecordDoctor(record);
|
|
475
479
|
if (!prior) return { prior: null, record, reason: "prior_run_without_doctor_observations" };
|
|
@@ -492,9 +496,9 @@ function loadPriorDoctorFindings({ baseDir, mapId = null, currentRunId = null }
|
|
|
492
496
|
* raw map id) have no Run Record home, so they get `unknown` / `no_prior_run`
|
|
493
497
|
* throughout — which is the truth, not a silence.
|
|
494
498
|
*/
|
|
495
|
-
export function annotateQaAssertionCauses(assertions, { baseDir = null, targetRepo = null, mapId = null, currentRunId = null, isFinding } = {}) {
|
|
499
|
+
export function annotateQaAssertionCauses(assertions, { baseDir = null, targetRepo = null, mapId = null, localSpecId = null, currentRunId = null, isFinding } = {}) {
|
|
496
500
|
const lookup = baseDir
|
|
497
|
-
? loadPriorQaVerdict({ baseDir, targetRepo, mapId, currentRunId })
|
|
501
|
+
? loadPriorQaVerdict({ baseDir, targetRepo, mapId, localSpecId, currentRunId })
|
|
498
502
|
: { verdict: null, record: null, path: null, reason: "no_prior_run" };
|
|
499
503
|
const prior = lookup.verdict ? priorSetFromVerdict(lookup.verdict, { isFinding }) : null;
|
|
500
504
|
const noPriorReason = lookup.reason || "no_prior_run";
|
|
@@ -528,9 +532,9 @@ const CAUSE_SUMMARY_SCHEMA = "campaigns-os-finding-cause/v0";
|
|
|
528
532
|
* The doctor twin. `errors` and `warnings` are the doctor output's own arrays;
|
|
529
533
|
* both are stamped in place. Returns the same summary shape.
|
|
530
534
|
*/
|
|
531
|
-
export function annotateDoctorIssueCauses({ errors = [], warnings = [], baseDir = null, mapId = null, currentRunId = null } = {}) {
|
|
535
|
+
export function annotateDoctorIssueCauses({ errors = [], warnings = [], baseDir = null, mapId = null, localSpecId = null, currentRunId = null } = {}) {
|
|
532
536
|
const lookup = baseDir
|
|
533
|
-
? loadPriorDoctorFindings({ baseDir, mapId, currentRunId })
|
|
537
|
+
? loadPriorDoctorFindings({ baseDir, mapId, localSpecId, currentRunId })
|
|
534
538
|
: { prior: null, record: null, reason: "no_prior_run" };
|
|
535
539
|
const noPriorReason = lookup.reason || "no_prior_run";
|
|
536
540
|
const findings = [];
|