@stacksjs/defaults 0.75.361 → 0.75.366
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/ai/skills/stacks-ai/SKILL.md +22 -0
- package/ai/skills/stacks-native/CATALOG.md +5 -5
- package/ai/skills/stacks-payments/SKILL.md +10 -0
- package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
- package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
- package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
- package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
- package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
- package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
- package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
- package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
- package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
- package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
- package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
- package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
- package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
- package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
- package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
- package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
- package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
- package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
- package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
- package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
- package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
- package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
- package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
- package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
- package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
- package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
- package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
- package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
- package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
- package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
- package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
- package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
- package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
- package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
- package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
- package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
- package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
- package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
- package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
- package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
- package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
- package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
- package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
- package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
- package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
- package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
- package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
- package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
- package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
- package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
- package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
- package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
- package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
- package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
- package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
- package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
- package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
- package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
- package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
- package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
- package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
- package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
- package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
- package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
- package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
- package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
- package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
- package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
- package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
- package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
- package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
- package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
- package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
- package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
- package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
- package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
- package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
- package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
- package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
- package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
- package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
- package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
- package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
- package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
- package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
- package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
- package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
- package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
- package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
- package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
- package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
- package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
- package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
- package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
- package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
- package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
- package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
- package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
- package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
- package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
- package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
- package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
- package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
- package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
- package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
- package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
- package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
- package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
- package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
- package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
- package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
- package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
- package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
- package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
- package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
- package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
- package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
- package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
- package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
- package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
- package/ai/skills/stacks-testing/SKILL.md +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1056 @@
|
|
|
1
|
+
import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { canonicalFuturePath, pathsAlias, resolveNativeOutputDirectory, resolveOutputPath } from '../renderers/shared/output-path.mjs';
|
|
8
|
+
import { boundedSidecarStem } from '../renderers/shared/sidecar-path.mjs';
|
|
9
|
+
import {
|
|
10
|
+
captureAtomicOutput, captureRegularFileBinding, publishRegularFileBinding,
|
|
11
|
+
releaseRegularFileBinding, removeOwnedRegularFile, verifyAtomicOutput,
|
|
12
|
+
} from '../renderers/shared/atomic-output.mjs';
|
|
13
|
+
import {
|
|
14
|
+
browserCheckSidecarPaths,
|
|
15
|
+
CAPTURE_VIEWPORTS,
|
|
16
|
+
ChromeVisualBrowser,
|
|
17
|
+
findChrome,
|
|
18
|
+
persistBrowserCheckFailure,
|
|
19
|
+
VISUAL_CHECK_VIEWPORTS,
|
|
20
|
+
} from './visual-check.mjs';
|
|
21
|
+
import { startDeliveryUpdateCheck } from './delivery-update.mjs';
|
|
22
|
+
|
|
23
|
+
export const FINALIZE_STAGES = Object.freeze(['validate', 'deliver', 'check', 'browser-check']);
|
|
24
|
+
const FINALIZE_UPDATE_DEADLINE_MS = 4_000;
|
|
25
|
+
|
|
26
|
+
function sha256(buffer) {
|
|
27
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function identity(file) {
|
|
31
|
+
try {
|
|
32
|
+
const bytes = fs.readFileSync(file);
|
|
33
|
+
return { path: path.resolve(file), sha256: sha256(bytes), bytes: bytes.byteLength };
|
|
34
|
+
} catch {
|
|
35
|
+
return { path: path.resolve(file) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function durationMs(start) {
|
|
40
|
+
return Number((process.hrtime.bigint() - start) / 1000000n);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function receiptWriteError(file, state) {
|
|
44
|
+
const error = new Error(`Could not publish finalize evidence "${file}" safely (${state.reason?.code || state.status}).`);
|
|
45
|
+
error.finalizeCode = 'finalize/receipt-publication';
|
|
46
|
+
error.finalizeEvidence = { file, state };
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function captureReceipt(file) {
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
const capture = captureAtomicOutput(file, { requestedEntryPolicy: 'regular-or-absent' });
|
|
53
|
+
if (capture.status !== 'captured') throw receiptWriteError(file, capture);
|
|
54
|
+
return capture;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeJsonAtomic(file, value, capture, beforeCommit) {
|
|
58
|
+
const serialized = Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
|
|
59
|
+
const temporary = path.join(path.dirname(capture.commitPath), `.archify-finalize-${randomUUID()}.json`);
|
|
60
|
+
let binding;
|
|
61
|
+
let stagedIdentity;
|
|
62
|
+
try {
|
|
63
|
+
fs.writeFileSync(temporary, serialized, { flag: 'wx' });
|
|
64
|
+
const metadata = fs.lstatSync(temporary, { bigint: true });
|
|
65
|
+
stagedIdentity = { device: metadata.dev, inode: metadata.ino };
|
|
66
|
+
if (capture.mode !== null) fs.chmodSync(temporary, capture.mode);
|
|
67
|
+
const staged = captureRegularFileBinding(temporary, {
|
|
68
|
+
subject: 'finalize-receipt', expectedIdentity: stagedIdentity,
|
|
69
|
+
expectedSha256: sha256(serialized), expectedBytes: serialized.byteLength,
|
|
70
|
+
...(capture.mode === null ? {} : { expectedMode: capture.mode }),
|
|
71
|
+
});
|
|
72
|
+
if (staged.status !== 'captured') throw receiptWriteError(file, staged);
|
|
73
|
+
binding = staged.binding;
|
|
74
|
+
beforeCommit();
|
|
75
|
+
const publication = publishRegularFileBinding(binding, temporary, capture.snapshot, { subject: 'finalize-receipt' });
|
|
76
|
+
if (!['committed', 'committed-with-warning'].includes(publication.status)) throw receiptWriteError(file, publication);
|
|
77
|
+
// Preserve recovery evidence instead of reporting a clean completion when cleanup failed.
|
|
78
|
+
if (publication.status === 'committed-with-warning') throw receiptWriteError(file, publication);
|
|
79
|
+
const next = captureReceipt(file);
|
|
80
|
+
if (!pathsAlias(next.commitPath, capture.commitPath)) {
|
|
81
|
+
throw receiptWriteError(file, { status: 'different', reason: { code: 'receipt-slot-changed' } });
|
|
82
|
+
}
|
|
83
|
+
return next;
|
|
84
|
+
} finally {
|
|
85
|
+
if (binding) releaseRegularFileBinding(binding);
|
|
86
|
+
if (stagedIdentity) removeOwnedRegularFile(temporary, stagedIdentity, { subject: 'finalize-receipt' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parsedReceipt(stdout) {
|
|
91
|
+
const source = String(stdout || '').trim();
|
|
92
|
+
if (!source) return null;
|
|
93
|
+
try { return JSON.parse(source); } catch { return null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isReceiptObject(receipt) {
|
|
97
|
+
return Boolean(receipt && typeof receipt === 'object' && !Array.isArray(receipt));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validIdentity(value) {
|
|
101
|
+
return isReceiptObject(value)
|
|
102
|
+
&& typeof value.sha256 === 'string'
|
|
103
|
+
&& /^[0-9a-f]{64}$/.test(value.sha256)
|
|
104
|
+
&& Number.isSafeInteger(value.bytes)
|
|
105
|
+
&& value.bytes >= 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function identitiesMatch(left, right) {
|
|
109
|
+
return validIdentity(left)
|
|
110
|
+
&& validIdentity(right)
|
|
111
|
+
&& left.sha256 === right.sha256
|
|
112
|
+
&& left.bytes === right.bytes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validReceiptId(value) {
|
|
116
|
+
return typeof value === 'string'
|
|
117
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function allChecksPassed(checks) {
|
|
121
|
+
return Array.isArray(checks) && checks.length > 0
|
|
122
|
+
&& checks.every((check) => isReceiptObject(check) && check.ok === true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validCheckComposition(receipt, quality) {
|
|
126
|
+
const composition = receipt?.composition;
|
|
127
|
+
return isReceiptObject(composition)
|
|
128
|
+
&& composition.schemaVersion === 1
|
|
129
|
+
&& composition.profile === quality
|
|
130
|
+
&& composition.status === 'pass'
|
|
131
|
+
&& composition.summary?.errors === 0
|
|
132
|
+
&& (quality !== 'showcase' || composition.summary?.warnings === 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exactViewportCoverage(entries, viewports, predicate) {
|
|
136
|
+
if (!Array.isArray(entries) || entries.length !== viewports.length) return false;
|
|
137
|
+
return viewports.every((expected) => {
|
|
138
|
+
const matching = entries.filter((entry) => (
|
|
139
|
+
isReceiptObject(entry)
|
|
140
|
+
&& entry.width === expected.width
|
|
141
|
+
&& entry.height === expected.height
|
|
142
|
+
&& (expected.theme === undefined || entry.requestedTheme === expected.theme)
|
|
143
|
+
));
|
|
144
|
+
return matching.length === 1 && predicate(matching[0], expected);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validBrowserEvidence(receipt) {
|
|
149
|
+
if (receipt.family === 'explainer') {
|
|
150
|
+
const viewports = [1280, 1440].flatMap(width => ['light', 'dark'].map(theme => ({ width, height: 900, theme })));
|
|
151
|
+
const groups = ['containment', 'readability', 'viewerChrome', 'themeStates'];
|
|
152
|
+
return receipt.viewportPolicy === 'explainer-desktop-v1'
|
|
153
|
+
&& groups.every(group => receipt[group]?.status === 'pass'
|
|
154
|
+
&& exactViewportCoverage(receipt[group].viewports?.map(entry => ({ ...entry, requestedTheme: entry.requestedTheme || entry.theme })), viewports, (entry, expected) =>
|
|
155
|
+
entry.ok === true && (entry.requestedTheme || entry.theme) === expected.theme))
|
|
156
|
+
&& ['commonQa', 'interactions', 'references', 'noJavaScript', 'print'].every(group =>
|
|
157
|
+
receipt[group]?.status === 'pass' && Array.isArray(receipt[group].observations) && receipt[group].observations.length > 0
|
|
158
|
+
&& receipt[group].observations.every(entry => entry.ok === true));
|
|
159
|
+
}
|
|
160
|
+
return exactViewportCoverage(receipt.containment?.viewports, VISUAL_CHECK_VIEWPORTS, (entry) => (
|
|
161
|
+
entry.theme === 'light' && entry.ok === true
|
|
162
|
+
))
|
|
163
|
+
&& exactViewportCoverage(receipt.readability?.viewports, VISUAL_CHECK_VIEWPORTS, (entry) => (
|
|
164
|
+
entry.theme === 'light' && entry.ok === true && entry.readabilityOk === true
|
|
165
|
+
))
|
|
166
|
+
&& exactViewportCoverage(receipt.viewerChrome?.viewports, VISUAL_CHECK_VIEWPORTS, (entry) => (
|
|
167
|
+
entry.theme === 'light' && entry.ok === true && entry.viewerChromeOk === true
|
|
168
|
+
))
|
|
169
|
+
&& exactViewportCoverage(receipt.themeStates?.viewports,
|
|
170
|
+
[...VISUAL_CHECK_VIEWPORTS.map((viewport) => ({ ...viewport, theme: 'light' })),
|
|
171
|
+
...CAPTURE_VIEWPORTS.map((viewport) => ({ ...viewport, theme: 'dark' }))],
|
|
172
|
+
(entry, expected) => entry.ok === true
|
|
173
|
+
&& entry.requestedTheme === expected.theme
|
|
174
|
+
&& entry.resolvedTheme === expected.theme);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function validDeliveryValidation(receipt, quality, { allowShowcaseWarnings = false } = {}) {
|
|
178
|
+
const validation = receipt?.validation;
|
|
179
|
+
return isReceiptObject(validation)
|
|
180
|
+
&& Number.isInteger(validation.checkCount)
|
|
181
|
+
&& validation.checkCount > 0
|
|
182
|
+
&& validation.checksPassed === validation.checkCount
|
|
183
|
+
&& validation.compositionStatus === 'pass'
|
|
184
|
+
&& validation.errors === 0
|
|
185
|
+
&& validation.compositionProfile === quality
|
|
186
|
+
&& (quality !== 'showcase' || allowShowcaseWarnings || validation.warnings === 0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validStageReceipt(stage, receipt, quality, options) {
|
|
190
|
+
if (!isReceiptObject(receipt) || receipt.ok !== true) return false;
|
|
191
|
+
if (receipt.status && receipt.status !== 'pass') return false;
|
|
192
|
+
if (stage === 'validate') return receipt.command === 'validate' && Array.isArray(receipt.checks);
|
|
193
|
+
if (stage === 'deliver') {
|
|
194
|
+
return receipt.command === 'deliver'
|
|
195
|
+
&& receipt.schemaVersion === 1
|
|
196
|
+
&& validReceiptId(receipt.receiptId)
|
|
197
|
+
&& isReceiptObject(receipt.specification)
|
|
198
|
+
&& validIdentity(receipt.specification)
|
|
199
|
+
&& validIdentity(receipt.artifact)
|
|
200
|
+
&& validDeliveryValidation(receipt, quality, options);
|
|
201
|
+
}
|
|
202
|
+
if (stage === 'check') {
|
|
203
|
+
return validIdentity(receipt.artifact)
|
|
204
|
+
&& allChecksPassed(receipt.checks)
|
|
205
|
+
&& receipt.provenance === 'current'
|
|
206
|
+
&& validReceiptId(receipt.deliveryReceiptId)
|
|
207
|
+
&& validCheckComposition(receipt, quality);
|
|
208
|
+
}
|
|
209
|
+
return receipt.command === 'browser-check'
|
|
210
|
+
&& receipt.schemaVersion === 1
|
|
211
|
+
&& receipt.status === 'pass'
|
|
212
|
+
&& receipt.evidenceKind === 'automated-browser'
|
|
213
|
+
&& validIdentity(receipt.artifact)
|
|
214
|
+
&& receipt.provenance === 'current'
|
|
215
|
+
&& validReceiptId(receipt.deliveryReceiptId)
|
|
216
|
+
&& receipt.containment?.status === 'pass'
|
|
217
|
+
&& receipt.themeStates?.status === 'pass'
|
|
218
|
+
&& receipt.readability?.status === 'pass'
|
|
219
|
+
&& receipt.viewerChrome?.status === 'pass'
|
|
220
|
+
&& validBrowserEvidence(receipt);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function identityMismatchDiagnostic({ stage, expected, actual, expectedReceiptId, actualReceiptId, output }) {
|
|
224
|
+
return {
|
|
225
|
+
code: 'finalize/artifact-binding-mismatch',
|
|
226
|
+
severity: 'error',
|
|
227
|
+
message: `The ${stage} receipt does not prove the artifact delivered by this finalize run.`,
|
|
228
|
+
subject: { stage, artifact: output },
|
|
229
|
+
evidence: {
|
|
230
|
+
expectedArtifact: expected,
|
|
231
|
+
actualArtifact: actual,
|
|
232
|
+
...(expectedReceiptId ? { expectedDeliveryReceiptId: expectedReceiptId } : {}),
|
|
233
|
+
...(actualReceiptId ? { actualDeliveryReceiptId: actualReceiptId } : {}),
|
|
234
|
+
},
|
|
235
|
+
supportedFixes: ['finish other delivery attempts for this output, then rerun finalize from the frozen candidate'],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function showcaseWarningDiagnostics(receipt) {
|
|
240
|
+
const issues = receipt.validation.compositionIssues;
|
|
241
|
+
const warnings = receipt.validation.warnings;
|
|
242
|
+
if (!Array.isArray(issues) || issues.length !== warnings
|
|
243
|
+
|| !issues.every((issue) => isReceiptObject(issue)
|
|
244
|
+
&& issue.severity === 'warning'
|
|
245
|
+
&& typeof issue.code === 'string'
|
|
246
|
+
&& issue.code.startsWith('composition/')
|
|
247
|
+
&& typeof issue.detail === 'string' && issue.detail.trim())) {
|
|
248
|
+
return [{
|
|
249
|
+
code: 'finalize/showcase-warnings', severity: 'error',
|
|
250
|
+
message: `Showcase delivery reported ${warnings} composition warning(s), but did not provide matching issue details.`,
|
|
251
|
+
subject: { stage: 'deliver', check: 'composition' },
|
|
252
|
+
evidence: { warnings, reportedIssues: Array.isArray(issues) ? issues.length : null },
|
|
253
|
+
supportedFixes: ['run validate on the frozen candidate to inspect the composition warnings, repair them, then rerun finalize'],
|
|
254
|
+
}];
|
|
255
|
+
}
|
|
256
|
+
return issues.map((issue) => {
|
|
257
|
+
const { code, severity, detail, ...evidence } = issue;
|
|
258
|
+
return {
|
|
259
|
+
code, severity: 'error',
|
|
260
|
+
message: `Showcase delivery reported ${code}; zero warnings are required.`,
|
|
261
|
+
subject: { stage: 'deliver', check: 'composition', ...(issue.nodeId ? { nodeId: issue.nodeId } : {}) },
|
|
262
|
+
evidence: { reportedSeverity: severity, ...evidence },
|
|
263
|
+
supportedFixes: [detail.replace(/^\[[^\]]+\]\s*/, '')],
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function stageBindingDiagnostic({ stage, receipt, expectedArtifact, expectedReceiptId, output, specification, type, deliveryValidation }) {
|
|
269
|
+
if (stage === 'deliver') {
|
|
270
|
+
if (receipt.type !== type) {
|
|
271
|
+
return {
|
|
272
|
+
code: 'finalize/delivery-type-mismatch',
|
|
273
|
+
severity: 'error',
|
|
274
|
+
message: 'The delivery command reported a different diagram type than finalize requested.',
|
|
275
|
+
subject: { stage: 'deliver', artifact: output },
|
|
276
|
+
evidence: { expectedType: type, actualType: receipt.type },
|
|
277
|
+
supportedFixes: ['rerun finalize with a delivery command for the requested diagram type'],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (!identitiesMatch(receipt.specification, specification)) {
|
|
281
|
+
return {
|
|
282
|
+
code: 'finalize/candidate-changed-during-delivery',
|
|
283
|
+
severity: 'error',
|
|
284
|
+
message: 'The delivery command froze a different candidate than finalize started with.',
|
|
285
|
+
subject: { stage: 'validate', candidate: specification.path },
|
|
286
|
+
evidence: {
|
|
287
|
+
expectedSha256: specification.sha256,
|
|
288
|
+
actualSha256: receipt.specification?.sha256,
|
|
289
|
+
expectedBytes: specification.bytes,
|
|
290
|
+
actualBytes: receipt.specification?.bytes,
|
|
291
|
+
},
|
|
292
|
+
supportedFixes: ['restore the frozen candidate and rerun finalize'],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if ((!receipt.output || !pathsAlias(receipt.output, output)) || !identitiesMatch(receipt.artifact, identity(output))) {
|
|
296
|
+
return identityMismatchDiagnostic({
|
|
297
|
+
stage,
|
|
298
|
+
expected: receipt.artifact,
|
|
299
|
+
actual: identity(output),
|
|
300
|
+
expectedReceiptId: receipt.receiptId,
|
|
301
|
+
output,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (stage === 'browser-check' && ((type === 'explainer') !== (receipt.family === 'explainer'))) {
|
|
307
|
+
return { code: 'finalize/browser-family-mismatch', severity: 'error', message: 'Browser evidence covers a different artifact family.',
|
|
308
|
+
subject: {stage, artifact: output}, evidence: {expectedType:type, actualFamily:receipt.family}, supportedFixes:['Rerun browser-check for the delivered artifact.'] };
|
|
309
|
+
}
|
|
310
|
+
if (stage === 'check' && deliveryValidation?.checkCount !== receipt.checks.length) {
|
|
311
|
+
return {
|
|
312
|
+
code: 'finalize/delivery-validation-mismatch',
|
|
313
|
+
severity: 'error',
|
|
314
|
+
message: 'The delivery validation count does not match the complete checker receipt for the delivered artifact.',
|
|
315
|
+
subject: { stage: 'check', artifact: output },
|
|
316
|
+
evidence: {
|
|
317
|
+
deliveryCheckCount: deliveryValidation?.checkCount,
|
|
318
|
+
checkerCheckCount: receipt.checks.length,
|
|
319
|
+
},
|
|
320
|
+
supportedFixes: ['restore the complete delivery validation receipt and rerun finalize from the frozen candidate'],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const receiptArtifactPath = receipt.artifact?.path;
|
|
324
|
+
const receiptFile = receipt.file;
|
|
325
|
+
const pathMatchesOutput = stage === 'check'
|
|
326
|
+
? typeof receiptFile === 'string' && pathsAlias(receiptFile, output)
|
|
327
|
+
: typeof receiptArtifactPath === 'string' && pathsAlias(receiptArtifactPath, output);
|
|
328
|
+
if (!pathMatchesOutput
|
|
329
|
+
|| !identitiesMatch(receipt.artifact, expectedArtifact)
|
|
330
|
+
|| receipt.deliveryReceiptId !== expectedReceiptId) {
|
|
331
|
+
return identityMismatchDiagnostic({
|
|
332
|
+
stage,
|
|
333
|
+
expected: expectedArtifact,
|
|
334
|
+
actual: receipt.artifact,
|
|
335
|
+
expectedReceiptId,
|
|
336
|
+
actualReceiptId: receipt.deliveryReceiptId,
|
|
337
|
+
output,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function finalArtifactDiagnostic({ output, expectedArtifact, expectedReceiptId, type, deliveryPath, inspectDelivery }) {
|
|
344
|
+
// Recheck dev's pending/lock, physical identity, and regular-file provenance barriers at handoff.
|
|
345
|
+
const provenance = inspectDelivery?.(output);
|
|
346
|
+
if (provenance && (!provenance.ok || provenance.receiptId !== expectedReceiptId)) {
|
|
347
|
+
return provenance.diagnostics?.[0] || identityMismatchDiagnostic({
|
|
348
|
+
stage: 'finalize', output, expected: expectedArtifact,
|
|
349
|
+
expectedReceiptId, actualReceiptId: provenance.receiptId,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
const currentArtifact = identity(output);
|
|
353
|
+
let delivery;
|
|
354
|
+
try {
|
|
355
|
+
delivery = JSON.parse(fs.readFileSync(deliveryPath, 'utf8'));
|
|
356
|
+
} catch {
|
|
357
|
+
delivery = null;
|
|
358
|
+
}
|
|
359
|
+
if (identitiesMatch(currentArtifact, expectedArtifact)
|
|
360
|
+
&& delivery?.schemaVersion === 1
|
|
361
|
+
&& delivery.command === 'deliver'
|
|
362
|
+
&& delivery.status === 'current'
|
|
363
|
+
&& delivery.receiptId === expectedReceiptId
|
|
364
|
+
&& delivery.type === type
|
|
365
|
+
&& typeof delivery.output === 'string' && pathsAlias(delivery.output, output)
|
|
366
|
+
&& identitiesMatch(delivery.artifact, expectedArtifact)) return null;
|
|
367
|
+
return {
|
|
368
|
+
code: 'finalize/final-artifact-mismatch',
|
|
369
|
+
severity: 'error',
|
|
370
|
+
message: 'The artifact or delivery receipt changed after the final gate completed.',
|
|
371
|
+
subject: { stage: 'finalize', artifact: output },
|
|
372
|
+
evidence: {
|
|
373
|
+
expectedArtifact,
|
|
374
|
+
currentArtifact,
|
|
375
|
+
expectedDeliveryReceiptId: expectedReceiptId,
|
|
376
|
+
expectedType: type,
|
|
377
|
+
...(delivery?.receiptId ? { currentDeliveryReceiptId: delivery.receiptId } : {}),
|
|
378
|
+
...(delivery?.type ? { currentType: delivery.type } : {}),
|
|
379
|
+
...(delivery?.artifact ? { currentDeliveryArtifact: delivery.artifact } : {}),
|
|
380
|
+
},
|
|
381
|
+
supportedFixes: ['finish other delivery attempts for this output, then rerun finalize from the frozen candidate'],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function stageStatus(stage, exitCode, receipt, quality) {
|
|
386
|
+
if (exitCode === 2 || receipt?.status === 'skipped') return 'skipped';
|
|
387
|
+
return exitCode === 0 && validStageReceipt(stage, receipt, quality) ? 'pass' : 'fail';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function consolidateDiagnostics(diagnostics) {
|
|
391
|
+
const result = [];
|
|
392
|
+
const seen = new Set();
|
|
393
|
+
const overflow = [];
|
|
394
|
+
let overflowIndex = -1;
|
|
395
|
+
for (const diagnostic of diagnostics || []) {
|
|
396
|
+
if (diagnostic?.code === 'viewer/viewport-overflow') {
|
|
397
|
+
if (overflowIndex < 0) overflowIndex = result.length;
|
|
398
|
+
overflow.push(diagnostic);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const key = `${diagnostic?.code}${diagnostic?.message}${JSON.stringify(diagnostic?.subject ?? null)}`;
|
|
402
|
+
if (seen.has(key)) continue;
|
|
403
|
+
seen.add(key);
|
|
404
|
+
result.push(diagnostic);
|
|
405
|
+
}
|
|
406
|
+
const slimEvidence = (evidence) => {
|
|
407
|
+
if (!evidence || typeof evidence !== 'object') return evidence;
|
|
408
|
+
const { innerWidth: _w, innerHeight: _h, readerLayout: _l, readerOverflow: _o, readerFit: _f, ...rest } = evidence;
|
|
409
|
+
return rest;
|
|
410
|
+
};
|
|
411
|
+
if (overflow.length > 1) {
|
|
412
|
+
const first = overflow[0];
|
|
413
|
+
const tightest = overflow
|
|
414
|
+
.map((entry) => entry.subject?.viewport)
|
|
415
|
+
.filter(Boolean)
|
|
416
|
+
.sort((a, b) => a.width * a.height - b.width * b.height)[0];
|
|
417
|
+
const fixes = tightest
|
|
418
|
+
? [`contain the rendered layout within ${tightest.width}x${tightest.height} (smallest reported viewport), then rerun browser-check`]
|
|
419
|
+
: [...new Set(overflow.flatMap((entry) => entry.supportedFixes || []))];
|
|
420
|
+
result.splice(overflowIndex, 0, {
|
|
421
|
+
...first,
|
|
422
|
+
message: `The rendered artifact overflows ${overflow.length} viewport/theme combinations.`,
|
|
423
|
+
subject: { diagramType: first.subject?.diagramType },
|
|
424
|
+
supportedFixes: fixes,
|
|
425
|
+
evidence: {
|
|
426
|
+
overflows: overflow.map((entry) => {
|
|
427
|
+
const { artifact: _artifact, ...subject } = entry.subject || {};
|
|
428
|
+
return {
|
|
429
|
+
// Predicted validate-stage overflow carries estimatedTextWidthPx;
|
|
430
|
+
// the browser-check measurement carries scroll* geometry instead.
|
|
431
|
+
detection: entry.evidence?.estimatedTextWidthPx != null ? 'validate-prediction' : 'browser-measurement',
|
|
432
|
+
subject,
|
|
433
|
+
evidence: slimEvidence(entry.evidence),
|
|
434
|
+
};
|
|
435
|
+
}),
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
} else if (overflow.length === 1) {
|
|
439
|
+
const [entry] = overflow;
|
|
440
|
+
const { artifact: _artifact, ...subject } = entry.subject || {};
|
|
441
|
+
result.splice(overflowIndex, 0, { ...entry, subject, evidence: slimEvidence(entry.evidence) });
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function failureDiagnostics(stage, result, receipt, quality) {
|
|
447
|
+
if (Array.isArray(receipt?.diagnostics) && receipt.diagnostics.length) return receipt.diagnostics;
|
|
448
|
+
const invalidReceipt = (result.status ?? 1) === 0 && !validStageReceipt(stage, receipt, quality);
|
|
449
|
+
return [{
|
|
450
|
+
code: invalidReceipt ? 'finalize/invalid-stage-receipt' : 'finalize/stage-failure',
|
|
451
|
+
severity: 'error',
|
|
452
|
+
message: invalidReceipt
|
|
453
|
+
? `The ${stage} stage exited successfully without a valid passing receipt.`
|
|
454
|
+
: `The ${stage} stage did not complete successfully.`,
|
|
455
|
+
subject: { stage },
|
|
456
|
+
evidence: {
|
|
457
|
+
exitCode: result.status ?? 1,
|
|
458
|
+
...(result.signal ? { signal: result.signal } : {}),
|
|
459
|
+
...(result.error?.message ? { reason: result.error.message } : {}),
|
|
460
|
+
...(String(result.stdout || '').trim() ? { stdout: String(result.stdout).trim().slice(0, 2000) } : {}),
|
|
461
|
+
...(String(result.stderr || '').trim() ? { stderr: String(result.stderr).trim().slice(0, 2000) } : {}),
|
|
462
|
+
},
|
|
463
|
+
supportedFixes: [invalidReceipt
|
|
464
|
+
? `restore the ${stage} JSON receipt contract before retrying finalize`
|
|
465
|
+
: 'use the compact finalize summary to repair the named subject in place, then rerun finalize once'],
|
|
466
|
+
}];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function sidecarFile(directory, value) {
|
|
470
|
+
if (!value) return null;
|
|
471
|
+
return path.resolve(directory, value);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function browserEvidence(receipt, artifactPath) {
|
|
475
|
+
if (!receipt) return {};
|
|
476
|
+
const directory = receipt.sidecars?.directory
|
|
477
|
+
? path.resolve(receipt.sidecars.directory)
|
|
478
|
+
: path.dirname(path.resolve(artifactPath));
|
|
479
|
+
return {
|
|
480
|
+
...(receipt.sidecars?.receipt ? { browserCheckReceipt: sidecarFile(directory, receipt.sidecars.receipt) } : {}),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Stage receipts (dense validate/check JSON) can exceed execFile's 1 MiB
|
|
485
|
+
// default. Track ARCHIFY_CHECK_MAX_BUFFER so a raised checker limit is not
|
|
486
|
+
// defeated one layer up, with a 4 MiB floor: a lowered knob makes failing
|
|
487
|
+
// stages emit compact classified receipts, but those receipts — and any
|
|
488
|
+
// successful receipt the knob let through — still have to fit the capture.
|
|
489
|
+
// The 64 KiB headroom covers the stage echo over the raw checker receipt:
|
|
490
|
+
// commandCheck appends provenance and deliveryReceiptId after the checker
|
|
491
|
+
// ran, so the printed receipt can exceed the knob that admitted it. Read the
|
|
492
|
+
// knob from the runner's env, not process.env: runFinalize callers supply
|
|
493
|
+
// their own env and the child stages inherit exactly that object.
|
|
494
|
+
function stageCaptureMaxBuffer(env = process.env) {
|
|
495
|
+
const parsed = Number(env.ARCHIFY_CHECK_MAX_BUFFER);
|
|
496
|
+
const knob = Number.isFinite(parsed) && Math.floor(parsed) >= 1
|
|
497
|
+
? Math.floor(parsed)
|
|
498
|
+
: 64 * 1024 * 1024;
|
|
499
|
+
return Math.max(knob, 4 * 1024 * 1024) + 64 * 1024;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function defaultRunner({ cliPath, args, cwd, env }) {
|
|
503
|
+
return new Promise((resolve) => {
|
|
504
|
+
execFile(process.execPath, runtimeArgs([cliPath, ...args]), { cwd, env, encoding: 'utf8', maxBuffer: stageCaptureMaxBuffer(env) }, (error, stdout, stderr) => {
|
|
505
|
+
resolve({
|
|
506
|
+
status: error ? (Number.isInteger(error.code) ? error.code : 1) : 0,
|
|
507
|
+
stdout,
|
|
508
|
+
stderr,
|
|
509
|
+
...(error ? { error, signal: error.signal } : {}),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function stageArguments({ stage, type, input, output, quality, repoRoot, outDir, writingMode }) {
|
|
516
|
+
const qualityArgs = ['--quality', quality];
|
|
517
|
+
const repoArgs = [...(repoRoot ? ['--repo-root', repoRoot] : []), ...(type==='explainer' && writingMode ? ['--writing',writingMode] : [])];
|
|
518
|
+
if (stage === 'validate') return ['validate', type, input, ...qualityArgs, ...repoArgs, '--json'];
|
|
519
|
+
if (stage === 'deliver') return ['deliver', type, input, output, ...qualityArgs, ...repoArgs, '--json'];
|
|
520
|
+
if (stage === 'check') return ['check', output, '--require-provenance'];
|
|
521
|
+
return [
|
|
522
|
+
'browser-check', output, '--json', '--require-provenance',
|
|
523
|
+
...(outDir ? ['--out-dir', outDir] : []),
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function defaultFinalizeReceiptPath(output, { outDir } = {}) {
|
|
528
|
+
// Reuse the physical artifact and cross-directory namespace used by browser evidence.
|
|
529
|
+
const browserReceipt = browserCheckSidecarPaths(output, { outDir }).receipt;
|
|
530
|
+
const stem = path.basename(browserReceipt).slice(0, -'.browser-check.json'.length);
|
|
531
|
+
return path.join(path.dirname(browserReceipt), `${boundedSidecarStem(stem, ['.finalize.json', '.finalize-summary.json'])}.finalize.json`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function defaultFinalizeSummaryPath(receiptPath) {
|
|
535
|
+
const receipt = path.resolve(receiptPath);
|
|
536
|
+
const component = path.basename(receipt);
|
|
537
|
+
const isDefault = /\.finalize\.json$/i.test(component);
|
|
538
|
+
const stem = component.replace(isDefault ? /\.finalize\.json$/i : /\.json$/i, '');
|
|
539
|
+
const suffix = isDefault ? '.finalize-summary.json' : '-summary.json';
|
|
540
|
+
// Already bounded default stems share the receipt namespace; avoid hashing twice.
|
|
541
|
+
const summaryStem = Buffer.byteLength(`${stem}${suffix}`) <= 255 && `${stem}${suffix}`.length <= 255
|
|
542
|
+
? stem : boundedSidecarStem(stem, [suffix]);
|
|
543
|
+
return path.join(path.dirname(receipt), `${summaryStem}${suffix}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function defaultDeliveryPaths(output) {
|
|
547
|
+
const artifact = canonicalFuturePath(output);
|
|
548
|
+
const delivery = artifact.replace(/\.html?$/i, '.delivery.json');
|
|
549
|
+
return {
|
|
550
|
+
provenance: delivery,
|
|
551
|
+
pending: delivery.replace(/\.json$/i, '-pending.json'),
|
|
552
|
+
lock: delivery.replace(/\.json$/i, '-lock.json'),
|
|
553
|
+
directoryLock: path.join(path.dirname(artifact), '.archify-delivery-lock.json'),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function reservedFinalizePaths({ input, output, outDir, deliveryPaths }) {
|
|
558
|
+
const browser = browserCheckSidecarPaths(output, { outDir });
|
|
559
|
+
return [path.resolve(input), path.resolve(output), ...Object.values(deliveryPaths(output)), browser.receipt];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Node moves that remove the measured crossings and detours, most specific
|
|
563
|
+
// first. Only positions and sizes change; every relationship keeps its endpoints.
|
|
564
|
+
function placementHints({ crossings = [], detours = [], crowdedSides = [] }) {
|
|
565
|
+
const name = (relation) => `${relation.from} → ${relation.to}`;
|
|
566
|
+
const other = (relation, shared) => (relation.from === shared ? relation.to : relation.from);
|
|
567
|
+
const hints = [];
|
|
568
|
+
for (const side of crowdedSides) {
|
|
569
|
+
hints.push(`${side.node} has ${side.relationships} relationships facing its ${side.side} side, which fits ${Math.max(1, Math.floor((side.sidePx - 32) / 14) + 1)} ports: make that side at least ${side.neededPx}px, or move some of those neighbours so they face another side of ${side.node}.`);
|
|
570
|
+
}
|
|
571
|
+
for (const crossing of crossings) {
|
|
572
|
+
const shared = crossing.sharedNode;
|
|
573
|
+
hints.push(shared
|
|
574
|
+
? `${name(crossing.left)} and ${name(crossing.right)} cross next to ${shared}: move node ${other(crossing.left, shared)} or ${other(crossing.right, shared)} so the two reach ${shared} from different sides (for example one level with it, one directly above or below it).`
|
|
575
|
+
: `${name(crossing.left)} crosses ${name(crossing.right)}: move the node of whichever is a branch, return, or second entrance to the other side of the main path, so that relationship runs through an empty corridor.`);
|
|
576
|
+
}
|
|
577
|
+
for (const detour of detours) {
|
|
578
|
+
if (detour.directCorridorBlockers?.length) continue;
|
|
579
|
+
hints.push(`${name(detour.relationship)} needs ${detour.bends} bends: move node ${detour.relationship.from} or ${detour.relationship.to} so they share a row or column with matching centers, or sit diagonally with a clear corner.`);
|
|
580
|
+
}
|
|
581
|
+
return [...new Set(hints)].slice(0, 8);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Route failures call for a connected-scene repair; size and text failures do not.
|
|
585
|
+
const ROUTE_DIAGNOSTIC = /^(clean-flow\/|composition\/(proper-crossing|ambiguous-corridor|border-run|container-border-run|excessive-route-detour|route-rhythm|arrowhead-collision))/;
|
|
586
|
+
|
|
587
|
+
export function compactFinalizeReceipt(receipt) {
|
|
588
|
+
const gates = {};
|
|
589
|
+
for (const stage of FINALIZE_STAGES) gates[stage] = receipt.stages?.[stage]?.status || 'not-run';
|
|
590
|
+
const allDiagnostics = receipt.diagnostics || [];
|
|
591
|
+
const diagnosticLimit = 8;
|
|
592
|
+
const selectedDiagnostics = [];
|
|
593
|
+
const selectedIndexes = new Set();
|
|
594
|
+
const seenCodes = new Set();
|
|
595
|
+
const seenSubjects = new Set();
|
|
596
|
+
const subjectKey = (entry) => {
|
|
597
|
+
const subject = entry?.subject;
|
|
598
|
+
if (!subject) return null;
|
|
599
|
+
if (typeof subject === 'string') return subject;
|
|
600
|
+
if (typeof subject !== 'object' || Array.isArray(subject)) return JSON.stringify(subject);
|
|
601
|
+
for (const key of ['id', 'edge', 'connection', 'relationship', 'component', 'node', 'path', 'stage']) {
|
|
602
|
+
if (subject[key] !== undefined) return `${key}:${JSON.stringify(subject[key])}`;
|
|
603
|
+
}
|
|
604
|
+
return JSON.stringify(subject);
|
|
605
|
+
};
|
|
606
|
+
const addDiagnostic = (entry, index) => {
|
|
607
|
+
if (selectedDiagnostics.length >= diagnosticLimit || selectedIndexes.has(index)) return;
|
|
608
|
+
selectedIndexes.add(index);
|
|
609
|
+
selectedDiagnostics.push({
|
|
610
|
+
code: entry.code,
|
|
611
|
+
severity: entry.severity || 'error',
|
|
612
|
+
message: entry.message,
|
|
613
|
+
...(entry.subject ? { subject: entry.subject } : {}),
|
|
614
|
+
...(entry.evidence && Object.keys(entry.evidence).length ? { evidence: entry.evidence } : {}),
|
|
615
|
+
// Verified repairs may require every edit in the set to be applied together.
|
|
616
|
+
...(Array.isArray(entry.supportedFixes) && entry.supportedFixes.length
|
|
617
|
+
? { supportedFixes: [...entry.supportedFixes] } : {}),
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
for (const [index, entry] of allDiagnostics.entries()) {
|
|
621
|
+
if (seenCodes.has(entry.code)) continue;
|
|
622
|
+
seenCodes.add(entry.code);
|
|
623
|
+
addDiagnostic(entry, index);
|
|
624
|
+
}
|
|
625
|
+
for (const [index, entry] of allDiagnostics.entries()) {
|
|
626
|
+
const key = subjectKey(entry);
|
|
627
|
+
if (!key || seenSubjects.has(key)) continue;
|
|
628
|
+
seenSubjects.add(key);
|
|
629
|
+
addDiagnostic(entry, index);
|
|
630
|
+
}
|
|
631
|
+
for (const [index, entry] of allDiagnostics.entries()) addDiagnostic(entry, index);
|
|
632
|
+
const truncated = allDiagnostics.length > selectedDiagnostics.length;
|
|
633
|
+
const omittedCounts = new Map();
|
|
634
|
+
if (truncated) {
|
|
635
|
+
for (const [index, entry] of allDiagnostics.entries()) {
|
|
636
|
+
omittedCounts.set(entry.code, (omittedCounts.get(entry.code) || 0) + (selectedIndexes.has(index) ? 0 : 1));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const compact = {
|
|
640
|
+
schemaVersion: 1,
|
|
641
|
+
ok: receipt.ok,
|
|
642
|
+
command: 'finalize',
|
|
643
|
+
status: receipt.status,
|
|
644
|
+
type: receipt.type,
|
|
645
|
+
quality: receipt.quality,
|
|
646
|
+
specification: receipt.specification,
|
|
647
|
+
artifact: receipt.artifact,
|
|
648
|
+
gates,
|
|
649
|
+
...(receipt.failedStage ? { failedStage: receipt.failedStage } : {}),
|
|
650
|
+
diagnostics: selectedDiagnostics,
|
|
651
|
+
diagnosticSummary: {
|
|
652
|
+
total: allDiagnostics.length,
|
|
653
|
+
shown: selectedDiagnostics.length,
|
|
654
|
+
truncated,
|
|
655
|
+
...(truncated ? { omittedByCode: Object.fromEntries([...omittedCounts].filter(([, count]) => count > 0)) } : {}),
|
|
656
|
+
},
|
|
657
|
+
evidence: receipt.evidence,
|
|
658
|
+
...(receipt.update && receipt.update.status !== 'unavailable'
|
|
659
|
+
? { update: receipt.update }
|
|
660
|
+
: {}),
|
|
661
|
+
visualReview: receipt.visualReview || 'not-requested',
|
|
662
|
+
durationMs: receipt.durationMs,
|
|
663
|
+
...(receipt.browserCheck ? {browserCheck:receipt.browserCheck} : {}),
|
|
664
|
+
};
|
|
665
|
+
const metrics = receipt.stages?.check?.receipt?.composition?.metrics;
|
|
666
|
+
const reviewSignals = Object.fromEntries([
|
|
667
|
+
'resolvedCrossovers', 'routesOverSuggestedBends', 'routesOverSuggestedStretch',
|
|
668
|
+
].filter((key) => Number.isFinite(metrics?.[key]) && metrics[key] > 0)
|
|
669
|
+
.map((key) => [key, metrics[key]]));
|
|
670
|
+
if (receipt.ok && Object.keys(reviewSignals).length) {
|
|
671
|
+
const routeReview = receipt.stages?.check?.receipt?.composition?.routeReview;
|
|
672
|
+
const hints = routeReview ? placementHints(routeReview) : [];
|
|
673
|
+
// Modes without free node placement cannot act on node-move hints.
|
|
674
|
+
const advisoryOnly = Boolean(receipt.type) && receipt.type !== 'architecture';
|
|
675
|
+
compact.visualReviewRecommendation = {
|
|
676
|
+
action: 'inspect-route-readability',
|
|
677
|
+
signals: reviewSignals,
|
|
678
|
+
reason: 'Automated gates passed, but crossings or detours still need perceptual review before claiming visual quality.',
|
|
679
|
+
...(routeReview ? {
|
|
680
|
+
affectedRoutes: {
|
|
681
|
+
crossings: routeReview.crossings.slice(0, 8),
|
|
682
|
+
detours: routeReview.detours.slice(0, 8),
|
|
683
|
+
truncated: routeReview.crossings.length > 8 || routeReview.detours.length > 8,
|
|
684
|
+
},
|
|
685
|
+
...(hints.length && !advisoryOnly ? { hints } : {}),
|
|
686
|
+
repair: !advisoryOnly
|
|
687
|
+
? 'Trace these relationships at the desktop viewport. For Architecture, use references/architecture-layout-repair.md: reflow a blocked main path or tangled connected scene, and repair an isolated defect locally only when the surrounding composition is accepted. Preserve all semantic content and user-fixed geometry. Rerun finalize once after the edit.'
|
|
688
|
+
: 'Advisory only: this mode places or routes these relationships itself. Report them; do not remove relationships or meaning to reduce crossings.',
|
|
689
|
+
} : {}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
const leadingSpace = receipt.stages?.check?.receipt?.composition?.leadingSpace;
|
|
693
|
+
if (receipt.ok && leadingSpace?.reviewSuggested === true) {
|
|
694
|
+
compact.layoutReviewRecommendation = {
|
|
695
|
+
action: 'inspect-leading-space',
|
|
696
|
+
evidence: leadingSpace,
|
|
697
|
+
reason: 'Measured content, including routes and labels, leaves a large empty area above the diagram. This is a composition suggestion, not a failed gate.',
|
|
698
|
+
repair: 'Check whether that leading space is intentional. If not, reposition the connected scene nearer the canvas origin while retaining room for its actual boundaries, labels and return routes. Preserve all meaning and user-fixed geometry, then rerun finalize. No screenshot is required.',
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const sequenceColumnSpace = receipt.stages?.check?.receipt?.composition?.sequenceColumnSpace;
|
|
702
|
+
if (receipt.ok && receipt.type === 'sequence' && sequenceColumnSpace?.reviewSuggested === true) {
|
|
703
|
+
compact.layoutReviewRecommendation = {
|
|
704
|
+
action: 'inspect-sequence-width',
|
|
705
|
+
evidence: sequenceColumnSpace,
|
|
706
|
+
reason: 'Fixed participant columns leave substantial unused space on the right, after accounting for message labels and notes. This is a layout suggestion, not a failed gate.',
|
|
707
|
+
repair: 'Check whether the explicit fixed column geometry is intentional. Only when changing that geometry is authorized, save the passing fixed candidate, set only meta.column_fit to "spread", and rerun the complete finalize once with --out-dir <folder>/width-review. If that attempt fails, restore the saved candidate and finalize it with --out-dir <folder>/width-restore; report the remaining suggestion instead of iterating. Preserve participant order, every message, its y position, labels, notes, sources and canvas dimensions. Retain intentional fixed layouts; report the suggestion instead of changing them automatically.',
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
if (!receipt.ok && receipt.status === 'fail' && receipt.failedStage === 'validate') {
|
|
711
|
+
compact.nextAction = {
|
|
712
|
+
action: 'edit-in-place',
|
|
713
|
+
candidate: receipt.specification?.path,
|
|
714
|
+
constraint: receipt.type === 'architecture' && (receipt.diagnostics || []).some((entry) => ROUTE_DIAGNOSTIC.test(entry.code || ''))
|
|
715
|
+
? 'Preserve all semantics and user-fixed geometry. Use references/architecture-layout-repair.md to choose a local repair or connected-scene reflow; edit the existing candidate.'
|
|
716
|
+
: 'Preserve unaffected semantics and geometry; apply each diagnostic\'s supportedFixes, or the fix its message names, and do not replace the whole candidate.',
|
|
717
|
+
then: 'finalize-once',
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return compact;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export async function runFinalize({
|
|
724
|
+
cliPath,
|
|
725
|
+
type,
|
|
726
|
+
input,
|
|
727
|
+
output,
|
|
728
|
+
quality = 'showcase',
|
|
729
|
+
repoRoot,
|
|
730
|
+
writingMode,
|
|
731
|
+
candidateSha256,
|
|
732
|
+
preparedExplainer,
|
|
733
|
+
browserCheck = false,
|
|
734
|
+
outDir,
|
|
735
|
+
receiptPath,
|
|
736
|
+
deliveryPaths = defaultDeliveryPaths,
|
|
737
|
+
inspectDelivery,
|
|
738
|
+
cwd = process.cwd(),
|
|
739
|
+
env = process.env,
|
|
740
|
+
runCommand = defaultRunner,
|
|
741
|
+
runBrowserCheck,
|
|
742
|
+
startUpdateCheck = startDeliveryUpdateCheck,
|
|
743
|
+
resolveChrome = findChrome,
|
|
744
|
+
createBrowser = (chromePath, options) => new ChromeVisualBrowser(chromePath, options),
|
|
745
|
+
} = {}) {
|
|
746
|
+
if (!cliPath || !type || !input || !output) throw new Error('finalize requires cliPath, type, input, and output.');
|
|
747
|
+
const needsBrowser = type !== 'explainer' || browserCheck;
|
|
748
|
+
const started = process.hrtime.bigint();
|
|
749
|
+
const startedAt = new Date().toISOString();
|
|
750
|
+
const resolvedInput = path.resolve(input);
|
|
751
|
+
const resolvedOutput = resolveOutputPath({ requestedOutput: output, inputPaths: [resolvedInput] }).outputPath;
|
|
752
|
+
const resolvedOutDir = outDir === undefined ? undefined : resolveNativeOutputDirectory(outDir);
|
|
753
|
+
const specification = identity(resolvedInput);
|
|
754
|
+
if (candidateSha256 && specification.sha256 !== candidateSha256) {
|
|
755
|
+
const error = new Error(`The candidate changed after validation: expected sha256 ${candidateSha256}, found ${specification.sha256 || 'unreadable'}.`);
|
|
756
|
+
error.finalizeCode = 'finalize/candidate-changed';
|
|
757
|
+
error.finalizeEvidence = {
|
|
758
|
+
candidate: resolvedInput,
|
|
759
|
+
expectedSha256: candidateSha256,
|
|
760
|
+
...(specification.sha256 ? { actualSha256: specification.sha256 } : {}),
|
|
761
|
+
};
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
// Validate raw native CLI paths before creating directories or normalizing them.
|
|
765
|
+
if (receiptPath !== undefined) resolveOutputPath({ requestedOutput: receiptPath, inputPaths: [resolvedInput], requiredExtension: '.json' });
|
|
766
|
+
fs.mkdirSync(path.dirname(canonicalFuturePath(resolvedOutput)), { recursive: true });
|
|
767
|
+
const resolvedReceipt = resolveOutputPath({
|
|
768
|
+
requestedOutput: receiptPath ?? defaultFinalizeReceiptPath(resolvedOutput, { outDir: resolvedOutDir }),
|
|
769
|
+
inputPaths: [resolvedInput], requiredExtension: '.json',
|
|
770
|
+
}).outputPath;
|
|
771
|
+
const resolvedSummary = resolveOutputPath({
|
|
772
|
+
requestedOutput: defaultFinalizeSummaryPath(resolvedReceipt), inputPaths: [resolvedInput], requiredExtension: '.json',
|
|
773
|
+
}).outputPath;
|
|
774
|
+
const assertReceiptPaths = () => {
|
|
775
|
+
const reserved = reservedFinalizePaths({ input: resolvedInput, output: resolvedOutput, outDir: resolvedOutDir, deliveryPaths });
|
|
776
|
+
const receiptCollision = reserved.find((file) => pathsAlias(resolvedReceipt, file));
|
|
777
|
+
if (receiptCollision) throw new Error(`The finalize receipt must be distinct from the specification, artifact, and gate sidecars: "${receiptCollision}".`);
|
|
778
|
+
const summaryCollision = [resolvedReceipt, ...reserved].find((file) => pathsAlias(resolvedSummary, file));
|
|
779
|
+
if (summaryCollision) throw new Error(`The finalize summary must be distinct from the full receipt, specification, artifact, and gate sidecars: "${summaryCollision}".`);
|
|
780
|
+
};
|
|
781
|
+
assertReceiptPaths();
|
|
782
|
+
// Capture both paths before changing either: an unsafe summary cannot leave a new full receipt behind.
|
|
783
|
+
let receiptCapture = captureReceipt(resolvedReceipt);
|
|
784
|
+
let summaryCapture = captureReceipt(resolvedSummary);
|
|
785
|
+
|
|
786
|
+
const receipt = {
|
|
787
|
+
schemaVersion: 1,
|
|
788
|
+
ok: false,
|
|
789
|
+
command: 'finalize',
|
|
790
|
+
status: 'running',
|
|
791
|
+
type,
|
|
792
|
+
quality,
|
|
793
|
+
startedAt,
|
|
794
|
+
specification,
|
|
795
|
+
artifact: { path: resolvedOutput },
|
|
796
|
+
stages: needsBrowser ? {} : {'browser-check':{status:'not-run',execution:'not-requested'}},
|
|
797
|
+
browserCheck: {requested:needsBrowser,ran:false},
|
|
798
|
+
diagnostics: [],
|
|
799
|
+
evidence: { receipt: resolvedReceipt, summaryReceipt: resolvedSummary },
|
|
800
|
+
visualReview: 'not-requested',
|
|
801
|
+
};
|
|
802
|
+
const persistReceipts = () => {
|
|
803
|
+
if (type === 'explainer') receipt.diagnostics = (receipt.diagnostics || []).map(entry => {
|
|
804
|
+
const location=preparedExplainer?.sourceMap?.[entry.subject?.path] || {};
|
|
805
|
+
return {...entry,subject:{input:resolvedInput,line:location.line || 1,component:'document',...(entry.subject || {})},
|
|
806
|
+
evidence:{reason:entry.code,example:entry.supportedFixes?.[0] || 'Repair the named original draft component and rerun finalize.',...(entry.evidence || {})}};
|
|
807
|
+
});
|
|
808
|
+
assertReceiptPaths();
|
|
809
|
+
for (const [file, capture] of [[resolvedReceipt, receiptCapture], [resolvedSummary, summaryCapture]]) {
|
|
810
|
+
const current = verifyAtomicOutput(capture.snapshot);
|
|
811
|
+
if (current.status !== 'match') throw receiptWriteError(file, current);
|
|
812
|
+
}
|
|
813
|
+
receiptCapture = writeJsonAtomic(resolvedReceipt, receipt, receiptCapture, assertReceiptPaths);
|
|
814
|
+
summaryCapture = writeJsonAtomic(resolvedSummary, compactFinalizeReceipt(receipt), summaryCapture, assertReceiptPaths);
|
|
815
|
+
};
|
|
816
|
+
persistReceipts();
|
|
817
|
+
// The gates below usually take seconds, so a slower network can finish the
|
|
818
|
+
// update check in parallel instead of timing out on every delivery.
|
|
819
|
+
const updateCheck = startUpdateCheck({ env, deadlineMs: needsBrowser ? FINALIZE_UPDATE_DEADLINE_MS : 200 });
|
|
820
|
+
|
|
821
|
+
// Resolve and launch Chrome only after delivery and strict artifact checks pass.
|
|
822
|
+
// Invalid candidates must not start a browser that cannot inspect an artifact.
|
|
823
|
+
let chromePath = null;
|
|
824
|
+
let browser;
|
|
825
|
+
let browserStartupError;
|
|
826
|
+
let browserTransferred = false;
|
|
827
|
+
const browserFactory = () => {
|
|
828
|
+
if (browserStartupError) throw browserStartupError;
|
|
829
|
+
if (browserTransferred || !browser) throw new Error('The finalize browser is unavailable or already consumed.');
|
|
830
|
+
browserTransferred = true;
|
|
831
|
+
return browser;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
let exitCode = 0;
|
|
836
|
+
for (const stage of (needsBrowser ? ['deliver', 'check', 'browser-check'] : ['deliver','check'])) {
|
|
837
|
+
const stageStarted = process.hrtime.bigint();
|
|
838
|
+
const args = stageArguments({
|
|
839
|
+
stage,
|
|
840
|
+
type,
|
|
841
|
+
input: resolvedInput,
|
|
842
|
+
output: resolvedOutput,
|
|
843
|
+
quality,
|
|
844
|
+
repoRoot,
|
|
845
|
+
writingMode,
|
|
846
|
+
outDir: resolvedOutDir,
|
|
847
|
+
});
|
|
848
|
+
const inProcess = stage === 'browser-check' && runBrowserCheck;
|
|
849
|
+
let result;
|
|
850
|
+
if (preparedExplainer) {
|
|
851
|
+
try {
|
|
852
|
+
const { assertPreparedDependencies } = await import('../explainer/prepare.mjs');
|
|
853
|
+
assertPreparedDependencies(preparedExplainer);
|
|
854
|
+
} catch (error) {
|
|
855
|
+
result = { status: 1, stdout: JSON.stringify({ok:false,status:'fail',command:stage,stage:'prepare',diagnostics:error.archifyDiagnostics || []}) };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (result) {
|
|
859
|
+
// The original resources changed between gates; do not run another gate on them.
|
|
860
|
+
} else if (inProcess) {
|
|
861
|
+
try {
|
|
862
|
+
chromePath = resolveChrome({ env });
|
|
863
|
+
if (chromePath) {
|
|
864
|
+
browser = createBrowser(chromePath, { env });
|
|
865
|
+
// Retain the startup rejection for the browser gate without emitting
|
|
866
|
+
// an unhandled rejection before inspect() awaits the same promise.
|
|
867
|
+
browser.sessionPromise.catch(() => {});
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
browserStartupError = error;
|
|
871
|
+
}
|
|
872
|
+
// Discovery errors have no executable for the browser factory. Record
|
|
873
|
+
// a failed gate instead of treating them as ordinary Chrome absence.
|
|
874
|
+
const checked = browserStartupError && !chromePath
|
|
875
|
+
? { exitCode: 1, receipt: persistBrowserCheckFailure(resolvedOutput, {
|
|
876
|
+
schemaVersion: 1, ok: false, command: 'browser-check', status: 'fail',
|
|
877
|
+
evidenceKind: 'automated-browser', visualReview: 'not-requested',
|
|
878
|
+
error: browserStartupError.message,
|
|
879
|
+
diagnostics: [{
|
|
880
|
+
code: 'viewer/browser-check-runtime', severity: 'error',
|
|
881
|
+
message: 'browser-check could not discover Chrome.',
|
|
882
|
+
subject: { artifact: resolvedOutput },
|
|
883
|
+
evidence: { reason: browserStartupError.message },
|
|
884
|
+
supportedFixes: ['resolve the reported Chrome discovery error, then rerun browser-check'],
|
|
885
|
+
}],
|
|
886
|
+
}, { outDir: resolvedOutDir }) }
|
|
887
|
+
: await runBrowserCheck({
|
|
888
|
+
artifactPath: resolvedOutput,
|
|
889
|
+
outDir: resolvedOutDir,
|
|
890
|
+
chromePath,
|
|
891
|
+
resolveChrome: () => chromePath,
|
|
892
|
+
browserFactory,
|
|
893
|
+
});
|
|
894
|
+
result = { status: checked.exitCode, stdout: JSON.stringify(checked.receipt) };
|
|
895
|
+
} else {
|
|
896
|
+
result = await runCommand({ stage, cliPath, args, cwd,
|
|
897
|
+
env: stage === 'deliver' ? { ...env, ARCHIFY_UPDATE_CHECK_DISABLED: '1' } : env });
|
|
898
|
+
}
|
|
899
|
+
if (preparedExplainer && result.status === 0) {
|
|
900
|
+
try {
|
|
901
|
+
const { assertPreparedDependencies } = await import('../explainer/prepare.mjs');
|
|
902
|
+
assertPreparedDependencies(preparedExplainer);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
result = { status: 1, stdout: JSON.stringify({ok:false,status:'fail',command:stage,stage:'prepare',diagnostics:error.archifyDiagnostics || []}) };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const stageReceipt = parsedReceipt(result.stdout);
|
|
908
|
+
if (stage === 'browser-check') receipt.browserCheck.ran = Boolean(stageReceipt?.containment?.viewports?.length);
|
|
909
|
+
const code = result.status ?? 1;
|
|
910
|
+
let status = stageStatus(stage, code, stageReceipt, quality);
|
|
911
|
+
let stageDiagnostics;
|
|
912
|
+
const command = [process.execPath, ...runtimeArgs([cliPath, ...args])];
|
|
913
|
+
const elapsed = durationMs(stageStarted);
|
|
914
|
+
if (status === 'pass') {
|
|
915
|
+
const deliveryReceipt = stage === 'deliver' ? stageReceipt : receipt.stages.deliver?.receipt;
|
|
916
|
+
const bindingDiagnostic = stageBindingDiagnostic({
|
|
917
|
+
stage,
|
|
918
|
+
receipt: stageReceipt,
|
|
919
|
+
expectedArtifact: deliveryReceipt?.artifact,
|
|
920
|
+
expectedReceiptId: deliveryReceipt?.receiptId,
|
|
921
|
+
output: resolvedOutput,
|
|
922
|
+
specification,
|
|
923
|
+
type,
|
|
924
|
+
deliveryValidation: deliveryReceipt?.validation,
|
|
925
|
+
});
|
|
926
|
+
if (bindingDiagnostic) stageDiagnostics = [bindingDiagnostic];
|
|
927
|
+
} else if (stage === 'deliver' && code === 0 && quality === 'showcase'
|
|
928
|
+
&& Number.isSafeInteger(stageReceipt?.validation?.warnings)
|
|
929
|
+
&& stageReceipt.validation.warnings > 0
|
|
930
|
+
&& validStageReceipt(stage, stageReceipt, quality, { allowShowcaseWarnings: true })) {
|
|
931
|
+
const bindingDiagnostic = stageBindingDiagnostic({
|
|
932
|
+
stage, receipt: stageReceipt, output: resolvedOutput, specification, type,
|
|
933
|
+
});
|
|
934
|
+
stageDiagnostics = bindingDiagnostic ? [bindingDiagnostic] : showcaseWarningDiagnostics(stageReceipt);
|
|
935
|
+
}
|
|
936
|
+
if (stageDiagnostics) {
|
|
937
|
+
status = 'fail';
|
|
938
|
+
}
|
|
939
|
+
const stageEntry = {
|
|
940
|
+
status,
|
|
941
|
+
exitCode: code,
|
|
942
|
+
durationMs: elapsed,
|
|
943
|
+
command,
|
|
944
|
+
...(inProcess ? { execution: 'in-process' } : {}),
|
|
945
|
+
...(stageReceipt ? { receipt: stageReceipt } : {}),
|
|
946
|
+
...(!stageReceipt && String(result.stdout || '').trim() ? { stdout: String(result.stdout).trim() } : {}),
|
|
947
|
+
...(String(result.stderr || '').trim() ? { stderr: String(result.stderr).trim() } : {}),
|
|
948
|
+
...(result.signal ? { signal: result.signal } : {}),
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
if (stage === 'deliver' && status === 'pass') {
|
|
952
|
+
// Non-blocking warnings (for example locale fallbacks) stay visible in
|
|
953
|
+
// a passing receipt; a later failing gate replaces them.
|
|
954
|
+
receipt.diagnostics = (stageReceipt.diagnostics || []).filter((entry) => entry?.severity === 'warning');
|
|
955
|
+
receipt.stages.validate = {
|
|
956
|
+
status: 'pass',
|
|
957
|
+
exitCode: 0,
|
|
958
|
+
durationMs: null,
|
|
959
|
+
execution: 'embedded-in-deliver',
|
|
960
|
+
command,
|
|
961
|
+
receipt: {
|
|
962
|
+
schemaVersion: 1,
|
|
963
|
+
ok: true,
|
|
964
|
+
command: 'validate',
|
|
965
|
+
specification: stageReceipt.specification,
|
|
966
|
+
validation: stageReceipt.validation,
|
|
967
|
+
},
|
|
968
|
+
};
|
|
969
|
+
receipt.stages.deliver = stageEntry;
|
|
970
|
+
} else if (stage === 'deliver') {
|
|
971
|
+
const validationFailed = stageDiagnostics?.some((diagnostic) => diagnostic.code === 'finalize/candidate-changed-during-delivery')
|
|
972
|
+
|| ['input', 'render', 'check'].includes(stageReceipt?.stage);
|
|
973
|
+
receipt.stages.validate = validationFailed ? {
|
|
974
|
+
...stageEntry,
|
|
975
|
+
status: 'fail',
|
|
976
|
+
durationMs: null,
|
|
977
|
+
execution: 'embedded-in-deliver',
|
|
978
|
+
} : { status: 'not-run', execution: 'embedded-in-deliver' };
|
|
979
|
+
receipt.stages.deliver = validationFailed
|
|
980
|
+
? { status: 'not-run', execution: 'blocked-by-validate' }
|
|
981
|
+
: stageEntry;
|
|
982
|
+
} else {
|
|
983
|
+
receipt.stages[stage] = stageEntry;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (stage === 'deliver' && stageReceipt?.artifact) receipt.artifact = {
|
|
987
|
+
path: resolvedOutput,
|
|
988
|
+
...stageReceipt.artifact,
|
|
989
|
+
};
|
|
990
|
+
if (stage === 'browser-check') {
|
|
991
|
+
receipt.evidence = {
|
|
992
|
+
receipt: resolvedReceipt,
|
|
993
|
+
summaryReceipt: resolvedSummary,
|
|
994
|
+
...browserEvidence(stageReceipt, resolvedOutput),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (status !== 'pass') {
|
|
999
|
+
exitCode = status === 'skipped' ? 2 : (code || 1);
|
|
1000
|
+
receipt.status = status;
|
|
1001
|
+
receipt.diagnostics = consolidateDiagnostics(
|
|
1002
|
+
stageDiagnostics || failureDiagnostics(stage, result, stageReceipt, quality),
|
|
1003
|
+
);
|
|
1004
|
+
receipt.failedStage = stage === 'deliver'
|
|
1005
|
+
&& receipt.stages.validate.status === 'fail' ? 'validate' : stage;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
persistReceipts();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
receipt.ok = exitCode === 0;
|
|
1012
|
+
receipt.status = receipt.ok ? 'pass' : receipt.status === 'running' ? 'fail' : receipt.status;
|
|
1013
|
+
if (receipt.ok) {
|
|
1014
|
+
const deliveryReceipt = receipt.stages.deliver?.receipt;
|
|
1015
|
+
const diagnostic = finalArtifactDiagnostic({
|
|
1016
|
+
output: resolvedOutput,
|
|
1017
|
+
expectedArtifact: deliveryReceipt?.artifact,
|
|
1018
|
+
expectedReceiptId: deliveryReceipt?.receiptId,
|
|
1019
|
+
type,
|
|
1020
|
+
deliveryPath: deliveryPaths(resolvedOutput).provenance,
|
|
1021
|
+
inspectDelivery,
|
|
1022
|
+
});
|
|
1023
|
+
if (diagnostic) {
|
|
1024
|
+
exitCode = 1;
|
|
1025
|
+
receipt.ok = false;
|
|
1026
|
+
receipt.status = 'fail';
|
|
1027
|
+
receipt.failedStage = 'finalize';
|
|
1028
|
+
receipt.diagnostics = [diagnostic];
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (receipt.ok && !identitiesMatch(specification, identity(resolvedInput))) {
|
|
1032
|
+
exitCode = 1;
|
|
1033
|
+
receipt.ok = false;
|
|
1034
|
+
receipt.status = 'fail';
|
|
1035
|
+
receipt.failedStage = 'finalize';
|
|
1036
|
+
receipt.diagnostics = [{
|
|
1037
|
+
code: 'finalize/candidate-changed', severity: 'error',
|
|
1038
|
+
message: 'The frozen candidate changed before the final handoff.',
|
|
1039
|
+
subject: { candidate: resolvedInput },
|
|
1040
|
+
evidence: { expected: specification, actual: identity(resolvedInput) },
|
|
1041
|
+
supportedFixes: ['restore the frozen candidate, or finalize the edited candidate as a new attempt'],
|
|
1042
|
+
}];
|
|
1043
|
+
}
|
|
1044
|
+
receipt.artifact = receipt.ok
|
|
1045
|
+
? { path: resolvedOutput, ...receipt.stages.deliver.receipt.artifact }
|
|
1046
|
+
: identity(resolvedOutput);
|
|
1047
|
+
receipt.finishedAt = new Date().toISOString();
|
|
1048
|
+
receipt.durationMs = durationMs(started);
|
|
1049
|
+
receipt.update = await updateCheck;
|
|
1050
|
+
persistReceipts();
|
|
1051
|
+
return { exitCode, receipt, summary: compactFinalizeReceipt(receipt) };
|
|
1052
|
+
} finally {
|
|
1053
|
+
if (browser && !browserTransferred) await browser.close();
|
|
1054
|
+
await updateCheck;
|
|
1055
|
+
}
|
|
1056
|
+
}
|