@nextcommerce/campaigns-os 1.50.0 → 1.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +474 -0
- package/agents/claude/CLAUDE.md +4 -2
- package/agents/codex/AGENTS.md +3 -1
- package/agents/copilot/copilot-instructions.md +3 -1
- package/agents/cursor/campaigns-os.mdc +3 -1
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
- package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
- package/campaign-spec/dist/rules/design-source-shape.js +13 -3
- package/campaign-spec/dist/rules/sdk-version.js +2 -1
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +26 -46
- package/contracts/effects.v1.json +282 -14
- package/contracts/release-ledger.json +1233 -0
- package/contracts/supported-surface.json +2 -2
- package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
- package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
- package/docs/brand-theme-bridge.md +2 -0
- package/docs/build-packet.md +148 -23
- package/docs/campaign-build-brief.md +57 -2
- package/docs/effects.md +14 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +12 -0
- package/docs/progress-snapshots.md +2 -4
- package/docs/qa-and-test-orders.md +47 -4
- package/docs/readback.md +19 -2
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/package.json +1 -1
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -3
- package/skills/next-campaigns-os/SKILL.md +11 -20
- package/skills/next-campaigns-os/references/session-intake.md +17 -9
- package/skills/next-campaigns-os-setup/SKILL.md +4 -3
- package/skills/next-campaigns-polish/SKILL.md +7 -5
- package/skills/next-campaigns-qa/SKILL.md +8 -5
- package/skills.json +10 -10
- package/src/adapter-decision-contract.mjs +1 -1
- package/src/brand-theme.mjs +33 -20
- package/src/brief-summary.mjs +275 -0
- package/src/build-brief.mjs +229 -30
- package/src/built-site-scope.mjs +39 -6
- package/src/built-smoke-qc.mjs +1117 -0
- package/src/campaign-identity.mjs +36 -2
- package/src/cart-placeholders.mjs +730 -0
- package/src/cli.mjs +467 -143
- package/src/commercial-journey.mjs +65 -4
- package/src/commercial-parity.mjs +6 -1
- package/src/contrast.mjs +649 -0
- package/src/design-source-publication.mjs +110 -3
- package/src/doctor/checks.mjs +415 -33
- package/src/doctor/inspect.mjs +53 -2
- package/src/doctor/next-step.mjs +16 -7
- package/src/input-currency.mjs +394 -0
- package/src/input-refresh.mjs +708 -0
- package/src/invocation.mjs +5 -2
- package/src/local-preview-policy.mjs +1 -1
- package/src/local-proof.mjs +4 -1
- package/src/polish-browser.mjs +660 -141
- package/src/polish-capture.mjs +1 -1
- package/src/polish-deadline.mjs +7 -0
- package/src/polish-media-weight.mjs +492 -0
- package/src/polish-node.mjs +400 -28
- package/src/polish-readability.mjs +828 -0
- package/src/progress-node.mjs +19 -3
- package/src/qa-browser.mjs +439 -180
- package/src/qa-content-params.mjs +889 -0
- package/src/qa-node.mjs +116 -13
- package/src/qa-order-bump.mjs +22 -1
- package/src/qa-policy-links.mjs +1019 -0
- package/src/qa-tracking-params.mjs +1389 -0
- package/src/qa-url-privacy.mjs +168 -0
- package/src/qc-accept.mjs +482 -0
- package/src/qc-check-registry.mjs +91 -0
- package/src/qc-results.mjs +1341 -0
- package/src/readback.mjs +58 -10
- package/src/sdk-attribute-index.mjs +71 -0
- package/src/sdk-markup.mjs +2 -2
- package/src/sdk-storage-compatibility.mjs +63 -3
- package/src/source-prep.mjs +37 -7
- package/src/stage-ledger.mjs +235 -44
- package/src/stage-record.mjs +165 -33
- package/src/tooling-setup.mjs +1 -1
package/src/stage-record.mjs
CHANGED
|
@@ -29,7 +29,8 @@ import { BRAND_LAYER_FILENAMES } from "./brand-theme.mjs";
|
|
|
29
29
|
import { computeBuildFingerprint, resolveBuiltSiteScope } from "./built-site-scope.mjs";
|
|
30
30
|
import { resolveCampaignWorkspace, targetRepoFor } from "./campaign-workspace.mjs";
|
|
31
31
|
import { isObject, optionalString, readJsonIfExists, requireArg } from "./cli-helpers.mjs";
|
|
32
|
-
import { isLocalServePacket } from "./local-proof.mjs";
|
|
32
|
+
import { LOCAL_PROOF_BUILD_ENVIRONMENT, LOCAL_PROOF_PRODUCTION_ENVIRONMENT, isLocalServePacket } from "./local-proof.mjs";
|
|
33
|
+
import { CARRIED_FORWARD } from "./local-preview-policy.mjs";
|
|
33
34
|
import { isLoopbackHostname } from "./remit.mjs";
|
|
34
35
|
import { campaignRouteRoot } from "./route-identity.mjs";
|
|
35
36
|
import { writeJsonAtomic } from "./doctor-sidecar.mjs";
|
|
@@ -45,17 +46,30 @@ import {
|
|
|
45
46
|
evaluatePolishGate,
|
|
46
47
|
} from "./polish-gate.mjs";
|
|
47
48
|
import { evaluateRecordedHiddenEagerMediaCheckpoint } from "./polish-node.mjs";
|
|
48
|
-
import {
|
|
49
|
+
import { effectiveStageStatus, effectiveStatusIsTerminal, inputStamps, stageWriteInputs } from "./input-currency.mjs";
|
|
50
|
+
import { demoteStages, recordBrief, recordSpec } from "./input-refresh.mjs";
|
|
51
|
+
import { applyDerivedAssemblyReportSummary, archiveStageRecord, assemblyReportMatchesPacket, commitAssemblyReport, inputChangeFor } from "./stage-ledger.mjs";
|
|
49
52
|
import { withTargetLockSync } from "./target-lock.mjs";
|
|
50
53
|
import { commerceScopeFromScope } from "./theme-gate.mjs";
|
|
51
54
|
|
|
52
55
|
export const RECORD_STAGES = Object.freeze(["setup", "build", "polish", "theme", "deploy"]);
|
|
53
56
|
|
|
54
|
-
// Every flag `record` reads, plus the
|
|
55
|
-
// lifecycle journal
|
|
56
|
-
|
|
57
|
+
// Every flag `record` reads, plus the three any command accepts (run id,
|
|
58
|
+
// lifecycle journal, and the deviation reason the deviation notice asks
|
|
59
|
+
// agents to declare). Anything else is refused before a file is read.
|
|
60
|
+
const RECORD_FLAGS = Object.freeze(["packet", "context", "report", "dry-run", "json", "run-id", "lifecycle-journal", "deviation-reason"]);
|
|
57
61
|
const POLISH_RECORD_FLAGS = Object.freeze(["evidence"]);
|
|
58
62
|
const DEPLOY_RECORD_FLAGS = Object.freeze(["base-url"]);
|
|
63
|
+
const BUILD_RECORD_FLAGS = Object.freeze(["build-environment"]);
|
|
64
|
+
// `record brief` saves a brief file: --brief names it (otherwise intake's
|
|
65
|
+
// discovery finds it).
|
|
66
|
+
const BRIEF_RECORD_FLAGS = Object.freeze(["brief"]);
|
|
67
|
+
// `record spec` binds the CampaignSpec as it is now; it takes no extra flag.
|
|
68
|
+
const RECORD_SUBCOMMANDS = Object.freeze([...RECORD_STAGES, "brief", "spec"]);
|
|
69
|
+
// The page-kit environment the built output was rendered in, recorded on
|
|
70
|
+
// stages.assembly.evidence.build_environment (local proof mode builds in
|
|
71
|
+
// development; doctor and page-kit parity read it).
|
|
72
|
+
export const BUILD_ENVIRONMENTS = Object.freeze([LOCAL_PROOF_BUILD_ENVIRONMENT, LOCAL_PROOF_PRODUCTION_ENVIRONMENT]);
|
|
59
73
|
|
|
60
74
|
// The keys a --evidence file may carry. `evidence` is stages.polish.evidence;
|
|
61
75
|
// `repair_loop_defect` is report.theme.repair_loop_defect; `blockers` (status
|
|
@@ -74,6 +88,13 @@ function schemaValidator(file) {
|
|
|
74
88
|
return validators.get(file);
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
// The value at an Ajv instancePath (JSON Pointer) in the validated document.
|
|
92
|
+
function valueAt(document, pointer) {
|
|
93
|
+
return pointer.split("/").slice(1)
|
|
94
|
+
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))
|
|
95
|
+
.reduce((node, key) => (node == null ? undefined : node[key]), document);
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
// Ajv's instancePath (`/theme/repair_loop_defect`) as the dotted field name
|
|
78
99
|
// the rest of the toolkit prints (`theme.repair_loop_defect`).
|
|
79
100
|
function schemaProblems(file, value, label) {
|
|
@@ -83,7 +104,11 @@ function schemaProblems(file, value, label) {
|
|
|
83
104
|
const problems = [];
|
|
84
105
|
for (const error of validate.errors || []) {
|
|
85
106
|
const field = error.instancePath.split("/").filter(Boolean).join(".") || "(root)";
|
|
86
|
-
|
|
107
|
+
// Ajv's enum message names no values; the allowed list is the remedy.
|
|
108
|
+
const allowed = error.keyword === "enum" && Array.isArray(error.params?.allowedValues)
|
|
109
|
+
? `: ${error.params.allowedValues.map((allowedValue) => JSON.stringify(allowedValue)).join(", ")} (got ${JSON.stringify(valueAt(value, error.instancePath))})`
|
|
110
|
+
: "";
|
|
111
|
+
const line = `${label} ${field} ${error.message}${allowed}`;
|
|
87
112
|
if (seen.has(line)) continue;
|
|
88
113
|
seen.add(line);
|
|
89
114
|
problems.push(line);
|
|
@@ -96,33 +121,45 @@ function typeName(value) {
|
|
|
96
121
|
return Array.isArray(value) ? "array" : typeof value;
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
|
|
124
|
+
// visual_review keys only `polish capture` writes. `record polish --evidence`
|
|
125
|
+
// refuses them by name and carries the captured values forward.
|
|
126
|
+
export const PACKAGE_OWNED_VISUAL_REVIEW_KEYS = Object.freeze(["page_load", "media_weight", "readability"]);
|
|
127
|
+
export const PACKAGE_OWNED_KEY_REFUSAL = "package_owned_key";
|
|
128
|
+
|
|
129
|
+
export function refuseRecord(stage, problems) {
|
|
100
130
|
return new Error(`record ${stage} refused; nothing was written:\n${problems.map((problem) => `- ${problem}`).join("\n")}`);
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
export function parseRecordArgs(args) {
|
|
104
134
|
const stage = args._[1];
|
|
105
|
-
if (!
|
|
106
|
-
throw refused(`Use: ${cmd("record")} <${
|
|
135
|
+
if (!RECORD_SUBCOMMANDS.includes(stage) || args._.length !== 2) {
|
|
136
|
+
throw refused(`Use: ${cmd("record")} <${RECORD_SUBCOMMANDS.join("|")}> --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json]; record polish also takes --evidence <polish-evidence.json>, record deploy --base-url <served url>, record build [--build-environment <${BUILD_ENVIRONMENTS.join("|")}>], and record brief [--brief <yaml|json>].`);
|
|
107
137
|
}
|
|
108
|
-
const known = new Set([...RECORD_FLAGS, ...(stage === "polish" ? POLISH_RECORD_FLAGS : []), ...(stage === "deploy" ? DEPLOY_RECORD_FLAGS : [])]);
|
|
138
|
+
const known = new Set([...RECORD_FLAGS, ...(stage === "polish" ? POLISH_RECORD_FLAGS : []), ...(stage === "deploy" ? DEPLOY_RECORD_FLAGS : []), ...(stage === "build" ? BUILD_RECORD_FLAGS : []), ...(stage === "brief" ? BRIEF_RECORD_FLAGS : [])]);
|
|
109
139
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !known.has(key));
|
|
110
140
|
if (unknown.length) {
|
|
111
141
|
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for record ${stage}: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${[...known].map((key) => `--${key}`).join(", ")}.`);
|
|
112
142
|
}
|
|
113
|
-
for (const flag of ["context", "report", "run-id", "lifecycle-journal"]) {
|
|
143
|
+
for (const flag of ["context", "report", "run-id", "lifecycle-journal", "deviation-reason", "brief"]) {
|
|
114
144
|
if (Object.hasOwn(args, flag)) requireArg(args, flag);
|
|
115
145
|
}
|
|
116
146
|
if (Object.hasOwn(args, "dry-run") && args["dry-run"] !== true) {
|
|
117
147
|
throw refused(`--dry-run takes no value (got ${JSON.stringify(args["dry-run"])}); write \`--dry-run\` on its own, after the other flags.`);
|
|
118
148
|
}
|
|
119
149
|
if (Object.hasOwn(args, "json") && args.json !== true) throw refused("--json is a boolean flag and takes no value.");
|
|
150
|
+
const buildEnvironment = Object.hasOwn(args, "build-environment") ? args["build-environment"] : null;
|
|
151
|
+
if (buildEnvironment !== null && !BUILD_ENVIRONMENTS.includes(buildEnvironment)) {
|
|
152
|
+
throw refused(`--build-environment must be one of: ${BUILD_ENVIRONMENTS.join(", ")} (got ${JSON.stringify(buildEnvironment)}).`);
|
|
153
|
+
}
|
|
120
154
|
return {
|
|
121
155
|
stage,
|
|
122
156
|
packetPath: resolve(requireArg(args, "packet")),
|
|
123
157
|
evidencePath: stage === "polish" ? resolve(requireArg(args, "evidence")) : null,
|
|
124
158
|
baseUrl: stage === "deploy" ? requireArg(args, "base-url") : null,
|
|
125
159
|
dryRun: args["dry-run"] === true,
|
|
160
|
+
buildEnvironment,
|
|
161
|
+
briefPath: stage === "brief" && Object.hasOwn(args, "brief") ? resolve(args.brief) : null,
|
|
162
|
+
deviationReason: optionalString(args["deviation-reason"]),
|
|
126
163
|
};
|
|
127
164
|
}
|
|
128
165
|
|
|
@@ -181,8 +218,13 @@ export function readPolishEvidenceFile(path) {
|
|
|
181
218
|
}
|
|
182
219
|
if (evidence.visual_review !== undefined && !isObject(evidence.visual_review)) {
|
|
183
220
|
problems.push(`evidence.visual_review must be an object with a screenshots array (got ${typeName(evidence.visual_review)}).`);
|
|
184
|
-
} else if (isObject(evidence.visual_review)
|
|
185
|
-
|
|
221
|
+
} else if (isObject(evidence.visual_review)) {
|
|
222
|
+
// Package-owned keys are refused by name and listed first, so the
|
|
223
|
+
// refusal leads with its code.
|
|
224
|
+
const owned = PACKAGE_OWNED_VISUAL_REVIEW_KEYS.filter((key) => Object.hasOwn(evidence.visual_review, key));
|
|
225
|
+
if (owned.length) {
|
|
226
|
+
problems.unshift(`${PACKAGE_OWNED_KEY_REFUSAL}: ${owned.map((key) => `evidence.visual_review.${key}`).join(" and ")} ${owned.length === 1 ? "is" : "are"} written only by ${cmd("polish")} capture; remove ${owned.length === 1 ? "it" : "them"} from the file (the captured value on the report is kept).`);
|
|
227
|
+
}
|
|
186
228
|
}
|
|
187
229
|
}
|
|
188
230
|
if (Object.hasOwn(input, "repair_loop_defect") && input.repair_loop_defect !== null && !isObject(input.repair_loop_defect)) {
|
|
@@ -235,28 +277,72 @@ function composeSetup(report, context, { now, recordedBy }) {
|
|
|
235
277
|
return { report: nextReport, context: nextContext };
|
|
236
278
|
}
|
|
237
279
|
|
|
238
|
-
|
|
280
|
+
// The fields a record restates on every write: its input stamps, and the
|
|
281
|
+
// operator decision that applies only to the record that carried it.
|
|
282
|
+
const STAMP_FIELDS = Object.freeze(["source_brief_material", "source_spec_material_hash"]);
|
|
283
|
+
// A replaced record in any of these statuses is checked for an input change.
|
|
284
|
+
const REPLACED_COMPLETED_STATUSES = Object.freeze(["completed", "completed_with_warnings", "completed_partial"]);
|
|
285
|
+
|
|
286
|
+
// The stamps a completed record writes: the inputs current at this write
|
|
287
|
+
// (the absent brief value for a packet without a brief). A value that cannot
|
|
288
|
+
// be computed is left out, so the record reads unknown rather than current.
|
|
289
|
+
function currentStamps(inputs) {
|
|
290
|
+
const stamps = inputStamps(inputs);
|
|
291
|
+
return Object.fromEntries(Object.entries(stamps).filter(([, value]) => value !== null));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function composeBuild(report, { now, recordedBy, fingerprint, buildEnvironment = null, inputs = {}, deviationReason = null }) {
|
|
239
295
|
const sourcePackageFingerprint = currentSourcePackageMaterialFingerprint(report);
|
|
240
|
-
const
|
|
241
|
-
|
|
296
|
+
const previousAssembly = stageObject(report, "assembly");
|
|
297
|
+
const stamps = currentStamps(inputs);
|
|
298
|
+
// A replaced completed record whose stamps differ from the current inputs
|
|
299
|
+
// records the change now, if nothing recorded it before.
|
|
300
|
+
const writeInputs = stageWriteInputs("assembly", inputs);
|
|
301
|
+
const detected = REPLACED_COMPLETED_STATUSES.includes(previousAssembly.status) ? writeInputs.detectChange(previousAssembly) : null;
|
|
302
|
+
let assembly = {
|
|
303
|
+
...withoutKeys(previousAssembly, ["source_package_material_fingerprint", "unchanged_output_reason", ...STAMP_FIELDS]),
|
|
304
|
+
...(buildEnvironment ? { evidence: { ...(isObject(previousAssembly.evidence) ? previousAssembly.evidence : {}), build_environment: buildEnvironment } } : {}),
|
|
242
305
|
stage: "assembly",
|
|
243
306
|
status: "completed",
|
|
244
307
|
build_fingerprint: fingerprint,
|
|
245
308
|
...(sourcePackageFingerprint ? { source_package_material_fingerprint: sourcePackageFingerprint } : {}),
|
|
309
|
+
...stamps,
|
|
246
310
|
completed_at: now,
|
|
247
311
|
recorded_by: recordedBy,
|
|
248
312
|
blockers: [],
|
|
249
313
|
};
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
//
|
|
314
|
+
if (detected) assembly.input_change = inputChangeFor("assembly", previousAssembly, { at: now, reason: detected });
|
|
315
|
+
// Output identical to the build the input change superseded does not make
|
|
316
|
+
// the build current, unless the operator's --deviation-reason records that
|
|
317
|
+
// the change needs no output change (completed_with_warnings).
|
|
318
|
+
const replayed = isObject(assembly.input_change) && assembly.input_change.superseded_build_fingerprint === fingerprint;
|
|
319
|
+
if (replayed && deviationReason) {
|
|
320
|
+
assembly.status = "completed_with_warnings";
|
|
321
|
+
assembly.unchanged_output_reason = {
|
|
322
|
+
text: deviationReason,
|
|
323
|
+
for_inputs: { brief_material: stamps.source_brief_material ?? null, spec_material_hash: stamps.source_spec_material_hash ?? null },
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (!replayed || deviationReason) {
|
|
327
|
+
if (writeInputs.stampsCurrent(assembly)) delete assembly.input_change;
|
|
328
|
+
}
|
|
329
|
+
assembly = archiveStageRecord(assembly, previousAssembly, assembly, { by: "record build", reason: detected || "rerecorded", at: now, stageKey: "assembly" });
|
|
330
|
+
// A detected change makes Polish and QA owed through the dependency map.
|
|
331
|
+
// Otherwise Polish evidence bound to this exact output stays and anything
|
|
332
|
+
// else is owed again; the evidence object is kept so `polish capture` has
|
|
333
|
+
// somewhere to attach page_load, and its stale identity fields are removed.
|
|
334
|
+
if (detected) {
|
|
335
|
+
const cause = detected === "spec_material_changed" ? "spec" : "presentation";
|
|
336
|
+
const demoted = demoteStages({ ...report, stages: { ...report.stages, assembly } }, { causes: [cause], now, by: "record build", only: ["polish", "qa"] });
|
|
337
|
+
return { report: demoted.report, context: null };
|
|
338
|
+
}
|
|
253
339
|
const previousPolish = stageObject(report, "polish");
|
|
254
340
|
const polishStillCurrent = stageIsTerminal(String(previousPolish.status || ""))
|
|
255
341
|
&& optionalString(previousPolish.source_build_fingerprint) === fingerprint;
|
|
256
342
|
const polish = polishStillCurrent
|
|
257
343
|
? previousPolish
|
|
258
344
|
: {
|
|
259
|
-
...withoutKeys(previousPolish, ["performed_by", "source_build_fingerprint", "source_package_material_fingerprint", "completed_at", "recorded_by"]),
|
|
345
|
+
...withoutKeys(previousPolish, ["performed_by", "source_build_fingerprint", "source_package_material_fingerprint", "completed_at", "recorded_by", ...STAMP_FIELDS]),
|
|
260
346
|
stage: "polish",
|
|
261
347
|
status: "required",
|
|
262
348
|
required_by: "build",
|
|
@@ -265,8 +351,11 @@ function composeBuild(report, { now, recordedBy, fingerprint }) {
|
|
|
265
351
|
return { report: { ...report, stages: { ...report.stages, assembly, polish } }, context: null };
|
|
266
352
|
}
|
|
267
353
|
|
|
268
|
-
function composePolish(report, { now, recordedBy, fingerprint, input }) {
|
|
354
|
+
function composePolish(report, { now, recordedBy, fingerprint, input, inputs = {} }) {
|
|
269
355
|
const previous = stageObject(report, "polish");
|
|
356
|
+
const completed = POLISH_COMPLETED_STATUSES.includes(input.status);
|
|
357
|
+
const writeInputs = stageWriteInputs("polish", inputs);
|
|
358
|
+
const detected = REPLACED_COMPLETED_STATUSES.includes(previous.status) ? writeInputs.detectChange(previous) : null;
|
|
270
359
|
const previousVisual = isObject(previous.evidence?.visual_review) ? previous.evidence.visual_review : {};
|
|
271
360
|
// A blocked or skipped record given no evidence keeps what is there (the
|
|
272
361
|
// capture's bounded evidence stays for diagnosis).
|
|
@@ -275,18 +364,21 @@ function composePolish(report, { now, recordedBy, fingerprint, input }) {
|
|
|
275
364
|
...input.evidence,
|
|
276
365
|
visual_review: {
|
|
277
366
|
...input.evidence.visual_review,
|
|
278
|
-
...(Object.hasOwn(previousVisual,
|
|
367
|
+
...Object.fromEntries(PACKAGE_OWNED_VISUAL_REVIEW_KEYS.filter((key) => Object.hasOwn(previousVisual, key)).map((key) => [key, previousVisual[key]])),
|
|
279
368
|
},
|
|
280
369
|
}
|
|
281
370
|
: previous.evidence;
|
|
282
371
|
const sourcePackageFingerprint = currentSourcePackageMaterialFingerprint(report);
|
|
283
|
-
|
|
284
|
-
...withoutKeys(previous, ["source_package_material_fingerprint", "completed_at", "skip_reason", "evidence"]),
|
|
372
|
+
let polish = {
|
|
373
|
+
...withoutKeys(previous, ["source_package_material_fingerprint", "completed_at", "skip_reason", "evidence", ...STAMP_FIELDS]),
|
|
285
374
|
stage: "polish",
|
|
286
375
|
status: input.status,
|
|
287
376
|
performed_by: POLISH_PRODUCER,
|
|
288
377
|
source_build_fingerprint: fingerprint,
|
|
289
378
|
...(sourcePackageFingerprint ? { source_package_material_fingerprint: sourcePackageFingerprint } : {}),
|
|
379
|
+
// Only a completed Polish is proof against inputs; a skip or a block
|
|
380
|
+
// carries no stamp.
|
|
381
|
+
...(completed ? currentStamps(inputs) : {}),
|
|
290
382
|
// A blocked Polish has not completed.
|
|
291
383
|
...(input.status === "blocked" ? {} : { completed_at: now }),
|
|
292
384
|
recorded_by: recordedBy,
|
|
@@ -294,6 +386,15 @@ function composePolish(report, { now, recordedBy, fingerprint, input }) {
|
|
|
294
386
|
...(input.skipReason ? { skip_reason: input.skipReason } : {}),
|
|
295
387
|
blockers: input.blockers,
|
|
296
388
|
};
|
|
389
|
+
if (detected) polish.input_change = inputChangeFor("polish", previous, { at: now, reason: detected });
|
|
390
|
+
// The change is cleared only by a completed record stamped with the current
|
|
391
|
+
// inputs whose attached package capture postdates it; any other write
|
|
392
|
+
// (a skip included) carries it.
|
|
393
|
+
if (completed && isObject(polish.input_change) && writeInputs.stampsCurrent(polish)) {
|
|
394
|
+
const capturedAt = polish.evidence?.visual_review?.page_load?.captured_at;
|
|
395
|
+
if (typeof capturedAt === "string" && Date.parse(capturedAt) > Date.parse(polish.input_change.at)) delete polish.input_change;
|
|
396
|
+
}
|
|
397
|
+
polish = archiveStageRecord(polish, previous, polish, { by: "record polish", reason: detected || "rerecorded", at: now, stageKey: "polish" });
|
|
297
398
|
const nextReport = { ...report, stages: { ...report.stages, polish } };
|
|
298
399
|
// A null defect on a report with no theme block says nothing to record.
|
|
299
400
|
if (input.hasRepairLoopDefect && (isObject(report.theme) || input.repairLoopDefect !== null)) {
|
|
@@ -580,10 +681,17 @@ function ladderProblems(stage, doctor, report) {
|
|
|
580
681
|
if (gate) return [`next answers prepare-build: ${gate.reason}`];
|
|
581
682
|
const problems = [];
|
|
582
683
|
for (const earlier of NEXT_STAGE_ORDER.slice(0, NEXT_STAGE_ORDER.indexOf(stage))) {
|
|
684
|
+
// The rule next's stage picker reads: on the local preview a missing polish
|
|
685
|
+
// is carried forward (local-preview-policy), and next moves on to deploy.
|
|
686
|
+
if (earlier === "polish" && doctor.derived?.polish_gate?.status === CARRIED_FORWARD) continue;
|
|
687
|
+
// The effective status: a stage owed again by an input change reads
|
|
688
|
+
// required, and one whose inputs cannot be confirmed reads unknown.
|
|
583
689
|
const key = reportKeyForCliStage(earlier);
|
|
584
690
|
const status = String(report.stages[key]?.status || "");
|
|
585
|
-
|
|
586
|
-
|
|
691
|
+
const effective = effectiveStageStatus(key, report.stages[key], doctor.derived?.input_currency);
|
|
692
|
+
if (!effectiveStatusIsTerminal(effective)) {
|
|
693
|
+
const why = effective === status ? "" : ` (recorded "${status || "(unset)"}"; ${doctor.derived?.input_currency?.reasons?.[key] || "inputs not confirmed"})`;
|
|
694
|
+
problems.push(`stages.${key}.status is "${effective || "(unset)"}"${why}, so next answers ${earlier}; run ${cmd("record")} ${earlier} first.`);
|
|
587
695
|
}
|
|
588
696
|
}
|
|
589
697
|
return problems;
|
|
@@ -671,7 +779,24 @@ function deployFacts(doctor, packet, probe) {
|
|
|
671
779
|
* records through recordStageCommand.
|
|
672
780
|
*/
|
|
673
781
|
export async function recordCommand(args, options = {}) {
|
|
674
|
-
const { stage, packetPath, baseUrl } = parseRecordArgs(args);
|
|
782
|
+
const { stage, packetPath, baseUrl, briefPath, dryRun } = parseRecordArgs(args);
|
|
783
|
+
if (stage === "brief") {
|
|
784
|
+
return recordBrief({
|
|
785
|
+
packetPath,
|
|
786
|
+
briefPath,
|
|
787
|
+
contextPath: args.context ? resolve(args.context) : undefined,
|
|
788
|
+
reportPath: args.report ? resolve(args.report) : undefined,
|
|
789
|
+
dryRun,
|
|
790
|
+
}, options.now ? { now: options.now } : {});
|
|
791
|
+
}
|
|
792
|
+
if (stage === "spec") {
|
|
793
|
+
return recordSpec({
|
|
794
|
+
packetPath,
|
|
795
|
+
contextPath: args.context ? resolve(args.context) : undefined,
|
|
796
|
+
reportPath: args.report ? resolve(args.report) : undefined,
|
|
797
|
+
dryRun,
|
|
798
|
+
}, options.now ? { now: options.now } : {});
|
|
799
|
+
}
|
|
675
800
|
const probe = stage === "deploy" && existsSync(packetPath)
|
|
676
801
|
? await probeLocalPreview({ packetPath, baseUrl, ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}) })
|
|
677
802
|
: null;
|
|
@@ -697,7 +822,7 @@ function readPacketFile(stage, packetPath) {
|
|
|
697
822
|
* composed or written.
|
|
698
823
|
*/
|
|
699
824
|
export function recordStageCommand(args, { now = () => new Date(), beforeLock = null, afterDoctorRead = null, probe = null } = {}) {
|
|
700
|
-
const { stage, packetPath, evidencePath, dryRun } = parseRecordArgs(args);
|
|
825
|
+
const { stage, packetPath, evidencePath, dryRun, buildEnvironment, deviationReason } = parseRecordArgs(args);
|
|
701
826
|
if (!existsSync(packetPath)) throw new Error(`record ${stage}: Build Packet not found at ${packetPath}; run ${cmd("start")} or ${cmd("prepare-build")} first.`);
|
|
702
827
|
if (stage === "deploy" && !probe) throw new Error("record deploy needs the served-route probe; run it through recordCommand.");
|
|
703
828
|
// Operator input, not target state: no campaigns-os writer produces it.
|
|
@@ -718,7 +843,7 @@ export function recordStageCommand(args, { now = () => new Date(), beforeLock =
|
|
|
718
843
|
// A dry run writes nothing, so it takes no lock and creates no lock files
|
|
719
844
|
// (the commitAssemblyReport preview convention); it reads in the same order.
|
|
720
845
|
const run = () => recordUnderLock({
|
|
721
|
-
stage, packetPath, sidecars, lockedTarget, input, dryRun, timestamp, recordedBy, afterDoctorRead,
|
|
846
|
+
stage, packetPath, sidecars, lockedTarget, input, dryRun, timestamp, recordedBy, afterDoctorRead, buildEnvironment, deviationReason,
|
|
722
847
|
});
|
|
723
848
|
const recorded = dryRun ? run() : withTargetLockSync(lockedTarget, run, { command: `record ${stage}` });
|
|
724
849
|
const { composed, facts, layer, reportPath, contextPath, after } = recorded;
|
|
@@ -735,6 +860,7 @@ export function recordStageCommand(args, { now = () => new Date(), beforeLock =
|
|
|
735
860
|
] : [
|
|
736
861
|
`stages.${stageKey}.status = ${composed.report.stages[stageKey].status}`,
|
|
737
862
|
...(facts.fingerprint ? [`build output fingerprint ${facts.fingerprint} (doctor derived.build_output_fingerprint.value)`] : []),
|
|
863
|
+
...(stage === "build" && buildEnvironment ? [`stages.assembly.evidence.build_environment = ${buildEnvironment}`] : []),
|
|
738
864
|
...(stage === "build" ? [`stages.polish.status = ${composed.report.stages.polish.status}`] : []),
|
|
739
865
|
...(composed.context ? ["Build Context scaffold.required = false"] : []),
|
|
740
866
|
];
|
|
@@ -767,7 +893,13 @@ export function recordStageCommand(args, { now = () => new Date(), beforeLock =
|
|
|
767
893
|
// re-check, and the post-write doctor read for `next_stage`. No campaigns-os
|
|
768
894
|
// writer can rebind, rewrite or republish any of them between the read and
|
|
769
895
|
// the write.
|
|
770
|
-
|
|
896
|
+
// The inputs current at this record, as doctor read them under the lock.
|
|
897
|
+
function recordInputs(doctor) {
|
|
898
|
+
const currency = doctor.derived?.input_currency;
|
|
899
|
+
return { briefMaterial: currency?.brief?.current ?? null, specMaterial: currency?.spec?.current ?? null };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function recordUnderLock({ stage, packetPath, sidecars, lockedTarget, input, dryRun, timestamp, recordedBy, afterDoctorRead, buildEnvironment = null, deviationReason = null }) {
|
|
771
903
|
// The same workspace `next` resolves, so the record lands in the report
|
|
772
904
|
// `next` reads now, not the one it read before the lock was free.
|
|
773
905
|
const workspace = resolveCampaignWorkspace(packetPath, { ...sidecars, followContextPointer: true });
|
|
@@ -802,13 +934,13 @@ function recordUnderLock({ stage, packetPath, sidecars, lockedTarget, input, dry
|
|
|
802
934
|
const next = stage === "setup"
|
|
803
935
|
? composeSetup(report, context, { now: timestamp, recordedBy })
|
|
804
936
|
: stage === "build"
|
|
805
|
-
? composeBuild(report, { now: timestamp, recordedBy, fingerprint: facts.fingerprint })
|
|
937
|
+
? composeBuild(report, { now: timestamp, recordedBy, fingerprint: facts.fingerprint, buildEnvironment, inputs: recordInputs(doctor), deviationReason })
|
|
806
938
|
: stage === "theme"
|
|
807
939
|
? composeTheme(report, { now: timestamp, recordedBy, layer })
|
|
808
940
|
: stage === "deploy"
|
|
809
941
|
? composeDeploy(report, packet, { now: timestamp, recordedBy, probe: input })
|
|
810
|
-
: composePolish(report, { now: timestamp, recordedBy, fingerprint: facts.fingerprint, input });
|
|
811
|
-
applyDerivedAssemblyReportSummary(next.report);
|
|
942
|
+
: composePolish(report, { now: timestamp, recordedBy, fingerprint: facts.fingerprint, input, inputs: recordInputs(doctor) });
|
|
943
|
+
applyDerivedAssemblyReportSummary(next.report, recordInputs(doctor));
|
|
812
944
|
// The packet's one new value, deploy.preview_url, is checked by
|
|
813
945
|
// localPreviewUrl; the rest of the packet is as the operator left it.
|
|
814
946
|
validateRecord(stage, { report: next.report, context: next.context, packet, fingerprint: facts.fingerprint });
|
package/src/tooling-setup.mjs
CHANGED
|
@@ -119,7 +119,7 @@ export function setupTooling(args, { packageRoot, installSkills, installAgentCon
|
|
|
119
119
|
regularDestination(target, dest);
|
|
120
120
|
const source = { "CLAUDE.md": "agents/claude/CLAUDE.md", "AGENTS.md": "agents/codex/AGENTS.md", "campaigns-os.mdc": "agents/cursor/campaigns-os.mdc", "copilot-instructions.md": "agents/copilot/copilot-instructions.md" }[name];
|
|
121
121
|
if (existsSync(dest) && readFileSync(dest, "utf8") !== readFileSync(join(packageRoot, source), "utf8")) {
|
|
122
|
-
throw new Error(`tooling setup: ${dest} differs from this toolkit's context. Preserve and reconcile it before rerunning setup; no files were changed.`);
|
|
122
|
+
throw new Error(`tooling setup: ${dest} differs from this toolkit's context. Preserve and reconcile it before rerunning setup; no files were changed. Refresh it with \`install-agent-context --target .\` (overwrites all four files; review the diff), then rerun setup.`);
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
const prior = existsSync(instructions) ? readFileSync(instructions, "utf8") : "";
|