@suveren/gateway 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/suveren-gateway.js +5 -1
- package/dist/control-plane/index.mjs +4 -0
- package/dist/mcp-server/chunk-ZRRZFGRL.mjs +2412 -0
- package/dist/mcp-server/http.mjs +390 -645
- package/dist/mcp-server/report-verify-cli.mjs +64 -7
- package/dist/ui/assets/index-DeQPO4Sj.js +111 -0
- package/dist/ui/assets/index-kObsFuwK.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/profiles/index.json +2 -1
- package/profiles/reporting/0.2.profile.json +98 -0
- package/dist/mcp-server/chunk-JUFH4SKS.mjs +0 -639
- package/dist/ui/assets/index-78vuGrVE.js +0 -111
- package/dist/ui/assets/index-Dfh38n-b.css +0 -1
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
CASE_TIME_UNKNOWN_REASON,
|
|
4
|
+
REPORT_BRIEF,
|
|
5
|
+
REPORT_ELEMENTS,
|
|
6
|
+
START_TIME_METRICS,
|
|
7
|
+
agentApproval,
|
|
8
|
+
agentMandate,
|
|
9
|
+
agentTicket,
|
|
10
|
+
agentTicketRow,
|
|
11
|
+
approvalFields,
|
|
12
|
+
buildExportBundle,
|
|
13
|
+
buildExportDocument,
|
|
14
|
+
checkedValueLine,
|
|
15
|
+
computeMetric,
|
|
4
16
|
fingerprintOf,
|
|
5
|
-
formatActionLabel,
|
|
6
|
-
formatCurrency,
|
|
7
17
|
formatDateTime,
|
|
8
|
-
|
|
9
|
-
|
|
18
|
+
glossaryUsage,
|
|
19
|
+
mandateFields,
|
|
10
20
|
parseCaseAttrs,
|
|
11
21
|
parseElements,
|
|
12
22
|
parseTimestampSeconds,
|
|
13
23
|
profileShortLabel,
|
|
14
24
|
readPairing,
|
|
25
|
+
renderReportHtml,
|
|
15
26
|
resolveApprovalElement,
|
|
16
27
|
resolveCaseElement,
|
|
17
28
|
resolveMandateElement,
|
|
18
|
-
resolveTicketElement
|
|
19
|
-
|
|
29
|
+
resolveTicketElement,
|
|
30
|
+
sanitizeReport,
|
|
31
|
+
suggestedFilename,
|
|
32
|
+
ticketFields
|
|
33
|
+
} from "./chunk-ZRRZFGRL.mjs";
|
|
20
34
|
|
|
21
35
|
// bin/http.ts
|
|
22
36
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -1677,8 +1691,8 @@ var ReceiptArchive = class {
|
|
|
1677
1691
|
*/
|
|
1678
1692
|
record(entry, now = Math.floor(Date.now() / 1e3)) {
|
|
1679
1693
|
let changed = false;
|
|
1680
|
-
const
|
|
1681
|
-
const alreadyArchived =
|
|
1694
|
+
const receiptId = typeof entry.receipt?.id === "string" ? entry.receipt.id : void 0;
|
|
1695
|
+
const alreadyArchived = receiptId !== void 0 && this.data.receipts.some((r) => r.receipt.id === receiptId);
|
|
1682
1696
|
if (!alreadyArchived) {
|
|
1683
1697
|
this.data.receipts.push({
|
|
1684
1698
|
archivedAt: now,
|
|
@@ -2083,115 +2097,6 @@ import { homedir as homedir7 } from "os";
|
|
|
2083
2097
|
import { dirname as dirname8, join as join10 } from "path";
|
|
2084
2098
|
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2085
2099
|
|
|
2086
|
-
// src/lib/report-brief.ts
|
|
2087
|
-
var REPORT_ELEMENTS = [
|
|
2088
|
-
"sv-ticket",
|
|
2089
|
-
"sv-approval",
|
|
2090
|
-
"sv-mandate",
|
|
2091
|
-
"sv-record",
|
|
2092
|
-
"sv-case",
|
|
2093
|
-
"sv-metric"
|
|
2094
|
-
];
|
|
2095
|
-
var REPORT_BRIEF = `How to write the report
|
|
2096
|
-
|
|
2097
|
-
Purpose: show the company's management what the AI achieved in this test \u2014 case by case, from the request that started it to the goal that completed it \u2014 with proof for every fact that matters.
|
|
2098
|
-
|
|
2099
|
-
Who reads it: managers who are not technical. Plain words, short sentences, the result first.
|
|
2100
|
-
|
|
2101
|
-
What you write: one HTML page (HTML, CSS and inline SVG only \u2014 no JavaScript, no external files or links except the check links the gateway adds). The layout, headings, charts, tables and your assessment are yours.
|
|
2102
|
-
|
|
2103
|
-
It must read well everywhere: on a phone, in a narrow side panel (about 360 pixels wide) and on a large screen. Use fluid widths (no fixed page widths), let columns stack on narrow screens (CSS grid/flex with wrapping, or media queries), make wide tables scroll horizontally or turn into stacked rows, give SVG charts a viewBox so they scale, and keep text at a readable size.
|
|
2104
|
-
|
|
2105
|
-
What you cannot write yourself: facts that need proof. Place these elements instead; the gateway looks the data up, checks it and draws the element. You give references, never the content:
|
|
2106
|
-
|
|
2107
|
-
- <sv-ticket ref="TICKET_ID"></sv-ticket> \u2014 one action you took, with its signed ticket.
|
|
2108
|
-
- <sv-approval ticket="TICKET_ID"></sv-approval> \u2014 who approved that action, when, and how long it waited.
|
|
2109
|
-
- <sv-mandate ticket="TICKET_ID"></sv-mandate> \u2014 the authority the action ran under: limits, mode, intent.
|
|
2110
|
-
- <sv-record system="email|crm|erp" ref="RECORD_ID"></sv-record> \u2014 an email, CRM entry, quote or order.
|
|
2111
|
-
- <sv-case start="email:MESSAGE_ID" goal="ticket:TICKET_ID" steps="TICKET_ID TICKET_ID"></sv-case> \u2014 one business case: the incoming email that started it, the ticket that completed it, and the tickets in between.
|
|
2112
|
-
- <sv-metric kind="KIND" cases="all"></sv-metric> \u2014 a number the gateway computes over the cases you defined. KIND: completed, median-time, average-time, without-approval, approvals, median-approval-wait, tickets, refusals. cases is "all" or a space-separated list of case ids, e.g. cases="C1 C3".
|
|
2113
|
-
|
|
2114
|
-
Rules:
|
|
2115
|
-
1. Define every case you worked on with sv-case \u2014 also the ones that did not reach their goal (omit goal, or say why in your text). The gateway shows management how many of the loaded cases your report covers and names the missing ones \u2014 and lists every ticket from the test period that your report does not reference, so name every step.
|
|
2116
|
-
2. Use sv-metric for every headline number. Numbers you compute yourself are shown as "AI analysis \u2014 not verified".
|
|
2117
|
-
3. A case's goal must be what actually completed it \u2014 for an email reply, the reply to the start email. The gateway checks this link.
|
|
2118
|
-
4. A wrong or unknown reference is shown as "not verifiable". Do not guess IDs \u2014 read them first.
|
|
2119
|
-
5. Never claim more than the elements prove. Say plainly what went wrong or waited for a person.
|
|
2120
|
-
|
|
2121
|
-
Suggested structure:
|
|
2122
|
-
1. The result in one sentence, then 3\u20134 sv-metric figures (completed, median time, share without approval, approvals).
|
|
2123
|
-
2. All cases at a glance \u2014 a table with one row per case, linked to its sv-case below.
|
|
2124
|
-
3. Each case: sv-case, then the sv-ticket and sv-approval elements that explain it.
|
|
2125
|
-
4. What was stopped or needed a person, and why (sv-approval, refusals).
|
|
2126
|
-
5. Your assessment: what worked, what slowed things down, what a different mandate would change \u2014 marked as your view.
|
|
2127
|
-
|
|
2128
|
-
Update the report whenever cases progress; each write replaces the previous report.`;
|
|
2129
|
-
|
|
2130
|
-
// src/lib/report/sanitize.ts
|
|
2131
|
-
import sanitizeHtmlLib from "sanitize-html";
|
|
2132
|
-
var FORBIDDEN_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "link"]);
|
|
2133
|
-
function isUrlAttribute(name) {
|
|
2134
|
-
const n = name.toLowerCase();
|
|
2135
|
-
return n === "href" || n === "src" || n === "xlink:href" || n.endsWith(":href");
|
|
2136
|
-
}
|
|
2137
|
-
function isAllowedUrl(value) {
|
|
2138
|
-
const v = value.trim();
|
|
2139
|
-
if (v.startsWith("#")) return true;
|
|
2140
|
-
if (/^data:image\//i.test(v)) return true;
|
|
2141
|
-
return false;
|
|
2142
|
-
}
|
|
2143
|
-
function sanitizeReportHtml(html) {
|
|
2144
|
-
return sanitizeHtmlLib(html, {
|
|
2145
|
-
// Allow every tag/attribute by default — see module doc comment for why
|
|
2146
|
-
// this is deny-by-name rather than allow-by-name. `transformTags['*']`
|
|
2147
|
-
// below is then the ONLY attribute filter (allowedAttributes: false
|
|
2148
|
-
// skips sanitize-html's own, so there is exactly one place attributes
|
|
2149
|
-
// are decided, not two that could disagree). Logs a harmless warning for
|
|
2150
|
-
// "script"/"style" on every call (sanitize-html's own vulnerable-tag
|
|
2151
|
-
// notice, fired from its internal tag list even though we pass `false`,
|
|
2152
|
-
// not an array containing them) — `allowVulnerableTags` is the
|
|
2153
|
-
// documented way to accept that, and <script> is still removed, by
|
|
2154
|
-
// `exclusiveFilter` below, regardless of this flag.
|
|
2155
|
-
allowedTags: false,
|
|
2156
|
-
allowedAttributes: false,
|
|
2157
|
-
allowVulnerableTags: true,
|
|
2158
|
-
// sanitize-html's OWN scheme check (independent of transformTags, and
|
|
2159
|
-
// applied to it/src/cite only — NOT xlink:href, which is why the
|
|
2160
|
-
// transformTags filter above is the one actually relied on for SVG) uses
|
|
2161
|
-
// this list; it has no "data" scheme by default, which would undo our
|
|
2162
|
-
// own decision to keep `data:image/*` sources. Nothing wider is opened:
|
|
2163
|
-
// transformTags has already removed every href/src that isn't a
|
|
2164
|
-
// fragment link or a data:image/* URI by the time this check runs.
|
|
2165
|
-
allowedSchemes: [...sanitizeHtmlLib.defaults.allowedSchemes, "data"],
|
|
2166
|
-
// Preserve attribute name casing — inline SVG uses camelCase attributes
|
|
2167
|
-
// (viewBox, preserveAspectRatio, …) that lowercasing would silently
|
|
2168
|
-
// break. Safe for the `on*`/url checks below: both compare case-folded.
|
|
2169
|
-
parser: { lowerCaseAttributeNames: false },
|
|
2170
|
-
// Removes the named tag AND all of its children/text — the only correct
|
|
2171
|
-
// behaviour for <script>: dropping just the tag would leave its JS as
|
|
2172
|
-
// visible text, but the content must not survive either.
|
|
2173
|
-
exclusiveFilter: (frame) => {
|
|
2174
|
-
const tag = frame.tag.toLowerCase();
|
|
2175
|
-
if (FORBIDDEN_TAGS.has(tag)) return true;
|
|
2176
|
-
if (tag === "meta" && Object.keys(frame.attribs).some((a) => a.toLowerCase() === "http-equiv")) {
|
|
2177
|
-
return true;
|
|
2178
|
-
}
|
|
2179
|
-
return false;
|
|
2180
|
-
},
|
|
2181
|
-
transformTags: {
|
|
2182
|
-
"*": (tagName, attribs) => {
|
|
2183
|
-
const kept = {};
|
|
2184
|
-
for (const [name, value] of Object.entries(attribs)) {
|
|
2185
|
-
if (name.toLowerCase().startsWith("on")) continue;
|
|
2186
|
-
if (isUrlAttribute(name) && !isAllowedUrl(value)) continue;
|
|
2187
|
-
kept[name] = value;
|
|
2188
|
-
}
|
|
2189
|
-
return { tagName, attribs: kept };
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
});
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
2100
|
// src/lib/report/record-resolvers.ts
|
|
2196
2101
|
function findById(rows, id) {
|
|
2197
2102
|
return rows.find((r) => r.id === id);
|
|
@@ -2240,64 +2145,6 @@ function resolveRecord(system, exportData, ref) {
|
|
|
2240
2145
|
return resolveCrmRecord(exportData, ref);
|
|
2241
2146
|
}
|
|
2242
2147
|
|
|
2243
|
-
// src/lib/report/metric-resolvers.ts
|
|
2244
|
-
var METRIC_KINDS = [
|
|
2245
|
-
"completed",
|
|
2246
|
-
"median-time",
|
|
2247
|
-
"average-time",
|
|
2248
|
-
"without-approval",
|
|
2249
|
-
"approvals",
|
|
2250
|
-
"median-approval-wait",
|
|
2251
|
-
"tickets",
|
|
2252
|
-
"refusals"
|
|
2253
|
-
];
|
|
2254
|
-
var UnknownMetricKindError = class extends Error {
|
|
2255
|
-
constructor(kind) {
|
|
2256
|
-
super(`Unknown sv-metric kind "${kind}" \u2014 expected one of: ${METRIC_KINDS.join(", ")}.`);
|
|
2257
|
-
this.name = "UnknownMetricKindError";
|
|
2258
|
-
}
|
|
2259
|
-
};
|
|
2260
|
-
function median(values) {
|
|
2261
|
-
if (values.length === 0) return 0;
|
|
2262
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
2263
|
-
const mid = Math.floor(sorted.length / 2);
|
|
2264
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
2265
|
-
}
|
|
2266
|
-
function average(values) {
|
|
2267
|
-
if (values.length === 0) return 0;
|
|
2268
|
-
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
2269
|
-
}
|
|
2270
|
-
function computeMetric(kind, cases, refusalTimes = []) {
|
|
2271
|
-
switch (kind) {
|
|
2272
|
-
case "completed":
|
|
2273
|
-
return cases.length;
|
|
2274
|
-
case "median-time":
|
|
2275
|
-
return median(cases.map((c) => c.totalDurationSeconds));
|
|
2276
|
-
case "average-time":
|
|
2277
|
-
return average(cases.map((c) => c.totalDurationSeconds));
|
|
2278
|
-
case "without-approval": {
|
|
2279
|
-
if (cases.length === 0) return 0;
|
|
2280
|
-
const withoutApproval = cases.filter((c) => c.approvals.length === 0).length;
|
|
2281
|
-
return withoutApproval / cases.length;
|
|
2282
|
-
}
|
|
2283
|
-
case "approvals":
|
|
2284
|
-
return cases.reduce((sum, c) => sum + c.approvals.length, 0);
|
|
2285
|
-
case "median-approval-wait":
|
|
2286
|
-
return median(cases.flatMap((c) => c.approvals.map((a) => a.waitSeconds).filter((w) => typeof w === "number")));
|
|
2287
|
-
case "tickets": {
|
|
2288
|
-
const ids = new Set(cases.flatMap((c) => c.ticketIds));
|
|
2289
|
-
return ids.size;
|
|
2290
|
-
}
|
|
2291
|
-
case "refusals":
|
|
2292
|
-
return cases.reduce(
|
|
2293
|
-
(sum, c) => sum + refusalTimes.filter((t) => t >= c.startTime && t <= c.goalTime).length,
|
|
2294
|
-
0
|
|
2295
|
-
);
|
|
2296
|
-
default:
|
|
2297
|
-
throw new UnknownMetricKindError(kind);
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
2148
|
// src/lib/report/types.ts
|
|
2302
2149
|
function isEmailExport(v) {
|
|
2303
2150
|
const o = v;
|
|
@@ -2315,7 +2162,7 @@ function isCrmExport(v) {
|
|
|
2315
2162
|
// src/lib/report/verify-report.ts
|
|
2316
2163
|
var KNOWN_KINDS = new Set(REPORT_ELEMENTS);
|
|
2317
2164
|
async function verifyReport(html, sources) {
|
|
2318
|
-
const sanitized =
|
|
2165
|
+
const { html: sanitized, notes: sanitizeNotes } = sanitizeReport(html);
|
|
2319
2166
|
const parsed = parseElements(sanitized);
|
|
2320
2167
|
const exportCache = /* @__PURE__ */ new Map();
|
|
2321
2168
|
function getExport(system) {
|
|
@@ -2336,7 +2183,16 @@ async function verifyReport(html, sources) {
|
|
|
2336
2183
|
try {
|
|
2337
2184
|
switch (p.kind) {
|
|
2338
2185
|
case "sv-ticket": {
|
|
2339
|
-
const
|
|
2186
|
+
const ref = p.attrs.ref ?? "";
|
|
2187
|
+
const r = await resolveTicketElement(sources.archive, ref);
|
|
2188
|
+
if (r.status === "verified" && (p.attrs.variant ?? "").trim().toLowerCase() === "full") {
|
|
2189
|
+
const m = await resolveMandateElement(sources.archive, ref);
|
|
2190
|
+
const a = await resolveApprovalElement(sources.archive, ref);
|
|
2191
|
+
const data = { ...r.data };
|
|
2192
|
+
if (m.status === "verified") data.mandate = m.data;
|
|
2193
|
+
if (a.status === "verified") data.approval = a.data;
|
|
2194
|
+
return { id: p.id, kind: p.kind, attrs: p.attrs, status: r.status, data };
|
|
2195
|
+
}
|
|
2340
2196
|
return { id: p.id, kind: p.kind, attrs: p.attrs, ...r };
|
|
2341
2197
|
}
|
|
2342
2198
|
case "sv-approval": {
|
|
@@ -2435,9 +2291,13 @@ async function verifyReport(html, sources) {
|
|
|
2435
2291
|
});
|
|
2436
2292
|
continue;
|
|
2437
2293
|
}
|
|
2294
|
+
if (START_TIME_METRICS.has(kind) && caseInputs.some((c) => c.totalDurationSeconds === null)) {
|
|
2295
|
+
elements.push({ id: p.id, kind: p.kind, attrs: p.attrs, status: "unverifiable", reason: CASE_TIME_UNKNOWN_REASON });
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2438
2298
|
const refusalTimes = kind === "refusals" ? await collectRefusalTimes(getExport) : [];
|
|
2439
2299
|
const value = computeMetric(kind, caseInputs, refusalTimes);
|
|
2440
|
-
const data = { kind, cases: casesAttr, value, caseCount: verifiedCount };
|
|
2300
|
+
const data = { kind, cases: casesAttr, value, caseCount: verifiedCount, caseIds: caseInputs.map((c) => c.caseId) };
|
|
2441
2301
|
if (verifiedCount < requestedCount) {
|
|
2442
2302
|
elements.push({
|
|
2443
2303
|
id: p.id,
|
|
@@ -2455,8 +2315,8 @@ async function verifyReport(html, sources) {
|
|
|
2455
2315
|
}
|
|
2456
2316
|
}
|
|
2457
2317
|
const proof = buildProof(elements, caseElements);
|
|
2458
|
-
const coverage = await buildCoverage(caseElements, getExport, sources.archive, proof.ticketsReferenced);
|
|
2459
|
-
return { html: sanitized, elements, proof, coverage };
|
|
2318
|
+
const coverage = await buildCoverage(caseElements, getExport, sources.archive, proof.ticketsReferenced, sources.window);
|
|
2319
|
+
return { html: sanitized, elements, proof, coverage, sanitizeNotes };
|
|
2460
2320
|
}
|
|
2461
2321
|
async function collectRefusalTimes(getExport) {
|
|
2462
2322
|
const times = [];
|
|
@@ -2472,37 +2332,7 @@ async function collectRefusalTimes(getExport) {
|
|
|
2472
2332
|
return times;
|
|
2473
2333
|
}
|
|
2474
2334
|
function summarize(el) {
|
|
2475
|
-
|
|
2476
|
-
switch (el.kind) {
|
|
2477
|
-
case "sv-ticket": {
|
|
2478
|
-
const actionLabel = typeof d.actionLabel === "string" ? d.actionLabel : formatActionLabel(d.action);
|
|
2479
|
-
const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
|
|
2480
|
-
return `${actionLabel} \u2014 ${timeLabel}`;
|
|
2481
|
-
}
|
|
2482
|
-
case "sv-approval": {
|
|
2483
|
-
const who = typeof d.whoLabel === "string" ? d.whoLabel : d.who?.join(", ") ?? "unknown";
|
|
2484
|
-
const waitLabel = typeof d.waitLabel === "string" ? d.waitLabel : typeof d.waitSeconds === "number" ? formatDuration(d.waitSeconds) : "unknown";
|
|
2485
|
-
return `Approval by ${who} \u2014 waited ${waitLabel}`;
|
|
2486
|
-
}
|
|
2487
|
-
case "sv-mandate": {
|
|
2488
|
-
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : profileShortLabel(typeof d.profile === "string" ? d.profile : void 0);
|
|
2489
|
-
const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
|
|
2490
|
-
return `Mandate: ${profileLabel} \xB7 ${owners}`;
|
|
2491
|
-
}
|
|
2492
|
-
case "sv-record":
|
|
2493
|
-
return `${String(d.kind ?? "record")} record checked`;
|
|
2494
|
-
case "sv-case": {
|
|
2495
|
-
const steps = d.steps?.length ?? 0;
|
|
2496
|
-
return `Case ${String(d.caseId)}: ${steps} ${steps === 1 ? "step" : "steps"}, ${formatDuration(d.totalDurationSeconds)}`;
|
|
2497
|
-
}
|
|
2498
|
-
case "sv-metric": {
|
|
2499
|
-
const kind = String(d.kind ?? "");
|
|
2500
|
-
const label = METRIC_LABELS[kind] ?? kind;
|
|
2501
|
-
return `${label} = ${formatMetricValue(kind, d.value)}`;
|
|
2502
|
-
}
|
|
2503
|
-
default:
|
|
2504
|
-
return el.id;
|
|
2505
|
-
}
|
|
2335
|
+
return checkedValueLine(el);
|
|
2506
2336
|
}
|
|
2507
2337
|
function buildProof(elements, caseElements) {
|
|
2508
2338
|
const ticketsReferenced = /* @__PURE__ */ new Set();
|
|
@@ -2543,7 +2373,7 @@ function buildProof(elements, caseElements) {
|
|
|
2543
2373
|
verifiedValues
|
|
2544
2374
|
};
|
|
2545
2375
|
}
|
|
2546
|
-
async function buildCoverage(caseElements, getExport, archive, referenced) {
|
|
2376
|
+
async function buildCoverage(caseElements, getExport, archive, referenced, window) {
|
|
2547
2377
|
const exp = await getExport("email");
|
|
2548
2378
|
const emailExportError = !exp.ok ? exp.reason : !isEmailExport(exp.data) ? "Email simulator export had an unexpected shape." : void 0;
|
|
2549
2379
|
const loadedCases = exp.ok && isEmailExport(exp.data) ? [...new Set(exp.data.inbox.map((m) => m.case_id).filter((c) => !!c))] : [];
|
|
@@ -2553,7 +2383,8 @@ async function buildCoverage(caseElements, getExport, archive, referenced) {
|
|
|
2553
2383
|
const coveredCases = loadedCases.filter((c) => covered.has(c));
|
|
2554
2384
|
const missingCases = loadedCases.filter((c) => !covered.has(c));
|
|
2555
2385
|
const loadedAt = exp.ok && isEmailExport(exp.data) ? exp.data.simulation_load?.loaded_at : void 0;
|
|
2556
|
-
const
|
|
2386
|
+
const loadedAtSeconds = loadedAt !== void 0 ? parseTimestampSeconds(loadedAt) ?? null : null;
|
|
2387
|
+
const periodStart = window ? loadedAtSeconds !== null ? Math.max(window.start, loadedAtSeconds) : window.start : loadedAtSeconds;
|
|
2557
2388
|
const inPeriod = archive.getReceipts().map((r) => ({ id: String(r.receipt.id ?? ""), time: Number(r.receipt.timestamp ?? r.archivedAt) })).filter((t) => t.id && (periodStart === null || t.time >= periodStart)).sort((a, b) => a.time - b.time).map((t) => t.id);
|
|
2558
2389
|
const referencedSet = new Set(referenced);
|
|
2559
2390
|
const ticketsReferenced = inPeriod.filter((id) => referencedSet.has(id));
|
|
@@ -2564,6 +2395,7 @@ async function buildCoverage(caseElements, getExport, archive, referenced) {
|
|
|
2564
2395
|
coveredCases,
|
|
2565
2396
|
missingCases,
|
|
2566
2397
|
periodStart,
|
|
2398
|
+
...window ? { window: { start: window.start, end: window.end, days: window.days, label: window.label } } : {},
|
|
2567
2399
|
ticketsInPeriod: inPeriod,
|
|
2568
2400
|
ticketsReferenced,
|
|
2569
2401
|
ticketsNotReferenced
|
|
@@ -3015,8 +2847,8 @@ var FOOTER_RE = /\n*(?:—|--) (?:Sent|Published) by (?:an AI agent|[^\n]+?'s AI
|
|
|
3015
2847
|
function shouldAttachFooter() {
|
|
3016
2848
|
return true;
|
|
3017
2849
|
}
|
|
3018
|
-
function receiptUrl(
|
|
3019
|
-
return `${AS_BASE}/r/${
|
|
2850
|
+
function receiptUrl(receiptId) {
|
|
2851
|
+
return `${AS_BASE}/r/${receiptId}`;
|
|
3020
2852
|
}
|
|
3021
2853
|
function verbForProfile(profile) {
|
|
3022
2854
|
return profile === "publish" ? "Published" : "Sent";
|
|
@@ -3024,11 +2856,11 @@ function verbForProfile(profile) {
|
|
|
3024
2856
|
function hapLine() {
|
|
3025
2857
|
return `-- ${OPERATOR_NAME} implements HAP, the open Human Agency Protocol: ${HAP_URL}`;
|
|
3026
2858
|
}
|
|
3027
|
-
function footerText(
|
|
2859
|
+
function footerText(receiptId, verb, subject) {
|
|
3028
2860
|
const identity = deriveIdentityLine(subject, { operatorName: OPERATOR_NAME }).replace(/^Sent\b/, verb);
|
|
3029
2861
|
return `
|
|
3030
2862
|
|
|
3031
|
-
-- ${identity}. Receipt: ${receiptUrl(
|
|
2863
|
+
-- ${identity}. Receipt: ${receiptUrl(receiptId)}
|
|
3032
2864
|
|
|
3033
2865
|
${hapLine()}`;
|
|
3034
2866
|
}
|
|
@@ -3047,13 +2879,13 @@ function detectContentField(tool) {
|
|
|
3047
2879
|
function stripFooter(value) {
|
|
3048
2880
|
return value.replace(FOOTER_RE, "").replace(/\n+$/, "");
|
|
3049
2881
|
}
|
|
3050
|
-
function appendVerificationFooter(tool, args,
|
|
2882
|
+
function appendVerificationFooter(tool, args, receiptId, subject) {
|
|
3051
2883
|
const profile = tool.gating?.profile;
|
|
3052
2884
|
if (!profile || !FOOTER_PROFILES.has(profile)) return args;
|
|
3053
2885
|
const field = detectContentField(tool);
|
|
3054
2886
|
if (!field) return args;
|
|
3055
2887
|
const current = typeof args[field] === "string" ? args[field] : "";
|
|
3056
|
-
return { ...args, [field]: stripFooter(current) + footerText(
|
|
2888
|
+
return { ...args, [field]: stripFooter(current) + footerText(receiptId, verbForProfile(profile), subject) };
|
|
3057
2889
|
}
|
|
3058
2890
|
|
|
3059
2891
|
// src/lib/content-binding.ts
|
|
@@ -3106,10 +2938,10 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
|
|
|
3106
2938
|
boundContent
|
|
3107
2939
|
};
|
|
3108
2940
|
}
|
|
3109
|
-
function attachReceiptId(tool, args,
|
|
2941
|
+
function attachReceiptId(tool, args, receiptId) {
|
|
3110
2942
|
const schema = tool.inputSchema;
|
|
3111
2943
|
if (!schema?.properties || !("receipt_id" in schema.properties)) return args;
|
|
3112
|
-
return { ...args, receipt_id:
|
|
2944
|
+
return { ...args, receipt_id: receiptId };
|
|
3113
2945
|
}
|
|
3114
2946
|
|
|
3115
2947
|
// src/lib/ticket-verify.ts
|
|
@@ -3660,6 +3492,25 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3660
3492
|
};
|
|
3661
3493
|
}
|
|
3662
3494
|
const { profile, executionMapping, staticExecution, category } = tool.gating;
|
|
3495
|
+
const eligibleMandates = () => {
|
|
3496
|
+
const refusals = [];
|
|
3497
|
+
const matching = state2.getEnrichedAuthorizations().filter((a) => a.complete && profileMatches(a.profileId, profile)).filter((a) => {
|
|
3498
|
+
const refusal = integrationManager2.mandateRefusal?.(tool, a) ?? null;
|
|
3499
|
+
if (refusal) refusals.push(refusal);
|
|
3500
|
+
return !refusal;
|
|
3501
|
+
});
|
|
3502
|
+
return { matching, refusals };
|
|
3503
|
+
};
|
|
3504
|
+
const noEligibleMandate = (refusals) => {
|
|
3505
|
+
const reasons = [...new Set(refusals)].join(" ");
|
|
3506
|
+
return {
|
|
3507
|
+
content: [{
|
|
3508
|
+
type: "text",
|
|
3509
|
+
text: refusals.length > 0 ? `Refused: ${reasons.charAt(0).toUpperCase() + reasons.slice(1)} No ticket was requested.` : `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
|
|
3510
|
+
}],
|
|
3511
|
+
isError: true
|
|
3512
|
+
};
|
|
3513
|
+
};
|
|
3663
3514
|
const lockedGuard = (inner) => async (args) => {
|
|
3664
3515
|
if (!state2.spClient.isUnlocked()) {
|
|
3665
3516
|
return {
|
|
@@ -3692,19 +3543,8 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3692
3543
|
`${tool.originalName} declares no read governance (no static gate, no read adapter, no explicit exemption), so the gateway cannot bound what it returns. This is a manifest defect \u2014 the tool must declare how its reads are limited before it can be used.`
|
|
3693
3544
|
);
|
|
3694
3545
|
}
|
|
3695
|
-
const
|
|
3696
|
-
|
|
3697
|
-
(a) => a.complete && profileMatches(a.profileId, profile)
|
|
3698
|
-
);
|
|
3699
|
-
if (matchingAuths.length === 0) {
|
|
3700
|
-
return {
|
|
3701
|
-
content: [{
|
|
3702
|
-
type: "text",
|
|
3703
|
-
text: `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
|
|
3704
|
-
}],
|
|
3705
|
-
isError: true
|
|
3706
|
-
};
|
|
3707
|
-
}
|
|
3546
|
+
const { matching: matchingAuths, refusals } = eligibleMandates();
|
|
3547
|
+
if (matchingAuths.length === 0) return noEligibleMandate(refusals);
|
|
3708
3548
|
if (readGate.boundField) {
|
|
3709
3549
|
const permitted = matchingAuths.some(
|
|
3710
3550
|
(a) => boundsSatisfyReadGate(a.bounds ?? a.frame, readGate)
|
|
@@ -3886,19 +3726,8 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3886
3726
|
};
|
|
3887
3727
|
}
|
|
3888
3728
|
}
|
|
3889
|
-
const
|
|
3890
|
-
|
|
3891
|
-
(a) => a.complete && profileMatches(a.profileId, profile)
|
|
3892
|
-
);
|
|
3893
|
-
if (matchingAuths.length === 0) {
|
|
3894
|
-
return {
|
|
3895
|
-
content: [{
|
|
3896
|
-
type: "text",
|
|
3897
|
-
text: `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
|
|
3898
|
-
}],
|
|
3899
|
-
isError: true
|
|
3900
|
-
};
|
|
3901
|
-
}
|
|
3729
|
+
const { matching: matchingAuths, refusals } = eligibleMandates();
|
|
3730
|
+
if (matchingAuths.length === 0) return noEligibleMandate(refusals);
|
|
3902
3731
|
const errors = [];
|
|
3903
3732
|
const passers = [];
|
|
3904
3733
|
for (const candidate of matchingAuths) {
|
|
@@ -3999,7 +3828,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
3999
3828
|
};
|
|
4000
3829
|
}
|
|
4001
3830
|
}
|
|
4002
|
-
let
|
|
3831
|
+
let receiptId;
|
|
4003
3832
|
try {
|
|
4004
3833
|
const actionType = declaredActionType;
|
|
4005
3834
|
const binding = computeContentBinding(auth.profileId, tool, args, actionType);
|
|
@@ -4021,7 +3850,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4021
3850
|
// field by field rather than spread.
|
|
4022
3851
|
...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
|
|
4023
3852
|
});
|
|
4024
|
-
|
|
3853
|
+
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
4025
3854
|
await verifyTicket(state2.cache, receipt, {
|
|
4026
3855
|
action: tool.namespacedName,
|
|
4027
3856
|
executionContext: { ...execution },
|
|
@@ -4175,12 +4004,12 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4175
4004
|
execution: { ...execution },
|
|
4176
4005
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
4177
4006
|
});
|
|
4178
|
-
let outgoingArgs = shouldAttachFooter() &&
|
|
4179
|
-
if (
|
|
4007
|
+
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
|
|
4008
|
+
if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
|
|
4180
4009
|
outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
|
|
4181
|
-
if (
|
|
4010
|
+
if (receiptId) {
|
|
4182
4011
|
const begun = state2.executionJournal.begin({
|
|
4183
|
-
ticketId:
|
|
4012
|
+
ticketId: receiptId,
|
|
4184
4013
|
tool: tool.namespacedName,
|
|
4185
4014
|
argsHash: hashToolArgs(args)
|
|
4186
4015
|
});
|
|
@@ -4188,17 +4017,17 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4188
4017
|
return {
|
|
4189
4018
|
content: [{
|
|
4190
4019
|
type: "text",
|
|
4191
|
-
text: `Blocked: ticket ${
|
|
4020
|
+
text: `Blocked: ticket ${receiptId} was already used to execute ${tool.namespacedName} (state: ${begun.existing.state}). Not running it again.`
|
|
4192
4021
|
}],
|
|
4193
4022
|
isError: true
|
|
4194
4023
|
};
|
|
4195
4024
|
}
|
|
4196
4025
|
try {
|
|
4197
4026
|
const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
4198
|
-
state2.executionJournal.complete(
|
|
4027
|
+
state2.executionJournal.complete(receiptId, "done");
|
|
4199
4028
|
return result;
|
|
4200
4029
|
} catch (err) {
|
|
4201
|
-
state2.executionJournal.complete(
|
|
4030
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
4202
4031
|
throw err;
|
|
4203
4032
|
}
|
|
4204
4033
|
}
|
|
@@ -4679,7 +4508,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4679
4508
|
};
|
|
4680
4509
|
}
|
|
4681
4510
|
}
|
|
4682
|
-
let
|
|
4511
|
+
let receiptId;
|
|
4683
4512
|
let replayed = false;
|
|
4684
4513
|
try {
|
|
4685
4514
|
const binding = computeContentBinding(
|
|
@@ -4712,7 +4541,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4712
4541
|
// this machine — see the archive call below.
|
|
4713
4542
|
...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
|
|
4714
4543
|
});
|
|
4715
|
-
|
|
4544
|
+
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
4716
4545
|
replayed = idempotent;
|
|
4717
4546
|
await verifyTicket(state2.cache, receipt, {
|
|
4718
4547
|
action: proposal.tool,
|
|
@@ -4767,14 +4596,14 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4767
4596
|
isError: true
|
|
4768
4597
|
};
|
|
4769
4598
|
}
|
|
4770
|
-
if (!
|
|
4599
|
+
if (!receiptId) {
|
|
4771
4600
|
return {
|
|
4772
4601
|
text: `Proposal ${proposal.id}: the ticket carries no id \u2014 refusing to execute an action that could not be recorded.`,
|
|
4773
4602
|
isError: true
|
|
4774
4603
|
};
|
|
4775
4604
|
}
|
|
4776
4605
|
const begun = state2.executionJournal.begin({
|
|
4777
|
-
ticketId:
|
|
4606
|
+
ticketId: receiptId,
|
|
4778
4607
|
proposalId: proposal.id,
|
|
4779
4608
|
tool: proposal.tool,
|
|
4780
4609
|
argsHash: hashToolArgs(proposal.toolArgs)
|
|
@@ -4784,36 +4613,36 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4784
4613
|
const when = new Date(prior.startedAt * 1e3).toISOString();
|
|
4785
4614
|
if (prior.state === "done") {
|
|
4786
4615
|
return {
|
|
4787
|
-
text: `Proposal ${proposal.id} was already executed under ticket ${
|
|
4616
|
+
text: `Proposal ${proposal.id} was already executed under ticket ${receiptId} at ${when}. Not running it again.`
|
|
4788
4617
|
};
|
|
4789
4618
|
}
|
|
4790
4619
|
return {
|
|
4791
|
-
text: `Proposal ${proposal.id}: an execution under ticket ${
|
|
4620
|
+
text: `Proposal ${proposal.id}: an execution under ticket ${receiptId} started at ${when} (pid ${prior.pid}) and ${prior.state === "failed" ? "reported failure" : "never reported completion"}. Not re-running: the action may already have taken effect. Check the downstream system before deciding.`,
|
|
4792
4621
|
isError: true
|
|
4793
4622
|
};
|
|
4794
4623
|
}
|
|
4795
4624
|
if (replayed) {
|
|
4796
4625
|
console.error(
|
|
4797
|
-
`[Suveren MCP] Proposal ${proposal.id}: ticket ${
|
|
4626
|
+
`[Suveren MCP] Proposal ${proposal.id}: ticket ${receiptId} was replayed by the AS with no local execution record \u2014 executing once (lost-response recovery).`
|
|
4798
4627
|
);
|
|
4799
4628
|
}
|
|
4800
4629
|
try {
|
|
4801
4630
|
let outgoingArgs = proposal.toolArgs;
|
|
4802
|
-
if (discovered &&
|
|
4631
|
+
if (discovered && receiptId) {
|
|
4803
4632
|
if (shouldAttachFooter()) {
|
|
4804
|
-
outgoingArgs = appendVerificationFooter(discovered, outgoingArgs,
|
|
4633
|
+
outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
|
|
4805
4634
|
}
|
|
4806
|
-
outgoingArgs = attachReceiptId(discovered, outgoingArgs,
|
|
4635
|
+
outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
|
|
4807
4636
|
outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
|
|
4808
4637
|
}
|
|
4809
4638
|
let result;
|
|
4810
4639
|
try {
|
|
4811
4640
|
result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
|
|
4812
4641
|
} catch (err) {
|
|
4813
|
-
state2.executionJournal.complete(
|
|
4642
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
4814
4643
|
throw err;
|
|
4815
4644
|
}
|
|
4816
|
-
state2.executionJournal.complete(
|
|
4645
|
+
state2.executionJournal.complete(receiptId, "done");
|
|
4817
4646
|
state2.executionLog.record({
|
|
4818
4647
|
profileId: proposal.profileId,
|
|
4819
4648
|
path: proposal.path,
|
|
@@ -5925,6 +5754,15 @@ var IntegrationManager = class {
|
|
|
5925
5754
|
const refusal = await def?.validate?.(args);
|
|
5926
5755
|
return refusal ? `Refused: ${refusal} No ticket was requested.` : null;
|
|
5927
5756
|
}
|
|
5757
|
+
/**
|
|
5758
|
+
* Why this tool's integration refuses the held mandate `auth`, or null when
|
|
5759
|
+
* it accepts it (always null for connectors and built-ins that declare no
|
|
5760
|
+
* `mandateRefusal`). tool-proxy.ts drops refused mandates before selection.
|
|
5761
|
+
*/
|
|
5762
|
+
mandateRefusal(tool, auth) {
|
|
5763
|
+
const builtin = this.builtins.get(tool.integrationId);
|
|
5764
|
+
return builtin?.def.mandateRefusal?.(auth) ?? null;
|
|
5765
|
+
}
|
|
5928
5766
|
/**
|
|
5929
5767
|
* Proxy a tool call to a downstream MCP server (or a built-in's handler).
|
|
5930
5768
|
*/
|
|
@@ -6258,238 +6096,11 @@ function createConnectorExportRunner(config) {
|
|
|
6258
6096
|
};
|
|
6259
6097
|
}
|
|
6260
6098
|
|
|
6261
|
-
// src/lib/report/render-report.ts
|
|
6262
|
-
import { Parser } from "htmlparser2";
|
|
6263
|
-
function findElementSpans(html) {
|
|
6264
|
-
const spans = [];
|
|
6265
|
-
const seenCount = /* @__PURE__ */ new Map();
|
|
6266
|
-
const openStack = [];
|
|
6267
|
-
let parserRef;
|
|
6268
|
-
const parser = new Parser(
|
|
6269
|
-
{
|
|
6270
|
-
onparserinit(p) {
|
|
6271
|
-
parserRef = p;
|
|
6272
|
-
},
|
|
6273
|
-
onopentag(name) {
|
|
6274
|
-
if (!name.startsWith("sv-") || !parserRef) return;
|
|
6275
|
-
const n = seenCount.get(name) ?? 0;
|
|
6276
|
-
seenCount.set(name, n + 1);
|
|
6277
|
-
openStack.push({ tag: name, id: `${name}-${n}`, start: parserRef.startIndex });
|
|
6278
|
-
},
|
|
6279
|
-
onclosetag(name) {
|
|
6280
|
-
if (!name.startsWith("sv-") || !parserRef) return;
|
|
6281
|
-
const idx = [...openStack].reverse().findIndex((o) => o.tag === name);
|
|
6282
|
-
if (idx === -1) return;
|
|
6283
|
-
const realIdx = openStack.length - 1 - idx;
|
|
6284
|
-
const [open] = openStack.splice(realIdx, 1);
|
|
6285
|
-
spans.push({ id: open.id, kind: open.tag, start: open.start, end: parserRef.endIndex + 1 });
|
|
6286
|
-
}
|
|
6287
|
-
},
|
|
6288
|
-
{ decodeEntities: true, recognizeSelfClosing: true }
|
|
6289
|
-
);
|
|
6290
|
-
parser.write(html);
|
|
6291
|
-
parser.end();
|
|
6292
|
-
return spans.sort((a, b) => a.start - b.start);
|
|
6293
|
-
}
|
|
6294
|
-
function escapeHtml(value) {
|
|
6295
|
-
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6296
|
-
}
|
|
6297
|
-
function detailLink(id, ticketId) {
|
|
6298
|
-
const qs = ticketId ? `?element=${encodeURIComponent(id)}&ticket=${encodeURIComponent(ticketId)}` : `?element=${encodeURIComponent(id)}`;
|
|
6299
|
-
return `/reports${qs}`;
|
|
6300
|
-
}
|
|
6301
|
-
function badge(status, okLabel) {
|
|
6302
|
-
if (status === "verified") return `<span class="sv-badge sv-badge-ok">\u2713 ${escapeHtml(okLabel)}</span>`;
|
|
6303
|
-
if (status === "warning") return `<span class="sv-badge sv-badge-warn">\u26A0 verified with a note</span>`;
|
|
6304
|
-
return `<span class="sv-badge sv-badge-bad">\u2717 not verifiable</span>`;
|
|
6305
|
-
}
|
|
6306
|
-
function unverifiableCard(el) {
|
|
6307
|
-
return `<div class="sv-el sv-el-unverifiable" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Not verifiable</b>${badge("unverifiable", "")}</div><div class="sv-el-reason">${escapeHtml(el.reason ?? "This reference could not be checked.")}</div></div>`;
|
|
6308
|
-
}
|
|
6309
|
-
function renderTicket(el, interactive) {
|
|
6310
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6311
|
-
const d = el.data;
|
|
6312
|
-
const href = typeof d.checkUrl === "string" ? d.checkUrl : void 0;
|
|
6313
|
-
const actionLabel = typeof d.actionLabel === "string" ? d.actionLabel : formatActionLabel(d.action);
|
|
6314
|
-
const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
|
|
6315
|
-
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : escapeHtml(d.profile ?? "");
|
|
6316
|
-
const links = [
|
|
6317
|
-
href ? `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">Check on suveren.ai \u2197</a>` : "",
|
|
6318
|
-
interactive ? `<a href="${escapeHtml(detailLink(el.id))}" target="_top">Details</a>` : ""
|
|
6319
|
-
].filter(Boolean);
|
|
6320
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(actionLabel)}</b>${badge(el.status, "signature valid")}</div><div class="sv-el-row"><span>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(timeLabel)}</span></div>` + (links.length > 0 ? `<div class="sv-el-row"><span>${links.join(" \xB7 ")}</span></div>` : "") + `</div>`;
|
|
6321
|
-
}
|
|
6322
|
-
function renderApproval(el, interactive) {
|
|
6323
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6324
|
-
const d = el.data;
|
|
6325
|
-
const who = typeof d.whoLabel === "string" ? d.whoLabel : Array.isArray(d.who) ? d.who.join(", ") : "unknown";
|
|
6326
|
-
const asked = typeof d.createdAtLabel === "string" ? d.createdAtLabel : "unknown";
|
|
6327
|
-
const approved = typeof d.decidedAtLabel === "string" ? d.decidedAtLabel : "unknown";
|
|
6328
|
-
const waited = typeof d.waitLabel === "string" ? d.waitLabel : "unknown";
|
|
6329
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Approval</b>${badge(el.status, "from ticket archive")}</div><div class="sv-el-row"><span>asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(approved)} by ${escapeHtml(who)} (${escapeHtml(waited)})</span></div>` + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id, String(d.ticketId ?? "")))}" target="_top">Details</a></div>` : "") + `</div>`;
|
|
6330
|
-
}
|
|
6331
|
-
function renderMandate(el, interactive) {
|
|
6332
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6333
|
-
const d = el.data;
|
|
6334
|
-
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : String(d.profile ?? "");
|
|
6335
|
-
const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
|
|
6336
|
-
const limits = Array.isArray(d.limits) && d.limits.length > 0 ? d.limits.join(" \xB7 ") : "";
|
|
6337
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(profileLabel)} \xB7 ${escapeHtml(owners)}</b>${badge(el.status, "matches ticket")}</div>` + (limits ? `<div class="sv-el-row">${escapeHtml(limits)}${d.mode ? ` \xB7 ${escapeHtml(d.mode)}` : ""}</div>` : "") + (d.intent ? `<div class="sv-el-intent">Intent: \u201C${escapeHtml(d.intent)}\u201D</div>` : "") + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id))}" target="_top">Details</a></div>` : "") + `</div>`;
|
|
6338
|
-
}
|
|
6339
|
-
var RECORD_FIELD_ORDER = [
|
|
6340
|
-
"subject",
|
|
6341
|
-
"from_email",
|
|
6342
|
-
"from_name",
|
|
6343
|
-
"to_json",
|
|
6344
|
-
"received_at",
|
|
6345
|
-
"number",
|
|
6346
|
-
"status",
|
|
6347
|
-
"net_total",
|
|
6348
|
-
"customer_id",
|
|
6349
|
-
"causingReceiptId",
|
|
6350
|
-
"message"
|
|
6351
|
-
];
|
|
6352
|
-
var RECORD_TIME_FIELDS = /* @__PURE__ */ new Set(["received_at", "created_at", "sent_at"]);
|
|
6353
|
-
function renderRecordField(key, d) {
|
|
6354
|
-
if (key === "net_total") {
|
|
6355
|
-
const currency = typeof d.currency === "string" ? d.currency : void 0;
|
|
6356
|
-
return `<div class="sv-el-row"><span>Value</span><span>${escapeHtml(formatCurrency(d.net_total, currency))}</span></div>`;
|
|
6357
|
-
}
|
|
6358
|
-
if (RECORD_TIME_FIELDS.has(key)) {
|
|
6359
|
-
const t = parseTimestampSeconds(d[key]);
|
|
6360
|
-
const display = t !== void 0 ? formatDateTime(t) : d[key];
|
|
6361
|
-
return `<div class="sv-el-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(display)}</span></div>`;
|
|
6362
|
-
}
|
|
6363
|
-
return `<div class="sv-el-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(d[key])}</span></div>`;
|
|
6364
|
-
}
|
|
6365
|
-
function renderRecord(el) {
|
|
6366
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6367
|
-
const d = el.data;
|
|
6368
|
-
const kind = String(d.kind ?? "record");
|
|
6369
|
-
const rows = RECORD_FIELD_ORDER.filter((k) => d[k] !== void 0 && d[k] !== null && d[k] !== "").slice(0, 5).map((k) => renderRecordField(k, d)).join("");
|
|
6370
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>${escapeHtml(kind)} record</b>${badge(el.status, "in simulator database")}</div>` + rows + `</div>`;
|
|
6371
|
-
}
|
|
6372
|
-
function renderCaseTicketStep(caseElementId, node, isGoal, interactive) {
|
|
6373
|
-
const cls = isGoal ? "sv-step sv-step-goal" : "sv-step";
|
|
6374
|
-
const kind = isGoal ? "goal" : "ticket";
|
|
6375
|
-
const inner = `<div class="sv-step-k">${kind}</div><div class="sv-step-t">${escapeHtml(formatActionLabel(node.action))}</div>${formatDateTime(node.time)}`;
|
|
6376
|
-
if (!interactive) return `<div class="${cls}">${inner}</div>`;
|
|
6377
|
-
return `<a class="${cls}" href="${escapeHtml(detailLink(caseElementId, node.ticketId))}" target="_top">${inner}</a>`;
|
|
6378
|
-
}
|
|
6379
|
-
function renderCaseApprovalStep(approval) {
|
|
6380
|
-
const who = approval.whoLabel ?? (Array.isArray(approval.who) ? approval.who.join(", ") : "unknown");
|
|
6381
|
-
const asked = approval.createdAtLabel ?? "unknown";
|
|
6382
|
-
const decided = approval.decidedAtLabel ?? "unknown";
|
|
6383
|
-
const waited = approval.waitLabel ?? "unknown";
|
|
6384
|
-
return `<div class="sv-step sv-step-approval"><div class="sv-step-k">approval</div><div class="sv-step-t">Approval</div><div class="sv-step-sub">asked ${escapeHtml(asked)} \xB7 approved ${escapeHtml(decided)} by ${escapeHtml(who)} (${escapeHtml(waited)})</div></div>`;
|
|
6385
|
-
}
|
|
6386
|
-
function renderCase(el, interactive) {
|
|
6387
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6388
|
-
const d = el.data;
|
|
6389
|
-
const steps = Array.isArray(d.steps) ? d.steps : [];
|
|
6390
|
-
const goal = d.goal;
|
|
6391
|
-
const approvalsByTicket = /* @__PURE__ */ new Map();
|
|
6392
|
-
if (Array.isArray(d.approvals)) {
|
|
6393
|
-
for (const a of d.approvals) {
|
|
6394
|
-
approvalsByTicket.set(a.ticketId, a);
|
|
6395
|
-
}
|
|
6396
|
-
}
|
|
6397
|
-
const start = d.start;
|
|
6398
|
-
const startStep = start ? { time: start.time, html: `<div class="sv-step"><div class="sv-step-k">start</div><div class="sv-step-t">Email in</div>${formatDateTime(start.time)}</div>` } : void 0;
|
|
6399
|
-
const ticketNodes = [
|
|
6400
|
-
...steps.map((s) => ({ node: s, isGoal: false })),
|
|
6401
|
-
...goal ? [{ node: goal, isGoal: true }] : []
|
|
6402
|
-
];
|
|
6403
|
-
const items = [];
|
|
6404
|
-
if (startStep) items.push(startStep);
|
|
6405
|
-
for (const { node, isGoal } of ticketNodes) {
|
|
6406
|
-
const approval = approvalsByTicket.get(node.ticketId);
|
|
6407
|
-
if (approval) {
|
|
6408
|
-
const approvalTime = Math.min(approval.decidedAt ?? approval.createdAt ?? node.time, node.time);
|
|
6409
|
-
items.push({ time: approvalTime, html: renderCaseApprovalStep(approval) });
|
|
6410
|
-
}
|
|
6411
|
-
items.push({ time: node.time, html: renderCaseTicketStep(el.id, node, isGoal, interactive) });
|
|
6412
|
-
}
|
|
6413
|
-
items.sort((a, b) => a.time - b.time);
|
|
6414
|
-
const stepHtml = items.map((i) => i.html).join("");
|
|
6415
|
-
return `<div class="sv-el sv-el-${el.status}" data-sv-id="${escapeHtml(el.id)}"><div class="sv-el-row"><b>Case ${escapeHtml(d.caseId)} \xB7 ${formatDuration(d.totalDurationSeconds)}</b>${badge(el.status, "assembled by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + `<div class="sv-tl">${stepHtml}</div>` + (interactive ? `<div class="sv-el-row"><a href="${escapeHtml(detailLink(el.id))}" target="_top">Case details</a></div>` : "") + `</div>`;
|
|
6416
|
-
}
|
|
6417
|
-
function renderMetric(el) {
|
|
6418
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6419
|
-
const d = el.data;
|
|
6420
|
-
const kind = String(d.kind ?? "");
|
|
6421
|
-
const label = METRIC_LABELS[kind] ?? kind;
|
|
6422
|
-
const display = formatMetricValue(kind, d.value);
|
|
6423
|
-
return `<div class="sv-el sv-el-${el.status} sv-el-metric" data-sv-id="${escapeHtml(el.id)}"><div class="sv-big">${escapeHtml(display)}</div><div class="sv-el-row"><span>${escapeHtml(label)} (${escapeHtml(d.caseCount ?? 0)} case${d.caseCount === 1 ? "" : "s"})</span>${badge(el.status, "computed by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + `</div>`;
|
|
6424
|
-
}
|
|
6425
|
-
function renderElement(el, interactive) {
|
|
6426
|
-
switch (el.kind) {
|
|
6427
|
-
case "sv-ticket":
|
|
6428
|
-
return renderTicket(el, interactive);
|
|
6429
|
-
case "sv-approval":
|
|
6430
|
-
return renderApproval(el, interactive);
|
|
6431
|
-
case "sv-mandate":
|
|
6432
|
-
return renderMandate(el, interactive);
|
|
6433
|
-
case "sv-record":
|
|
6434
|
-
return renderRecord(el);
|
|
6435
|
-
case "sv-case":
|
|
6436
|
-
return renderCase(el, interactive);
|
|
6437
|
-
case "sv-metric":
|
|
6438
|
-
return renderMetric(el);
|
|
6439
|
-
default:
|
|
6440
|
-
return unverifiableCard(el);
|
|
6441
|
-
}
|
|
6442
|
-
}
|
|
6443
|
-
var DRAWN_ELEMENT_STYLES = `
|
|
6444
|
-
.sv-el { border:1px solid #e5e5e5; border-radius:10px; padding:10px 12px; margin:10px 0; font:13px/1.5 system-ui, sans-serif; background:#fff; }
|
|
6445
|
-
.sv-el-unverifiable { outline:2px solid #b91c1c; outline-offset:2px; }
|
|
6446
|
-
.sv-el-verified, .sv-el-warning { outline:2px solid #15803d; outline-offset:2px; }
|
|
6447
|
-
.sv-el-row { display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; margin:2px 0; }
|
|
6448
|
-
.sv-el-reason, .sv-el-warn { color:#b45309; font-size:12px; margin-top:4px; }
|
|
6449
|
-
.sv-el-intent { color:#666; font-size:12px; margin-top:4px; }
|
|
6450
|
-
.sv-badge { display:inline-flex; gap:4px; font-size:10.5px; font-weight:600; padding:1px 7px; border-radius:999px; white-space:nowrap; }
|
|
6451
|
-
.sv-badge-ok { color:#15803d; background:#ecfdf3; }
|
|
6452
|
-
.sv-badge-warn { color:#b45309; background:#fff7e6; }
|
|
6453
|
-
.sv-badge-bad { color:#b91c1c; background:#fdecea; }
|
|
6454
|
-
.sv-big { font-size:24px; font-weight:700; }
|
|
6455
|
-
.sv-tl { display:flex; flex-wrap:wrap; gap:10px 16px; padding:6px 0; }
|
|
6456
|
-
.sv-step { min-width:100px; border:1px solid #e5e5e5; border-radius:8px; padding:6px 8px; font-size:11.5px; text-decoration:none; color:inherit; flex-shrink:0; }
|
|
6457
|
-
.sv-step-k { font-size:9.5px; text-transform:uppercase; color:#666; }
|
|
6458
|
-
.sv-step-t { font-weight:600; font-size:12px; }
|
|
6459
|
-
.sv-step-sub { color:#666; white-space:normal; }
|
|
6460
|
-
/* The approval step's text ("asked ... approved ... by ... (duration)") is
|
|
6461
|
-
* longer than a ticket step's \u2014 without a width to wrap against, a
|
|
6462
|
-
* flex item with flex-shrink:0 takes its one-line content width instead of
|
|
6463
|
-
* wrapping, which reads as the text being cut off at the container's visible
|
|
6464
|
-
* edge (polish 2026-10-05, second pass). */
|
|
6465
|
-
.sv-step-approval { max-width:240px; white-space:normal; }
|
|
6466
|
-
@media (max-width: 480px) {
|
|
6467
|
-
.sv-tl { flex-direction:column; overflow-x:visible; }
|
|
6468
|
-
.sv-step { min-width:0; }
|
|
6469
|
-
.sv-step-approval { max-width:none; }
|
|
6470
|
-
.sv-el-row { flex-direction:column; gap:2px; }
|
|
6471
|
-
}
|
|
6472
|
-
`;
|
|
6473
|
-
function renderReportHtml(html, elements, interactive = true) {
|
|
6474
|
-
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
6475
|
-
const spans = findElementSpans(html);
|
|
6476
|
-
let out = "";
|
|
6477
|
-
let cursor = 0;
|
|
6478
|
-
for (const span of spans) {
|
|
6479
|
-
out += html.slice(cursor, span.start);
|
|
6480
|
-
const el = byId.get(span.id) ?? { id: span.id, kind: span.kind, attrs: {}, status: "unverifiable", reason: "No verification result for this element." };
|
|
6481
|
-
out += renderElement(el, interactive);
|
|
6482
|
-
cursor = span.end;
|
|
6483
|
-
}
|
|
6484
|
-
out += html.slice(cursor);
|
|
6485
|
-
const styleTag = `<style data-sv-drawn-styles="1">${DRAWN_ELEMENT_STYLES}</style>`;
|
|
6486
|
-
if (/<body[^>]*>/i.test(out)) {
|
|
6487
|
-
return out.replace(/<body([^>]*)>/i, (m) => `${m}${styleTag}`);
|
|
6488
|
-
}
|
|
6489
|
-
return styleTag + out;
|
|
6490
|
-
}
|
|
6491
|
-
|
|
6492
6099
|
// src/lib/report/ticket-details.ts
|
|
6100
|
+
function withFields(fact, build) {
|
|
6101
|
+
if (fact.status !== "verified" || !fact.data) return fact;
|
|
6102
|
+
return { ...fact, data: { ...fact.data, fields: build(fact.data) } };
|
|
6103
|
+
}
|
|
6493
6104
|
async function buildTicketDetails(archive, ticketIds) {
|
|
6494
6105
|
const out = {};
|
|
6495
6106
|
for (const id of ticketIds) {
|
|
@@ -6498,118 +6109,170 @@ async function buildTicketDetails(archive, ticketIds) {
|
|
|
6498
6109
|
resolveApprovalElement(archive, id),
|
|
6499
6110
|
resolveMandateElement(archive, id)
|
|
6500
6111
|
]);
|
|
6501
|
-
out[id] = {
|
|
6112
|
+
out[id] = {
|
|
6113
|
+
ticket: withFields(ticket, ticketFields),
|
|
6114
|
+
approval: withFields(approval, approvalFields),
|
|
6115
|
+
mandate: withFields(mandate, mandateFields)
|
|
6116
|
+
};
|
|
6502
6117
|
}
|
|
6503
6118
|
return out;
|
|
6504
6119
|
}
|
|
6505
6120
|
|
|
6506
|
-
// src/lib/report/
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
const
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6121
|
+
// src/lib/report/window.ts
|
|
6122
|
+
import { getProfile as getProfile6 } from "@hap/core";
|
|
6123
|
+
var REPORT_AGE_FIELD = "read_age_days";
|
|
6124
|
+
var REPORTING_PROFILE = "reporting";
|
|
6125
|
+
var END_SKEW_SECONDS = 120;
|
|
6126
|
+
var OLD_PROFILE_REFUSAL = "this reporting mandate is from an older profile version \u2014 create a new reporting mandate (reporting@0.2).";
|
|
6127
|
+
var DAY = 86400;
|
|
6128
|
+
function boundsOf(a) {
|
|
6129
|
+
return a.bounds ?? a.frame;
|
|
6130
|
+
}
|
|
6131
|
+
function windowLabel(start, days, loadedAt) {
|
|
6132
|
+
const since = `since ${formatDateTime(start)}`;
|
|
6133
|
+
if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
|
|
6134
|
+
return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
|
|
6135
|
+
}
|
|
6136
|
+
var defaultLookup = (id) => getProfile6(id);
|
|
6137
|
+
function reportingMandateRefusal(m, lookup = defaultLookup) {
|
|
6138
|
+
const schema = lookup(m.profileId)?.boundsSchema;
|
|
6139
|
+
const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
|
|
6140
|
+
if (!field) return OLD_PROFILE_REFUSAL;
|
|
6141
|
+
if (maxReadAgeDays([boundsOf(m)], field) === null) {
|
|
6142
|
+
return `the reporting mandate does not set ${field} (the reporting window), so nothing can be shown \u2014 create a new reporting mandate.`;
|
|
6521
6143
|
}
|
|
6144
|
+
return void 0;
|
|
6145
|
+
}
|
|
6146
|
+
function resolveReportWindow(input) {
|
|
6147
|
+
const lookup = input.lookupProfile ?? defaultLookup;
|
|
6148
|
+
const mandates = input.authorizations.filter((a) => a.complete && profileMatches(a.profileId, REPORTING_PROFILE));
|
|
6149
|
+
if (mandates.length === 0) {
|
|
6150
|
+
return { ok: false, reason: "No active reporting mandate \u2014 the reporting window comes from it, so no evidence can be shown." };
|
|
6151
|
+
}
|
|
6152
|
+
const candidates = [];
|
|
6153
|
+
const refusals = [];
|
|
6154
|
+
for (const m of mandates) {
|
|
6155
|
+
const refusal = reportingMandateRefusal(m, lookup);
|
|
6156
|
+
if (refusal) {
|
|
6157
|
+
refusals.push(refusal);
|
|
6158
|
+
continue;
|
|
6159
|
+
}
|
|
6160
|
+
const schema = lookup(m.profileId)?.boundsSchema;
|
|
6161
|
+
const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
|
|
6162
|
+
const declared = maxReadAgeDays([boundsOf(m)], field);
|
|
6163
|
+
const def = schema?.fields?.[field] ?? {};
|
|
6164
|
+
const maximum = typeof def.maximum === "number" && Number.isFinite(def.maximum) ? def.maximum : null;
|
|
6165
|
+
const days = Math.max(0, maximum !== null ? Math.min(declared, maximum) : declared);
|
|
6166
|
+
let start = input.now - days * DAY;
|
|
6167
|
+
let clampedBy = null;
|
|
6168
|
+
if (input.simulation && input.loadedAt !== null && input.loadedAt > start) {
|
|
6169
|
+
start = input.loadedAt;
|
|
6170
|
+
clampedBy = input.loadedAt;
|
|
6171
|
+
}
|
|
6172
|
+
candidates.push({ start, days, loadedAt: clampedBy });
|
|
6173
|
+
}
|
|
6174
|
+
if (candidates.length === 0) {
|
|
6175
|
+
const reason = [...new Set(refusals)].join(" ");
|
|
6176
|
+
return { ok: false, reason: reason.charAt(0).toUpperCase() + reason.slice(1) };
|
|
6177
|
+
}
|
|
6178
|
+
const best = candidates.reduce((a, b) => b.start < a.start ? b : a);
|
|
6522
6179
|
return {
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6180
|
+
ok: true,
|
|
6181
|
+
window: {
|
|
6182
|
+
start: best.start,
|
|
6183
|
+
end: input.now,
|
|
6184
|
+
days: best.days,
|
|
6185
|
+
loadedAt: best.loadedAt,
|
|
6186
|
+
label: windowLabel(best.start, best.days, best.loadedAt)
|
|
6187
|
+
}
|
|
6188
|
+
};
|
|
6189
|
+
}
|
|
6190
|
+
function isInWindow(time, window) {
|
|
6191
|
+
return time !== void 0 && Number.isFinite(time) && time >= window.start && time <= window.end + END_SKEW_SECONDS;
|
|
6192
|
+
}
|
|
6193
|
+
function ticketTime(entry) {
|
|
6194
|
+
const t = entry.receipt.timestamp;
|
|
6195
|
+
return typeof t === "number" && Number.isFinite(t) ? t : void 0;
|
|
6196
|
+
}
|
|
6197
|
+
var OUTSIDE_WINDOW_PREFIX = "not verifiable \u2014 outside the reporting window";
|
|
6198
|
+
function windowArchive(base, window) {
|
|
6199
|
+
const inWindow = (r) => isInWindow(ticketTime(r), window);
|
|
6200
|
+
return {
|
|
6201
|
+
getReceipts() {
|
|
6202
|
+
return base.getReceipts().filter(inWindow);
|
|
6203
|
+
},
|
|
6204
|
+
getAuthorizations() {
|
|
6205
|
+
const used = new Set(base.getReceipts().filter(inWindow).map((r) => r.authorizationId));
|
|
6206
|
+
return base.getAuthorizations().filter((a) => used.has(a.authorizationId));
|
|
6207
|
+
},
|
|
6208
|
+
outsideWindowReason(ticketId) {
|
|
6209
|
+
const entry = base.getReceipts().find((r) => r.receipt.id === ticketId);
|
|
6210
|
+
if (!entry || inWindow(entry)) return void 0;
|
|
6211
|
+
return `${OUTSIDE_WINDOW_PREFIX} (${window.label}).`;
|
|
6212
|
+
}
|
|
6213
|
+
};
|
|
6214
|
+
}
|
|
6215
|
+
var RECORD_TIME_COLUMNS = ["received_at", "created_at", "at", "date"];
|
|
6216
|
+
function recordTime(row, loadedAt, ticketTimes) {
|
|
6217
|
+
if (!row || typeof row !== "object") return void 0;
|
|
6218
|
+
const r = row;
|
|
6219
|
+
if (typeof r.receipt_id === "string" && ticketTimes.has(r.receipt_id)) return ticketTimes.get(r.receipt_id);
|
|
6220
|
+
for (const col of RECORD_TIME_COLUMNS) {
|
|
6221
|
+
const t = parseTimestampSeconds(r[col]);
|
|
6222
|
+
if (t !== void 0) {
|
|
6223
|
+
return loadedAt !== null ? Math.max(t, loadedAt) : t;
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
return void 0;
|
|
6227
|
+
}
|
|
6228
|
+
function windowExport(exported, window, loadedAt, ticketTimes = /* @__PURE__ */ new Map()) {
|
|
6229
|
+
if (!exported || typeof exported !== "object" || Array.isArray(exported)) return exported;
|
|
6230
|
+
const out = {};
|
|
6231
|
+
for (const [key, value] of Object.entries(exported)) {
|
|
6232
|
+
out[key] = Array.isArray(value) ? value.filter((row) => isInWindow(recordTime(row, loadedAt, ticketTimes), window)) : value;
|
|
6233
|
+
}
|
|
6234
|
+
return out;
|
|
6235
|
+
}
|
|
6236
|
+
async function readLoadedAt(runExport) {
|
|
6237
|
+
try {
|
|
6238
|
+
const exp = await runExport("email");
|
|
6239
|
+
if (!isEmailExport(exp)) return null;
|
|
6240
|
+
return parseTimestampSeconds(exp.simulation_load?.loaded_at) ?? null;
|
|
6241
|
+
} catch {
|
|
6242
|
+
return null;
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
async function scopeReportSources(base, opts) {
|
|
6246
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
6247
|
+
const hasReporting = opts.authorizations.some((a) => a.complete && profileMatches(a.profileId, REPORTING_PROFILE));
|
|
6248
|
+
const loadedAt = hasReporting && opts.simulation ? await readLoadedAt(base.runExport) : null;
|
|
6249
|
+
const resolved = resolveReportWindow({
|
|
6250
|
+
authorizations: opts.authorizations,
|
|
6251
|
+
simulation: opts.simulation,
|
|
6252
|
+
loadedAt,
|
|
6253
|
+
now,
|
|
6254
|
+
lookupProfile: opts.lookupProfile
|
|
6255
|
+
});
|
|
6256
|
+
if (!resolved.ok) return resolved;
|
|
6257
|
+
return { ok: true, window: resolved.window, sources: scopedSources(base, resolved.window, loadedAt) };
|
|
6258
|
+
}
|
|
6259
|
+
function scopedSources(base, window, loadedAt) {
|
|
6260
|
+
const runExport = async (system) => {
|
|
6261
|
+
const ticketTimes = /* @__PURE__ */ new Map();
|
|
6262
|
+
for (const r of base.archive.getReceipts()) {
|
|
6263
|
+
const id = r.receipt.id;
|
|
6264
|
+
const t = ticketTime(r);
|
|
6265
|
+
if (typeof id === "string" && t !== void 0) ticketTimes.set(id, t);
|
|
6266
|
+
}
|
|
6267
|
+
return windowExport(await base.runExport(system), window, loadedAt, ticketTimes);
|
|
6533
6268
|
};
|
|
6269
|
+
return { archive: windowArchive(base.archive, window), runExport, window };
|
|
6534
6270
|
}
|
|
6535
|
-
function
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
const periodNote = coverage.periodStart === null ? `<p class="sv-export-note">${coverage.emailExportError ? "Test period start unknown \u2014 the email simulator could not be read. All saved tickets were counted." : "Test period start unknown \u2014 all saved tickets were counted."}</p>` : "";
|
|
6541
|
-
const missingParts = [];
|
|
6542
|
-
if (coverage.missingCases.length > 0) missingParts.push(`cases ${coverage.missingCases.map(escapeHtml).join(", ")}`);
|
|
6543
|
-
if (coverage.ticketsNotReferenced.length > 0) missingParts.push(`${coverage.ticketsNotReferenced.length} ticket${coverage.ticketsNotReferenced.length === 1 ? "" : "s"}`);
|
|
6544
|
-
const missingLine = missingParts.length > 0 ? `<p class="sv-export-missing">Not in the report: ${missingParts.join(" \xB7 ")}</p>` : "";
|
|
6545
|
-
const checkedValues = proof.verifiedValues.length > 0 ? `<div class="sv-export-card"><div class="sv-export-card-title">Checked values</div>${proof.verifiedValues.map((v) => `<div class="sv-export-row"><span>${escapeHtml(v.summary)}</span></div>`).join("")}</div>` : "";
|
|
6546
|
-
return `<aside class="sv-export-side"><div class="sv-export-card"><div class="sv-export-card-title">Proof</div><div class="sv-export-row"><span>Tickets referenced</span><b>${proof.ticketsReferenced.length}</b></div><div class="sv-export-row"><span>Signatures valid</span><b>${escapeHtml(countLine(proof.signaturesValid))}</b></div><div class="sv-export-row"><span>Records checked</span><b>${escapeHtml(countLine(proof.recordsChecked))}</b></div><div class="sv-export-row"><span>Not verifiable</span><b>${proof.unverifiableCount}</b></div></div><div class="sv-export-card"><div class="sv-export-card-title">Coverage</div><div class="sv-export-row"><span>Cases</span><b>${casesLine}</b></div><div class="sv-export-row"><span>Tickets in test period</span><b>${coverage.ticketsReferenced.length} of ${coverage.ticketsInPeriod.length}</b></div>` + periodNote + missingLine + `</div>` + checkedValues + `</aside>`;
|
|
6547
|
-
}
|
|
6548
|
-
function ensureHtmlDocument(html) {
|
|
6549
|
-
if (/<html[^>]*>/i.test(html)) return html;
|
|
6550
|
-
return `<!doctype html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
|
|
6551
|
-
}
|
|
6552
|
-
function injectIntoHead(html, insert) {
|
|
6553
|
-
if (/<head[^>]*>/i.test(html)) return html.replace(/<head([^>]*)>/i, (m) => `${m}${insert}`);
|
|
6554
|
-
if (/<html[^>]*>/i.test(html)) return html.replace(/<html([^>]*)>/i, (m) => `${m}<head>${insert}</head>`);
|
|
6555
|
-
return `<head>${insert}</head>${html}`;
|
|
6556
|
-
}
|
|
6557
|
-
function injectAfterBodyOpen(html, insert) {
|
|
6558
|
-
if (/<body[^>]*>/i.test(html)) return html.replace(/<body([^>]*)>/i, (m) => `${m}${insert}`);
|
|
6559
|
-
return `${insert}${html}`;
|
|
6560
|
-
}
|
|
6561
|
-
function injectBeforeBodyClose(html, insert) {
|
|
6562
|
-
if (/<\/body>/i.test(html)) return html.replace(/<\/body>/i, `${insert}</body>`);
|
|
6563
|
-
return `${html}${insert}`;
|
|
6564
|
-
}
|
|
6565
|
-
var EXPORT_PAGE_STYLES = `
|
|
6566
|
-
.sv-export-header { font:13px/1.5 system-ui, sans-serif; background:#f6f6f4; border-bottom:1px solid #e5e5e5; padding:14px 20px; }
|
|
6567
|
-
.sv-export-meta { margin:0 0 6px 0; font-weight:600; }
|
|
6568
|
-
.sv-export-howto { margin:0; color:#444; }
|
|
6569
|
-
.sv-export-howto code { background:#eee; border-radius:4px; padding:1px 5px; font-size:12px; }
|
|
6570
|
-
.sv-export-layout { display:flex; gap:20px; align-items:flex-start; padding:20px; font:13px/1.5 system-ui, sans-serif; }
|
|
6571
|
-
.sv-export-main { flex:1 1 auto; min-width:0; }
|
|
6572
|
-
.sv-export-side { flex:0 0 260px; }
|
|
6573
|
-
.sv-export-card { border:1px solid #e5e5e5; border-radius:10px; padding:10px 12px; margin-bottom:12px; background:#fff; }
|
|
6574
|
-
.sv-export-card-title { font-weight:700; margin-bottom:6px; }
|
|
6575
|
-
.sv-export-row { display:flex; justify-content:space-between; gap:10px; margin:2px 0; }
|
|
6576
|
-
.sv-export-note, .sv-export-missing { color:#b45309; font-size:12px; margin:4px 0 0 0; }
|
|
6577
|
-
@media (max-width: 720px) {
|
|
6578
|
-
.sv-export-layout { flex-direction:column; }
|
|
6579
|
-
.sv-export-side { flex:1 1 auto; width:100%; }
|
|
6580
|
-
}
|
|
6581
|
-
`;
|
|
6582
|
-
function formatDateTimeWithYear(value) {
|
|
6583
|
-
const base = formatDateTime(value);
|
|
6584
|
-
const year = new Date(value * 1e3).getFullYear();
|
|
6585
|
-
return base.replace(",", ` ${year},`);
|
|
6586
|
-
}
|
|
6587
|
-
function utcOffsetLabel(value) {
|
|
6588
|
-
const offsetMin = -new Date(value * 1e3).getTimezoneOffset();
|
|
6589
|
-
const sign = offsetMin >= 0 ? "+" : "-";
|
|
6590
|
-
const abs = Math.abs(offsetMin);
|
|
6591
|
-
const hh = Math.floor(abs / 60);
|
|
6592
|
-
const mm = abs % 60;
|
|
6593
|
-
return `UTC${sign}${hh}${mm ? ":" + String(mm).padStart(2, "0") : ""}`;
|
|
6594
|
-
}
|
|
6595
|
-
function buildExportDocument(params) {
|
|
6596
|
-
const { bundle, renderedHtml } = params;
|
|
6597
|
-
const exportedDate = new Date(bundle.exportedAt * 1e3).toISOString().slice(0, 10);
|
|
6598
|
-
const exportedLabel = formatDateTimeWithYear(bundle.exportedAt);
|
|
6599
|
-
const checkedLabel = formatDateTime(bundle.report.checkedAt);
|
|
6600
|
-
const tzLabel = utcOffsetLabel(bundle.exportedAt);
|
|
6601
|
-
const headerHtml = `<div class="sv-export-header"><p class="sv-export-meta">Suveren Gateway ${escapeHtml(bundle.gatewayVersion)} \u2014 exported ${escapeHtml(exportedLabel)} · checked ${escapeHtml(checkedLabel)} (times in ${escapeHtml(tzLabel)})</p><p class="sv-export-howto">How to check this report: each ticket below links to its public record on suveren.ai ("Check on suveren.ai \u2197"). To verify this entire file offline (including every signature), run <code>suveren-gateway verify-report ${escapeHtml(suggestedFilename(bundle))}</code> from a terminal with the Suveren gateway CLI installed.</p></div>`;
|
|
6602
|
-
const proofScriptJson = JSON.stringify(bundle).replace(/<\//g, "<\\/");
|
|
6603
|
-
const proofScript = `<script type="application/json" id="suveren-proof">${proofScriptJson}</script>`;
|
|
6604
|
-
let doc = ensureHtmlDocument(renderedHtml);
|
|
6605
|
-
doc = injectIntoHead(doc, `<meta charset="utf-8"><title>Suveren report export \u2014 ${escapeHtml(exportedDate)}</title><style>${DRAWN_ELEMENT_STYLES}${EXPORT_PAGE_STYLES}</style>`);
|
|
6606
|
-
doc = injectAfterBodyOpen(doc, headerHtml + `<div class="sv-export-layout"><div class="sv-export-main">`);
|
|
6607
|
-
doc = injectBeforeBodyClose(doc, `</div>${renderProofCoveragePanels(bundle.proof, bundle.coverage)}</div>${proofScript}`);
|
|
6608
|
-
return doc;
|
|
6609
|
-
}
|
|
6610
|
-
function suggestedFilename(bundle) {
|
|
6611
|
-
const date = new Date(bundle.exportedAt * 1e3).toISOString().slice(0, 10);
|
|
6612
|
-
return `suveren-report-${date}.html`;
|
|
6271
|
+
async function scopeReportSourcesToStoredWindow(base, stored, opts) {
|
|
6272
|
+
const now = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
6273
|
+
const loadedAt = opts.simulation ? await readLoadedAt(base.runExport) : null;
|
|
6274
|
+
const window = { start: stored.start, end: now, days: stored.days, loadedAt: null, label: stored.label };
|
|
6275
|
+
return { ok: true, window, sources: scopedSources(base, window, loadedAt) };
|
|
6613
6276
|
}
|
|
6614
6277
|
|
|
6615
6278
|
// src/lib/profile-loader.ts
|
|
@@ -6680,10 +6343,19 @@ var CRM_RECORD_KINDS = ["contacts", "deals", "tasks", "activities", "changes", "
|
|
|
6680
6343
|
function errorText(text) {
|
|
6681
6344
|
return { content: [{ type: "text", text }], isError: true };
|
|
6682
6345
|
}
|
|
6346
|
+
function scope(deps) {
|
|
6347
|
+
return scopeReportSources(deps.reportSources, {
|
|
6348
|
+
authorizations: deps.state.getEnrichedAuthorizations(),
|
|
6349
|
+
simulation: isSimulationMode()
|
|
6350
|
+
});
|
|
6351
|
+
}
|
|
6352
|
+
function windowInfo(w) {
|
|
6353
|
+
return { since: w.label, start: w.start, end: w.end };
|
|
6354
|
+
}
|
|
6683
6355
|
function listTicketsTool(deps) {
|
|
6684
6356
|
return {
|
|
6685
6357
|
name: "list_tickets",
|
|
6686
|
-
description: "List tickets (signed executions) from the local receipt archive: id, time, action, action type, profile,
|
|
6358
|
+
description: "List tickets (signed executions) from the local receipt archive inside your reporting window (the number of days back your reporting mandate allows): id, time, action, action type, profile, the limits used, and whether an approval was archived for it. Older tickets are not shown and cannot be used in the report. Optionally narrow further by since/until (unix seconds). Use the ids with get_ticket for full detail, and with <sv-ticket>, <sv-approval>, <sv-mandate> and <sv-case> in the report.",
|
|
6687
6359
|
inputSchema: {
|
|
6688
6360
|
type: "object",
|
|
6689
6361
|
properties: {
|
|
@@ -6692,30 +6364,22 @@ function listTicketsTool(deps) {
|
|
|
6692
6364
|
}
|
|
6693
6365
|
},
|
|
6694
6366
|
handler: async (args) => {
|
|
6367
|
+
const scoped = await scope(deps);
|
|
6368
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6695
6369
|
const since = typeof args.since === "number" ? args.since : void 0;
|
|
6696
6370
|
const until = typeof args.until === "number" ? args.until : void 0;
|
|
6697
|
-
const tickets =
|
|
6371
|
+
const tickets = scoped.sources.archive.getReceipts().map((entry) => {
|
|
6698
6372
|
const r = entry.receipt;
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
time,
|
|
6703
|
-
action: r.action,
|
|
6704
|
-
actionType: r.actionType ?? null,
|
|
6705
|
-
profile: r.profileId,
|
|
6706
|
-
authorizationId: r.authorizationId,
|
|
6707
|
-
limitsUsed: r.limits ?? r.executionContext ?? {},
|
|
6708
|
-
hasApproval: Boolean(entry.proposal)
|
|
6709
|
-
};
|
|
6710
|
-
}).filter((t) => t.id !== "").filter((t) => since === void 0 || t.time !== void 0 && t.time >= since).filter((t) => until === void 0 || t.time !== void 0 && t.time <= until).sort((a, b) => (a.time ?? 0) - (b.time ?? 0));
|
|
6711
|
-
return builtinText(JSON.stringify({ tickets }, null, 2));
|
|
6373
|
+
return agentTicketRow(r, Boolean(entry.proposal), profileShortLabel(typeof r.profileId === "string" ? r.profileId : void 0));
|
|
6374
|
+
}).filter((t) => t.id !== "").filter((t) => since === void 0 || typeof t.time === "number" && t.time >= since).filter((t) => until === void 0 || typeof t.time === "number" && t.time <= until).sort((a, b) => (a.time ?? 0) - (b.time ?? 0));
|
|
6375
|
+
return builtinText(JSON.stringify({ window: windowInfo(scoped.window), tickets }, null, 2));
|
|
6712
6376
|
}
|
|
6713
6377
|
};
|
|
6714
6378
|
}
|
|
6715
6379
|
function getTicketTool(deps) {
|
|
6716
6380
|
return {
|
|
6717
6381
|
name: "get_ticket",
|
|
6718
|
-
description: 'Full detail for one ticket: the ticket itself (action, time, limits used, public-check link), its archived approval (who approved it, when it was requested and decided, how long it waited), and the mandate it ran under (limits, commitment mode, intent). Each part reports "unverifiable" on its own if it could not be checked \u2014 e.g. an automatic-mode ticket has no approval. Use this to decide what a ticket actually proves before referencing it in the report.',
|
|
6382
|
+
description: 'Full detail for one ticket: the ticket itself (action, time, limits used, public-check link), its archived approval (who approved it, when it was requested and decided, how long it waited), and the mandate it ran under (limits, commitment mode, intent). Each part reports "unverifiable" on its own if it could not be checked \u2014 e.g. an automatic-mode ticket has no approval. Only tickets inside your reporting window can be opened. Use this to decide what a ticket actually proves before referencing it in the report.',
|
|
6719
6383
|
inputSchema: {
|
|
6720
6384
|
type: "object",
|
|
6721
6385
|
properties: { id: { type: "string", description: "The ticket id, as listed by list_tickets." } },
|
|
@@ -6724,15 +6388,17 @@ function getTicketTool(deps) {
|
|
|
6724
6388
|
handler: async (args) => {
|
|
6725
6389
|
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6726
6390
|
if (!id) return errorText("id is required.");
|
|
6727
|
-
const
|
|
6391
|
+
const scoped = await scope(deps);
|
|
6392
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6393
|
+
const details = await buildTicketDetails(scoped.sources.archive, [id]);
|
|
6728
6394
|
const detail = details[id];
|
|
6729
6395
|
if (!detail || detail.ticket.status !== "verified") {
|
|
6730
6396
|
return errorText(detail?.ticket.reason ?? `No ticket "${id}" in the local archive.`);
|
|
6731
6397
|
}
|
|
6732
6398
|
return builtinText(JSON.stringify({
|
|
6733
|
-
ticket: detail.ticket.data,
|
|
6734
|
-
approval: detail.approval.status === "verified" ? { verified: true, ...detail.approval.data } : { verified: false, reason: detail.approval.reason },
|
|
6735
|
-
mandate: detail.mandate.status === "verified" ? { verified: true, ...detail.mandate.data } : { verified: false, reason: detail.mandate.reason }
|
|
6399
|
+
ticket: agentTicket(detail.ticket.data),
|
|
6400
|
+
approval: detail.approval.status === "verified" ? { verified: true, ...agentApproval(detail.approval.data) } : { verified: false, reason: detail.approval.reason },
|
|
6401
|
+
mandate: detail.mandate.status === "verified" ? { verified: true, ...agentMandate(detail.mandate.data) } : { verified: false, reason: detail.mandate.reason }
|
|
6736
6402
|
}, null, 2));
|
|
6737
6403
|
}
|
|
6738
6404
|
};
|
|
@@ -6740,12 +6406,14 @@ function getTicketTool(deps) {
|
|
|
6740
6406
|
function listCasesTool(deps) {
|
|
6741
6407
|
return {
|
|
6742
6408
|
name: "list_cases",
|
|
6743
|
-
description: "Loaded business cases from the connected email system, plus every sent mail \u2014 so you can define sv-case elements without guessing ids. Returns: when the test data was loaded (testDataLoadedAt); cases (caseId, the starting inbox message id, sender, subject, received time); and sent mails (id, inReplyTo, the receiptId of the ticket that sent it, subject, sentAt) so you can confirm which ticket closed which case. Never includes the team's own reference replies \u2014 those are not part of this report.",
|
|
6409
|
+
description: "Loaded business cases from the connected email system, plus every sent mail \u2014 so you can define sv-case elements without guessing ids. Returns: when the test data was loaded (testDataLoadedAt); cases (caseId, the starting inbox message id, sender, subject, received time); and sent mails (id, inReplyTo, the receiptId of the ticket that sent it, subject, sentAt) so you can confirm which ticket closed which case. Only records inside your reporting window. Never includes the team's own reference replies \u2014 those are not part of this report.",
|
|
6744
6410
|
inputSchema: { type: "object", properties: {} },
|
|
6745
6411
|
handler: async () => {
|
|
6412
|
+
const scoped = await scope(deps);
|
|
6413
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6746
6414
|
let exported;
|
|
6747
6415
|
try {
|
|
6748
|
-
exported = await
|
|
6416
|
+
exported = await scoped.sources.runExport("email");
|
|
6749
6417
|
} catch (err) {
|
|
6750
6418
|
return errorText(`Could not read email records: ${err instanceof Error ? err.message : String(err)}`);
|
|
6751
6419
|
}
|
|
@@ -6766,6 +6434,7 @@ function listCasesTool(deps) {
|
|
|
6766
6434
|
sentAt: m.received_at
|
|
6767
6435
|
}));
|
|
6768
6436
|
return builtinText(JSON.stringify({
|
|
6437
|
+
window: windowInfo(scoped.window),
|
|
6769
6438
|
testDataLoadedAt: exp.simulation_load?.loaded_at ?? null,
|
|
6770
6439
|
cases,
|
|
6771
6440
|
sent
|
|
@@ -6781,7 +6450,7 @@ function recordKindsFor(system) {
|
|
|
6781
6450
|
function getRecordsTool(deps) {
|
|
6782
6451
|
return {
|
|
6783
6452
|
name: "get_records",
|
|
6784
|
-
description: `Rows from one connected system (email, erp or crm) that you may reference with <sv-record system="..." ref="...">. Optionally narrow to one table via "kind" (email: inbox/sent/changes/refusals; erp: quotes/orders/changes/refusals; crm: contacts/deals/tasks/activities/changes/refusals) \u2014 omit it to get every table for that system. Never returns the team's own reference replies.`,
|
|
6453
|
+
description: `Rows from one connected system (email, erp or crm) that you may reference with <sv-record system="..." ref="...">. Optionally narrow to one table via "kind" (email: inbox/sent/changes/refusals; erp: quotes/orders/changes/refusals; crm: contacts/deals/tasks/activities/changes/refusals) \u2014 omit it to get every table for that system. Only rows inside your reporting window. Never returns the team's own reference replies.`,
|
|
6785
6454
|
inputSchema: {
|
|
6786
6455
|
type: "object",
|
|
6787
6456
|
properties: {
|
|
@@ -6795,9 +6464,11 @@ function getRecordsTool(deps) {
|
|
|
6795
6464
|
if (system !== "email" && system !== "erp" && system !== "crm") {
|
|
6796
6465
|
return errorText('system must be one of "email", "erp", "crm".');
|
|
6797
6466
|
}
|
|
6467
|
+
const scoped = await scope(deps);
|
|
6468
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6798
6469
|
let exported;
|
|
6799
6470
|
try {
|
|
6800
|
-
exported = await
|
|
6471
|
+
exported = await scoped.sources.runExport(system);
|
|
6801
6472
|
} catch (err) {
|
|
6802
6473
|
return errorText(`Could not read ${system} records: ${err instanceof Error ? err.message : String(err)}`);
|
|
6803
6474
|
}
|
|
@@ -6814,11 +6485,11 @@ function getRecordsTool(deps) {
|
|
|
6814
6485
|
for (const kind of kinds) {
|
|
6815
6486
|
records[kind] = exp[kind] ?? [];
|
|
6816
6487
|
}
|
|
6817
|
-
return builtinText(JSON.stringify({ system, records }, null, 2));
|
|
6488
|
+
return builtinText(JSON.stringify({ system, window: windowInfo(scoped.window), records }, null, 2));
|
|
6818
6489
|
}
|
|
6819
6490
|
};
|
|
6820
6491
|
}
|
|
6821
|
-
function summarizeWrite(result) {
|
|
6492
|
+
function summarizeWrite(result, rawHtml) {
|
|
6822
6493
|
const verified = result.elements.filter((e) => e.status === "verified");
|
|
6823
6494
|
const warnings = result.elements.filter((e) => e.status === "warning");
|
|
6824
6495
|
const unverifiable = result.elements.filter((e) => e.status === "unverifiable");
|
|
@@ -6834,7 +6505,20 @@ function summarizeWrite(result) {
|
|
|
6834
6505
|
lines.push("Not verifiable \u2014 fix these references and write again:");
|
|
6835
6506
|
for (const u of unverifiable) lines.push(` - ${u.kind} (${u.id}): ${u.reason ?? "unspecified"}`);
|
|
6836
6507
|
}
|
|
6508
|
+
const n = result.sanitizeNotes;
|
|
6509
|
+
if (n) {
|
|
6510
|
+
if (n.droppedBlocks > 0) lines.push(`dropped: ${n.droppedBlocks} block(s) outside sv-ai \u2014 put your own content inside <sv-ai>\u2026</sv-ai>; only gateway elements, sv-row and one sv-glossary may stand outside it.`);
|
|
6511
|
+
if (n.droppedSvInsideAi > 0) lines.push(`dropped: ${n.droppedSvInsideAi} sv-* element(s) inside sv-ai \u2014 place gateway elements outside sv-ai blocks.`);
|
|
6512
|
+
if (n.droppedStyles > 0) lines.push(`dropped: ${n.droppedStyles} <style> block(s) \u2014 use inline style="\u2026" attributes inside sv-ai.`);
|
|
6513
|
+
if (n.extraGlossaries > 0) lines.push(`dropped: ${n.extraGlossaries} extra sv-glossary \u2014 one per report.`);
|
|
6514
|
+
}
|
|
6515
|
+
const g = glossaryUsage(rawHtml, result.elements);
|
|
6516
|
+
if (g) {
|
|
6517
|
+
lines.push(`Glossary: ${g.applied.length} term(s) shown as translation (when the reader switches it on).`);
|
|
6518
|
+
for (const r of g.rejected) lines.push(` - ignored "${r.key}": ${r.reason}`);
|
|
6519
|
+
}
|
|
6837
6520
|
const cov = result.coverage;
|
|
6521
|
+
if (cov.window) lines.push(`Reporting window: ${cov.window.label}. Evidence from before it is not verifiable in this report.`);
|
|
6838
6522
|
if (cov.emailExportError) {
|
|
6839
6523
|
lines.push(`Coverage could not be checked: ${cov.emailExportError}`);
|
|
6840
6524
|
} else {
|
|
@@ -6869,9 +6553,15 @@ function writeReportTool(deps) {
|
|
|
6869
6553
|
},
|
|
6870
6554
|
required: ["html"]
|
|
6871
6555
|
},
|
|
6872
|
-
// Empty/oversize html
|
|
6873
|
-
//
|
|
6874
|
-
|
|
6556
|
+
// Empty/oversize html, or no reporting window to check it against, is
|
|
6557
|
+
// refused here, BEFORE the gate requests a ticket (precheckBuiltin) — a
|
|
6558
|
+
// refused write must not consume report_daily_max.
|
|
6559
|
+
validate: async (args) => {
|
|
6560
|
+
const refusal = writeReportRefusal(args);
|
|
6561
|
+
if (refusal) return refusal;
|
|
6562
|
+
const scoped = await scope(deps);
|
|
6563
|
+
return scoped.ok ? void 0 : scoped.reason;
|
|
6564
|
+
},
|
|
6875
6565
|
handler: async (args) => {
|
|
6876
6566
|
const refusal = writeReportRefusal(args);
|
|
6877
6567
|
if (refusal) return errorText(`Refused: ${refusal}`);
|
|
@@ -6879,9 +6569,11 @@ function writeReportTool(deps) {
|
|
|
6879
6569
|
if (deps.state.reportStore.isLocked()) {
|
|
6880
6570
|
return errorText("The report store is locked (the vault is not unlocked) \u2014 cannot save a report right now.");
|
|
6881
6571
|
}
|
|
6572
|
+
const scoped = await scope(deps);
|
|
6573
|
+
if (!scoped.ok) return errorText(`Refused: ${scoped.reason}`);
|
|
6882
6574
|
try {
|
|
6883
|
-
const stored = await deps.state.reportStore.saveReport(html,
|
|
6884
|
-
return builtinText(summarizeWrite(stored.result));
|
|
6575
|
+
const stored = await deps.state.reportStore.saveReport(html, scoped.sources);
|
|
6576
|
+
return builtinText(summarizeWrite(stored.result, html));
|
|
6885
6577
|
} catch (err) {
|
|
6886
6578
|
return errorText(`Could not verify and store the report: ${err instanceof Error ? err.message : String(err)}`);
|
|
6887
6579
|
}
|
|
@@ -6898,6 +6590,11 @@ var reportBuiltin = (deps) => ({
|
|
|
6898
6590
|
// read-only export CLI; writes only the local, vault-encrypted report file)
|
|
6899
6591
|
// — safe during a simulation-mode test.
|
|
6900
6592
|
simulation: true,
|
|
6593
|
+
// The same predicate the reporting window uses to refuse a mandate
|
|
6594
|
+
// (reporting@0.1, or no window set): the gate never selects such a mandate
|
|
6595
|
+
// for ANY report tool — so write_report is never ticketed under (and never
|
|
6596
|
+
// charges report_daily_max of) a mandate every read tool refuses (RR7).
|
|
6597
|
+
mandateRefusal: (auth) => reportingMandateRefusal(auth),
|
|
6901
6598
|
toolGating: {
|
|
6902
6599
|
overrides: {
|
|
6903
6600
|
list_tickets: READ_GATE,
|
|
@@ -7733,12 +7430,36 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
|
|
|
7733
7430
|
gates: state.gateStore.getAll()
|
|
7734
7431
|
});
|
|
7735
7432
|
});
|
|
7736
|
-
|
|
7737
|
-
|
|
7433
|
+
function reportScope() {
|
|
7434
|
+
return scopeReportSources(reportSources, {
|
|
7435
|
+
authorizations: state.getEnrichedAuthorizations(),
|
|
7436
|
+
simulation: isSimulationMode()
|
|
7437
|
+
});
|
|
7438
|
+
}
|
|
7439
|
+
function storedReportScope(stored) {
|
|
7440
|
+
const w = stored?.result.coverage.window;
|
|
7441
|
+
if (w) return scopeReportSourcesToStoredWindow(reportSources, w, { simulation: isSimulationMode() });
|
|
7442
|
+
return reportScope();
|
|
7443
|
+
}
|
|
7444
|
+
async function reportResponsePayload(stored, archive) {
|
|
7445
|
+
let detailArchive = state.receiptArchive;
|
|
7446
|
+
if (archive) {
|
|
7447
|
+
detailArchive = archive;
|
|
7448
|
+
} else {
|
|
7449
|
+
const scoped = await storedReportScope(stored);
|
|
7450
|
+
if (scoped.ok) detailArchive = scoped.sources.archive;
|
|
7451
|
+
}
|
|
7452
|
+
const ticketDetails = await buildTicketDetails(detailArchive, stored.result.proof.ticketsReferenced);
|
|
7453
|
+
const glossary = glossaryUsage(stored.result.html, stored.result.elements);
|
|
7454
|
+
const dropped = sanitizeReport(stored.html).notes;
|
|
7455
|
+
const notShown = dropped.droppedBlocks + dropped.droppedSvInsideAi + dropped.droppedStyles;
|
|
7738
7456
|
return {
|
|
7739
7457
|
savedAt: stored.savedAt,
|
|
7740
7458
|
checkedAt: stored.checkedAt,
|
|
7741
7459
|
renderedHtml: renderReportHtml(stored.result.html, stored.result.elements),
|
|
7460
|
+
...glossary && glossary.applied.length > 0 ? { renderedHtmlGloss: renderReportHtml(stored.result.html, stored.result.elements, { gloss: "on" }) } : {},
|
|
7461
|
+
...glossary ? { glossary } : {},
|
|
7462
|
+
...notShown > 0 ? { formatNotice: { blocksNotShown: notShown } } : {},
|
|
7742
7463
|
proof: stored.result.proof,
|
|
7743
7464
|
coverage: stored.result.coverage,
|
|
7744
7465
|
elements: stored.result.elements,
|
|
@@ -7767,9 +7488,14 @@ app.post("/internal/report", internalOnly, async (req, res) => {
|
|
|
7767
7488
|
res.status(400).json({ error: "Missing required field: html (non-empty string)" });
|
|
7768
7489
|
return;
|
|
7769
7490
|
}
|
|
7491
|
+
const scoped = await reportScope();
|
|
7492
|
+
if (!scoped.ok) {
|
|
7493
|
+
res.status(409).json({ error: scoped.reason });
|
|
7494
|
+
return;
|
|
7495
|
+
}
|
|
7770
7496
|
try {
|
|
7771
|
-
const stored = await state.reportStore.saveReport(html,
|
|
7772
|
-
res.json({ report: await reportResponsePayload(stored) });
|
|
7497
|
+
const stored = await state.reportStore.saveReport(html, scoped.sources);
|
|
7498
|
+
res.json({ report: await reportResponsePayload(stored, scoped.sources.archive) });
|
|
7773
7499
|
} catch (err) {
|
|
7774
7500
|
console.error("[Suveren MCP] /internal/report save failed:", err);
|
|
7775
7501
|
res.status(500).json({ error: err instanceof Error ? err.message : "Report verification failed" });
|
|
@@ -7780,13 +7506,23 @@ app.post("/internal/report/recheck", internalOnly, async (_req, res) => {
|
|
|
7780
7506
|
res.status(503).json({ error: "Vault locked \u2014 cannot recheck until sign-in." });
|
|
7781
7507
|
return;
|
|
7782
7508
|
}
|
|
7509
|
+
const current = state.reportStore.getReport();
|
|
7510
|
+
if (!current) {
|
|
7511
|
+
res.json({ report: null });
|
|
7512
|
+
return;
|
|
7513
|
+
}
|
|
7514
|
+
const scoped = await storedReportScope(current);
|
|
7515
|
+
if (!scoped.ok) {
|
|
7516
|
+
res.status(409).json({ error: scoped.reason });
|
|
7517
|
+
return;
|
|
7518
|
+
}
|
|
7783
7519
|
try {
|
|
7784
|
-
const stored = await state.reportStore.recheck(
|
|
7520
|
+
const stored = await state.reportStore.recheck(scoped.sources);
|
|
7785
7521
|
if (!stored) {
|
|
7786
7522
|
res.json({ report: null });
|
|
7787
7523
|
return;
|
|
7788
7524
|
}
|
|
7789
|
-
res.json({ report: await reportResponsePayload(stored) });
|
|
7525
|
+
res.json({ report: await reportResponsePayload(stored, scoped.sources.archive) });
|
|
7790
7526
|
} catch (err) {
|
|
7791
7527
|
console.error("[Suveren MCP] /internal/report/recheck failed:", err);
|
|
7792
7528
|
res.status(500).json({ error: err instanceof Error ? err.message : "Recheck failed" });
|
|
@@ -7804,9 +7540,19 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
|
|
|
7804
7540
|
res.status(503).json({ error: "Vault locked \u2014 the report is encrypted until sign-in." });
|
|
7805
7541
|
return;
|
|
7806
7542
|
}
|
|
7543
|
+
const current = state.reportStore.getReport();
|
|
7544
|
+
if (!current) {
|
|
7545
|
+
res.status(404).json({ error: "No report to export yet \u2014 the AI writes it with the Reporting mandate." });
|
|
7546
|
+
return;
|
|
7547
|
+
}
|
|
7548
|
+
const scoped = await storedReportScope(current);
|
|
7549
|
+
if (!scoped.ok) {
|
|
7550
|
+
res.status(409).json({ error: scoped.reason });
|
|
7551
|
+
return;
|
|
7552
|
+
}
|
|
7807
7553
|
let stored;
|
|
7808
7554
|
try {
|
|
7809
|
-
stored = await state.reportStore.recheck(
|
|
7555
|
+
stored = await state.reportStore.recheck(scoped.sources);
|
|
7810
7556
|
} catch (err) {
|
|
7811
7557
|
console.error("[Suveren MCP] /internal/report/export recheck failed:", err);
|
|
7812
7558
|
res.status(500).json({ error: err instanceof Error ? err.message : "Recheck failed" });
|
|
@@ -7826,12 +7572,11 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
|
|
|
7826
7572
|
const gatewayVersion = typeof req.query.gatewayVersion === "string" ? req.query.gatewayVersion : "unknown";
|
|
7827
7573
|
const bundle = buildExportBundle({
|
|
7828
7574
|
stored,
|
|
7829
|
-
archive:
|
|
7575
|
+
archive: scoped.sources.archive,
|
|
7830
7576
|
authorityServer,
|
|
7831
7577
|
gatewayVersion
|
|
7832
7578
|
});
|
|
7833
|
-
const
|
|
7834
|
-
const document = buildExportDocument({ bundle, renderedHtml });
|
|
7579
|
+
const document = buildExportDocument({ bundle });
|
|
7835
7580
|
res.setHeader("Content-Type", "text/html; charset=utf-8").setHeader("Content-Disposition", `attachment; filename="${suggestedFilename(bundle)}"`).setHeader("Cache-Control", "no-store").send(document);
|
|
7836
7581
|
});
|
|
7837
7582
|
app.get("/internal/brief", internalOnly, (_req, res) => {
|