@nextcommerce/campaigns-os 1.59.0 → 1.60.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 +116 -0
- package/README.md +6 -5
- package/compatibility.json +1 -1
- package/contracts/commerce-surface-catalog.json +18 -18
- package/contracts/release-ledger.json +543 -0
- package/contracts/supported-surface.json +1 -1
- package/contracts/template-brand-contract.shared-commerce.v0.json +1 -1
- package/docs/build-packet.md +14 -12
- package/docs/campaign-build-brief.md +1 -1
- package/docs/campaigns-os-build-flow.md +1 -1
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +1 -1
- package/docs/runtime-readiness.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 +4 -4
- package/skills/next-campaigns-os/SKILL.md +3 -3
- package/skills/next-campaigns-os-setup/SKILL.md +3 -3
- package/skills/next-campaigns-polish/SKILL.md +9 -10
- package/skills/next-campaigns-qa/SKILL.md +3 -3
- package/skills/next-campaigns-sdk-bump/SKILL.md +193 -0
- package/skills.json +18 -10
- package/src/built-site-scope.mjs +5 -12
- package/src/campaign-identity.mjs +6 -4
- package/src/cli.mjs +37 -60
- package/src/doctor/checks.mjs +6 -3
- package/src/doctor/inspect.mjs +2 -8
- package/src/doctor/next-step.mjs +6 -3
- package/src/orchestration-stage-contract.mjs +6 -3
- package/src/page-kit-build-summary.mjs +21 -1
- package/src/page-kit-campaign-config.mjs +19 -0
- package/src/polish-gate.mjs +1 -5
- package/src/progress-node.mjs +2 -1
- package/src/spec-derive.mjs +24 -9
- package/src/stage-ledger.mjs +27 -1
- package/src/stage-record.mjs +18 -2
package/src/built-site-scope.mjs
CHANGED
|
@@ -290,9 +290,8 @@ export function synthesizeMinimalBuildPacket({
|
|
|
290
290
|
// the same build. The manifest is `<path>\n<sha256>\n` per file, paths
|
|
291
291
|
// relative to the output root with `/` separators and sorted by code point,
|
|
292
292
|
// so directory walk order and the absolute location never leak into the
|
|
293
|
-
// value. Page Kit (0.2.0) writes only rendered HTML and copied
|
|
294
|
-
// _site/, nothing it timestamps, so
|
|
295
|
-
// takes root-relative paths for a consumer whose build does stamp a file.
|
|
293
|
+
// value. Page Kit (0.2.x and 0.3.x) writes only rendered HTML and copied
|
|
294
|
+
// assets into _site/, nothing it timestamps, so every file is hashed.
|
|
296
295
|
// Symbolic links are never build output and are skipped, not followed.
|
|
297
296
|
export const BUILD_FINGERPRINT_ALGORITHM = "sha256-manifest/v1";
|
|
298
297
|
|
|
@@ -326,22 +325,17 @@ function sha256Hex(buffer) {
|
|
|
326
325
|
* hand (`sha256sum` over each file, sort by path, hash the lines). `ok: false`
|
|
327
326
|
* with `error` when the root is not a directory; an empty tree still hashes
|
|
328
327
|
* (the empty manifest) so a wiped _site/ reads as a changed build, not a
|
|
329
|
-
* missing one.
|
|
328
|
+
* missing one. `excluded` is always empty; doctor's build_output_fingerprint
|
|
329
|
+
* still reports it.
|
|
330
330
|
*/
|
|
331
|
-
export function computeBuildFingerprint(outputDir
|
|
331
|
+
export function computeBuildFingerprint(outputDir) {
|
|
332
332
|
const root = String(outputDir || "");
|
|
333
333
|
const base = { ok: false, algorithm: BUILD_FINGERPRINT_ALGORITHM, root, fingerprint: null, file_count: 0, excluded: [], manifest: null };
|
|
334
334
|
if (!root || !existsSync(root) || !statSync(root).isDirectory()) {
|
|
335
335
|
return { ...base, error: `Build output directory does not exist: ${root || "(empty)"}` };
|
|
336
336
|
}
|
|
337
|
-
const excludeSet = new Set((Array.isArray(exclude) ? exclude : []).map((path) => String(path).split(sep).join("/")));
|
|
338
|
-
const excluded = [];
|
|
339
337
|
const lines = [];
|
|
340
338
|
for (const path of listFilesRelative(root)) {
|
|
341
|
-
if (excludeSet.has(path)) {
|
|
342
|
-
excluded.push(path);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
339
|
lines.push(`${path}\n${sha256Hex(readFileSync(join(root, path)))}\n`);
|
|
346
340
|
}
|
|
347
341
|
const manifest = lines.join("");
|
|
@@ -350,7 +344,6 @@ export function computeBuildFingerprint(outputDir, { exclude = [] } = {}) {
|
|
|
350
344
|
ok: true,
|
|
351
345
|
fingerprint: `sha256:${sha256Hex(Buffer.from(manifest, "utf8"))}`,
|
|
352
346
|
file_count: lines.length,
|
|
353
|
-
excluded,
|
|
354
347
|
manifest,
|
|
355
348
|
error: null,
|
|
356
349
|
};
|
|
@@ -263,9 +263,11 @@ function gateBase(subject) {
|
|
|
263
263
|
* A page marked `spec_page: false` is built output that no CampaignSpec page
|
|
264
264
|
* builds to. It is still scanned, because it is still served, but a finding
|
|
265
265
|
* that names it says so: such a file is usually left in `_site/` by an earlier
|
|
266
|
-
* build, or built from an HTML file copied into the source
|
|
267
|
-
*
|
|
268
|
-
*
|
|
266
|
+
* build, or built from an HTML file copied into the source, and the repair is
|
|
267
|
+
* to remove it, not to retag it. Both are page-kit 0.2.x behaviour (no
|
|
268
|
+
* `_site/` pruning; a design export's `index.html` under `assets/` builds as
|
|
269
|
+
* a page). 0.3.0 cleans `_site/<slug>/` and ignores `.html` under `assets/`,
|
|
270
|
+
* so there a stray is a source page no spec page claims.
|
|
269
271
|
*
|
|
270
272
|
* @param {{ subject: object, pages: Array<{ page_id: string, route?: string|null,
|
|
271
273
|
* file?: string|null, content: string, spec_page?: boolean,
|
|
@@ -415,7 +417,7 @@ export function evaluateCampaignIdentity({ subject, pages = [], expectedApiKey =
|
|
|
415
417
|
// own instruction (retag, or add a missing tag to a real page) is dropped:
|
|
416
418
|
// that edit would land on the wrong file.
|
|
417
419
|
const named = stray.length > 2 ? `${stray.slice(0, -1).join(", ")}, and ${stray.at(-1)}` : stray.join(" and ");
|
|
418
|
-
finding.message = `${named} ${stray.length === 1 ? "is" : "are"} not the built page of any CampaignSpec page: built output left by an earlier build, or an HTML file copied into the source (a design export's index.html under assets/ builds as its own page). Remove the source file if there is one, delete the built file, then rebuild and record the build again; do not retag either page. What it causes: ${finding.evidence}`;
|
|
420
|
+
finding.message = `${named} ${stray.length === 1 ? "is" : "are"} not the built page of any CampaignSpec page: built output left by an earlier build, or an HTML file copied into the source (on page-kit 0.2.x a design export's index.html under assets/ builds as its own page). Remove the source file if there is one, delete the built file, then rebuild and record the build again; do not retag either page. What it causes: ${finding.evidence}`;
|
|
419
421
|
}
|
|
420
422
|
|
|
421
423
|
// evidence is the message's first sentence, kept apart only so a stray
|
package/src/cli.mjs
CHANGED
|
@@ -193,13 +193,9 @@ import {
|
|
|
193
193
|
import {
|
|
194
194
|
ASSEMBLY_REPORT_STAGE_KEYS,
|
|
195
195
|
NEXT_STAGE_ORDER,
|
|
196
|
-
STAGE_TERMINAL_STATUS_PREFIXES,
|
|
197
196
|
stageIsTerminal,
|
|
198
197
|
} from "./orchestration-stage-contract.mjs";
|
|
199
|
-
import {
|
|
200
|
-
currentBuildFingerprint,
|
|
201
|
-
evaluatePolishGate,
|
|
202
|
-
} from "./polish-gate.mjs";
|
|
198
|
+
import { currentBuildFingerprint } from "./polish-gate.mjs";
|
|
203
199
|
import {
|
|
204
200
|
assertPolishCaptureBindingUnchanged,
|
|
205
201
|
capturePolishPageLoad,
|
|
@@ -221,6 +217,7 @@ import {
|
|
|
221
217
|
import {
|
|
222
218
|
loadPageKitCampaignEntry,
|
|
223
219
|
PAGE_KIT_CAMPAIGNS_REL_PATH,
|
|
220
|
+
pageKitIgnoresAssetsHtml,
|
|
224
221
|
} from "./page-kit-campaign-config.mjs";
|
|
225
222
|
import {
|
|
226
223
|
evaluatePageKitStoreProfile,
|
|
@@ -240,6 +237,7 @@ import {
|
|
|
240
237
|
applySpecDerive,
|
|
241
238
|
formatDeriveValue,
|
|
242
239
|
isPageTreeIgnoredDir,
|
|
240
|
+
isPageTreeIgnoredPath,
|
|
243
241
|
frontmatterPermalink,
|
|
244
242
|
pageRouteForFile,
|
|
245
243
|
permalinkRoute,
|
|
@@ -293,7 +291,7 @@ import {
|
|
|
293
291
|
addPrepareBuildGateErrors,
|
|
294
292
|
assessPurchaseProofCoverage,
|
|
295
293
|
deployUrlFromReportOutputs,
|
|
296
|
-
|
|
294
|
+
readinessStatus,
|
|
297
295
|
} from "./doctor/next-step.mjs";
|
|
298
296
|
|
|
299
297
|
export { derivePackagePin, localInstallStatus };
|
|
@@ -3546,10 +3544,10 @@ export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.
|
|
|
3546
3544
|
// the route each one builds to (filename-derived, or the file's permalink;
|
|
3547
3545
|
// a permalink derive cannot read as a campaign route carries `problem` and a
|
|
3548
3546
|
// null route). Discovery follows page-kit's own: every .html outside
|
|
3549
|
-
// `_layouts/` and `_includes
|
|
3550
|
-
//
|
|
3551
|
-
// than "no pages".
|
|
3552
|
-
function listPageKitPageFiles(outputDir, publicRouteSlug) {
|
|
3547
|
+
// `_layouts/` and `_includes/` (and, from page-kit 0.3.0, the top-level
|
|
3548
|
+
// `assets/`), symlinked entries included. Returns null when the directory
|
|
3549
|
+
// does not exist, so the plan can say "no page tree" rather than "no pages".
|
|
3550
|
+
function listPageKitPageFiles(outputDir, publicRouteSlug, { assetsHtmlIgnored = false } = {}) {
|
|
3553
3551
|
if (!outputDir || !existsSync(outputDir) || !statSync(outputDir).isDirectory()) return null;
|
|
3554
3552
|
const files = [];
|
|
3555
3553
|
const walk = (dir) => {
|
|
@@ -3563,8 +3561,7 @@ function listPageKitPageFiles(outputDir, publicRouteSlug) {
|
|
|
3563
3561
|
}
|
|
3564
3562
|
if (!stats.isFile() || extname(entry.name).toLowerCase() !== ".html") continue;
|
|
3565
3563
|
const path = relative(outputDir, fullPath).split(sep).join("/");
|
|
3566
|
-
|
|
3567
|
-
if (segments.slice(0, -1).some(isPageTreeIgnoredDir)) continue;
|
|
3564
|
+
if (isPageTreeIgnoredPath(path, { assetsHtmlIgnored })) continue;
|
|
3568
3565
|
// page-kit honours a permalink before its filename rule, so the
|
|
3569
3566
|
// frontmatter is read first (quoting, comments, BOM and CRLF as
|
|
3570
3567
|
// page-kit's reader takes them); only a file with no usable permalink
|
|
@@ -3773,7 +3770,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3773
3770
|
try {
|
|
3774
3771
|
if (existsSync(outputDir)) {
|
|
3775
3772
|
const realOutputDir = realpathSync(outputDir);
|
|
3776
|
-
if (realOutputDir === realTargetRepo || realOutputDir.startsWith(`${realTargetRepo}${sep}`)) pageFiles = listPageKitPageFiles(realOutputDir, publicRouteSlug);
|
|
3773
|
+
if (realOutputDir === realTargetRepo || realOutputDir.startsWith(`${realTargetRepo}${sep}`)) pageFiles = listPageKitPageFiles(realOutputDir, publicRouteSlug, { assetsHtmlIgnored: pageKitIgnoresAssetsHtml(targetRepo) });
|
|
3777
3774
|
else addIssue(result.warnings, "spec.derive.page_tree_escapes_repo", `${outputDir} resolves to ${realOutputDir}, outside the target repo; routes were not read from it.`);
|
|
3778
3775
|
}
|
|
3779
3776
|
} catch (error) {
|
|
@@ -4318,7 +4315,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4318
4315
|
}
|
|
4319
4316
|
}
|
|
4320
4317
|
const themeGate = doctor.derived?.theme_gate || null;
|
|
4321
|
-
const polishGate = doctor.derived?.polish_gate
|
|
4318
|
+
const polishGate = doctor.derived?.polish_gate;
|
|
4322
4319
|
const polishCheckpointGate = doctor.derived?.polish_checkpoint_gate || null;
|
|
4323
4320
|
// Packet 03 (INV-5 first slice): compare the ledger's self-report against
|
|
4324
4321
|
// the repo's artifacts before any recommendation goes out. Additive:
|
|
@@ -4398,21 +4395,25 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4398
4395
|
const ready = [...doctor.ready];
|
|
4399
4396
|
if (!doctor.ok && !doctorHasOnlyPolishGateErrors) errors.push(...doctor.errors.map((issue) => withPacketSubstitutedIssue(issue, packetPath)));
|
|
4400
4397
|
|
|
4401
|
-
//
|
|
4402
|
-
//
|
|
4403
|
-
//
|
|
4404
|
-
//
|
|
4405
|
-
if (
|
|
4398
|
+
// prepare-build is upstream of every stage. While its authoritative gate is
|
|
4399
|
+
// blocked or unavailable, doctor's picker chooses prepare-build, and an
|
|
4400
|
+
// explicit stage request gets the same recovery-only shape (requested_stage
|
|
4401
|
+
// names it) instead of that stage's prompt or executable actions.
|
|
4402
|
+
if (prepareBuildGate) {
|
|
4406
4403
|
addPrepareBuildGateErrors(errors, report, prepareBuildGate);
|
|
4407
4404
|
return finalize({
|
|
4408
4405
|
ok: false,
|
|
4409
4406
|
status: "blocked",
|
|
4410
4407
|
stage: "prepare-build",
|
|
4411
|
-
requested_stage: stage,
|
|
4408
|
+
...(stage ? { requested_stage: stage } : {}),
|
|
4412
4409
|
reason: prepareBuildGate.reason,
|
|
4413
4410
|
errors,
|
|
4414
4411
|
warnings,
|
|
4415
4412
|
ready,
|
|
4413
|
+
// Packet 03: when the artifacts contradict the ledger, do NOT tell
|
|
4414
|
+
// the operator to start over — rerunning prepare-build/start is the
|
|
4415
|
+
// most destructive documented recovery, and the ledger alone is not
|
|
4416
|
+
// trustworthy evidence that it is needed.
|
|
4416
4417
|
prompt: prepareBuildRecoveryPrompt,
|
|
4417
4418
|
stage_blocked: true,
|
|
4418
4419
|
});
|
|
@@ -4438,34 +4439,16 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4438
4439
|
prompt: `Resolve the doctor errors above before continuing. Re-run \`${cmd("doctor")} --packet <path> --write\` to record the recovery, then \`${cmd("next")} --packet <path>\` to advance.`,
|
|
4439
4440
|
});
|
|
4440
4441
|
}
|
|
4441
|
-
if (picked.stage === "prepare-build") {
|
|
4442
|
-
addPrepareBuildGateErrors(errors, report, prepareBuildGate);
|
|
4443
|
-
return finalize({
|
|
4444
|
-
ok: false,
|
|
4445
|
-
status: "blocked",
|
|
4446
|
-
stage: "prepare-build",
|
|
4447
|
-
reason: picked.reason,
|
|
4448
|
-
errors,
|
|
4449
|
-
warnings,
|
|
4450
|
-
ready,
|
|
4451
|
-
// Packet 03: when the artifacts contradict the ledger, do NOT tell
|
|
4452
|
-
// the operator to start over — rerunning prepare-build/start is the
|
|
4453
|
-
// most destructive documented recovery, and the ledger alone is not
|
|
4454
|
-
// trustworthy evidence that it is needed.
|
|
4455
|
-
prompt: prepareBuildRecoveryPrompt,
|
|
4456
|
-
stage_blocked: true,
|
|
4457
|
-
});
|
|
4458
|
-
}
|
|
4459
4442
|
if (picked.stage === "done") {
|
|
4460
4443
|
return finalize({
|
|
4461
4444
|
ok: true,
|
|
4462
|
-
status:
|
|
4445
|
+
status: readinessStatus(warnings, doctor.derived),
|
|
4463
4446
|
stage: "done",
|
|
4464
4447
|
reason: picked.reason,
|
|
4465
4448
|
errors,
|
|
4466
4449
|
warnings,
|
|
4467
4450
|
ready,
|
|
4468
|
-
prompt: `Pipeline complete. All stages in the assembly report are in a terminal status. To repeat build work, do the work and run \`${cmd("record")} build --packet <path>\`; this makes downstream stages owed as needed.
|
|
4451
|
+
prompt: `Pipeline complete. All stages in the assembly report are in a terminal status. To repeat build work, do the work and run \`${cmd("record")} build --packet <path>\`; this makes downstream stages owed as needed. \`${cmd("record")} deploy\` records the served deploy, a loopback URL for a local-serve target or an https URL for a hosted one. Run \`${cmd("qa")} run\` for QA. Then call \`${cmd("next")}\` again. If a run session is active, finish it with \`${cmd("run")} end\` so the Run Record is assembled and the session closes.${report?.stages?.qa?.status === "completed_with_warnings" ? " QA passed with exceptions; report them to the operator without clearing or waiving them or changing markup just to make them pass." : ""}`,
|
|
4469
4452
|
});
|
|
4470
4453
|
}
|
|
4471
4454
|
stage = picked.stage;
|
|
@@ -4509,13 +4492,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4509
4492
|
addThemeGateErrors(errors, themeGate, "qa");
|
|
4510
4493
|
prompt = qaPrompt(packetPath, reportPath, packet, spec, intentSummary, polishCheckpointGate);
|
|
4511
4494
|
}
|
|
4512
|
-
const status = errors.length
|
|
4513
|
-
? "blocked"
|
|
4514
|
-
: checkpointExceptionPresent(doctor.derived)
|
|
4515
|
-
? "ready_with_waivers"
|
|
4516
|
-
: warnings.length
|
|
4517
|
-
? "ready_with_warnings"
|
|
4518
|
-
: "ready";
|
|
4495
|
+
const status = errors.length ? "blocked" : readinessStatus(warnings, doctor.derived);
|
|
4519
4496
|
const result = {
|
|
4520
4497
|
ok: errors.length === 0,
|
|
4521
4498
|
status,
|
|
@@ -5105,7 +5082,7 @@ Rules:
|
|
|
5105
5082
|
- Each checkout_step page's checkout form (<form data-next-checkout="form">) carries data-next-checkout-step="<its next_step>" and data-next-step-number="<step_number>" (Build Context page_kit frontmatter next_step and step_number); the checkout page's form carries neither. On select and checkout_step pages, frontmatter next_url is the Checkout's post-payment destination, the same meaning it has on the checkout page, and the layout renders it as next-success-url because the SDK reads that tag for express orders; next_step is the forward link. A landing page keeps next_url as its call-to-action target; if it has express checkout buttons, set its next-success-url to the Checkout's own next_url from the Build Context page_map, never to the landing page's next_url. Forward navigation uses the step form attribute or a plain link, never that meta tag. Page Kit frontmatter has no success_url key (success_url remains a CampaignSpec field on checkout pages).
|
|
5106
5083
|
- After page-kit build, inspect rendered _site output before handoff: each active page should have a body, Campaign Cart runtime markers, SDK meta tags from CampaignSpec sdk_hints.meta_tags, and no stale copied funnel attribution.
|
|
5107
5084
|
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}${isLocalServePacket(packet) ? ` --build-environment ${LOCAL_PROOF_BUILD_ENVIRONMENT}` : ""} --adapter-decision <key>=<value>[,<key>=<value>...]\`. Record the value that is true for this build: for example, raw_html_conversion_status=completed once source HTML is converted, or not_required when there is none. Put all scalar adapter decisions in one comma-separated flag; a repeated flag keeps only the last. Use the allowed values in docs/build-packet.md. Record the nine recordable scalar decisions with the command, never by editing those fields in the Assembly Report. wrapper_policy is selected at intake with prepare-build --wrapper-policy or the source-html manifest's wrapper_policy option; the object-valued template_files_copied proof has no flag. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, run \`${cmd("record")} theme --packet ${packetPath}\` after record build: it reads each built commerce page's stylesheet links and records report.theme (status applied, load_order=after-next-core, css_path, commerce_pages, evidence), and refuses, writing nothing, when a page that loads next-core.css does not load the brand layer after it. Never hand-edit report.theme.
|
|
5108
|
-
- Capture the machine-readable build summary as an artifact: \`${PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND}\` (requires next-campaign-page-kit >= 0.1.4). Doctor verifies it for per-page build errors and Page Kit shape warnings (NESTED_NO_PERMALINK, DUPLICATE_OUTPUT, MISSING_FRONTMATTER, LAYOUT_NOT_FOUND). If the installed page-kit predates --json, record that in the assembly report instead of skipping silently.${localProofPromptLines(packet, packetPath)}`;
|
|
5085
|
+
- Capture the machine-readable build summary as an artifact: \`${PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND}\` (requires next-campaign-page-kit >= 0.1.4). Doctor verifies it for per-page build errors and Page Kit shape warnings (NESTED_NO_PERMALINK, DUPLICATE_OUTPUT, MISSING_FRONTMATTER, LAYOUT_NOT_FOUND), and on page-kit >= 0.3.0 for its top-level assetWarnings: ASSET_IGNORED names an .html file under assets/ that is neither built nor copied, so a link or iframe to it 404s. If the installed page-kit predates --json, record that in the assembly report instead of skipping silently.${localProofPromptLines(packet, packetPath)}`;
|
|
5109
5086
|
}
|
|
5110
5087
|
|
|
5111
5088
|
// Local proof mode lines for the build prompt: under local-serve the build is
|
|
@@ -5248,10 +5225,8 @@ function localServePlan(packet) {
|
|
|
5248
5225
|
}
|
|
5249
5226
|
|
|
5250
5227
|
function deployPrompt(packetPath, reportPath, packet, polishGate = null, polishCheckpointGate = null) {
|
|
5251
|
-
if (packet
|
|
5252
|
-
|
|
5253
|
-
&& !packet.deploy?.preview_url) {
|
|
5254
|
-
return `Every mapped page is template stock, and the hosted preview URL is not recorded. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\` after the preview is deployed, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5228
|
+
if (hostedTemplateNeedsUsablePreview(packet, polishCheckpointGate)) {
|
|
5229
|
+
return `Every mapped page is template stock, and deploy needs a hosted preview URL that differs from the production URL. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\` after the preview is deployed, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5255
5230
|
}
|
|
5256
5231
|
const target = packet.deploy?.target || "unknown";
|
|
5257
5232
|
const liveUrlPath = packet.deploy?.live_url_path || packet.campaign?.live_url_path || campaignRouteRoot(packet) || "/<slug>/";
|
|
@@ -5344,10 +5319,8 @@ function orderBumpRunReason(bumpCart) {
|
|
|
5344
5319
|
}
|
|
5345
5320
|
|
|
5346
5321
|
function qaPrompt(packetPath, reportPath, packet, spec = null, intent, polishCheckpointGate = null) {
|
|
5347
|
-
if (packet
|
|
5348
|
-
|
|
5349
|
-
&& !packet.deploy?.preview_url) {
|
|
5350
|
-
return `Every mapped page is template stock, and QA needs the hosted preview URL. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`, then \`${cmd("next")} qa --packet ${shellToken(packetPath)}\`.`;
|
|
5322
|
+
if (hostedTemplateNeedsUsablePreview(packet, polishCheckpointGate)) {
|
|
5323
|
+
return `Every mapped page is template stock, and QA needs a hosted preview URL that differs from the production URL. Run \`${cmd("qa")} policy set --packet ${shellToken(packetPath)} --preview-url <url>\`, then \`${cmd("next")} --packet ${shellToken(packetPath)}\`.`;
|
|
5351
5324
|
}
|
|
5352
5325
|
const url = packet.deploy?.preview_url || packet.deploy?.production_url || "<preview-url>";
|
|
5353
5326
|
const briefPath = packet.build_brief?.normalized_path || "(missing)";
|
|
@@ -7117,6 +7090,12 @@ function runSessionStatus(args, ambient = null) {
|
|
|
7117
7090
|
// Session progress: incomplete assembly-report stages, the deviation count,
|
|
7118
7091
|
// and the exact next command. Read-only and best-effort; the status command
|
|
7119
7092
|
// must never fail because an artifact is missing or torn.
|
|
7093
|
+
//
|
|
7094
|
+
// The incomplete stages are the recorded statuses. Whether every stage is
|
|
7095
|
+
// actually done is `next`'s decision, not this one: an input change or a new
|
|
7096
|
+
// build owes a stage again while its recorded status still reads completed.
|
|
7097
|
+
// So the next command is always `next`, which names `run end` once the
|
|
7098
|
+
// campaign is done and a session is active.
|
|
7120
7099
|
function runSessionProgress(found) {
|
|
7121
7100
|
const packetPath = found.session.packet;
|
|
7122
7101
|
if (!isNonEmptyString(packetPath) || !existsSync(packetPath)) return null;
|
|
@@ -7126,7 +7105,7 @@ function runSessionProgress(found) {
|
|
|
7126
7105
|
const incomplete = [];
|
|
7127
7106
|
for (const key of ASSEMBLY_REPORT_STAGE_KEYS) {
|
|
7128
7107
|
const status = String(report?.stages?.[key]?.status || "");
|
|
7129
|
-
if (!
|
|
7108
|
+
if (!stageIsTerminal(status)) {
|
|
7130
7109
|
incomplete.push({ stage: key, status: status || null });
|
|
7131
7110
|
}
|
|
7132
7111
|
}
|
|
@@ -7135,9 +7114,7 @@ function runSessionProgress(found) {
|
|
|
7135
7114
|
incomplete_stages: incomplete,
|
|
7136
7115
|
deviations,
|
|
7137
7116
|
qa_attempt_count: Array.isArray(found.session.qa_attempts) ? found.session.qa_attempts.length : 0,
|
|
7138
|
-
next_command:
|
|
7139
|
-
? `${cmd("next")} --packet ${packetPath} --json`
|
|
7140
|
-
: `${cmd("run")} end --packet ${packetPath}`,
|
|
7117
|
+
next_command: `${cmd("next")} --packet ${packetPath} --json`,
|
|
7141
7118
|
};
|
|
7142
7119
|
} catch {
|
|
7143
7120
|
return null;
|
package/src/doctor/checks.mjs
CHANGED
|
@@ -99,7 +99,7 @@ import {
|
|
|
99
99
|
assessAssemblySourcePackageFreshnessWaivers,
|
|
100
100
|
currentBuildFingerprint,
|
|
101
101
|
} from "../polish-gate.mjs";
|
|
102
|
-
import { loadPageKitCampaignEntry, projectPageKitCampaignLoad } from "../page-kit-campaign-config.mjs";
|
|
102
|
+
import { loadPageKitCampaignEntry, pageKitIgnoresAssetsHtml, projectPageKitCampaignLoad } from "../page-kit-campaign-config.mjs";
|
|
103
103
|
import { evaluatePageKitStoreProfile, PAGE_KIT_STORE_PROFILE_SCOPE, storeProfileIntentionallyEmptyNote } from "../page-kit-store-profile.mjs";
|
|
104
104
|
import { evaluatePageKitSdkVersion, PAGE_KIT_SDK_VERSION_SCOPE } from "../page-kit-sdk-version.mjs";
|
|
105
105
|
// ADR-003: the public, canonical CampaignSpec rule registry. The doctor and any
|
|
@@ -3039,7 +3039,7 @@ function builtHtmlPathForPage(targetRepo, publicRouteSlug, page, derived = {}) {
|
|
|
3039
3039
|
mapping: derived?.page_kit_entry_mappings?.[page?.id],
|
|
3040
3040
|
});
|
|
3041
3041
|
if (entryRoute) return join(targetRepo, "_site", publicRouteSlug, ...(entryRoute.route ? [entryRoute.route] : []), "index.html");
|
|
3042
|
-
const sourcePermalink = sourcePermalinkForPage(derived?.target_output_dir, publicRouteSlug, page);
|
|
3042
|
+
const sourcePermalink = sourcePermalinkForPage(derived?.target_output_dir, publicRouteSlug, page, { assetsHtmlIgnored: pageKitIgnoresAssetsHtml(targetRepo) });
|
|
3043
3043
|
const route = sourcePermalink || runtimeRelativeRouteForSpecValue(publicRouteForPage(page), publicRouteSlug);
|
|
3044
3044
|
if (!route) return join(targetRepo, "_site", publicRouteSlug, "index.html");
|
|
3045
3045
|
const clean = route.replace(/^\/+|\/+$/g, "");
|
|
@@ -3103,13 +3103,16 @@ export function entryPageKitFileRoute(targetRepo, publicRouteSlug, page, { targe
|
|
|
3103
3103
|
return routes[0] ?? null;
|
|
3104
3104
|
}
|
|
3105
3105
|
|
|
3106
|
-
|
|
3106
|
+
// page-kit 0.3.0 renders no .html under the campaign's top-level assets/, so
|
|
3107
|
+
// a permalink stated there is no route on that version (assetsHtmlIgnored).
|
|
3108
|
+
function sourcePermalinkForPage(targetOutputDir, publicRouteSlug, page, { assetsHtmlIgnored = false } = {}) {
|
|
3107
3109
|
if (!targetOutputDir || !existsSync(targetOutputDir) || !statSync(targetOutputDir).isDirectory()) return null;
|
|
3108
3110
|
|
|
3109
3111
|
const expectedTerminal = terminalRouteSegment(publicRouteForPage(page));
|
|
3110
3112
|
const candidates = [];
|
|
3111
3113
|
for (const file of collectHtmlFiles(targetOutputDir)) {
|
|
3112
3114
|
if (file.path.includes("_includes/") || file.path.includes("_layouts/")) continue;
|
|
3115
|
+
if (assetsHtmlIgnored && file.path.split(sep)[0] === "assets") continue;
|
|
3113
3116
|
const fullPath = join(targetOutputDir, file.path);
|
|
3114
3117
|
const content = readFileSync(fullPath, "utf8");
|
|
3115
3118
|
const permalink = extractFrontmatterValue(content, "permalink");
|
package/src/doctor/inspect.mjs
CHANGED
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
nextPrepareBuildBindingIssues,
|
|
64
64
|
prepareBuildGateIssue,
|
|
65
65
|
addPrepareBuildGateErrors,
|
|
66
|
-
|
|
66
|
+
readinessStatus,
|
|
67
67
|
buildNextStep,
|
|
68
68
|
} from "./next-step.mjs";
|
|
69
69
|
|
|
@@ -664,13 +664,7 @@ function inspectDoctorPacket(packetPath, { contextPath = undefined, reportPath =
|
|
|
664
664
|
}
|
|
665
665
|
}
|
|
666
666
|
}
|
|
667
|
-
const status = errors.length
|
|
668
|
-
? "blocked"
|
|
669
|
-
: checkpointExceptionPresent(derived)
|
|
670
|
-
? "ready_with_waivers"
|
|
671
|
-
: warnings.length
|
|
672
|
-
? "ready_with_warnings"
|
|
673
|
-
: "ready";
|
|
667
|
+
const status = errors.length ? "blocked" : readinessStatus(warnings, derived);
|
|
674
668
|
const result = {
|
|
675
669
|
schema_version: DOCTOR_SIDECAR_SCHEMA,
|
|
676
670
|
generated_at: new Date().toISOString(),
|
package/src/doctor/next-step.mjs
CHANGED
|
@@ -413,7 +413,7 @@ export function assessPurchaseProofCoverage({ packet = null, report = null } = {
|
|
|
413
413
|
};
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
function pickNextStage(report, { errors = [], derived = null }, prepareBuildGate, purchaseProof = null
|
|
416
|
+
function pickNextStage(report, { errors = [], derived = null }, prepareBuildGate, purchaseProof = null) {
|
|
417
417
|
const polishGate = derived?.polish_gate || evaluatePolishGate({ report });
|
|
418
418
|
const polishCheckpointGate = derived?.polish_checkpoint_gate || null;
|
|
419
419
|
// prepare-build is the earliest lifecycle prerequisite. Surface its
|
|
@@ -542,6 +542,9 @@ function checkpointExceptionPresent(derived) {
|
|
|
542
542
|
|| derived?.polish_checkpoint_gate?.status === "waived";
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
// The readiness ladder below "blocked", written once: doctor's status, the
|
|
546
|
+
// `next` command's status and doctor's `next` block each decide "blocked"
|
|
547
|
+
// first, then call it with their own warnings.
|
|
545
548
|
function readinessStatus(warnings, derived) {
|
|
546
549
|
if (checkpointExceptionPresent(derived)) return "ready_with_waivers";
|
|
547
550
|
return warnings.length ? "ready_with_warnings" : "ready";
|
|
@@ -664,7 +667,7 @@ function buildNextStep(errors, warnings, derived, report = null, packet = null,
|
|
|
664
667
|
&& (polishGate.status === "blocked" || polishCheckpointGate?.status === "blocked");
|
|
665
668
|
const codes = new Set([...errors, ...warnings].map((issue) => issue.code));
|
|
666
669
|
const purchaseProof = report ? assessPurchaseProofCoverage({ packet, report }) : null;
|
|
667
|
-
const picked = pickNextStage(report, { errors, derived }, prepareBuildGate, purchaseProof
|
|
670
|
+
const picked = pickNextStage(report, { errors, derived }, prepareBuildGate, purchaseProof);
|
|
668
671
|
// The picker's vocabulary and this table must not drift apart: a stage the
|
|
669
672
|
// table does not know would otherwise be relabelled as an operator step and
|
|
670
673
|
// sliced into the whole ladder. Fail loudly instead.
|
|
@@ -758,6 +761,6 @@ export {
|
|
|
758
761
|
prepareBuildGateIssue,
|
|
759
762
|
addPrepareBuildGateErrors,
|
|
760
763
|
deployUrlFromReportOutputs,
|
|
761
|
-
|
|
764
|
+
readinessStatus,
|
|
762
765
|
buildNextStep,
|
|
763
766
|
};
|
|
@@ -50,9 +50,12 @@ export function reportKeyForCliStage(cliStage) {
|
|
|
50
50
|
* "completed", "completed_with_warnings", and "completed_partial" all
|
|
51
51
|
* count as terminal under "completed". This matches how the existing
|
|
52
52
|
* stages already report sub-statuses (see report.stages.assembly.status
|
|
53
|
-
* shapes in src/cli.mjs and qa/shared/qa-verdict.js).
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* shapes in src/cli.mjs and qa/shared/qa-verdict.js). This is the rule for a
|
|
54
|
+
* RECORDED status only (the session progress summary's incomplete stages,
|
|
55
|
+
* the prepare-build gate, doctor's checks). Whether a stage is done is
|
|
56
|
+
* decided on its effective status (src/input-currency.mjs), which the `next`
|
|
57
|
+
* picker and the Assembly Report's derived summary (src/stage-ledger.mjs)
|
|
58
|
+
* read: a stage owed again is not done, whatever status it records.
|
|
56
59
|
*/
|
|
57
60
|
export const STAGE_TERMINAL_STATUS_PREFIXES = Object.freeze(["completed", "skipped"]);
|
|
58
61
|
|
|
@@ -165,9 +165,29 @@ export function evaluatePageKitBuildSummary({
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
// page-kit >= 0.3.0 neither builds nor copies .html under assets/ and lists
|
|
169
|
+
// each such file in a top-level assetWarnings array (ASSET_IGNORED), never
|
|
170
|
+
// in pages[].warnings. A link or iframe to one 404s on the built site, and
|
|
171
|
+
// this is the only signal, so it is surfaced, but never escalated: removing
|
|
172
|
+
// or moving the file is the author's call. Entries carry no campaignSlug;
|
|
173
|
+
// the file path (src/<slug>/assets/...) names the campaign.
|
|
174
|
+
const assetWarnings = Array.isArray(summary.assetWarnings)
|
|
175
|
+
? summary.assetWarnings.filter((warning) => typeof warning?.file === "string" && `/${warning.file.replace(/\\/g, "/")}`.includes(`/${publicRouteSlug}/assets/`))
|
|
176
|
+
: [];
|
|
177
|
+
for (const warning of assetWarnings) {
|
|
178
|
+
flagged += 1;
|
|
179
|
+
warnings.push({
|
|
180
|
+
code: "built_output.build_asset_warning",
|
|
181
|
+
message:
|
|
182
|
+
`Page Kit did not build or copy "${warning.file}": [${warning.code}] ${warning.message}. ` +
|
|
183
|
+
`A link or iframe to it 404s on the built site; move it out of assets/ if it is a page, or remove it.`,
|
|
184
|
+
detail: { file: warning.file, warning_code: warning.code },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
if (failed === 0 && flagged === 0) {
|
|
169
189
|
ready.push(
|
|
170
|
-
`Page Kit build summary verified for "${publicRouteSlug}": ${campaignPages.length} page(s) built, no build errors or
|
|
190
|
+
`Page Kit build summary verified for "${publicRouteSlug}": ${campaignPages.length} page(s) built, no build errors, shape warnings or asset warnings`
|
|
171
191
|
);
|
|
172
192
|
}
|
|
173
193
|
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { compareVersions, extractVersion } from "./repo-scan.mjs";
|
|
3
4
|
import { normalizePublicRouteSlug } from "./route-identity.mjs";
|
|
4
5
|
|
|
5
6
|
export const PAGE_KIT_CAMPAIGNS_REL_PATH = "_data/campaigns.json";
|
|
6
7
|
|
|
8
|
+
// next-campaign-page-kit 0.3.0 stopped building or copying .html under a
|
|
9
|
+
// campaign's top-level assets/. Source-page discovery follows the target's
|
|
10
|
+
// installed page-kit, read from node_modules the way tooling setup reads it;
|
|
11
|
+
// a missing, unreadable or malformed install keeps the 0.2.x rule. That is
|
|
12
|
+
// deliberate: doctor's built-page lookup calls this from about ten checks, so
|
|
13
|
+
// throwing would take doctor down over a broken install that the build itself
|
|
14
|
+
// fails on loudly, and tooling setup already refuses a declared page-kit that
|
|
15
|
+
// is not installed.
|
|
16
|
+
export const PAGE_KIT_ASSETS_HTML_IGNORED_FROM = "0.3.0";
|
|
17
|
+
export function pageKitIgnoresAssetsHtml(targetRepo) {
|
|
18
|
+
try {
|
|
19
|
+
const { version } = JSON.parse(readFileSync(join(targetRepo, "node_modules", "next-campaign-page-kit", "package.json"), "utf8"));
|
|
20
|
+
return extractVersion(version) !== null && compareVersions(version, PAGE_KIT_ASSETS_HTML_IGNORED_FROM) >= 0;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
// Public doctor/next/verdict evidence must never inherit arbitrary merchant
|
|
8
27
|
// configuration from campaigns.json. The raw entry is validation-local; this
|
|
9
28
|
// fixed projection is the only loader metadata safe to retain in artifacts.
|
package/src/polish-gate.mjs
CHANGED
|
@@ -45,11 +45,7 @@ function terminalAssembly(report) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function currentBuildFingerprint(report) {
|
|
48
|
-
return normalizeString(report?.stages?.assembly?.build_fingerprint)
|
|
49
|
-
|| normalizeString(report?.stages?.assembly?.artifact_fingerprint)
|
|
50
|
-
|| normalizeString(report?.build_fingerprint)
|
|
51
|
-
|| normalizeString(report?.artifact_fingerprint)
|
|
52
|
-
|| null;
|
|
48
|
+
return normalizeString(report?.stages?.assembly?.build_fingerprint) || null;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
export function currentSourcePackageMaterialFingerprint(report) {
|
package/src/progress-node.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {join,resolve,dirname} from 'node:path';
|
|
|
6
6
|
import {PROGRESS_SCHEMA_VERSION,PROGRESS_STAGES,PROGRESS_STAGE_STATUSES,PROGRESS_CONTINUATIONS,PROGRESS_ACTION_IDS,PROGRESS_GATE_IDS,canonicalProgressJson,progressSnapshotId,verifyProgressSnapshot} from './progress.mjs';
|
|
7
7
|
import {specMaterialHash} from './spec-identity.mjs';
|
|
8
8
|
import {effectiveStageStatus} from './input-currency.mjs';
|
|
9
|
+
import {qaRecordedBuildFingerprint} from './stage-ledger.mjs';
|
|
9
10
|
import {sameFile} from './fs-identity.mjs';
|
|
10
11
|
import {withDirectoryLock} from './directory-lock.mjs';
|
|
11
12
|
import {resolveConsent,CANONICAL_REMIT_SCOPE,normalizeConsentScope,announceDefaultOnTelemetry} from './consent.mjs';
|
|
@@ -64,7 +65,7 @@ export function projectProgressObservation({workspace,context,report,doctor,cont
|
|
|
64
65
|
// The report is also bound to the spec material on disk now, so output and
|
|
65
66
|
// verdict evidence recorded against it can match.
|
|
66
67
|
const reportBound=stagesBound&&localHash===hash(report?.identity?.spec_material_hash);
|
|
67
|
-
const qaSource=hash(report
|
|
68
|
+
const qaSource=hash(qaRecordedBuildFingerprint(report));
|
|
68
69
|
const verdict=qaResult?.verdict;
|
|
69
70
|
const qa=verdict?{
|
|
70
71
|
verdict_id:id(verdict.run_id),disposition:accepted(verdict.disposition,['ready','ready_with_exceptions','blocked']),
|
package/src/spec-derive.mjs
CHANGED
|
@@ -56,15 +56,29 @@ const ROUTING_HINT_FIELDS = Object.freeze({
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
// The directories page-kit's own discovery ignores (`_layouts`, `_includes`;
|
|
59
|
-
// everything else under the campaign root, `assets/` and `_data/`
|
|
60
|
-
// is rendered when it is .html), plus the two no build reads. The
|
|
61
|
-
// prunes with this and pageRouteForFile filters with it, so the
|
|
62
|
-
// in one place.
|
|
59
|
+
// on 0.2.x everything else under the campaign root, `assets/` and `_data/`
|
|
60
|
+
// included, is rendered when it is .html), plus the two no build reads. The
|
|
61
|
+
// CLI walker prunes with this and pageRouteForFile filters with it, so the
|
|
62
|
+
// list lives in one place.
|
|
63
63
|
export const PAGE_TREE_IGNORED_DIRS = Object.freeze(["_layouts", "_includes", "node_modules", ".git"]);
|
|
64
64
|
export function isPageTreeIgnoredDir(name) {
|
|
65
65
|
return PAGE_TREE_IGNORED_DIRS.includes(name);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Whether a campaign-relative file sits where page-kit renders no page. From
|
|
69
|
+
// 0.3.0 page-kit also skips the campaign's top-level `assets/` (its
|
|
70
|
+
// `*/assets/**` glob; a nested `offers/assets/` still renders), so the caller
|
|
71
|
+
// says which rule the target's installed page-kit follows. Backslashes and
|
|
72
|
+
// leading slashes read as pageRouteForFile reads them.
|
|
73
|
+
export function isPageTreeIgnoredPath(relativePath, { assetsHtmlIgnored = false } = {}) {
|
|
74
|
+
const dirs = normalizePageTreePath(relativePath).split("/").slice(0, -1);
|
|
75
|
+
return dirs.some(isPageTreeIgnoredDir) || (assetsHtmlIgnored && dirs[0] === "assets");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizePageTreePath(relativePath) {
|
|
79
|
+
return String(relativePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
function isPlainObject(value) {
|
|
69
83
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
84
|
}
|
|
@@ -114,13 +128,14 @@ function containerProblem(spec, path) {
|
|
|
114
128
|
// `upsell/`, intermediate directories ignored with its NESTED_NO_PERMALINK
|
|
115
129
|
// warning), and `index.html` is the entry route "". A nested `index.html`
|
|
116
130
|
// would collide with the campaign root (page-kit's DUPLICATE_OUTPUT), so it
|
|
117
|
-
// is not a page this reads. Files under `_layouts/` or `_includes/`
|
|
118
|
-
// null, as page-kit
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
// is not a page this reads. Files under `_layouts/` or `_includes/` (and,
|
|
132
|
+
// with assetsHtmlIgnored, the top-level `assets/`) return null, as page-kit
|
|
133
|
+
// never renders them.
|
|
134
|
+
export function pageRouteForFile(relativePath, { assetsHtmlIgnored = false } = {}) {
|
|
135
|
+
const path = normalizePageTreePath(relativePath);
|
|
121
136
|
if (!path || !/\.html$/i.test(path)) return null;
|
|
122
137
|
const segments = path.split("/");
|
|
123
|
-
if (
|
|
138
|
+
if (isPageTreeIgnoredPath(path, { assetsHtmlIgnored })) return null;
|
|
124
139
|
const basename = segments[segments.length - 1].replace(/\.html$/i, "");
|
|
125
140
|
if (basename === "index") return segments.length === 1 ? "" : null;
|
|
126
141
|
return `${basename}/`;
|
package/src/stage-ledger.mjs
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
NEXT_STAGE_CONTRACTS,
|
|
12
12
|
NEXT_STAGE_OWNERS,
|
|
13
13
|
stageIsBlocked,
|
|
14
|
-
stageIsTerminal,
|
|
15
14
|
} from "./orchestration-stage-contract.mjs";
|
|
16
15
|
|
|
17
16
|
const PRODUCER_STAGES = new Set(["doctor", "qa"]);
|
|
@@ -200,6 +199,33 @@ export function archiveSupersededRecord(stage, previous, { by, reason, at }) {
|
|
|
200
199
|
return appendHistoryEntry(isPlainObject(stage) ? { ...stage } : {}, historyEntry(previous, { by, reason, at }));
|
|
201
200
|
}
|
|
202
201
|
|
|
202
|
+
/**
|
|
203
|
+
* #668: the Polish record a CampaignSpec change moved to `polish.history`,
|
|
204
|
+
* restored as the live record stamped with `stamps` (the current inputs), or
|
|
205
|
+
* null. It is the same review of the same pages, so it is restored only when
|
|
206
|
+
* the live record's input_change is a CampaignSpec change over exactly
|
|
207
|
+
* `fingerprint`, the newest history entry is the completed Polish that change
|
|
208
|
+
* archived (same build fingerprint and source package fingerprint), and the
|
|
209
|
+
* brief's presentation stamp is unchanged: a presentation change still owes a
|
|
210
|
+
* new review. The caller decides that the operator kept this output.
|
|
211
|
+
*/
|
|
212
|
+
export function restorePolishKeptWithOutput(polish, { fingerprint, sourcePackageFingerprint = null, stamps = {}, completedStatuses }) {
|
|
213
|
+
const change = isPlainObject(polish) ? polish.input_change : null;
|
|
214
|
+
if (!isPlainObject(change) || change.reason !== "spec_material_changed" || change.superseded_build_fingerprint !== fingerprint) return null;
|
|
215
|
+
const history = Array.isArray(polish.history) ? polish.history : [];
|
|
216
|
+
const superseded = history[history.length - 1];
|
|
217
|
+
if (!isPlainObject(superseded) || superseded.archived_at !== change.at || !completedStatuses.includes(superseded.status)) return null;
|
|
218
|
+
if (superseded.source_build_fingerprint !== fingerprint) return null;
|
|
219
|
+
if ((superseded.source_package_material_fingerprint ?? null) !== (sourcePackageFingerprint ?? null)) return null;
|
|
220
|
+
if ((superseded.source_brief_material?.presentation ?? null) !== (stamps.source_brief_material?.presentation ?? null)) return null;
|
|
221
|
+
const restored = { ...polish };
|
|
222
|
+
for (const field of ["required_by", "required_for", "input_change", "source_brief_material", "source_spec_material_hash"]) delete restored[field];
|
|
223
|
+
for (const field of ["status", "completed_at", "recorded_by", "performed_by", "source_build_fingerprint", "source_package_material_fingerprint", "outputs", "evidence"]) {
|
|
224
|
+
if (Object.hasOwn(superseded, field)) restored[field] = JSON.parse(JSON.stringify(superseded[field]));
|
|
225
|
+
}
|
|
226
|
+
return { ...restored, stage: "polish", ...stamps };
|
|
227
|
+
}
|
|
228
|
+
|
|
203
229
|
/**
|
|
204
230
|
* True when any stage of `report` carries a non-empty history: recorded
|
|
205
231
|
* operator-visible state an intake must not silently drop.
|