@rubytech/create-sitedesk-code 0.1.429 → 0.1.431
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/plugins/admin/skills/platform-architecture/SKILL.md +69 -3
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/internals.md +2 -2
- package/payload/platform/plugins/docs/references/workflows.md +61 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.d.ts +8 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.d.ts.map +1 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.js +8 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/resolve-read-account.js.map +1 -1
- package/payload/platform/plugins/workflows/PLUGIN.md +29 -9
- package/payload/platform/plugins/workflows/mcp/dist/index.js +34 -0
- package/payload/platform/plugins/workflows/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/step-resolver.d.ts +2 -2
- package/payload/platform/plugins/workflows/mcp/dist/lib/step-resolver.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/lib/step-resolver.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.d.ts +25 -0
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.js +14 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-create.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.d.ts +25 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +79 -15
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.d.ts +2 -0
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.js +4 -0
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-get.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.d.ts +3 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.d.ts.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.js +8 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-update.js.map +1 -1
- package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +24 -3
- package/payload/platform/services/telegram-channel/dist/instructions.js +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +10 -5
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +9 -6
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +15 -5
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js +1 -1
- package/payload/platform/services/webchat-channel/dist/instructions.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +12 -10
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +12 -11
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/server.js +12 -1
- package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +11 -6
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +11 -8
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +39 -14
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +17 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +37 -2
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/premium-plugins/sitedesk/lib/mcp-lifeline/dist/index.d.ts +8 -0
- package/payload/premium-plugins/sitedesk/lib/mcp-lifeline/dist/index.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/lib/mcp-lifeline/dist/index.js +71 -0
- package/payload/premium-plugins/sitedesk/lib/mcp-lifeline/dist/index.js.map +1 -0
- package/payload/premium-plugins/sitedesk/lib/mcp-lifeline/package.json +7 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/PLUGIN.md +39 -3
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/index.d.ts +2 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/index.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/index.js +32 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/index.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/account.d.ts +10 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/account.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/account.js +21 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/account.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/engine-run.d.ts +24 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/engine-run.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/engine-run.js +59 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/engine-run.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.d.ts +22 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.js +40 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/gates.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/geometry.d.ts +47 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/geometry.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/geometry.js +60 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/geometry.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/log.d.ts +7 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/log.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/log.js +9 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/log.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.d.ts +29 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js +113 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/lib/render-run.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-engine-run.d.ts +3 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-engine-run.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-engine-run.js +48 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-engine-run.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.d.ts +3 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.js +50 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/quote-render.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/takeoff-compute.d.ts +3 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/takeoff-compute.d.ts.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/takeoff-compute.js +42 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/dist/tools/takeoff-compute.js.map +1 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/package-lock.json +2684 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/package.json +21 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/index.ts +39 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/account.test.ts +49 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/engine-run.test.ts +105 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/gates.test.ts +51 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/geometry.test.ts +83 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/__tests__/render-run.test.ts +111 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/account.ts +20 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/engine-run.ts +80 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/gates.ts +39 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/geometry.ts +93 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/log.ts +8 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/lib/render-run.ts +125 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/tools/quote-engine-run.ts +66 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/tools/quote-render.ts +66 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/tools/takeoff-compute.ts +51 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/tsconfig.json +20 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/skills/quotation/references/quote-engine.md +24 -0
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/skills/quotation/references/quote-generation.md +67 -51
- package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/skills/takeoff/SKILL.md +12 -0
- package/payload/server/public/assets/{AdminLoginScreens-C4AZT7cQ.js → AdminLoginScreens-CcW3pAAZ.js} +1 -1
- package/payload/server/public/assets/{admin-SaF1F4e-.js → admin-Co3hOTNp.js} +1 -1
- package/payload/server/public/assets/{audio-attachment-mime-BBW07eyG.js → audio-attachment-mime-CwYrV1pg.js} +1 -1
- package/payload/server/public/assets/{chat-B9Qjt7C9.js → chat-rxbGimwf.js} +1 -1
- package/payload/server/public/assets/data-D0YFYP1i.js +1 -0
- package/payload/server/public/assets/{operator-CTJjI8te.js → operator-Djv3MdcL.js} +1 -1
- package/payload/server/public/assets/{page-JzxyFGle.js → page-Ci27UrJz.js} +5 -5
- package/payload/server/public/assets/{page-CbI1wtef.js → page-D1EAVC_9.js} +1 -1
- package/payload/server/public/assets/{public-DzeW3_eM.js → public-C6k-fPfx.js} +1 -1
- package/payload/server/public/chat.html +3 -3
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/public/operator.html +4 -4
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +95 -25
- package/payload/server/public/assets/data-BKE6Sa-g.js +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Deterministic takeoff geometry. Pure arithmetic over agent-supplied measured
|
|
2
|
+
// inputs — scale to a real dimension, a scaled area from two sides, and a count
|
|
3
|
+
// roll-up — plus a cross-check of a scaled quantity against a printed figure.
|
|
4
|
+
// Reading the drawing and eliciting the inputs is the takeoff skill's job, not
|
|
5
|
+
// this module's. No business content: it multiplies numbers, it prices nothing.
|
|
6
|
+
|
|
7
|
+
export type Basis = "printed" | "scaled" | "count" | "operator";
|
|
8
|
+
|
|
9
|
+
/** A drawing scale: a stated ratio (1:R → R), or a known-dimension calibration
|
|
10
|
+
* where `actual/measuredRef` is the factor. */
|
|
11
|
+
export type Scale = { ratio: number } | { measuredRef: number; actual: number };
|
|
12
|
+
|
|
13
|
+
export interface InLine {
|
|
14
|
+
element: string;
|
|
15
|
+
basis: Basis;
|
|
16
|
+
value?: number; // printed | operator
|
|
17
|
+
unit?: string;
|
|
18
|
+
measured?: number; // scaled
|
|
19
|
+
measuredB?: number; // scaled area — second side
|
|
20
|
+
kind?: "length" | "area"; // scaled
|
|
21
|
+
count?: number | number[]; // count
|
|
22
|
+
crossCheck?: { value: number; unit?: string; tolerance?: number };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OutLine {
|
|
26
|
+
element: string;
|
|
27
|
+
quantity: number;
|
|
28
|
+
unit?: string;
|
|
29
|
+
basis: Basis;
|
|
30
|
+
crossCheck?: { printed: number; delta: number; disagree: boolean };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TakeoffResult {
|
|
34
|
+
lines: OutLine[];
|
|
35
|
+
counts: { exact: number; scaled: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scaleFactor(scale: Scale | undefined, element: string): number {
|
|
39
|
+
if (!scale) throw new Error(`takeoff: line "${element}" is scale-derived but no scale was supplied`);
|
|
40
|
+
const factor = "ratio" in scale ? scale.ratio : scale.actual / scale.measuredRef;
|
|
41
|
+
if (!Number.isFinite(factor)) {
|
|
42
|
+
throw new Error(`takeoff: line "${element}" has a degenerate scale (factor=${factor})`);
|
|
43
|
+
}
|
|
44
|
+
return factor;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function quantityFor(line: InLine, scale: Scale | undefined): number {
|
|
48
|
+
switch (line.basis) {
|
|
49
|
+
case "printed":
|
|
50
|
+
case "operator": {
|
|
51
|
+
if (typeof line.value !== "number") {
|
|
52
|
+
throw new Error(`takeoff: line "${line.element}" (${line.basis}) is missing its value`);
|
|
53
|
+
}
|
|
54
|
+
return line.value;
|
|
55
|
+
}
|
|
56
|
+
case "count": {
|
|
57
|
+
if (line.count === undefined) {
|
|
58
|
+
throw new Error(`takeoff: line "${line.element}" (count) is missing its count`);
|
|
59
|
+
}
|
|
60
|
+
return Array.isArray(line.count) ? line.count.reduce((a, b) => a + b, 0) : line.count;
|
|
61
|
+
}
|
|
62
|
+
case "scaled": {
|
|
63
|
+
if (typeof line.measured !== "number") {
|
|
64
|
+
throw new Error(`takeoff: line "${line.element}" (scaled) is missing its measured length`);
|
|
65
|
+
}
|
|
66
|
+
const factor = scaleFactor(scale, line.element);
|
|
67
|
+
if (line.kind === "area") {
|
|
68
|
+
if (typeof line.measuredB !== "number") {
|
|
69
|
+
throw new Error(`takeoff: line "${line.element}" (scaled area) is missing its second side`);
|
|
70
|
+
}
|
|
71
|
+
return line.measured * line.measuredB * factor * factor;
|
|
72
|
+
}
|
|
73
|
+
return line.measured * factor;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function computeTakeoff(input: { scale?: Scale; lines: InLine[] }): TakeoffResult {
|
|
79
|
+
const lines: OutLine[] = input.lines.map((line) => {
|
|
80
|
+
const quantity = quantityFor(line, input.scale);
|
|
81
|
+
const out: OutLine = { element: line.element, quantity, unit: line.unit, basis: line.basis };
|
|
82
|
+
if (line.basis === "scaled" && line.crossCheck) {
|
|
83
|
+
const printed = line.crossCheck.value;
|
|
84
|
+
const delta = quantity - printed;
|
|
85
|
+
const tolerance = line.crossCheck.tolerance ?? 0;
|
|
86
|
+
out.crossCheck = { printed, delta, disagree: Math.abs(delta) > tolerance };
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const scaled = lines.filter((l) => l.basis === "scaled").length;
|
|
92
|
+
return { lines, counts: { exact: lines.length - scaled, scaled } };
|
|
93
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit one structured observability line to stderr. Every tool invocation logs
|
|
3
|
+
* one line here, correlated by `jobId`; journald captures stderr, so the
|
|
4
|
+
* diagnostic path is `journalctl --user | grep -E '\\[quote-engine\\]|...'`.
|
|
5
|
+
*/
|
|
6
|
+
export function logLine(s: string): void {
|
|
7
|
+
process.stderr.write(s + "\n");
|
|
8
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Execute the operator's persisted render script (quoting/render.mjs) to turn
|
|
2
|
+
// figures into the three reconciling HTML documents, then enforce the three
|
|
3
|
+
// deterministic gates and file each doc by its scope. The render script owns
|
|
4
|
+
// brand/layout as its own data; this module templates nothing. HTML only — the
|
|
5
|
+
// PDF is produced downstream by the browser plugin's browser-pdf-save.
|
|
6
|
+
//
|
|
7
|
+
// Fail-closed and atomic: if any gate fails, no document is filed and a
|
|
8
|
+
// cause-naming fault is returned. "Fail loudly and stop", never a partial issue.
|
|
9
|
+
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { gateReconcile, gateMarginLeak, gateSignLink, type SignLinkVerdict } from "./gates.js";
|
|
14
|
+
|
|
15
|
+
export type RenderFault = "render-absent" | "render-error" | "reconcile-fail" | "margin-leak" | "sign-link";
|
|
16
|
+
|
|
17
|
+
export interface RunRenderArgs {
|
|
18
|
+
accountDir: string;
|
|
19
|
+
renderScriptPath: string; // absolute
|
|
20
|
+
figures: Record<string, unknown>;
|
|
21
|
+
signUrl?: string;
|
|
22
|
+
jobId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RenderResult =
|
|
26
|
+
| {
|
|
27
|
+
ok: true;
|
|
28
|
+
docs: { internal: string; breakdown: string; quote: string };
|
|
29
|
+
reconcile: { internal: boolean; breakdown: boolean; quote: boolean };
|
|
30
|
+
signLink: SignLinkVerdict;
|
|
31
|
+
}
|
|
32
|
+
| { ok: false; reason: RenderFault; detail: string };
|
|
33
|
+
|
|
34
|
+
interface DocHtml { internal: string; breakdown: string; quote: string }
|
|
35
|
+
|
|
36
|
+
export async function runRender(a: RunRenderArgs): Promise<RenderResult> {
|
|
37
|
+
if (!existsSync(a.renderScriptPath)) {
|
|
38
|
+
return { ok: false, reason: "render-absent", detail: `no render script at ${a.renderScriptPath}` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const payload = JSON.stringify({ figures: a.figures, signUrl: a.signUrl ?? null });
|
|
42
|
+
const { code, stdout, stderr } = await new Promise<{ code: number | null; stdout: string; stderr: string }>((resolve) => {
|
|
43
|
+
const child = spawn("node", [a.renderScriptPath], { cwd: a.accountDir });
|
|
44
|
+
let out = "";
|
|
45
|
+
let err = "";
|
|
46
|
+
child.stdout.on("data", (d) => (out += d));
|
|
47
|
+
child.stderr.on("data", (d) => (err += d));
|
|
48
|
+
child.on("error", (e) => resolve({ code: -1, stdout: out, stderr: String(e) }));
|
|
49
|
+
child.on("close", (c) => resolve({ code: c, stdout: out, stderr: err }));
|
|
50
|
+
// A script that exits before draining stdin closes the read end mid-write,
|
|
51
|
+
// raising EPIPE. Swallow it: the exit code from `close` carries the fault, so
|
|
52
|
+
// an early exit surfaces as render-error rather than crashing the server.
|
|
53
|
+
child.stdin.on("error", () => {});
|
|
54
|
+
child.stdin.write(payload);
|
|
55
|
+
child.stdin.end();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (code !== 0) {
|
|
59
|
+
return { ok: false, reason: "render-error", detail: `render script exited ${code}: ${stderr.trim().slice(0, 400)}` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let html: DocHtml;
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(stdout) as Partial<DocHtml>;
|
|
65
|
+
if (typeof parsed.internal !== "string" || typeof parsed.breakdown !== "string" || typeof parsed.quote !== "string") {
|
|
66
|
+
return { ok: false, reason: "render-error", detail: "render output must be { internal, breakdown, quote } HTML strings" };
|
|
67
|
+
}
|
|
68
|
+
html = parsed as DocHtml;
|
|
69
|
+
} catch {
|
|
70
|
+
return { ok: false, reason: "render-error", detail: `render stdout was not JSON: ${stdout.trim().slice(0, 200)}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const engineTotal = Number(a.figures.total);
|
|
74
|
+
const phone = (a.figures.header as { phone?: string } | undefined)?.phone;
|
|
75
|
+
if (!phone) {
|
|
76
|
+
return { ok: false, reason: "render-error", detail: "figures.header.phone is required to file the documents by scope" };
|
|
77
|
+
}
|
|
78
|
+
// The phone is a filing-path segment; reject a separator or traversal so a
|
|
79
|
+
// crafted header cannot escape the customer scope, the same guard account.ts
|
|
80
|
+
// applies to ACCOUNT_ID.
|
|
81
|
+
if (phone.includes("/") || phone.includes("..")) {
|
|
82
|
+
return { ok: false, reason: "render-error", detail: `figures.header.phone contains a path separator or traversal: ${phone}` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Gate every document before filing any, so the log reports each doc's verdict
|
|
86
|
+
// and nothing is written when one fails.
|
|
87
|
+
const recInternal = gateReconcile(html.internal, engineTotal);
|
|
88
|
+
const recBreakdown = gateReconcile(html.breakdown, engineTotal);
|
|
89
|
+
const recQuote = gateReconcile(html.quote, engineTotal);
|
|
90
|
+
const leakBreakdown = gateMarginLeak(html.breakdown);
|
|
91
|
+
const leakQuote = gateMarginLeak(html.quote);
|
|
92
|
+
const signLink = gateSignLink(html.quote);
|
|
93
|
+
|
|
94
|
+
const internalOk = recInternal.pass;
|
|
95
|
+
const breakdownOk = recBreakdown.pass && leakBreakdown.pass;
|
|
96
|
+
const quoteOk = recQuote.pass && leakQuote.pass && signLink.pass;
|
|
97
|
+
|
|
98
|
+
if (!internalOk || !breakdownOk || !quoteOk) {
|
|
99
|
+
// Reason names the first failing gate, in document then gate order.
|
|
100
|
+
let reason: RenderFault;
|
|
101
|
+
let detail: string;
|
|
102
|
+
if (!recInternal.pass) { reason = "reconcile-fail"; detail = `internal total ${recInternal.shown} != engine ${engineTotal}`; }
|
|
103
|
+
else if (!recBreakdown.pass) { reason = "reconcile-fail"; detail = `breakdown total ${recBreakdown.shown} != engine ${engineTotal}`; }
|
|
104
|
+
else if (!leakBreakdown.pass) { reason = "margin-leak"; detail = "breakdown carries data-internal-only"; }
|
|
105
|
+
else if (!recQuote.pass) { reason = "reconcile-fail"; detail = `quote total ${recQuote.shown} != engine ${engineTotal}`; }
|
|
106
|
+
else if (!leakQuote.pass) { reason = "margin-leak"; detail = "quote carries data-internal-only"; }
|
|
107
|
+
else { reason = "sign-link"; detail = `quote Sign-Online link ${signLink.reason} (href=${signLink.href ?? ""})`; }
|
|
108
|
+
return { ok: false, reason, detail };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const internalPath = join(a.accountDir, "memory/admin/customers", phone, `quote-${a.jobId}-internal.html`);
|
|
112
|
+
const breakdownPath = join(a.accountDir, "memory/users", phone, "documents", `quote-${a.jobId}-breakdown.html`);
|
|
113
|
+
const quotePath = join(a.accountDir, "memory/users", phone, "documents", `quote-${a.jobId}.html`);
|
|
114
|
+
for (const [p, body] of [[internalPath, html.internal], [breakdownPath, html.breakdown], [quotePath, html.quote]] as const) {
|
|
115
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
116
|
+
writeFileSync(p, body);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
docs: { internal: internalPath, breakdown: breakdownPath, quote: quotePath },
|
|
122
|
+
reconcile: { internal: internalOk, breakdown: breakdownOk, quote: quoteOk },
|
|
123
|
+
signLink,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/tools/quote-engine-run.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// quote-engine-run — execute the operator's persisted pricing engine against a
|
|
2
|
+
// job and return structured figures. Runs the already-persisted engine; never
|
|
3
|
+
// regenerates it, never embeds a rate/catalogue/roll-up. A fault returns a
|
|
4
|
+
// cause-naming error and zero figures — never a partial or guessed total.
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { lifelineTool } from "../../../../../../../platform/lib/mcp-lifeline/dist/index.js";
|
|
10
|
+
import { resolveAccountDir } from "../lib/account.js";
|
|
11
|
+
import { runEngine, writeFigures } from "../lib/engine-run.js";
|
|
12
|
+
import { logLine } from "../lib/log.js";
|
|
13
|
+
|
|
14
|
+
export function registerQuoteEngineRun(server: McpServer): void {
|
|
15
|
+
lifelineTool(
|
|
16
|
+
server,
|
|
17
|
+
"quote-engine-run",
|
|
18
|
+
"Execute the operator's already-persisted pricing engine (quoting/engine.mjs) against a job's quantities and return line prices, group subtotals and total. mode=verify runs the engine against a past job and returns the per-level reproduction delta. Runs the operator's own script; it is not a pricing model and embeds no rate, catalogue or roll-up. Faults (engine-absent, method-absent, script-error) return zero figures.",
|
|
19
|
+
{
|
|
20
|
+
jobId: z.string().describe("Correlation id; figures persist under quoting/jobs/<jobId>/."),
|
|
21
|
+
job: z.record(z.any()).describe("The job: header + lines. Passed to the engine verbatim; not interpreted here."),
|
|
22
|
+
mode: z.enum(["quote", "verify"]).default("quote").describe("quote = price the job; verify = reproduce a past job and return the delta."),
|
|
23
|
+
enginePath: z.string().default("quoting/engine.mjs").describe("Engine path relative to the account workspace."),
|
|
24
|
+
methodPath: z.string().default("quoting/method.json").describe("Method path relative to the account workspace."),
|
|
25
|
+
},
|
|
26
|
+
async (params: Record<string, unknown>) => {
|
|
27
|
+
const { jobId, job, mode, enginePath, methodPath } = params as {
|
|
28
|
+
jobId: string;
|
|
29
|
+
job: unknown;
|
|
30
|
+
mode: "quote" | "verify";
|
|
31
|
+
enginePath: string;
|
|
32
|
+
methodPath: string;
|
|
33
|
+
};
|
|
34
|
+
const accountId = process.env.ACCOUNT_ID ?? "local";
|
|
35
|
+
const accountDir = resolveAccountDir();
|
|
36
|
+
if (!accountDir) {
|
|
37
|
+
const detail = "ACCOUNT_DIR unset and PLATFORM_ROOT+ACCOUNT_ID unavailable";
|
|
38
|
+
logLine(`[quote-engine] op=fault jobId=${jobId} reason=no-account-context detail=${detail}`);
|
|
39
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, reason: "no-account-context", detail }) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const absEngine = join(accountDir, enginePath);
|
|
43
|
+
const absMethod = join(accountDir, methodPath);
|
|
44
|
+
const r = await runEngine({ accountDir, enginePath: absEngine, methodPath: absMethod, job, mode });
|
|
45
|
+
|
|
46
|
+
if (!r.ok) {
|
|
47
|
+
logLine(`[quote-engine] op=fault jobId=${jobId} reason=${r.reason} detail=${r.detail}`);
|
|
48
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, reason: r.reason, detail: r.detail }, null, 2) }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (mode === "verify") {
|
|
52
|
+
const deltas = (r.figures.deltas as unknown[]) ?? [];
|
|
53
|
+
const worst = (r.figures.worst as number) ?? 0;
|
|
54
|
+
logLine(`[quote-engine] op=verify jobId=${jobId} accountId=${accountId} worst=${worst} levels=${deltas.length}`);
|
|
55
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ jobId, deltas, worst }, null, 2) }] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const total = r.figures.total as number;
|
|
59
|
+
const lineCount = Array.isArray(r.figures.lines) ? r.figures.lines.length : 0;
|
|
60
|
+
const reconcile = r.figures.reconcile;
|
|
61
|
+
const figuresPath = writeFigures(accountDir, jobId, r.figures);
|
|
62
|
+
logLine(`[quote-engine] op=run jobId=${jobId} accountId=${accountId} enginePath=${enginePath} methodPath=${methodPath} lines=${lineCount} total=${total}`);
|
|
63
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ jobId, total, lineCount, reconcile, figuresPath }, null, 2) }] };
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// quote-render — turn engine figures into the three reconciling HTML documents
|
|
2
|
+
// via the operator's persisted render script, enforcing the reconcile,
|
|
3
|
+
// margin-leak and link-liveness gates, and filing each doc by its scope. HTML
|
|
4
|
+
// only: the PDF is produced downstream by the browser plugin's browser-pdf-save.
|
|
5
|
+
// A gate failure is fail-closed — no document is filed.
|
|
6
|
+
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
+
import { lifelineTool } from "../../../../../../../platform/lib/mcp-lifeline/dist/index.js";
|
|
12
|
+
import { resolveAccountDir } from "../lib/account.js";
|
|
13
|
+
import { runRender } from "../lib/render-run.js";
|
|
14
|
+
import { logLine } from "../lib/log.js";
|
|
15
|
+
|
|
16
|
+
export function registerQuoteRender(server: McpServer): void {
|
|
17
|
+
lifelineTool(
|
|
18
|
+
server,
|
|
19
|
+
"quote-render",
|
|
20
|
+
"Turn engine figures into the three reconciling HTML documents (internal margin view, client breakdown, client quote) via the operator's persisted quoting/render.mjs, enforcing three gates — reconcile (displayed total == engine total), margin-leak (no cost/margin in client docs), link-liveness (the client quote's Sign-Online href is absolute https). Files each doc by scope and returns the HTML paths. HTML only; print the PDFs with the browser plugin's browser-pdf-save. A gate failure files nothing.",
|
|
21
|
+
{
|
|
22
|
+
jobId: z.string().describe("Correlation id; figures load from quoting/jobs/<jobId>/figures.json unless figures is supplied inline."),
|
|
23
|
+
figures: z.record(z.any()).optional().describe("Inline figures (overrides the persisted figures.json) — for fixed-figure render parity."),
|
|
24
|
+
signUrl: z.string().optional().describe("The deployed e-sign signing URL injected into the client quote's acceptance anchor. Absent or non-https fails the link-liveness gate."),
|
|
25
|
+
renderScriptPath: z.string().default("quoting/render.mjs").describe("Render script path relative to the account workspace."),
|
|
26
|
+
},
|
|
27
|
+
async (params: Record<string, unknown>) => {
|
|
28
|
+
const { jobId, figures: inlineFigures, signUrl, renderScriptPath } = params as {
|
|
29
|
+
jobId: string;
|
|
30
|
+
figures?: Record<string, unknown>;
|
|
31
|
+
signUrl?: string;
|
|
32
|
+
renderScriptPath: string;
|
|
33
|
+
};
|
|
34
|
+
const accountDir = resolveAccountDir();
|
|
35
|
+
if (!accountDir) {
|
|
36
|
+
const detail = "ACCOUNT_DIR unset and PLATFORM_ROOT+ACCOUNT_ID unavailable";
|
|
37
|
+
logLine(`[quote-render] op=fault jobId=${jobId} reason=no-account-context detail=${detail}`);
|
|
38
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, reason: "no-account-context", detail }) }], isError: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let figures: Record<string, unknown>;
|
|
42
|
+
if (inlineFigures) {
|
|
43
|
+
figures = inlineFigures;
|
|
44
|
+
} else {
|
|
45
|
+
const figuresPath = join(accountDir, "quoting", "jobs", jobId, "figures.json");
|
|
46
|
+
if (!existsSync(figuresPath)) {
|
|
47
|
+
const detail = `no figures at ${figuresPath} — run quote-engine-run first`;
|
|
48
|
+
logLine(`[quote-render] op=fault jobId=${jobId} reason=figures-absent detail=${detail}`);
|
|
49
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, reason: "figures-absent", detail }) }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
figures = JSON.parse(readFileSync(figuresPath, "utf8"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const absRender = join(accountDir, renderScriptPath);
|
|
55
|
+
const r = await runRender({ accountDir, renderScriptPath: absRender, figures, signUrl, jobId });
|
|
56
|
+
|
|
57
|
+
if (!r.ok) {
|
|
58
|
+
logLine(`[quote-render] op=fault jobId=${jobId} reason=${r.reason} detail=${r.detail}`);
|
|
59
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, reason: r.reason, detail: r.detail }, null, 2) }], isError: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logLine(`[quote-render] op=render jobId=${jobId} docs=3 internal=${r.reconcile.internal} breakdown=${r.reconcile.breakdown} quote=${r.reconcile.quote}`);
|
|
63
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ jobId, docs: r.docs, reconcile: r.reconcile, signLink: r.signLink }, null, 2) }] };
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
package/payload/premium-plugins/sitedesk/plugins/sitedesk-job/mcp/src/tools/takeoff-compute.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// takeoff-compute — deterministic takeoff geometry over agent-supplied measured
|
|
2
|
+
// inputs. Reading the drawing and eliciting inputs stay in the takeoff skill;
|
|
3
|
+
// this tool only does the arithmetic. Carries no business content.
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { lifelineTool } from "../../../../../../../platform/lib/mcp-lifeline/dist/index.js";
|
|
8
|
+
import { computeTakeoff, type InLine, type Scale } from "../lib/geometry.js";
|
|
9
|
+
import { logLine } from "../lib/log.js";
|
|
10
|
+
|
|
11
|
+
export function registerTakeoffCompute(server: McpServer): void {
|
|
12
|
+
lifelineTool(
|
|
13
|
+
server,
|
|
14
|
+
"takeoff-compute",
|
|
15
|
+
"Deterministic takeoff geometry: turn agent-supplied measured inputs into a tagged quantity list — scale a measured length to its real dimension, a scaled area from two sides, roll up counts, and cross-check a scaled quantity against a printed figure. Reading the drawing and eliciting the inputs is the takeoff skill; this tool does only the arithmetic and carries no business content.",
|
|
16
|
+
{
|
|
17
|
+
jobId: z.string().describe("Correlation id for this quote/takeoff."),
|
|
18
|
+
scale: z
|
|
19
|
+
.union([z.object({ ratio: z.number() }), z.object({ measuredRef: z.number(), actual: z.number() })])
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Drawing scale: {ratio} for 1:R, or {measuredRef, actual} calibration. Required only when a line is scale-derived."),
|
|
22
|
+
lines: z
|
|
23
|
+
.array(
|
|
24
|
+
z.object({
|
|
25
|
+
element: z.string(),
|
|
26
|
+
basis: z.enum(["printed", "scaled", "count", "operator"]),
|
|
27
|
+
value: z.number().optional(),
|
|
28
|
+
unit: z.string().optional(),
|
|
29
|
+
measured: z.number().optional(),
|
|
30
|
+
measuredB: z.number().optional(),
|
|
31
|
+
kind: z.enum(["length", "area"]).optional(),
|
|
32
|
+
count: z.union([z.number(), z.array(z.number())]).optional(),
|
|
33
|
+
crossCheck: z.object({ value: z.number(), unit: z.string().optional(), tolerance: z.number().optional() }).optional(),
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
.describe("The measured lines to compute."),
|
|
37
|
+
},
|
|
38
|
+
async (params: Record<string, unknown>) => {
|
|
39
|
+
const { jobId, scale, lines } = params as { jobId: string; scale?: Scale; lines: InLine[] };
|
|
40
|
+
try {
|
|
41
|
+
const r = computeTakeoff({ scale, lines });
|
|
42
|
+
logLine(`[takeoff-compute] op=compute jobId=${jobId} lines=${r.lines.length} exact=${r.counts.exact} scaled=${r.counts.scaled}`);
|
|
43
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ jobId, ...r }, null, 2) }] };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46
|
+
logLine(`[takeoff-compute] op=fault jobId=${jobId} detail=${msg}`);
|
|
47
|
+
return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src"],
|
|
19
|
+
"exclude": ["src/**/__tests__/**", "src/**/*.test.ts"]
|
|
20
|
+
}
|
|
@@ -95,6 +95,30 @@ The engine reads one method and one job and produces the priced result. It must:
|
|
|
95
95
|
(line, group, final) against the figures the owner actually issued and report each as a match or
|
|
96
96
|
a mismatch.
|
|
97
97
|
|
|
98
|
+
## The apply-path tool runs the engine
|
|
99
|
+
|
|
100
|
+
The deterministic apply step is the `sitedesk-job:quote-engine-run` MCP tool, not an inline Bash run
|
|
101
|
+
of the script. The tool executes the **already-persisted** engine and never regenerates it. Generate
|
|
102
|
+
the engine to the canonical layout and I/O boundary the tool invokes:
|
|
103
|
+
|
|
104
|
+
- the engine at `quoting/engine.mjs` and the method at `quoting/method.json`, both under the account
|
|
105
|
+
workspace (`$ACCOUNT_DIR`);
|
|
106
|
+
- the engine reads `{ methodPath, job, mode }` as one JSON object on **stdin**, loads its method from
|
|
107
|
+
`methodPath`, and writes its result as one JSON object on **stdout**;
|
|
108
|
+
- in `mode: "quote"` the result is the figures the seven duties above produce: `{ lines, groups,
|
|
109
|
+
rollup, total, reconcile }`;
|
|
110
|
+
- in `mode: "verify"` the result is the per-level delta `{ deltas, worst }` that feeds the
|
|
111
|
+
reproduction report in `references/verification.md`;
|
|
112
|
+
- a non-zero exit, non-JSON stdout, or a missing `total`/`reconcile` (quote) or `deltas` (verify) is
|
|
113
|
+
the fault the tool surfaces as `script-error`; a missing engine or method is `engine-absent` or
|
|
114
|
+
`method-absent`. Every fault returns zero figures, never a partial total.
|
|
115
|
+
|
|
116
|
+
`quote-engine-run` persists the quote-mode figures to `quoting/jobs/<jobId>/figures.json` and returns
|
|
117
|
+
`{ jobId, total, lineCount, reconcile }`. The render step (`quote-render`, in
|
|
118
|
+
`references/quote-generation.md`) reads those figures by `jobId`, so a Workflow chains
|
|
119
|
+
`quote-engine-run` then `quote-render` by passing `jobId` alone. Building and maintaining the engine's
|
|
120
|
+
content stays as this reference and the method-capture flow define; only the run seam is now the tool.
|
|
121
|
+
|
|
98
122
|
## Expressiveness — bounded by the owner's history
|
|
99
123
|
|
|
100
124
|
Build the engine to support **exactly** the quantity, pricing, and roll-up forms the owner's own
|
|
@@ -32,6 +32,38 @@ equal the final total. You never recompute these figures — you display them.
|
|
|
32
32
|
The engine itself, and how a line's quantity, rate, cost and sell are derived, are out of scope here
|
|
33
33
|
(that is the compute core). This contract only reads the result.
|
|
34
34
|
|
|
35
|
+
## The apply-path tool renders the documents
|
|
36
|
+
|
|
37
|
+
Rendering is the `sitedesk-job:quote-render` MCP tool, not hand-authored HTML each run. The tool runs
|
|
38
|
+
the operator's **persisted render script** at `quoting/render.mjs` (the render analog of the pricing
|
|
39
|
+
engine): it reads `{ figures, signUrl }` on stdin and writes the three documents as one JSON object of
|
|
40
|
+
HTML strings on stdout, `{ internal, breakdown, quote }`. The script holds the operator's brand,
|
|
41
|
+
layout, headings, narrative, charts, page size, contact and terms as their own data. The tool
|
|
42
|
+
templates nothing and carries no brand. Generating and maintaining that render script is the build
|
|
43
|
+
half of this skill, exactly as generating the pricing engine is the build half of the compute core.
|
|
44
|
+
|
|
45
|
+
The render script marks three stable attributes so the tool gates the output deterministically,
|
|
46
|
+
whatever the operator's brand or language:
|
|
47
|
+
|
|
48
|
+
- **`data-quote-total`** on each document's displayed grand total, carrying the numeric total, so the
|
|
49
|
+
tool reconciles it against the engine total;
|
|
50
|
+
- **`data-internal-only`** on every cost, margin, or internal-rate figure in the internal view, so the
|
|
51
|
+
tool asserts the two client documents carry none;
|
|
52
|
+
- **`data-esign-accept`** on the client quote's Sign-Online anchor (one line, double-quoted `href`), so
|
|
53
|
+
the tool asserts the acceptance link is live.
|
|
54
|
+
|
|
55
|
+
`quote-render` enforces three fail-closed gates before it files anything: **reconcile** (each
|
|
56
|
+
document's `data-quote-total` equals the engine total), **margin-leak** (no `data-internal-only` in the
|
|
57
|
+
client breakdown or client quote), and **link-liveness** (the client quote's `data-esign-accept` href
|
|
58
|
+
is an absolute `https://` URL). If any gate fails the tool files nothing and returns a cause-naming
|
|
59
|
+
fault (`reconcile-fail`, `margin-leak`, `sign-link`, `render-absent`, `render-error`). On success it
|
|
60
|
+
files each document to its scope (below) and returns the three HTML paths plus the gate verdicts.
|
|
61
|
+
|
|
62
|
+
The tool produces **HTML, not PDF**. Print each returned path to PDF with the `browser` plugin as the
|
|
63
|
+
final step: `browser-navigate` to its `file://` URL, then `browser-pdf-save` to a `.pdf` alongside it,
|
|
64
|
+
confirming a non-zero byte count. A Workflow chains `quote-engine-run`, then `quote-render`, then one
|
|
65
|
+
`browser` `browser-pdf-save` step per document.
|
|
66
|
+
|
|
35
67
|
## The three documents
|
|
36
68
|
|
|
37
69
|
The operator defines the content and styling of each; the **roles** are fixed, the **contents** are
|
|
@@ -64,17 +96,19 @@ rendered until the deploy has run**. The order is fixed and load-bearing:
|
|
|
64
96
|
|
|
65
97
|
1. run **e-sign § 5** to deploy the signing page and take its returned absolute `https://` URL (e-sign
|
|
66
98
|
§ 5 names it as an explicit hand-back);
|
|
67
|
-
2.
|
|
68
|
-
|
|
99
|
+
2. pass that URL to `quote-render` as `signUrl`; the render script injects it as the `href` of the
|
|
100
|
+
acceptance block's `data-esign-accept` anchor, and the tool's link-liveness gate holds it to an
|
|
101
|
+
absolute `https://` target or files nothing;
|
|
102
|
+
3. print the returned client-quote HTML to PDF (`Producing the PDF`, below);
|
|
69
103
|
4. send.
|
|
70
104
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
wording, or language. The gate
|
|
74
|
-
on a **single line** with a **double-quoted `href`** (the default for these self-contained
|
|
75
|
-
a split tag or single-quoted `href` reads as no anchor and fails the gate closed. The
|
|
76
|
-
is **never** a placeholder, a bare `#`, or a `file://`/relative path: it is the
|
|
77
|
-
or the quote does not ship.
|
|
105
|
+
The render script marks that one anchor with the stable attribute `data-esign-accept` so
|
|
106
|
+
`quote-render`'s link-liveness gate finds it deterministically, whatever the operator's brand,
|
|
107
|
+
wording, or language. The gate matches the anchor's opening tag, so the render script emits that
|
|
108
|
+
opening tag on a **single line** with a **double-quoted `href`** (the default for these self-contained
|
|
109
|
+
documents); a split tag or single-quoted `href` reads as no anchor and fails the gate closed. The
|
|
110
|
+
anchor's `href` is **never** a placeholder, a bare `#`, or a `file://`/relative path: it is the
|
|
111
|
+
deployed signing URL or the quote does not ship.
|
|
78
112
|
|
|
79
113
|
When the quote is **confidential to a named client**, the acceptance is gated per e-sign § 1a. That
|
|
80
114
|
section's **same-origin invariant** is load-bearing: the `/api/accept` and `/api/status` capture
|
|
@@ -113,51 +147,33 @@ the document:
|
|
|
113
147
|
|
|
114
148
|
## Producing the PDF
|
|
115
149
|
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
`quote-render` renders each document as self-contained HTML (the render script inlines styles and
|
|
151
|
+
references images by absolute local path) and files it to its filing-scope path, returning the paths.
|
|
152
|
+
For each returned path, print it the same way as every other business-assistant document:
|
|
118
153
|
|
|
119
|
-
1.
|
|
120
|
-
2. `browser-
|
|
121
|
-
3.
|
|
122
|
-
4. confirm a non-zero byte count from the `Saved PDF to <path> (<bytes>)` line before treating the PDF
|
|
154
|
+
1. `browser-navigate` to it via a `file://` absolute URL;
|
|
155
|
+
2. `browser-pdf-save` to an absolute `.pdf` path alongside the HTML;
|
|
156
|
+
3. confirm a non-zero byte count from the `Saved PDF to <path> (<bytes>)` line before treating the PDF
|
|
123
157
|
as done.
|
|
124
158
|
|
|
125
|
-
##
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
tag=$(grep -oiE '<a[^>]*\bdata-esign-accept\b[^>]*>' "$QUOTE_HTML" | head -1)
|
|
144
|
-
href=$(printf '%s' "$tag" | grep -oiE 'href="[^"]*"' | head -1 | sed -E 's/^href="(.*)"$/\1/')
|
|
145
|
-
verdict() { echo "op=sign-link doc_ref=${DOC_REF} href=${href} live=$1"; }
|
|
146
|
-
if [ -z "$tag" ]; then verdict "fail reason=no-anchor"; exit 1; fi
|
|
147
|
-
case "$href" in
|
|
148
|
-
https://*) verdict ok ;;
|
|
149
|
-
"") verdict "fail reason=empty"; exit 1 ;;
|
|
150
|
-
'#'*) verdict "fail reason=fragment"; exit 1 ;;
|
|
151
|
-
*://*|*:*) verdict "fail reason=non-https"; exit 1 ;; # http://, file://, about:, mailto:
|
|
152
|
-
*) verdict "fail reason=relative"; exit 1 ;; # /path, path, ./x, ../x
|
|
153
|
-
esac
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
A client quote PDF exists **only** when this line read `live=ok`. The per-run log for a client quote
|
|
157
|
-
therefore carries one `op=sign-link doc_ref=<ref> href=<value> live=ok` line — naming the absolute
|
|
158
|
-
`https://` href — alongside the roles / page-count / reconciliation lines above. A `Saved PDF to
|
|
159
|
-
<path>` line for a client quote with no preceding `live=ok` for the same `doc_ref` is itself a
|
|
160
|
-
regression signature: the gate was bypassed.
|
|
159
|
+
## The tool enforces the reconciliation and link-liveness gates
|
|
160
|
+
|
|
161
|
+
`quote-render` runs three fail-closed gates over the rendered HTML before it files any document, so
|
|
162
|
+
these checks are enforced deterministically at render time, not as a separate hand-run step:
|
|
163
|
+
|
|
164
|
+
- **reconciliation.** Each document's `data-quote-total` must equal the engine total. A mismatch files
|
|
165
|
+
nothing and returns `reconcile-fail`. Never ship a document whose figures disagree with the engine.
|
|
166
|
+
- **margin-leak.** The client breakdown and client quote must carry no `data-internal-only` marker. A
|
|
167
|
+
leak files nothing and returns `margin-leak`. The margin boundary is structural, not a matter of
|
|
168
|
+
remembering to omit a column.
|
|
169
|
+
- **link-liveness.** The client quote's `data-esign-accept` href must be an absolute `https://` URL,
|
|
170
|
+
not empty, a bare fragment (`#…`), relative, `file://`, `about:`, or any non-https target. A dead
|
|
171
|
+
link files nothing and returns `sign-link`, because a dead link emits no other signal.
|
|
172
|
+
|
|
173
|
+
The tool emits one `[quote-render] op=render jobId=… docs=3 internal=… breakdown=… quote=…` line on
|
|
174
|
+
success and `[quote-render] op=fault jobId=… reason=… detail=…` on any gate failure. A `Saved PDF to
|
|
175
|
+
<path>` line for a client quote with no preceding successful `op=render` for the same job is itself a
|
|
176
|
+
regression signature: the render gate was bypassed.
|
|
161
177
|
|
|
162
178
|
**No-event standing check — filed quotes against live signing pages.** The gate fires only while a
|
|
163
179
|
quote is being produced; it cannot catch a quote that shipped a bad link before the gate existed.
|
|
@@ -55,6 +55,18 @@ Every line names how its quantity was measured. There are four bases and no othe
|
|
|
55
55
|
|
|
56
56
|
`references/measuring.md` carries how to establish the reference and work each basis.
|
|
57
57
|
|
|
58
|
+
## Computing the quantities
|
|
59
|
+
|
|
60
|
+
Reading the drawing and eliciting the missing figures is this skill's work. Turning the readings into
|
|
61
|
+
quantities is the `sitedesk-job:takeoff-compute` tool. Pass it the measured lines, each tagged with its
|
|
62
|
+
basis, and (when any line is scale-derived) the scale as a ratio or a known-dimension calibration. The
|
|
63
|
+
tool applies the deterministic arithmetic (a scaled length from the scale, a scaled area from two
|
|
64
|
+
measured sides, a count roll-up by summation, and printed and operator-supplied figures passed through
|
|
65
|
+
exactly) and returns the tagged quantity list with a count of how many lines are exact versus
|
|
66
|
+
scale-approximated. Where a line carries a printed figure to check a scaled reading against, the tool
|
|
67
|
+
records the delta and whether it disagrees beyond the tolerance you give. The tool measures nothing off
|
|
68
|
+
the drawing itself and asks the operator nothing; those stay this skill's job.
|
|
69
|
+
|
|
58
70
|
## Cross-check
|
|
59
71
|
|
|
60
72
|
Where a quantity is scale-derived and the drawing also prints a dimension for the same element, record
|