@rubytech/create-sitedesk-code 0.1.491 → 0.1.493
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/package.json +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +46 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js +135 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/pages-output-dir.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +7 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +26 -14
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts +19 -0
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.js +185 -0
- package/payload/platform/lib/storage-broker/dist/pages-output-dir.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +65 -0
- package/payload/platform/lib/storage-broker/src/__tests__/pages-output-dir.test.ts +167 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +40 -16
- package/payload/platform/lib/storage-broker/src/pages-output-dir.ts +194 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/quote-render-pdf-conformance.test.sh +99 -0
- package/payload/platform/plugins/admin/hooks/quote-render-pdf-conformance.sh +108 -0
- package/payload/platform/plugins/admin/skills/agent-builder/references/agent-pattern.md +18 -6
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +13 -1
- package/payload/platform/plugins/cloudflare/bin/__tests__/portal-brand-css.test.sh +1 -1
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +22 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +111 -3
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +13 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +8 -4
- package/payload/platform/plugins/cloudflare/references/hosting-sites.md +1 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +4 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +12 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +8 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +6 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +35 -6
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +62 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +9 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.d.ts +80 -0
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.js +216 -0
- package/payload/platform/services/claude-session-manager/dist/skill-staleness-audit.js.map +1 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +5 -1
- package/payload/platform/templates/agents/passive/IDENTITY.md +2 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +2 -0
- package/payload/platform/templates/specialists/agents/citation-auditor.md +1 -0
- package/payload/platform/templates/specialists/agents/coding-assistant.md +1 -0
- package/payload/platform/templates/specialists/agents/compiled-truth-rewriter.md +3 -0
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -0
- package/payload/platform/templates/specialists/agents/data-manager.md +1 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +1 -0
- package/payload/platform/templates/specialists/agents/librarian.md +1 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -0
- package/payload/platform/templates/specialists/agents/project-manager.md +1 -0
- package/payload/platform/templates/specialists/agents/public-session-reviewer.md +1 -0
- package/payload/platform/templates/specialists/agents/research-assistant.md +3 -0
- package/payload/platform/templates/specialists/agents/typed-edge-classifier.md +1 -0
- package/payload/premium-plugins/sitedesk/agents/sitedesk--media-producer.md +1 -0
- package/payload/premium-plugins/sitedesk/agents/sitedesk--payroll-clerk.md +1 -0
- package/payload/premium-plugins/sitedesk/agents/sitedesk--quoter.md +1 -0
- package/payload/premium-plugins/sitedesk/agents/sitedesk--valuer.md +1 -0
- package/payload/premium-plugins/sitedesk/agents/worker-checkin/IDENTITY.md +2 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.d.ts +39 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.d.ts.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.js +86 -2
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.js.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.d.ts +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.d.ts.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js +27 -4
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js.map +1 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/gates.test.ts +81 -1
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/render-run.test.ts +98 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/gates.ts +86 -2
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/render-run.ts +36 -5
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/skills/quotation/references/quote-generation.md +41 -12
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/skills/quotation/references/verification.md +51 -0
- package/payload/server/{chunk-KZFE3MOY.js → chunk-RPUTKSAJ.js} +134 -16
- package/payload/server/public/assets/{chat-DWvYv9jq.js → chat-1SSmFsjd.js} +1 -1
- package/payload/server/public/assets/{operator-CldXXilj.js → operator-Cxq9TxaH.js} +1 -1
- package/payload/server/public/assets/{page-BIwdWXnv.js → page-DBHzC59_.js} +1 -1
- package/payload/server/public/assets/{public-BKQEQGPE.js → public-jIzvdjt_.js} +1 -1
- package/payload/server/public/chat.html +2 -2
- package/payload/server/public/operator.html +2 -2
- package/payload/server/public/public.html +2 -2
- package/payload/server/server.js +40 -31
- package/payload/server/{src-UYDFMWF4.js → src-JN6PIAJN.js} +1 -1
package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/gates.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
gateReconcile,
|
|
4
|
+
gateMarginLeak,
|
|
5
|
+
gateSignLink,
|
|
6
|
+
gateHeaderConformance,
|
|
7
|
+
gateTermsWeight,
|
|
8
|
+
gateReferenceConsistency,
|
|
9
|
+
} from "../gates.js";
|
|
3
10
|
|
|
4
11
|
describe("gateReconcile — displayed total must equal the engine total", () => {
|
|
5
12
|
it("passes when data-quote-total matches", () => {
|
|
@@ -62,3 +69,76 @@ describe("gateSignLink — client quote's acceptance anchor must be absolute htt
|
|
|
62
69
|
expect(gateSignLink('<a data-esign-accept href="/sign">Sign</a>')).toEqual({ pass: false, href: "/sign", reason: "relative" });
|
|
63
70
|
});
|
|
64
71
|
});
|
|
72
|
+
|
|
73
|
+
describe("gateHeaderConformance — rendered header must be the declared canonical template verbatim", () => {
|
|
74
|
+
const TEMPLATE = '<div data-doc-header><img src="data:image/png;base64,AAAAlogoBBBB"><span>Acme Ltd</span></div>';
|
|
75
|
+
it("skips when the account declares no header template (template null)", () => {
|
|
76
|
+
expect(gateHeaderConformance("<div data-doc-header>anything</div>", null)).toEqual({ pass: true, skipped: true });
|
|
77
|
+
});
|
|
78
|
+
it("skips an empty or whitespace-only template rather than passing everything via includes(\"\")", () => {
|
|
79
|
+
expect(gateHeaderConformance("<div data-doc-header>anything</div>", " \n ")).toEqual({ pass: true, skipped: true });
|
|
80
|
+
});
|
|
81
|
+
it("passes when the rendered quote embeds the template verbatim (whitespace-tolerant)", () => {
|
|
82
|
+
const rendered = `<body>\n <div data-doc-header>\n <img src="data:image/png;base64,AAAAlogoBBBB">\n <span>Acme Ltd</span>\n </div>\n</body>`;
|
|
83
|
+
expect(gateHeaderConformance(rendered, TEMPLATE)).toEqual({ pass: true });
|
|
84
|
+
});
|
|
85
|
+
it("fails closed when the render marked no header block", () => {
|
|
86
|
+
expect(gateHeaderConformance("<body><h1>Quote</h1></body>", TEMPLATE)).toEqual({ pass: false, reason: "no-header-marker" });
|
|
87
|
+
});
|
|
88
|
+
it("fails a rebuilt approximation whose logo data differs", () => {
|
|
89
|
+
const rebuilt = '<div data-doc-header><img src="data:image/png;base64,DIFFERENTlogo"><span>Acme Ltd</span></div>';
|
|
90
|
+
expect(gateHeaderConformance(rebuilt, TEMPLATE)).toEqual({ pass: false, reason: "mismatch" });
|
|
91
|
+
});
|
|
92
|
+
it("fails a rebuilt approximation whose element structure differs", () => {
|
|
93
|
+
const rebuilt = '<div data-doc-header><h2>Acme Ltd</h2></div>';
|
|
94
|
+
expect(gateHeaderConformance(rebuilt, TEMPLATE)).toEqual({ pass: false, reason: "mismatch" });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("gateTermsWeight — the terms section must carry no above-normal weight", () => {
|
|
99
|
+
it("skips when the quote marks no terms section", () => {
|
|
100
|
+
expect(gateTermsWeight("<body><h1>Quote</h1></body>")).toEqual({ pass: true, skipped: true });
|
|
101
|
+
});
|
|
102
|
+
it("passes a terms section in normal weight", () => {
|
|
103
|
+
expect(gateTermsWeight('<section data-doc-terms><p>Payment due in 30 days.</p></section>')).toEqual({ pass: true });
|
|
104
|
+
});
|
|
105
|
+
it("fails a <strong> inside the terms section", () => {
|
|
106
|
+
expect(gateTermsWeight('<section data-doc-terms><p>Payment <strong>due now</strong>.</p></section>')).toEqual({ pass: false, reason: "bold-in-terms" });
|
|
107
|
+
});
|
|
108
|
+
it("fails a <b> inside the terms section", () => {
|
|
109
|
+
expect(gateTermsWeight('<section data-doc-terms><p><b>Note</b></p></section>')).toEqual({ pass: false, reason: "bold-in-terms" });
|
|
110
|
+
});
|
|
111
|
+
it("fails an inline font-weight:bold inside the terms section", () => {
|
|
112
|
+
expect(gateTermsWeight('<section data-doc-terms><p style="font-weight: bold">x</p></section>')).toEqual({ pass: false, reason: "bold-in-terms" });
|
|
113
|
+
});
|
|
114
|
+
it("fails an inline numeric font-weight:700 inside the terms section", () => {
|
|
115
|
+
expect(gateTermsWeight('<section data-doc-terms><p style="font-weight:700">x</p></section>')).toEqual({ pass: false, reason: "bold-in-terms" });
|
|
116
|
+
});
|
|
117
|
+
it("ignores bold outside the terms section", () => {
|
|
118
|
+
expect(gateTermsWeight('<h1><b>ACME QUOTE</b></h1><section data-doc-terms><p>normal terms</p></section>')).toEqual({ pass: true });
|
|
119
|
+
});
|
|
120
|
+
it("catches bold after a nested same-name element (a <div data-doc-terms> nesting a <div>)", () => {
|
|
121
|
+
// depth-balanced extraction: a non-greedy match would stop at the first
|
|
122
|
+
// </div> and miss the <strong> that follows the nested container.
|
|
123
|
+
const html = '<div data-doc-terms><div class="logo"></div><strong>NET 7 DAYS</strong></div>';
|
|
124
|
+
expect(gateTermsWeight(html)).toEqual({ pass: false, reason: "bold-in-terms" });
|
|
125
|
+
});
|
|
126
|
+
it("does not read bold from a sibling element after the terms container closes", () => {
|
|
127
|
+
const html = '<div data-doc-terms><p>normal terms</p></div><footer><b>Acme</b></footer>';
|
|
128
|
+
expect(gateTermsWeight(html)).toEqual({ pass: true });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("gateReferenceConsistency — one document reference across every marked place", () => {
|
|
133
|
+
it("passes when every data-doc-ref carries the same string", () => {
|
|
134
|
+
const html = '<header data-doc-ref="1848">…</header><footer data-doc-ref="1848">…</footer><div data-doc-ref="1848">accept</div>';
|
|
135
|
+
expect(gateReferenceConsistency(html)).toEqual({ pass: true, ref: "1848" });
|
|
136
|
+
});
|
|
137
|
+
it("skips when the quote marks no document reference (opt-in like header and terms)", () => {
|
|
138
|
+
expect(gateReferenceConsistency("<body><h1>Quote</h1></body>")).toEqual({ pass: true, skipped: true });
|
|
139
|
+
});
|
|
140
|
+
it("fails when a second variant appears (the 1848/1 class)", () => {
|
|
141
|
+
const html = '<header data-doc-ref="1848">…</header><div data-doc-ref="1848/1">accept</div>';
|
|
142
|
+
expect(gateReferenceConsistency(html)).toEqual({ pass: false, reason: "inconsistent", refs: ["1848", "1848/1"] });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -60,6 +60,37 @@ process.stdin.on("end", () => {
|
|
|
60
60
|
});
|
|
61
61
|
`;
|
|
62
62
|
|
|
63
|
+
// A no-signer render whose quote carries an above-normal weight inside the
|
|
64
|
+
// marked standing-terms section → terms-weight, fail-closed.
|
|
65
|
+
const BOLD_TERMS = `
|
|
66
|
+
let input = "";
|
|
67
|
+
process.stdin.on("data", (d) => (input += d));
|
|
68
|
+
process.stdin.on("end", () => {
|
|
69
|
+
const { figures } = JSON.parse(input);
|
|
70
|
+
const t = figures.total;
|
|
71
|
+
process.stdout.write(JSON.stringify({
|
|
72
|
+
internal: '<html><body><td data-internal-only>cost 900</td><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
73
|
+
breakdown: '<html><body><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
74
|
+
quote: '<html><body><p data-quote-total="' + t + '">Total ' + t + '</p><section data-doc-terms><p><strong>NET 7 DAYS</strong></p></section></body></html>',
|
|
75
|
+
}));
|
|
76
|
+
});
|
|
77
|
+
`;
|
|
78
|
+
// A no-signer render whose quote carries two distinct document references (the
|
|
79
|
+
// 1848/1 class) → ref-inconsistent, fail-closed.
|
|
80
|
+
const REF_BAD = `
|
|
81
|
+
let input = "";
|
|
82
|
+
process.stdin.on("data", (d) => (input += d));
|
|
83
|
+
process.stdin.on("end", () => {
|
|
84
|
+
const { figures } = JSON.parse(input);
|
|
85
|
+
const t = figures.total;
|
|
86
|
+
process.stdout.write(JSON.stringify({
|
|
87
|
+
internal: '<html><body><td data-internal-only>cost 900</td><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
88
|
+
breakdown: '<html><body><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
89
|
+
quote: '<html><body><header data-doc-ref="1848">ref</header><p data-quote-total="' + t + '">Total ' + t + '</p><div data-doc-ref="1848/1">accept</div></body></html>',
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
`;
|
|
93
|
+
|
|
63
94
|
let dir: string;
|
|
64
95
|
const figures = { total: 1250, header: { phone: "44700900000" }, lines: [] };
|
|
65
96
|
const signUrl = "https://q.pages.dev/sign";
|
|
@@ -75,6 +106,8 @@ beforeAll(() => {
|
|
|
75
106
|
writeFileSync(join(dir, "quoting", "render-subpenny.mjs"), SUBPENNY);
|
|
76
107
|
writeFileSync(join(dir, "quoting", "render-throw.mjs"), THROW);
|
|
77
108
|
writeFileSync(join(dir, "quoting", "render-nosigner.mjs"), NOSIGNER);
|
|
109
|
+
writeFileSync(join(dir, "quoting", "render-boldterms.mjs"), BOLD_TERMS);
|
|
110
|
+
writeFileSync(join(dir, "quoting", "render-refbad.mjs"), REF_BAD);
|
|
78
111
|
});
|
|
79
112
|
afterAll(() => rmSync(dir, { recursive: true, force: true }));
|
|
80
113
|
const rs = (n: string) => join(dir, "quoting", n);
|
|
@@ -170,6 +203,71 @@ describe("runRender — a failing gate is fail-closed and atomic (no doc filed)"
|
|
|
170
203
|
});
|
|
171
204
|
});
|
|
172
205
|
|
|
206
|
+
describe("runRender — document-conformance gates over the client quote (fail-closed)", () => {
|
|
207
|
+
it("terms-weight when the quote's standing-terms section carries bold", async () => {
|
|
208
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-boldterms.mjs"), figures, jobId: "job-bold" });
|
|
209
|
+
expect(r).toMatchObject({ ok: false, reason: "terms-weight" });
|
|
210
|
+
expect(existsSync(join(dir, "memory/users/44700900000/documents/quote-job-bold.html"))).toBe(false);
|
|
211
|
+
expect(existsSync(receiptPath(dir, "job-bold"))).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
it("ref-inconsistent when the quote carries a second document-reference variant (the 1848/1 class)", async () => {
|
|
214
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-refbad.mjs"), figures, jobId: "job-ref" });
|
|
215
|
+
expect(r).toMatchObject({ ok: false, reason: "ref-inconsistent" });
|
|
216
|
+
if (r.ok) return;
|
|
217
|
+
expect(r.detail).toMatch(/1848.*1848\/1/);
|
|
218
|
+
expect(existsSync(receiptPath(dir, "job-ref"))).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
it("files normally when the quote carries no header template, no terms marker, and one consistent reference (all opt-in gates satisfied or skipped)", async () => {
|
|
221
|
+
// render-nosigner carries none of the new markers → header/terms skip, ref
|
|
222
|
+
// skips; the render still files, proving the new gates do not break an
|
|
223
|
+
// existing render script that has not adopted the markers.
|
|
224
|
+
const r = await runRender({ accountDir: dir, renderScriptPath: rs("render-nosigner.mjs"), figures, jobId: "job-optin-plain" });
|
|
225
|
+
expect(r.ok).toBe(true);
|
|
226
|
+
expect(existsSync(receiptPath(dir, "job-optin-plain"))).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("runRender — header conformance against a declared canonical template", () => {
|
|
231
|
+
let hdir: string;
|
|
232
|
+
const HEADER_TPL = '<div data-doc-header><img src="data:image/png;base64,AAAAlogoBBBB"><span>Acme Ltd</span></div>';
|
|
233
|
+
const renderEmitting = (headerBlock: string) => `
|
|
234
|
+
let input = "";
|
|
235
|
+
process.stdin.on("data", (d) => (input += d));
|
|
236
|
+
process.stdin.on("end", () => {
|
|
237
|
+
const { figures } = JSON.parse(input);
|
|
238
|
+
const t = figures.total;
|
|
239
|
+
const header = ${JSON.stringify(headerBlock)};
|
|
240
|
+
process.stdout.write(JSON.stringify({
|
|
241
|
+
internal: '<html><body><td data-internal-only>c</td><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
242
|
+
breakdown: '<html><body><p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
243
|
+
quote: '<html><body>' + header + '<p data-quote-total="' + t + '">Total ' + t + '</p></body></html>',
|
|
244
|
+
}));
|
|
245
|
+
});
|
|
246
|
+
`;
|
|
247
|
+
beforeAll(() => {
|
|
248
|
+
hdir = mkdtempSync(join(tmpdir(), "sd-render-hdr-"));
|
|
249
|
+
mkdirSync(join(hdir, "quoting", "templates"), { recursive: true });
|
|
250
|
+
writeFileSync(join(hdir, "quoting", "templates", "header.html"), HEADER_TPL);
|
|
251
|
+
// conforming render embeds the declared template verbatim (whitespace
|
|
252
|
+
// tolerance is covered by the gates unit test)
|
|
253
|
+
writeFileSync(join(hdir, "quoting", "render-ok.mjs"), renderEmitting(HEADER_TPL));
|
|
254
|
+
// rebuilt approximation: structure differs, logo data absent
|
|
255
|
+
writeFileSync(join(hdir, "quoting", "render-bad.mjs"), renderEmitting('<div data-doc-header><h2>Acme Ltd</h2></div>'));
|
|
256
|
+
});
|
|
257
|
+
afterAll(() => rmSync(hdir, { recursive: true, force: true }));
|
|
258
|
+
|
|
259
|
+
it("passes when the rendered header embeds the declared template verbatim", async () => {
|
|
260
|
+
const r = await runRender({ accountDir: hdir, renderScriptPath: join(hdir, "quoting", "render-ok.mjs"), figures, jobId: "job-hdr-ok" });
|
|
261
|
+
expect(r.ok).toBe(true);
|
|
262
|
+
expect(existsSync(receiptPath(hdir, "job-hdr-ok"))).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it("header-mismatch when the header is a rebuilt approximation, not the declared template", async () => {
|
|
265
|
+
const r = await runRender({ accountDir: hdir, renderScriptPath: join(hdir, "quoting", "render-bad.mjs"), figures, jobId: "job-hdr-bad" });
|
|
266
|
+
expect(r).toMatchObject({ ok: false, reason: "header-mismatch" });
|
|
267
|
+
expect(existsSync(receiptPath(hdir, "job-hdr-bad"))).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
173
271
|
describe("runRender — script faults", () => {
|
|
174
272
|
it("render-absent when the render script is missing", async () => {
|
|
175
273
|
const r = await runRender({ accountDir: dir, renderScriptPath: rs("nope.mjs"), figures, signUrl, jobId });
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
1
|
+
// The deterministic, fail-closed gates quote-render enforces on the HTML the
|
|
2
|
+
// operator's render script emits. They are structural checks over stable
|
|
3
3
|
// attributes the render script marks — not business content:
|
|
4
4
|
// data-quote-total the displayed grand total (reconcile against the engine)
|
|
5
5
|
// data-internal-only cost/margin-bearing figures (must be absent from client docs)
|
|
6
6
|
// data-esign-accept the client quote's Sign-Online anchor (absolute https only)
|
|
7
|
+
// data-doc-header the header block (must be the declared canonical template verbatim)
|
|
8
|
+
// data-doc-terms the standing-terms section (must carry no above-normal weight)
|
|
9
|
+
// data-doc-ref every place the document reference appears (must all agree)
|
|
7
10
|
|
|
8
11
|
export interface ReconcileVerdict { pass: boolean; shown?: number }
|
|
9
12
|
export interface LeakVerdict { pass: boolean }
|
|
10
13
|
export interface SignLinkVerdict { pass: boolean; href?: string; reason?: "no-anchor" | "empty" | "fragment" | "non-https" | "relative" }
|
|
14
|
+
export interface HeaderVerdict { pass: boolean; skipped?: boolean; reason?: "no-header-marker" | "mismatch" }
|
|
15
|
+
export interface TermsVerdict { pass: boolean; skipped?: boolean; reason?: "bold-in-terms" }
|
|
16
|
+
export interface RefVerdict { pass: boolean; skipped?: boolean; ref?: string; reason?: "inconsistent"; refs?: string[] }
|
|
11
17
|
|
|
12
18
|
/** The document's displayed grand total (data-quote-total) must equal the
|
|
13
19
|
* engine total at penny precision — money is penny-precise, so a sub-penny
|
|
@@ -43,3 +49,81 @@ export function gateSignLink(html: string): SignLinkVerdict {
|
|
|
43
49
|
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return { pass: false, href, reason: "non-https" };
|
|
44
50
|
return { pass: false, href, reason: "relative" };
|
|
45
51
|
}
|
|
52
|
+
|
|
53
|
+
/** Whitespace-normalise for a structure-and-content comparison that tolerates
|
|
54
|
+
* the indentation a template engine adds but nothing else: whitespace between
|
|
55
|
+
* tags is insignificant (a compact template vs a pretty-printed render), and
|
|
56
|
+
* runs of whitespace inside text collapse to one space. */
|
|
57
|
+
function normaliseHtml(s: string): string {
|
|
58
|
+
return s.replace(/>\s+</g, "><").replace(/\s+/g, " ").trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The rendered quote's header must be the operator's declared canonical header
|
|
62
|
+
* (`quoting/templates/header.html`) verbatim — its element structure and its
|
|
63
|
+
* embedded logo data, not a rebuilt approximation. The template is passed in
|
|
64
|
+
* (null when the account declares none). No template → skip. Template present
|
|
65
|
+
* but the render marked no header block → fail closed (an unverifiable header
|
|
66
|
+
* is not shipped). Otherwise the template must appear verbatim (whitespace
|
|
67
|
+
* tolerant) in the rendered document; a rebuilt header — different structure or
|
|
68
|
+
* altered/absent logo data — is not a substring and fails. */
|
|
69
|
+
export function gateHeaderConformance(html: string, template: string | null): HeaderVerdict {
|
|
70
|
+
// A null or empty/whitespace-only template is "no canonical header declared":
|
|
71
|
+
// skip, rather than let `includes("")` report a pass that enforced nothing.
|
|
72
|
+
if (template === null || normaliseHtml(template) === "") return { pass: true, skipped: true };
|
|
73
|
+
if (!/\bdata-doc-header\b/i.test(html)) return { pass: false, reason: "no-header-marker" };
|
|
74
|
+
return normaliseHtml(html).includes(normaliseHtml(template)) ? { pass: true } : { pass: false, reason: "mismatch" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Extract the inner content of the single element the render marks with
|
|
78
|
+
* `attr`, matched by **balanced tag depth** so a nested element of the same
|
|
79
|
+
* name does not truncate the capture. A non-greedy regex would stop at the
|
|
80
|
+
* first same-name close tag, letting a violation that sits after a nested
|
|
81
|
+
* `<div>`/`<section>` escape a fail-closed gate; depth-balancing captures the
|
|
82
|
+
* container's true extent. null when the marker is absent; on unbalanced HTML
|
|
83
|
+
* (a render-script defect) it captures to end, the fail-closed direction. */
|
|
84
|
+
function markedInner(html: string, attr: string): string | null {
|
|
85
|
+
const open = new RegExp(`<(\\w+)[^>]*\\b${attr}\\b[^>]*>`, "i").exec(html);
|
|
86
|
+
if (!open) return null;
|
|
87
|
+
const tag = open[1];
|
|
88
|
+
const start = open.index + open[0].length;
|
|
89
|
+
const tagRe = new RegExp(`<(/?)${tag}\\b[^>]*>`, "gi");
|
|
90
|
+
tagRe.lastIndex = start;
|
|
91
|
+
let depth = 1;
|
|
92
|
+
let m: RegExpExecArray | null;
|
|
93
|
+
while ((m = tagRe.exec(html)) !== null) {
|
|
94
|
+
if (m[1] === "/") {
|
|
95
|
+
if (--depth === 0) return html.slice(start, m.index);
|
|
96
|
+
} else {
|
|
97
|
+
depth++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return html.slice(start);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The standing-terms section (marked `data-doc-terms`) must carry no
|
|
104
|
+
* above-normal font weight: no `<b>`/`<strong>` element and no inline
|
|
105
|
+
* `font-weight` of bold, bolder, or a numeric 500–900. No terms marker → skip
|
|
106
|
+
* (a quote may carry no terms). The check is scoped to the marked section, so
|
|
107
|
+
* a bold heading elsewhere in the document does not fail it. */
|
|
108
|
+
export function gateTermsWeight(html: string): TermsVerdict {
|
|
109
|
+
const terms = markedInner(html, "data-doc-terms");
|
|
110
|
+
if (terms === null) return { pass: true, skipped: true };
|
|
111
|
+
const bold = /<b[\s>]/i.test(terms)
|
|
112
|
+
|| /<strong[\s>]/i.test(terms)
|
|
113
|
+
|| /font-weight\s*:\s*(bold|bolder|[5-9]00)\b/i.test(terms);
|
|
114
|
+
return bold ? { pass: false, reason: "bold-in-terms" } : { pass: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Every place the render marks a document reference (`data-doc-ref="<ref>"`)
|
|
118
|
+
* must carry the same string. The marker is opt-in like the header and terms
|
|
119
|
+
* markers: zero markers → skip (an existing render script that has not adopted
|
|
120
|
+
* it is not broken). One distinct value → pass. A second distinct value — the
|
|
121
|
+
* `1848/1` class, where the header says `1848` and the acceptance block says
|
|
122
|
+
* `1848/1` — fails and reports the distinct values in first-seen order. */
|
|
123
|
+
export function gateReferenceConsistency(html: string): RefVerdict {
|
|
124
|
+
const refs = [...html.matchAll(/\bdata-doc-ref="([^"]*)"/gi)].map((m) => m[1]);
|
|
125
|
+
if (refs.length === 0) return { pass: true, skipped: true };
|
|
126
|
+
const distinct = [...new Set(refs)];
|
|
127
|
+
if (distinct.length === 1) return { pass: true, ref: distinct[0] };
|
|
128
|
+
return { pass: false, reason: "inconsistent", refs: distinct };
|
|
129
|
+
}
|
|
@@ -8,12 +8,28 @@
|
|
|
8
8
|
// cause-naming fault is returned. "Fail loudly and stop", never a partial issue.
|
|
9
9
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
gateReconcile,
|
|
15
|
+
gateMarginLeak,
|
|
16
|
+
gateSignLink,
|
|
17
|
+
gateHeaderConformance,
|
|
18
|
+
gateTermsWeight,
|
|
19
|
+
gateReferenceConsistency,
|
|
20
|
+
type SignLinkVerdict,
|
|
21
|
+
} from "./gates.js";
|
|
14
22
|
import { writeRenderReceipt } from "./render-receipt.js";
|
|
15
23
|
|
|
16
|
-
export type RenderFault =
|
|
24
|
+
export type RenderFault =
|
|
25
|
+
| "render-absent"
|
|
26
|
+
| "render-error"
|
|
27
|
+
| "reconcile-fail"
|
|
28
|
+
| "margin-leak"
|
|
29
|
+
| "sign-link"
|
|
30
|
+
| "header-mismatch"
|
|
31
|
+
| "terms-weight"
|
|
32
|
+
| "ref-inconsistent";
|
|
17
33
|
|
|
18
34
|
export interface RunRenderArgs {
|
|
19
35
|
accountDir: string;
|
|
@@ -114,9 +130,21 @@ export async function runRender(a: RunRenderArgs): Promise<RenderResult> {
|
|
|
114
130
|
// The verdict is reported only when a signer was requested; null otherwise.
|
|
115
131
|
const signLink: SignLinkVerdict | null = esignRequested ? signVerdict : null;
|
|
116
132
|
|
|
133
|
+
// Document-conformance gates over the issued client quote. The header must be
|
|
134
|
+
// the account's declared canonical template verbatim (a fixed-path opt-in:
|
|
135
|
+
// quoting/templates/header.html — absent → the header check is skipped); the
|
|
136
|
+
// standing-terms section must carry no above-normal weight; and every marked
|
|
137
|
+
// document reference must agree. Each is fail-closed, so a violation writes no
|
|
138
|
+
// receipt and the browser-pdf-save gate then refuses the print.
|
|
139
|
+
const headerTemplatePath = join(a.accountDir, "quoting", "templates", "header.html");
|
|
140
|
+
const headerTemplate = existsSync(headerTemplatePath) ? readFileSync(headerTemplatePath, "utf8") : null;
|
|
141
|
+
const header = gateHeaderConformance(html.quote, headerTemplate);
|
|
142
|
+
const terms = gateTermsWeight(html.quote);
|
|
143
|
+
const ref = gateReferenceConsistency(html.quote);
|
|
144
|
+
|
|
117
145
|
const internalOk = recInternal.pass;
|
|
118
146
|
const breakdownOk = recBreakdown.pass && leakBreakdown.pass;
|
|
119
|
-
const quoteOk = recQuote.pass && leakQuote.pass && signOk;
|
|
147
|
+
const quoteOk = recQuote.pass && leakQuote.pass && signOk && header.pass && terms.pass && ref.pass;
|
|
120
148
|
|
|
121
149
|
if (!internalOk || !breakdownOk || !quoteOk) {
|
|
122
150
|
// Reason names the first failing gate, in document then gate order.
|
|
@@ -127,7 +155,10 @@ export async function runRender(a: RunRenderArgs): Promise<RenderResult> {
|
|
|
127
155
|
else if (!leakBreakdown.pass) { reason = "margin-leak"; detail = "breakdown carries data-internal-only"; }
|
|
128
156
|
else if (!recQuote.pass) { reason = "reconcile-fail"; detail = `quote total ${recQuote.shown} != engine ${engineTotal}`; }
|
|
129
157
|
else if (!leakQuote.pass) { reason = "margin-leak"; detail = "quote carries data-internal-only"; }
|
|
130
|
-
else { reason = "sign-link"; detail = signDetail; }
|
|
158
|
+
else if (!signOk) { reason = "sign-link"; detail = signDetail; }
|
|
159
|
+
else if (!header.pass) { reason = "header-mismatch"; detail = header.reason === "no-header-marker" ? "quote marked no header block to verify against the declared canonical template" : "quote header does not match the declared canonical template"; }
|
|
160
|
+
else if (!terms.pass) { reason = "terms-weight"; detail = "quote terms section carries above-normal font weight (bold/strong/font-weight >= 500)"; }
|
|
161
|
+
else { reason = "ref-inconsistent"; detail = `quote document references disagree: ${(ref.refs ?? []).join(", ")}`; }
|
|
131
162
|
return { ok: false, reason, detail };
|
|
132
163
|
}
|
|
133
164
|
|
|
@@ -42,7 +42,7 @@ layout, headings, narrative, charts, page size, contact and terms as their own d
|
|
|
42
42
|
templates nothing and carries no brand. Generating and maintaining that render script is the build
|
|
43
43
|
half of this skill, exactly as generating the pricing engine is the build half of the compute core.
|
|
44
44
|
|
|
45
|
-
The render script marks
|
|
45
|
+
The render script marks stable attributes so the tool gates the output deterministically,
|
|
46
46
|
whatever the operator's brand or language:
|
|
47
47
|
|
|
48
48
|
- **`data-quote-total`** on each document's displayed grand total, carrying the numeric total, so the
|
|
@@ -53,19 +53,41 @@ whatever the operator's brand or language:
|
|
|
53
53
|
present **only when the operator opted the quote into online acceptance** — so the tool asserts the
|
|
54
54
|
acceptance link is live.
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
Three further attributes let the tool enforce the operator's own document-conformance standards on the
|
|
57
|
+
client quote (see `references/verification.md`, "Verifying the issued document conforms"):
|
|
58
|
+
|
|
59
|
+
- **`data-doc-header`** on the header block, so the tool can hold it to the account's declared canonical
|
|
60
|
+
header template at `quoting/templates/header.html` (verbatim, structure and embedded logo data). The
|
|
61
|
+
template is a fixed-path opt-in: when the account declares none the header check is skipped.
|
|
62
|
+
- **`data-doc-terms`** on the standing-terms section, so the tool asserts it carries no above-normal
|
|
63
|
+
weight (no `<b>`/`<strong>`, no inline `font-weight` of bold, bolder, or 500 to 900). No terms marker,
|
|
64
|
+
no check.
|
|
65
|
+
- **`data-doc-ref="<ref>"`** on every place the document reference appears (header, footers, title,
|
|
66
|
+
acceptance section), all carrying the same string, so a second variant (the `1848/1` class) fails. No
|
|
67
|
+
reference marker, no check, so an existing render script is not broken by adopting the marker later.
|
|
68
|
+
|
|
69
|
+
These conformance markers are opt-in, exactly like `data-esign-accept`: the gate for a marker runs only
|
|
70
|
+
once the render script emits it (and, for the header, only once the account drops its template file).
|
|
71
|
+
|
|
72
|
+
`quote-render` enforces its fail-closed gates before it files anything: **reconcile** (each document's
|
|
73
|
+
`data-quote-total` equals the engine total), **margin-leak** (no `data-internal-only` in the client
|
|
74
|
+
breakdown or client quote), **link-liveness** (the client quote's `data-esign-accept` href is an
|
|
75
|
+
absolute `https://` URL), and the client-quote **document-conformance** gates above (header,
|
|
76
|
+
terms-weight, reference-consistency, each opt-in on its marker). Link-liveness runs **only on the
|
|
77
|
+
opt-in path** — when a `signUrl` was passed; on the default, signer-less path the quote must carry no
|
|
78
|
+
acceptance anchor, and a stray one fails closed. If any gate fails the tool files nothing and returns a
|
|
79
|
+
cause-naming fault (`reconcile-fail`, `margin-leak`, `sign-link`, `header-mismatch`, `terms-weight`,
|
|
80
|
+
`ref-inconsistent`, `render-absent`, `render-error`). On success it files each document to its scope
|
|
81
|
+
(below) and returns the three HTML paths plus the gate verdicts.
|
|
64
82
|
|
|
65
83
|
The tool produces **HTML, not PDF**. Print each returned path to PDF with the `browser` plugin as the
|
|
66
84
|
final step: `browser-navigate` to its `file://` URL, then `browser-pdf-save` to a `.pdf` alongside it,
|
|
67
85
|
confirming a non-zero byte count. A Workflow chains `quote-engine-run`, then `quote-render`, then one
|
|
68
|
-
`browser` `browser-pdf-save` step per document.
|
|
86
|
+
`browser` `browser-pdf-save` step per document. A **PostToolUse gate on `browser-pdf-save`** then holds
|
|
87
|
+
the printed client-quote PDF to its two PDF-only conformance checks: its `/URI` count is at least the
|
|
88
|
+
source HTML's absolute-link count, and every embedded `https://` link answers 200. A failure blocks the
|
|
89
|
+
print with `[quote-render] op=bypass reason=pdf-link-conformance`, so a link-stripped or dead-link PDF
|
|
90
|
+
is not sent.
|
|
69
91
|
|
|
70
92
|
## The three documents
|
|
71
93
|
|
|
@@ -170,9 +192,9 @@ For each returned path, print it the same way as every other business-assistant
|
|
|
170
192
|
3. confirm a non-zero byte count from the `Saved PDF to <path> (<bytes>)` line before treating the PDF
|
|
171
193
|
as done.
|
|
172
194
|
|
|
173
|
-
## The tool enforces the reconciliation
|
|
195
|
+
## The tool enforces the reconciliation, link-liveness and document-conformance gates
|
|
174
196
|
|
|
175
|
-
`quote-render` runs
|
|
197
|
+
`quote-render` runs its fail-closed gates over the rendered HTML before it files any document, so
|
|
176
198
|
these checks are enforced deterministically at render time, not as a separate hand-run step:
|
|
177
199
|
|
|
178
200
|
- **reconciliation.** Each document's `data-quote-total` must equal the engine total. A mismatch files
|
|
@@ -187,6 +209,13 @@ these checks are enforced deterministically at render time, not as a separate ha
|
|
|
187
209
|
other signal. On the default, signer-less path (no `signUrl`) the quote must carry **no** acceptance
|
|
188
210
|
anchor at all; a stray anchor also returns `sign-link` (a signer link with no signing page is a dead
|
|
189
211
|
button).
|
|
212
|
+
- **document conformance (opt-in per marker).** The client quote's header must be the account's
|
|
213
|
+
declared canonical template (`header-mismatch`), its terms section must carry no above-normal weight
|
|
214
|
+
(`terms-weight`), and every marked document reference must agree (`ref-inconsistent`). Each runs only
|
|
215
|
+
once the render marks its attribute (and, for the header, once the account declares its template), so
|
|
216
|
+
a render script that has not adopted a marker is not broken. `references/verification.md` owns why and
|
|
217
|
+
what each asserts. The two PDF-only conformance checks (link count kept, links live) run afterward in
|
|
218
|
+
the PostToolUse gate on `browser-pdf-save`, because they need the printed PDF.
|
|
190
219
|
|
|
191
220
|
The tool emits one `[quote-render] op=render jobId=… docs=3 internal=… breakdown=… quote=…` line on
|
|
192
221
|
success and `[quote-render] op=fault jobId=… reason=… detail=…` on any gate failure. On success it also
|
|
@@ -49,3 +49,54 @@ execute, a direct invocation that bypasses the persisted `quoting/engine.mjs`
|
|
|
49
49
|
the quote the operator will actually issue. The tool records each verify as `[quote-engine] op=verify
|
|
50
50
|
jobId=… worst=…`; absence of that line for a method claimed verified is the bypass this discipline
|
|
51
51
|
exists to catch.
|
|
52
|
+
|
|
53
|
+
# Verifying the issued document conforms to the operator's standards
|
|
54
|
+
|
|
55
|
+
The section above accepts the **method**: that it reproduces the operator's past figures. This
|
|
56
|
+
section accepts the **document**: that the issued quote matches the operator's own presentation
|
|
57
|
+
standards. They are two distinct acceptance disciplines over two different things. A quote can
|
|
58
|
+
reproduce every figure to the penny and still go out with a hand-built header, bold buried in the
|
|
59
|
+
terms, or two different quote numbers on the same page. Those are mechanically checkable, so they are
|
|
60
|
+
checked deterministically rather than left to the eye.
|
|
61
|
+
|
|
62
|
+
## The gate runs on the deterministic path, not as a side script
|
|
63
|
+
|
|
64
|
+
The conformance checks are enforced where the deterministic render path already runs, so there is no
|
|
65
|
+
second surface an operator can forget. The **HTML checks run inside `quote-render`** (the same tool
|
|
66
|
+
and the same fail-closed sequence as reconcile, margin-leak and link-liveness): a violation files no
|
|
67
|
+
document and writes no render receipt, and the `browser-pdf-save` gate then refuses to print a client
|
|
68
|
+
quote that has no receipt. The **PDF checks run in a PostToolUse gate on `browser-pdf-save`**, because
|
|
69
|
+
they need the printed PDF, which does not exist until that tool runs. A conformance violation is
|
|
70
|
+
therefore surfaced at render or print time, before the draft reaches the operator, not by the operator
|
|
71
|
+
noticing it in a delivered quote.
|
|
72
|
+
|
|
73
|
+
## What each check asserts
|
|
74
|
+
|
|
75
|
+
Each check is structural, over a stable attribute the operator's render script marks (the same pattern
|
|
76
|
+
as `data-quote-total`), so the gate carries no brand and no business content.
|
|
77
|
+
|
|
78
|
+
- **Header is the declared canonical template.** When the account declares a canonical header at the
|
|
79
|
+
fixed path `quoting/templates/header.html`, the rendered quote's header block (marked
|
|
80
|
+
`data-doc-header`) must contain that template verbatim, its element structure and its embedded logo
|
|
81
|
+
data, not a rebuilt approximation. No declared template means the check is skipped.
|
|
82
|
+
- **The terms carry no above-normal weight.** Inside the standing-terms section (marked
|
|
83
|
+
`data-doc-terms`) there is no `<b>`, no `<strong>`, and no inline `font-weight` of bold, bolder, or a
|
|
84
|
+
numeric 500 to 900. No terms marker means the check is skipped.
|
|
85
|
+
- **One document reference, everywhere.** Every place the render marks the document reference
|
|
86
|
+
(`data-doc-ref="<ref>"`) carries the same string. A second variant, the `1848/1` class where the
|
|
87
|
+
header says `1848` and the acceptance block says `1848/1`, fails. No reference marker means the check
|
|
88
|
+
is skipped, so a render script that has not yet adopted the marker is not broken.
|
|
89
|
+
- **The PDF keeps every link and each link is live.** The printed PDF's `/URI` annotation count is at
|
|
90
|
+
least the source HTML's absolute-link count (a rasterised or flattened print that dropped links is
|
|
91
|
+
caught), and every embedded `https://` link answers 200 (a Sign-Online link that a pending redeploy
|
|
92
|
+
would 404 is caught before the PDF is sent).
|
|
93
|
+
|
|
94
|
+
The signature-asset check for order-class documents is not covered here, because this skill's render
|
|
95
|
+
contract issues quote documents, not orders; it is tracked separately.
|
|
96
|
+
|
|
97
|
+
## What a conformance failure tells you
|
|
98
|
+
|
|
99
|
+
A failure names the check and the failing value on the tool's fault line (`[quote-render] op=fault
|
|
100
|
+
reason=header-mismatch|terms-weight|ref-inconsistent …`) or the PDF gate's bypass line
|
|
101
|
+
(`[quote-render] op=bypass reason=pdf-link-conformance …`). Fix the render script or the input so the
|
|
102
|
+
document conforms, then re-render. The threshold is never widened; the document is corrected.
|