@suveren/gateway 0.18.2 → 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 +385 -674
- 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-QOKPECVY.mjs +0 -751
- package/dist/ui/assets/index-Bfi-G4qJ.css +0 -1
- package/dist/ui/assets/index-D0uXCqQB.js +0 -111
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CASE_TIME_UNKNOWN_REASON,
|
|
4
|
-
|
|
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,
|
|
5
16
|
fingerprintOf,
|
|
6
|
-
formatActionLabel,
|
|
7
|
-
formatCurrency,
|
|
8
17
|
formatDateTime,
|
|
9
|
-
|
|
10
|
-
|
|
18
|
+
glossaryUsage,
|
|
19
|
+
mandateFields,
|
|
11
20
|
parseCaseAttrs,
|
|
12
21
|
parseElements,
|
|
13
22
|
parseTimestampSeconds,
|
|
14
23
|
profileShortLabel,
|
|
15
24
|
readPairing,
|
|
25
|
+
renderReportHtml,
|
|
16
26
|
resolveApprovalElement,
|
|
17
27
|
resolveCaseElement,
|
|
18
28
|
resolveMandateElement,
|
|
19
|
-
resolveTicketElement
|
|
20
|
-
|
|
29
|
+
resolveTicketElement,
|
|
30
|
+
sanitizeReport,
|
|
31
|
+
suggestedFilename,
|
|
32
|
+
ticketFields
|
|
33
|
+
} from "./chunk-ZRRZFGRL.mjs";
|
|
21
34
|
|
|
22
35
|
// bin/http.ts
|
|
23
36
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -1678,8 +1691,8 @@ var ReceiptArchive = class {
|
|
|
1678
1691
|
*/
|
|
1679
1692
|
record(entry, now = Math.floor(Date.now() / 1e3)) {
|
|
1680
1693
|
let changed = false;
|
|
1681
|
-
const
|
|
1682
|
-
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);
|
|
1683
1696
|
if (!alreadyArchived) {
|
|
1684
1697
|
this.data.receipts.push({
|
|
1685
1698
|
archivedAt: now,
|
|
@@ -2084,125 +2097,6 @@ import { homedir as homedir7 } from "os";
|
|
|
2084
2097
|
import { dirname as dirname8, join as join10 } from "path";
|
|
2085
2098
|
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2086
2099
|
|
|
2087
|
-
// src/lib/report-brief.ts
|
|
2088
|
-
var REPORT_ELEMENTS = [
|
|
2089
|
-
"sv-ticket",
|
|
2090
|
-
"sv-approval",
|
|
2091
|
-
"sv-mandate",
|
|
2092
|
-
"sv-record",
|
|
2093
|
-
"sv-case",
|
|
2094
|
-
"sv-metric"
|
|
2095
|
-
];
|
|
2096
|
-
var REPORT_BRIEF = `How to write the report
|
|
2097
|
-
|
|
2098
|
-
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.
|
|
2099
|
-
|
|
2100
|
-
Who reads it: managers who are not technical. Plain words, short sentences, the result first.
|
|
2101
|
-
|
|
2102
|
-
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.
|
|
2103
|
-
|
|
2104
|
-
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.
|
|
2105
|
-
|
|
2106
|
-
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:
|
|
2107
|
-
|
|
2108
|
-
- <sv-ticket ref="TICKET_ID"></sv-ticket> \u2014 one action you took, with its signed ticket.
|
|
2109
|
-
- <sv-approval ticket="TICKET_ID"></sv-approval> \u2014 who approved that action, when, and how long it waited.
|
|
2110
|
-
- <sv-mandate ticket="TICKET_ID"></sv-mandate> \u2014 the authority the action ran under: limits, mode, intent.
|
|
2111
|
-
- <sv-record system="email|crm|erp" ref="RECORD_ID"></sv-record> \u2014 an email, CRM entry, quote or order.
|
|
2112
|
-
- <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.
|
|
2113
|
-
- <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".
|
|
2114
|
-
|
|
2115
|
-
Rules:
|
|
2116
|
-
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.
|
|
2117
|
-
2. Use sv-metric for every headline number. Everything you write yourself \u2014 text, tables, charts, numbers you compute \u2014 sits under the gateway's label "AI analysis \u2014 not verified"; only the drawn elements carry the gateway's green check. Do not imitate them: class names starting with "sv-" are removed from your HTML.
|
|
2118
|
-
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. A case is timed from when its email reached the test (the test data may date its emails earlier \u2014 the gateway uses the later of the two), so steps from before that moment do not belong to it.
|
|
2119
|
-
4. A wrong or unknown reference is shown as "not verifiable". Do not guess IDs \u2014 read them first.
|
|
2120
|
-
5. Never claim more than the elements prove. Say plainly what went wrong or waited for a person.
|
|
2121
|
-
|
|
2122
|
-
Suggested structure:
|
|
2123
|
-
1. The result in one sentence, then 3\u20134 sv-metric figures (completed, median time, share without approval, approvals).
|
|
2124
|
-
2. All cases at a glance \u2014 a table with one row per case, linked to its sv-case below.
|
|
2125
|
-
3. Each case: sv-case, then the sv-ticket and sv-approval elements that explain it.
|
|
2126
|
-
4. What was stopped or needed a person, and why (sv-approval, refusals).
|
|
2127
|
-
5. Your assessment: what worked, what slowed things down, what a different mandate would change \u2014 marked as your view.
|
|
2128
|
-
|
|
2129
|
-
Update the report whenever cases progress; each write replaces the previous report.`;
|
|
2130
|
-
|
|
2131
|
-
// src/lib/report/sanitize.ts
|
|
2132
|
-
import sanitizeHtmlLib from "sanitize-html";
|
|
2133
|
-
var FORBIDDEN_TAGS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "link"]);
|
|
2134
|
-
function isUrlAttribute(name) {
|
|
2135
|
-
const n = name.toLowerCase();
|
|
2136
|
-
return n === "href" || n === "src" || n === "xlink:href" || n.endsWith(":href");
|
|
2137
|
-
}
|
|
2138
|
-
function isAllowedUrl(value) {
|
|
2139
|
-
const v = value.trim();
|
|
2140
|
-
if (v.startsWith("#")) return true;
|
|
2141
|
-
if (/^data:image\//i.test(v)) return true;
|
|
2142
|
-
return false;
|
|
2143
|
-
}
|
|
2144
|
-
function stripGatewayClasses(value) {
|
|
2145
|
-
return value.split(/\s+/).filter((c) => c && !/^sv-/i.test(c)).join(" ");
|
|
2146
|
-
}
|
|
2147
|
-
function sanitizeReportHtml(html) {
|
|
2148
|
-
return sanitizeHtmlLib(html, {
|
|
2149
|
-
// Allow every tag/attribute by default — see module doc comment for why
|
|
2150
|
-
// this is deny-by-name rather than allow-by-name. `transformTags['*']`
|
|
2151
|
-
// below is then the ONLY attribute filter (allowedAttributes: false
|
|
2152
|
-
// skips sanitize-html's own, so there is exactly one place attributes
|
|
2153
|
-
// are decided, not two that could disagree). Logs a harmless warning for
|
|
2154
|
-
// "script"/"style" on every call (sanitize-html's own vulnerable-tag
|
|
2155
|
-
// notice, fired from its internal tag list even though we pass `false`,
|
|
2156
|
-
// not an array containing them) — `allowVulnerableTags` is the
|
|
2157
|
-
// documented way to accept that, and <script> is still removed, by
|
|
2158
|
-
// `exclusiveFilter` below, regardless of this flag.
|
|
2159
|
-
allowedTags: false,
|
|
2160
|
-
allowedAttributes: false,
|
|
2161
|
-
allowVulnerableTags: true,
|
|
2162
|
-
// sanitize-html's OWN scheme check (independent of transformTags, and
|
|
2163
|
-
// applied to it/src/cite only — NOT xlink:href, which is why the
|
|
2164
|
-
// transformTags filter above is the one actually relied on for SVG) uses
|
|
2165
|
-
// this list; it has no "data" scheme by default, which would undo our
|
|
2166
|
-
// own decision to keep `data:image/*` sources. Nothing wider is opened:
|
|
2167
|
-
// transformTags has already removed every href/src that isn't a
|
|
2168
|
-
// fragment link or a data:image/* URI by the time this check runs.
|
|
2169
|
-
allowedSchemes: [...sanitizeHtmlLib.defaults.allowedSchemes, "data"],
|
|
2170
|
-
// Preserve attribute name casing — inline SVG uses camelCase attributes
|
|
2171
|
-
// (viewBox, preserveAspectRatio, …) that lowercasing would silently
|
|
2172
|
-
// break. Safe for the `on*`/url checks below: both compare case-folded.
|
|
2173
|
-
parser: { lowerCaseAttributeNames: false },
|
|
2174
|
-
// Removes the named tag AND all of its children/text — the only correct
|
|
2175
|
-
// behaviour for <script>: dropping just the tag would leave its JS as
|
|
2176
|
-
// visible text, but the content must not survive either.
|
|
2177
|
-
exclusiveFilter: (frame) => {
|
|
2178
|
-
const tag = frame.tag.toLowerCase();
|
|
2179
|
-
if (FORBIDDEN_TAGS.has(tag)) return true;
|
|
2180
|
-
if (tag === "meta" && Object.keys(frame.attribs).some((a) => a.toLowerCase() === "http-equiv")) {
|
|
2181
|
-
return true;
|
|
2182
|
-
}
|
|
2183
|
-
return false;
|
|
2184
|
-
},
|
|
2185
|
-
transformTags: {
|
|
2186
|
-
"*": (tagName, attribs) => {
|
|
2187
|
-
const kept = {};
|
|
2188
|
-
for (const [name, value] of Object.entries(attribs)) {
|
|
2189
|
-
const lower = name.toLowerCase();
|
|
2190
|
-
if (lower.startsWith("on")) continue;
|
|
2191
|
-
if (isUrlAttribute(name) && !isAllowedUrl(value)) continue;
|
|
2192
|
-
if (lower.startsWith("data-sv")) continue;
|
|
2193
|
-
if (lower === "class") {
|
|
2194
|
-
const classes = stripGatewayClasses(value);
|
|
2195
|
-
if (classes) kept[name] = classes;
|
|
2196
|
-
continue;
|
|
2197
|
-
}
|
|
2198
|
-
kept[name] = value;
|
|
2199
|
-
}
|
|
2200
|
-
return { tagName, attribs: kept };
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
});
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
2100
|
// src/lib/report/record-resolvers.ts
|
|
2207
2101
|
function findById(rows, id) {
|
|
2208
2102
|
return rows.find((r) => r.id === id);
|
|
@@ -2251,68 +2145,6 @@ function resolveRecord(system, exportData, ref) {
|
|
|
2251
2145
|
return resolveCrmRecord(exportData, ref);
|
|
2252
2146
|
}
|
|
2253
2147
|
|
|
2254
|
-
// src/lib/report/metric-resolvers.ts
|
|
2255
|
-
var METRIC_KINDS = [
|
|
2256
|
-
"completed",
|
|
2257
|
-
"median-time",
|
|
2258
|
-
"average-time",
|
|
2259
|
-
"without-approval",
|
|
2260
|
-
"approvals",
|
|
2261
|
-
"median-approval-wait",
|
|
2262
|
-
"tickets",
|
|
2263
|
-
"refusals"
|
|
2264
|
-
];
|
|
2265
|
-
var UnknownMetricKindError = class extends Error {
|
|
2266
|
-
constructor(kind) {
|
|
2267
|
-
super(`Unknown sv-metric kind "${kind}" \u2014 expected one of: ${METRIC_KINDS.join(", ")}.`);
|
|
2268
|
-
this.name = "UnknownMetricKindError";
|
|
2269
|
-
}
|
|
2270
|
-
};
|
|
2271
|
-
var START_TIME_METRICS = /* @__PURE__ */ new Set(["median-time", "average-time", "refusals"]);
|
|
2272
|
-
function median(values) {
|
|
2273
|
-
if (values.length === 0) return 0;
|
|
2274
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
2275
|
-
const mid = Math.floor(sorted.length / 2);
|
|
2276
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
2277
|
-
}
|
|
2278
|
-
function average(values) {
|
|
2279
|
-
if (values.length === 0) return 0;
|
|
2280
|
-
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
2281
|
-
}
|
|
2282
|
-
function computeMetric(kind, cases, refusalTimes = []) {
|
|
2283
|
-
if (START_TIME_METRICS.has(kind) && cases.some((c) => c.totalDurationSeconds === null)) {
|
|
2284
|
-
throw new Error("Case start times are not verifiable.");
|
|
2285
|
-
}
|
|
2286
|
-
switch (kind) {
|
|
2287
|
-
case "completed":
|
|
2288
|
-
return cases.length;
|
|
2289
|
-
case "median-time":
|
|
2290
|
-
return median(cases.map((c) => c.totalDurationSeconds));
|
|
2291
|
-
case "average-time":
|
|
2292
|
-
return average(cases.map((c) => c.totalDurationSeconds));
|
|
2293
|
-
case "without-approval": {
|
|
2294
|
-
if (cases.length === 0) return 0;
|
|
2295
|
-
const withoutApproval = cases.filter((c) => c.approvals.length === 0).length;
|
|
2296
|
-
return withoutApproval / cases.length;
|
|
2297
|
-
}
|
|
2298
|
-
case "approvals":
|
|
2299
|
-
return cases.reduce((sum, c) => sum + c.approvals.length, 0);
|
|
2300
|
-
case "median-approval-wait":
|
|
2301
|
-
return median(cases.flatMap((c) => c.approvals.map((a) => a.waitSeconds).filter((w) => typeof w === "number")));
|
|
2302
|
-
case "tickets": {
|
|
2303
|
-
const ids = new Set(cases.flatMap((c) => c.ticketIds));
|
|
2304
|
-
return ids.size;
|
|
2305
|
-
}
|
|
2306
|
-
case "refusals":
|
|
2307
|
-
return cases.reduce(
|
|
2308
|
-
(sum, c) => sum + refusalTimes.filter((t) => t >= c.startTime && t <= c.goalTime).length,
|
|
2309
|
-
0
|
|
2310
|
-
);
|
|
2311
|
-
default:
|
|
2312
|
-
throw new UnknownMetricKindError(kind);
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
2148
|
// src/lib/report/types.ts
|
|
2317
2149
|
function isEmailExport(v) {
|
|
2318
2150
|
const o = v;
|
|
@@ -2330,7 +2162,7 @@ function isCrmExport(v) {
|
|
|
2330
2162
|
// src/lib/report/verify-report.ts
|
|
2331
2163
|
var KNOWN_KINDS = new Set(REPORT_ELEMENTS);
|
|
2332
2164
|
async function verifyReport(html, sources) {
|
|
2333
|
-
const sanitized =
|
|
2165
|
+
const { html: sanitized, notes: sanitizeNotes } = sanitizeReport(html);
|
|
2334
2166
|
const parsed = parseElements(sanitized);
|
|
2335
2167
|
const exportCache = /* @__PURE__ */ new Map();
|
|
2336
2168
|
function getExport(system) {
|
|
@@ -2351,7 +2183,16 @@ async function verifyReport(html, sources) {
|
|
|
2351
2183
|
try {
|
|
2352
2184
|
switch (p.kind) {
|
|
2353
2185
|
case "sv-ticket": {
|
|
2354
|
-
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
|
+
}
|
|
2355
2196
|
return { id: p.id, kind: p.kind, attrs: p.attrs, ...r };
|
|
2356
2197
|
}
|
|
2357
2198
|
case "sv-approval": {
|
|
@@ -2456,7 +2297,7 @@ async function verifyReport(html, sources) {
|
|
|
2456
2297
|
}
|
|
2457
2298
|
const refusalTimes = kind === "refusals" ? await collectRefusalTimes(getExport) : [];
|
|
2458
2299
|
const value = computeMetric(kind, caseInputs, refusalTimes);
|
|
2459
|
-
const data = { kind, cases: casesAttr, value, caseCount: verifiedCount };
|
|
2300
|
+
const data = { kind, cases: casesAttr, value, caseCount: verifiedCount, caseIds: caseInputs.map((c) => c.caseId) };
|
|
2460
2301
|
if (verifiedCount < requestedCount) {
|
|
2461
2302
|
elements.push({
|
|
2462
2303
|
id: p.id,
|
|
@@ -2474,8 +2315,8 @@ async function verifyReport(html, sources) {
|
|
|
2474
2315
|
}
|
|
2475
2316
|
}
|
|
2476
2317
|
const proof = buildProof(elements, caseElements);
|
|
2477
|
-
const coverage = await buildCoverage(caseElements, getExport, sources.archive, proof.ticketsReferenced);
|
|
2478
|
-
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 };
|
|
2479
2320
|
}
|
|
2480
2321
|
async function collectRefusalTimes(getExport) {
|
|
2481
2322
|
const times = [];
|
|
@@ -2491,38 +2332,7 @@ async function collectRefusalTimes(getExport) {
|
|
|
2491
2332
|
return times;
|
|
2492
2333
|
}
|
|
2493
2334
|
function summarize(el) {
|
|
2494
|
-
|
|
2495
|
-
switch (el.kind) {
|
|
2496
|
-
case "sv-ticket": {
|
|
2497
|
-
const actionLabel = typeof d.actionLabel === "string" ? d.actionLabel : formatActionLabel(d.action);
|
|
2498
|
-
const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
|
|
2499
|
-
return `${actionLabel} \u2014 ${timeLabel}`;
|
|
2500
|
-
}
|
|
2501
|
-
case "sv-approval": {
|
|
2502
|
-
const who = typeof d.whoLabel === "string" ? d.whoLabel : d.who?.join(", ") ?? "unknown";
|
|
2503
|
-
const waitLabel = typeof d.waitLabel === "string" ? d.waitLabel : typeof d.waitSeconds === "number" ? formatDuration(d.waitSeconds) : "unknown";
|
|
2504
|
-
return `Approval by ${who} \u2014 waited ${waitLabel}`;
|
|
2505
|
-
}
|
|
2506
|
-
case "sv-mandate": {
|
|
2507
|
-
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : profileShortLabel(typeof d.profile === "string" ? d.profile : void 0);
|
|
2508
|
-
const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
|
|
2509
|
-
return `Mandate: ${profileLabel} \xB7 ${owners}`;
|
|
2510
|
-
}
|
|
2511
|
-
case "sv-record":
|
|
2512
|
-
return `${String(d.kind ?? "record")} record checked`;
|
|
2513
|
-
case "sv-case": {
|
|
2514
|
-
const steps = d.steps?.length ?? 0;
|
|
2515
|
-
const time = typeof d.totalDurationSeconds === "number" ? formatDuration(d.totalDurationSeconds) : "time not verifiable";
|
|
2516
|
-
return `Case ${String(d.caseId)}: ${steps} ${steps === 1 ? "step" : "steps"}, ${time}`;
|
|
2517
|
-
}
|
|
2518
|
-
case "sv-metric": {
|
|
2519
|
-
const kind = String(d.kind ?? "");
|
|
2520
|
-
const label = METRIC_LABELS[kind] ?? kind;
|
|
2521
|
-
return `${label} = ${formatMetricValue(kind, d.value)}`;
|
|
2522
|
-
}
|
|
2523
|
-
default:
|
|
2524
|
-
return el.id;
|
|
2525
|
-
}
|
|
2335
|
+
return checkedValueLine(el);
|
|
2526
2336
|
}
|
|
2527
2337
|
function buildProof(elements, caseElements) {
|
|
2528
2338
|
const ticketsReferenced = /* @__PURE__ */ new Set();
|
|
@@ -2563,7 +2373,7 @@ function buildProof(elements, caseElements) {
|
|
|
2563
2373
|
verifiedValues
|
|
2564
2374
|
};
|
|
2565
2375
|
}
|
|
2566
|
-
async function buildCoverage(caseElements, getExport, archive, referenced) {
|
|
2376
|
+
async function buildCoverage(caseElements, getExport, archive, referenced, window) {
|
|
2567
2377
|
const exp = await getExport("email");
|
|
2568
2378
|
const emailExportError = !exp.ok ? exp.reason : !isEmailExport(exp.data) ? "Email simulator export had an unexpected shape." : void 0;
|
|
2569
2379
|
const loadedCases = exp.ok && isEmailExport(exp.data) ? [...new Set(exp.data.inbox.map((m) => m.case_id).filter((c) => !!c))] : [];
|
|
@@ -2573,7 +2383,8 @@ async function buildCoverage(caseElements, getExport, archive, referenced) {
|
|
|
2573
2383
|
const coveredCases = loadedCases.filter((c) => covered.has(c));
|
|
2574
2384
|
const missingCases = loadedCases.filter((c) => !covered.has(c));
|
|
2575
2385
|
const loadedAt = exp.ok && isEmailExport(exp.data) ? exp.data.simulation_load?.loaded_at : void 0;
|
|
2576
|
-
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;
|
|
2577
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);
|
|
2578
2389
|
const referencedSet = new Set(referenced);
|
|
2579
2390
|
const ticketsReferenced = inPeriod.filter((id) => referencedSet.has(id));
|
|
@@ -2584,6 +2395,7 @@ async function buildCoverage(caseElements, getExport, archive, referenced) {
|
|
|
2584
2395
|
coveredCases,
|
|
2585
2396
|
missingCases,
|
|
2586
2397
|
periodStart,
|
|
2398
|
+
...window ? { window: { start: window.start, end: window.end, days: window.days, label: window.label } } : {},
|
|
2587
2399
|
ticketsInPeriod: inPeriod,
|
|
2588
2400
|
ticketsReferenced,
|
|
2589
2401
|
ticketsNotReferenced
|
|
@@ -3035,8 +2847,8 @@ var FOOTER_RE = /\n*(?:—|--) (?:Sent|Published) by (?:an AI agent|[^\n]+?'s AI
|
|
|
3035
2847
|
function shouldAttachFooter() {
|
|
3036
2848
|
return true;
|
|
3037
2849
|
}
|
|
3038
|
-
function receiptUrl(
|
|
3039
|
-
return `${AS_BASE}/r/${
|
|
2850
|
+
function receiptUrl(receiptId) {
|
|
2851
|
+
return `${AS_BASE}/r/${receiptId}`;
|
|
3040
2852
|
}
|
|
3041
2853
|
function verbForProfile(profile) {
|
|
3042
2854
|
return profile === "publish" ? "Published" : "Sent";
|
|
@@ -3044,11 +2856,11 @@ function verbForProfile(profile) {
|
|
|
3044
2856
|
function hapLine() {
|
|
3045
2857
|
return `-- ${OPERATOR_NAME} implements HAP, the open Human Agency Protocol: ${HAP_URL}`;
|
|
3046
2858
|
}
|
|
3047
|
-
function footerText(
|
|
2859
|
+
function footerText(receiptId, verb, subject) {
|
|
3048
2860
|
const identity = deriveIdentityLine(subject, { operatorName: OPERATOR_NAME }).replace(/^Sent\b/, verb);
|
|
3049
2861
|
return `
|
|
3050
2862
|
|
|
3051
|
-
-- ${identity}. Receipt: ${receiptUrl(
|
|
2863
|
+
-- ${identity}. Receipt: ${receiptUrl(receiptId)}
|
|
3052
2864
|
|
|
3053
2865
|
${hapLine()}`;
|
|
3054
2866
|
}
|
|
@@ -3067,13 +2879,13 @@ function detectContentField(tool) {
|
|
|
3067
2879
|
function stripFooter(value) {
|
|
3068
2880
|
return value.replace(FOOTER_RE, "").replace(/\n+$/, "");
|
|
3069
2881
|
}
|
|
3070
|
-
function appendVerificationFooter(tool, args,
|
|
2882
|
+
function appendVerificationFooter(tool, args, receiptId, subject) {
|
|
3071
2883
|
const profile = tool.gating?.profile;
|
|
3072
2884
|
if (!profile || !FOOTER_PROFILES.has(profile)) return args;
|
|
3073
2885
|
const field = detectContentField(tool);
|
|
3074
2886
|
if (!field) return args;
|
|
3075
2887
|
const current = typeof args[field] === "string" ? args[field] : "";
|
|
3076
|
-
return { ...args, [field]: stripFooter(current) + footerText(
|
|
2888
|
+
return { ...args, [field]: stripFooter(current) + footerText(receiptId, verbForProfile(profile), subject) };
|
|
3077
2889
|
}
|
|
3078
2890
|
|
|
3079
2891
|
// src/lib/content-binding.ts
|
|
@@ -3126,10 +2938,10 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
|
|
|
3126
2938
|
boundContent
|
|
3127
2939
|
};
|
|
3128
2940
|
}
|
|
3129
|
-
function attachReceiptId(tool, args,
|
|
2941
|
+
function attachReceiptId(tool, args, receiptId) {
|
|
3130
2942
|
const schema = tool.inputSchema;
|
|
3131
2943
|
if (!schema?.properties || !("receipt_id" in schema.properties)) return args;
|
|
3132
|
-
return { ...args, receipt_id:
|
|
2944
|
+
return { ...args, receipt_id: receiptId };
|
|
3133
2945
|
}
|
|
3134
2946
|
|
|
3135
2947
|
// src/lib/ticket-verify.ts
|
|
@@ -3680,6 +3492,25 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3680
3492
|
};
|
|
3681
3493
|
}
|
|
3682
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
|
+
};
|
|
3683
3514
|
const lockedGuard = (inner) => async (args) => {
|
|
3684
3515
|
if (!state2.spClient.isUnlocked()) {
|
|
3685
3516
|
return {
|
|
@@ -3712,19 +3543,8 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3712
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.`
|
|
3713
3544
|
);
|
|
3714
3545
|
}
|
|
3715
|
-
const
|
|
3716
|
-
|
|
3717
|
-
(a) => a.complete && profileMatches(a.profileId, profile)
|
|
3718
|
-
);
|
|
3719
|
-
if (matchingAuths.length === 0) {
|
|
3720
|
-
return {
|
|
3721
|
-
content: [{
|
|
3722
|
-
type: "text",
|
|
3723
|
-
text: `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
|
|
3724
|
-
}],
|
|
3725
|
-
isError: true
|
|
3726
|
-
};
|
|
3727
|
-
}
|
|
3546
|
+
const { matching: matchingAuths, refusals } = eligibleMandates();
|
|
3547
|
+
if (matchingAuths.length === 0) return noEligibleMandate(refusals);
|
|
3728
3548
|
if (readGate.boundField) {
|
|
3729
3549
|
const permitted = matchingAuths.some(
|
|
3730
3550
|
(a) => boundsSatisfyReadGate(a.bounds ?? a.frame, readGate)
|
|
@@ -3906,19 +3726,8 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
3906
3726
|
};
|
|
3907
3727
|
}
|
|
3908
3728
|
}
|
|
3909
|
-
const
|
|
3910
|
-
|
|
3911
|
-
(a) => a.complete && profileMatches(a.profileId, profile)
|
|
3912
|
-
);
|
|
3913
|
-
if (matchingAuths.length === 0) {
|
|
3914
|
-
return {
|
|
3915
|
-
content: [{
|
|
3916
|
-
type: "text",
|
|
3917
|
-
text: `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
|
|
3918
|
-
}],
|
|
3919
|
-
isError: true
|
|
3920
|
-
};
|
|
3921
|
-
}
|
|
3729
|
+
const { matching: matchingAuths, refusals } = eligibleMandates();
|
|
3730
|
+
if (matchingAuths.length === 0) return noEligibleMandate(refusals);
|
|
3922
3731
|
const errors = [];
|
|
3923
3732
|
const passers = [];
|
|
3924
3733
|
for (const candidate of matchingAuths) {
|
|
@@ -4019,7 +3828,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4019
3828
|
};
|
|
4020
3829
|
}
|
|
4021
3830
|
}
|
|
4022
|
-
let
|
|
3831
|
+
let receiptId;
|
|
4023
3832
|
try {
|
|
4024
3833
|
const actionType = declaredActionType;
|
|
4025
3834
|
const binding = computeContentBinding(auth.profileId, tool, args, actionType);
|
|
@@ -4041,7 +3850,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
4041
3850
|
// field by field rather than spread.
|
|
4042
3851
|
...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
|
|
4043
3852
|
});
|
|
4044
|
-
|
|
3853
|
+
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
4045
3854
|
await verifyTicket(state2.cache, receipt, {
|
|
4046
3855
|
action: tool.namespacedName,
|
|
4047
3856
|
executionContext: { ...execution },
|
|
@@ -4195,12 +4004,12 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4195
4004
|
execution: { ...execution },
|
|
4196
4005
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
4197
4006
|
});
|
|
4198
|
-
let outgoingArgs = shouldAttachFooter() &&
|
|
4199
|
-
if (
|
|
4007
|
+
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
|
|
4008
|
+
if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
|
|
4200
4009
|
outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
|
|
4201
|
-
if (
|
|
4010
|
+
if (receiptId) {
|
|
4202
4011
|
const begun = state2.executionJournal.begin({
|
|
4203
|
-
ticketId:
|
|
4012
|
+
ticketId: receiptId,
|
|
4204
4013
|
tool: tool.namespacedName,
|
|
4205
4014
|
argsHash: hashToolArgs(args)
|
|
4206
4015
|
});
|
|
@@ -4208,17 +4017,17 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
|
|
|
4208
4017
|
return {
|
|
4209
4018
|
content: [{
|
|
4210
4019
|
type: "text",
|
|
4211
|
-
text: `Blocked: ticket ${
|
|
4020
|
+
text: `Blocked: ticket ${receiptId} was already used to execute ${tool.namespacedName} (state: ${begun.existing.state}). Not running it again.`
|
|
4212
4021
|
}],
|
|
4213
4022
|
isError: true
|
|
4214
4023
|
};
|
|
4215
4024
|
}
|
|
4216
4025
|
try {
|
|
4217
4026
|
const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
|
|
4218
|
-
state2.executionJournal.complete(
|
|
4027
|
+
state2.executionJournal.complete(receiptId, "done");
|
|
4219
4028
|
return result;
|
|
4220
4029
|
} catch (err) {
|
|
4221
|
-
state2.executionJournal.complete(
|
|
4030
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
4222
4031
|
throw err;
|
|
4223
4032
|
}
|
|
4224
4033
|
}
|
|
@@ -4699,7 +4508,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4699
4508
|
};
|
|
4700
4509
|
}
|
|
4701
4510
|
}
|
|
4702
|
-
let
|
|
4511
|
+
let receiptId;
|
|
4703
4512
|
let replayed = false;
|
|
4704
4513
|
try {
|
|
4705
4514
|
const binding = computeContentBinding(
|
|
@@ -4732,7 +4541,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4732
4541
|
// this machine — see the archive call below.
|
|
4733
4542
|
...binding ? { contentHash: binding.contentHash, contentBinding: binding.contentBinding } : {}
|
|
4734
4543
|
});
|
|
4735
|
-
|
|
4544
|
+
receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
|
|
4736
4545
|
replayed = idempotent;
|
|
4737
4546
|
await verifyTicket(state2.cache, receipt, {
|
|
4738
4547
|
action: proposal.tool,
|
|
@@ -4787,14 +4596,14 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4787
4596
|
isError: true
|
|
4788
4597
|
};
|
|
4789
4598
|
}
|
|
4790
|
-
if (!
|
|
4599
|
+
if (!receiptId) {
|
|
4791
4600
|
return {
|
|
4792
4601
|
text: `Proposal ${proposal.id}: the ticket carries no id \u2014 refusing to execute an action that could not be recorded.`,
|
|
4793
4602
|
isError: true
|
|
4794
4603
|
};
|
|
4795
4604
|
}
|
|
4796
4605
|
const begun = state2.executionJournal.begin({
|
|
4797
|
-
ticketId:
|
|
4606
|
+
ticketId: receiptId,
|
|
4798
4607
|
proposalId: proposal.id,
|
|
4799
4608
|
tool: proposal.tool,
|
|
4800
4609
|
argsHash: hashToolArgs(proposal.toolArgs)
|
|
@@ -4804,36 +4613,36 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
4804
4613
|
const when = new Date(prior.startedAt * 1e3).toISOString();
|
|
4805
4614
|
if (prior.state === "done") {
|
|
4806
4615
|
return {
|
|
4807
|
-
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.`
|
|
4808
4617
|
};
|
|
4809
4618
|
}
|
|
4810
4619
|
return {
|
|
4811
|
-
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.`,
|
|
4812
4621
|
isError: true
|
|
4813
4622
|
};
|
|
4814
4623
|
}
|
|
4815
4624
|
if (replayed) {
|
|
4816
4625
|
console.error(
|
|
4817
|
-
`[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).`
|
|
4818
4627
|
);
|
|
4819
4628
|
}
|
|
4820
4629
|
try {
|
|
4821
4630
|
let outgoingArgs = proposal.toolArgs;
|
|
4822
|
-
if (discovered &&
|
|
4631
|
+
if (discovered && receiptId) {
|
|
4823
4632
|
if (shouldAttachFooter()) {
|
|
4824
|
-
outgoingArgs = appendVerificationFooter(discovered, outgoingArgs,
|
|
4633
|
+
outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
|
|
4825
4634
|
}
|
|
4826
|
-
outgoingArgs = attachReceiptId(discovered, outgoingArgs,
|
|
4635
|
+
outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
|
|
4827
4636
|
outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
|
|
4828
4637
|
}
|
|
4829
4638
|
let result;
|
|
4830
4639
|
try {
|
|
4831
4640
|
result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
|
|
4832
4641
|
} catch (err) {
|
|
4833
|
-
state2.executionJournal.complete(
|
|
4642
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
4834
4643
|
throw err;
|
|
4835
4644
|
}
|
|
4836
|
-
state2.executionJournal.complete(
|
|
4645
|
+
state2.executionJournal.complete(receiptId, "done");
|
|
4837
4646
|
state2.executionLog.record({
|
|
4838
4647
|
profileId: proposal.profileId,
|
|
4839
4648
|
path: proposal.path,
|
|
@@ -5945,6 +5754,15 @@ var IntegrationManager = class {
|
|
|
5945
5754
|
const refusal = await def?.validate?.(args);
|
|
5946
5755
|
return refusal ? `Refused: ${refusal} No ticket was requested.` : null;
|
|
5947
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
|
+
}
|
|
5948
5766
|
/**
|
|
5949
5767
|
* Proxy a tool call to a downstream MCP server (or a built-in's handler).
|
|
5950
5768
|
*/
|
|
@@ -6278,252 +6096,11 @@ function createConnectorExportRunner(config) {
|
|
|
6278
6096
|
};
|
|
6279
6097
|
}
|
|
6280
6098
|
|
|
6281
|
-
// src/lib/report/render-report.ts
|
|
6282
|
-
import { Parser } from "htmlparser2";
|
|
6283
|
-
function findElementSpans(html) {
|
|
6284
|
-
const spans = [];
|
|
6285
|
-
const seenCount = /* @__PURE__ */ new Map();
|
|
6286
|
-
const openStack = [];
|
|
6287
|
-
let parserRef;
|
|
6288
|
-
const parser = new Parser(
|
|
6289
|
-
{
|
|
6290
|
-
onparserinit(p) {
|
|
6291
|
-
parserRef = p;
|
|
6292
|
-
},
|
|
6293
|
-
onopentag(name) {
|
|
6294
|
-
if (!name.startsWith("sv-") || !parserRef) return;
|
|
6295
|
-
const n = seenCount.get(name) ?? 0;
|
|
6296
|
-
seenCount.set(name, n + 1);
|
|
6297
|
-
openStack.push({ tag: name, id: `${name}-${n}`, start: parserRef.startIndex });
|
|
6298
|
-
},
|
|
6299
|
-
onclosetag(name) {
|
|
6300
|
-
if (!name.startsWith("sv-") || !parserRef) return;
|
|
6301
|
-
const idx = [...openStack].reverse().findIndex((o) => o.tag === name);
|
|
6302
|
-
if (idx === -1) return;
|
|
6303
|
-
const realIdx = openStack.length - 1 - idx;
|
|
6304
|
-
const [open] = openStack.splice(realIdx, 1);
|
|
6305
|
-
spans.push({ id: open.id, kind: open.tag, start: open.start, end: parserRef.endIndex + 1 });
|
|
6306
|
-
}
|
|
6307
|
-
},
|
|
6308
|
-
{ decodeEntities: true, recognizeSelfClosing: true }
|
|
6309
|
-
);
|
|
6310
|
-
parser.write(html);
|
|
6311
|
-
parser.end();
|
|
6312
|
-
return spans.sort((a, b) => a.start - b.start);
|
|
6313
|
-
}
|
|
6314
|
-
function escapeHtml(value) {
|
|
6315
|
-
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6316
|
-
}
|
|
6317
|
-
function badge(status, okLabel) {
|
|
6318
|
-
if (status === "verified") return `<span class="sv-badge sv-badge-ok">\u2713 ${escapeHtml(okLabel)}</span>`;
|
|
6319
|
-
if (status === "warning") return `<span class="sv-badge sv-badge-warn">\u26A0 verified with a note</span>`;
|
|
6320
|
-
return `<span class="sv-badge sv-badge-bad">\u2717 not verifiable</span>`;
|
|
6321
|
-
}
|
|
6322
|
-
function unverifiableCard(el) {
|
|
6323
|
-
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>`;
|
|
6324
|
-
}
|
|
6325
|
-
function renderTicket(el) {
|
|
6326
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6327
|
-
const d = el.data;
|
|
6328
|
-
const href = typeof d.checkUrl === "string" ? d.checkUrl : void 0;
|
|
6329
|
-
const actionLabel = typeof d.actionLabel === "string" ? d.actionLabel : formatActionLabel(d.action);
|
|
6330
|
-
const timeLabel = typeof d.timeLabel === "string" ? d.timeLabel : formatDateTime(d.time);
|
|
6331
|
-
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : escapeHtml(d.profile ?? "");
|
|
6332
|
-
const links = [
|
|
6333
|
-
href ? `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">Check on suveren.ai \u2197</a>` : ""
|
|
6334
|
-
].filter(Boolean);
|
|
6335
|
-
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>`;
|
|
6336
|
-
}
|
|
6337
|
-
function renderApproval(el) {
|
|
6338
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6339
|
-
const d = el.data;
|
|
6340
|
-
const who = typeof d.whoLabel === "string" ? d.whoLabel : Array.isArray(d.who) ? d.who.join(", ") : "unknown";
|
|
6341
|
-
const asked = typeof d.createdAtLabel === "string" ? d.createdAtLabel : "unknown";
|
|
6342
|
-
const approved = typeof d.decidedAtLabel === "string" ? d.decidedAtLabel : "unknown";
|
|
6343
|
-
const waited = typeof d.waitLabel === "string" ? d.waitLabel : "unknown";
|
|
6344
|
-
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></div>`;
|
|
6345
|
-
}
|
|
6346
|
-
function renderMandate(el) {
|
|
6347
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6348
|
-
const d = el.data;
|
|
6349
|
-
const profileLabel = typeof d.profileLabel === "string" ? d.profileLabel : String(d.profile ?? "");
|
|
6350
|
-
const owners = Array.isArray(d.owners) && d.owners.length > 0 ? d.owners.join(", ") : "unknown owner";
|
|
6351
|
-
const limits = Array.isArray(d.limits) && d.limits.length > 0 ? d.limits.join(" \xB7 ") : "";
|
|
6352
|
-
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>` : "") + `</div>`;
|
|
6353
|
-
}
|
|
6354
|
-
var RECORD_FIELD_ORDER = [
|
|
6355
|
-
"subject",
|
|
6356
|
-
"from_email",
|
|
6357
|
-
"from_name",
|
|
6358
|
-
"to_json",
|
|
6359
|
-
"received_at",
|
|
6360
|
-
"number",
|
|
6361
|
-
"status",
|
|
6362
|
-
"net_total",
|
|
6363
|
-
"customer_id",
|
|
6364
|
-
"causingReceiptId",
|
|
6365
|
-
"message"
|
|
6366
|
-
];
|
|
6367
|
-
var RECORD_TIME_FIELDS = /* @__PURE__ */ new Set(["received_at", "created_at", "sent_at"]);
|
|
6368
|
-
function renderRecordField(key, d) {
|
|
6369
|
-
if (key === "net_total") {
|
|
6370
|
-
const currency = typeof d.currency === "string" ? d.currency : void 0;
|
|
6371
|
-
return `<div class="sv-el-row"><span>Value</span><span>${escapeHtml(formatCurrency(d.net_total, currency))}</span></div>`;
|
|
6372
|
-
}
|
|
6373
|
-
if (RECORD_TIME_FIELDS.has(key)) {
|
|
6374
|
-
const t = parseTimestampSeconds(d[key]);
|
|
6375
|
-
const display = t !== void 0 ? formatDateTime(t) : d[key];
|
|
6376
|
-
return `<div class="sv-el-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(display)}</span></div>`;
|
|
6377
|
-
}
|
|
6378
|
-
return `<div class="sv-el-row"><span>${escapeHtml(key)}</span><span>${escapeHtml(d[key])}</span></div>`;
|
|
6379
|
-
}
|
|
6380
|
-
function renderRecord(el) {
|
|
6381
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6382
|
-
const d = el.data;
|
|
6383
|
-
const kind = String(d.kind ?? "record");
|
|
6384
|
-
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("");
|
|
6385
|
-
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>`;
|
|
6386
|
-
}
|
|
6387
|
-
function renderCaseTicketStep(node, isGoal) {
|
|
6388
|
-
const cls = isGoal ? "sv-step sv-step-goal" : "sv-step";
|
|
6389
|
-
const kind = isGoal ? "goal" : "ticket";
|
|
6390
|
-
const inner = `<div class="sv-step-k">${kind}</div><div class="sv-step-t">${escapeHtml(formatActionLabel(node.action))}</div>${formatDateTime(node.time)}`;
|
|
6391
|
-
return `<div class="${cls}">${inner}</div>`;
|
|
6392
|
-
}
|
|
6393
|
-
function renderCaseApprovalStep(approval) {
|
|
6394
|
-
const who = approval.whoLabel ?? (Array.isArray(approval.who) ? approval.who.join(", ") : "unknown");
|
|
6395
|
-
const asked = approval.createdAtLabel ?? "unknown";
|
|
6396
|
-
const decided = approval.decidedAtLabel ?? "unknown";
|
|
6397
|
-
const waited = approval.waitLabel ?? "unknown";
|
|
6398
|
-
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>`;
|
|
6399
|
-
}
|
|
6400
|
-
function renderCaseStartStep(start) {
|
|
6401
|
-
let sub = "";
|
|
6402
|
-
if (start.basis === "loaded" && typeof start.emailTime === "number") {
|
|
6403
|
-
sub = `<div class="sv-step-sub">Email dated ${escapeHtml(formatDateTime(start.emailTime))} \xB7 timed from when the test data was loaded</div>`;
|
|
6404
|
-
} else if (start.basis === "load-unknown") {
|
|
6405
|
-
sub = `<div class="sv-step-sub">Email dated ${escapeHtml(formatDateTime(start.emailTime ?? start.time))} \xB7 start time not verifiable</div>`;
|
|
6406
|
-
}
|
|
6407
|
-
const time = start.basis === "load-unknown" ? "" : escapeHtml(formatDateTime(start.time));
|
|
6408
|
-
return `<div class="sv-step sv-step-start"><div class="sv-step-k">start</div><div class="sv-step-t">Email in</div>${time}${sub}</div>`;
|
|
6409
|
-
}
|
|
6410
|
-
function renderCase(el) {
|
|
6411
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6412
|
-
const d = el.data;
|
|
6413
|
-
const steps = Array.isArray(d.steps) ? d.steps : [];
|
|
6414
|
-
const goal = d.goal;
|
|
6415
|
-
const approvalsByTicket = /* @__PURE__ */ new Map();
|
|
6416
|
-
if (Array.isArray(d.approvals)) {
|
|
6417
|
-
for (const a of d.approvals) {
|
|
6418
|
-
approvalsByTicket.set(a.ticketId, a);
|
|
6419
|
-
}
|
|
6420
|
-
}
|
|
6421
|
-
const start = d.start;
|
|
6422
|
-
const startStep = start ? { time: start.time, html: renderCaseStartStep(start) } : void 0;
|
|
6423
|
-
const ticketNodes = [
|
|
6424
|
-
...steps.map((s) => ({ node: s, isGoal: false })),
|
|
6425
|
-
...goal ? [{ node: goal, isGoal: true }] : []
|
|
6426
|
-
];
|
|
6427
|
-
const items = [];
|
|
6428
|
-
if (startStep) items.push(startStep);
|
|
6429
|
-
for (const { node, isGoal } of ticketNodes) {
|
|
6430
|
-
const approval = approvalsByTicket.get(node.ticketId);
|
|
6431
|
-
if (approval) {
|
|
6432
|
-
const approvalTime = Math.min(approval.decidedAt ?? approval.createdAt ?? node.time, node.time);
|
|
6433
|
-
items.push({ time: approvalTime, html: renderCaseApprovalStep(approval) });
|
|
6434
|
-
}
|
|
6435
|
-
items.push({ time: node.time, html: renderCaseTicketStep(node, isGoal) });
|
|
6436
|
-
}
|
|
6437
|
-
items.sort((a, b) => a.time - b.time);
|
|
6438
|
-
const stepHtml = items.map((i) => i.html).join("");
|
|
6439
|
-
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 ${typeof d.totalDurationSeconds === "number" ? formatDuration(d.totalDurationSeconds) : "time not verifiable"}</b>${badge(el.status, "assembled by gateway")}</div>` + (el.status === "warning" && el.reason ? `<div class="sv-el-warn">${escapeHtml(el.reason)}</div>` : "") + (typeof d.totalDurationSeconds !== "number" ? `<div class="sv-el-warn">${escapeHtml(d.timeUnverifiableReason ?? "Case time not verifiable.")}</div>` : "") + `<div class="sv-tl">${stepHtml}</div></div>`;
|
|
6440
|
-
}
|
|
6441
|
-
function renderMetric(el) {
|
|
6442
|
-
if (el.status === "unverifiable" || !el.data) return unverifiableCard(el);
|
|
6443
|
-
const d = el.data;
|
|
6444
|
-
const kind = String(d.kind ?? "");
|
|
6445
|
-
const label = METRIC_LABELS[kind] ?? kind;
|
|
6446
|
-
const display = formatMetricValue(kind, d.value);
|
|
6447
|
-
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>`;
|
|
6448
|
-
}
|
|
6449
|
-
function renderElement(el) {
|
|
6450
|
-
switch (el.kind) {
|
|
6451
|
-
case "sv-ticket":
|
|
6452
|
-
return renderTicket(el);
|
|
6453
|
-
case "sv-approval":
|
|
6454
|
-
return renderApproval(el);
|
|
6455
|
-
case "sv-mandate":
|
|
6456
|
-
return renderMandate(el);
|
|
6457
|
-
case "sv-record":
|
|
6458
|
-
return renderRecord(el);
|
|
6459
|
-
case "sv-case":
|
|
6460
|
-
return renderCase(el);
|
|
6461
|
-
case "sv-metric":
|
|
6462
|
-
return renderMetric(el);
|
|
6463
|
-
default:
|
|
6464
|
-
return unverifiableCard(el);
|
|
6465
|
-
}
|
|
6466
|
-
}
|
|
6467
|
-
var DRAWN_ELEMENT_STYLES = `
|
|
6468
|
-
.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; }
|
|
6469
|
-
.sv-el-unverifiable { outline:2px solid #b91c1c; outline-offset:2px; }
|
|
6470
|
-
.sv-el-verified, .sv-el-warning { outline:2px solid #15803d; outline-offset:2px; }
|
|
6471
|
-
.sv-el-row { display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; margin:2px 0; }
|
|
6472
|
-
.sv-el-reason, .sv-el-warn { color:#b45309; font-size:12px; margin-top:4px; }
|
|
6473
|
-
.sv-el-intent { color:#666; font-size:12px; margin-top:4px; }
|
|
6474
|
-
.sv-badge { display:inline-flex; gap:4px; font-size:10.5px; font-weight:600; padding:1px 7px; border-radius:999px; white-space:nowrap; }
|
|
6475
|
-
.sv-badge-ok { color:#15803d; background:#ecfdf3; }
|
|
6476
|
-
.sv-badge-warn { color:#b45309; background:#fff7e6; }
|
|
6477
|
-
.sv-badge-bad { color:#b91c1c; background:#fdecea; }
|
|
6478
|
-
.sv-big { font-size:24px; font-weight:700; }
|
|
6479
|
-
.sv-tl { display:flex; flex-wrap:wrap; gap:10px 16px; padding:6px 0; }
|
|
6480
|
-
.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; }
|
|
6481
|
-
.sv-step-k { font-size:9.5px; text-transform:uppercase; color:#666; }
|
|
6482
|
-
.sv-step-t { font-weight:600; font-size:12px; }
|
|
6483
|
-
.sv-step-sub { color:#666; white-space:normal; }
|
|
6484
|
-
/* The approval step's text ("asked ... approved ... by ... (duration)") is
|
|
6485
|
-
* longer than a ticket step's \u2014 without a width to wrap against, a
|
|
6486
|
-
* flex item with flex-shrink:0 takes its one-line content width instead of
|
|
6487
|
-
* wrapping, which reads as the text being cut off at the container's visible
|
|
6488
|
-
* edge (polish 2026-10-05, second pass). */
|
|
6489
|
-
.sv-step-approval { max-width:240px; white-space:normal; }
|
|
6490
|
-
.sv-step-start { max-width:240px; white-space:normal; }
|
|
6491
|
-
@media (max-width: 480px) {
|
|
6492
|
-
.sv-tl { flex-direction:column; overflow-x:visible; }
|
|
6493
|
-
.sv-step { min-width:0; }
|
|
6494
|
-
.sv-step-approval { max-width:none; }
|
|
6495
|
-
.sv-step-start { max-width:none; }
|
|
6496
|
-
.sv-el-row { flex-direction:column; gap:2px; }
|
|
6497
|
-
}
|
|
6498
|
-
`;
|
|
6499
|
-
var AI_ANALYSIS_LABEL = "AI analysis \u2014 not verified";
|
|
6500
|
-
var AI_LEGEND_TEXT = "Boxes with a green \u2713 are drawn and checked by the gateway, and each one is listed under \u201CChecked values\u201D. Everything else is the AI's own analysis and is not verified.";
|
|
6501
|
-
function aiLegendBanner() {
|
|
6502
|
-
const box = "display:block !important;visibility:visible !important;opacity:1 !important;position:relative !important;transform:none !important;clip-path:none !important;filter:none !important;z-index:2147483647 !important;margin:0 0 12px 0 !important;padding:8px 12px !important;border:1px solid #d4d4d8 !important;border-left:4px solid #a1a1aa !important;border-radius:8px !important;background:#f4f4f5 !important;color:#3f3f46 !important;font:12.5px/1.45 system-ui, sans-serif !important;text-align:left !important;max-width:none !important;width:auto !important;height:auto !important;";
|
|
6503
|
-
const strong = "display:inline !important;visibility:visible !important;font-weight:700 !important;color:#18181b !important;font-size:inherit !important;";
|
|
6504
|
-
return `<div class="sv-ai-legend" role="note" style="${box}"><b style="${strong}">${escapeHtml(AI_ANALYSIS_LABEL)}.</b> ${escapeHtml(AI_LEGEND_TEXT)}</div>`;
|
|
6505
|
-
}
|
|
6506
|
-
function renderReportHtml(rawHtml, elements) {
|
|
6507
|
-
const html = sanitizeReportHtml(rawHtml);
|
|
6508
|
-
const byId = new Map(elements.map((e) => [e.id, e]));
|
|
6509
|
-
const spans = findElementSpans(html);
|
|
6510
|
-
let out = "";
|
|
6511
|
-
let cursor = 0;
|
|
6512
|
-
for (const span of spans) {
|
|
6513
|
-
out += html.slice(cursor, span.start);
|
|
6514
|
-
const el = byId.get(span.id) ?? { id: span.id, kind: span.kind, attrs: {}, status: "unverifiable", reason: "No verification result for this element." };
|
|
6515
|
-
out += renderElement(el);
|
|
6516
|
-
cursor = span.end;
|
|
6517
|
-
}
|
|
6518
|
-
out += html.slice(cursor);
|
|
6519
|
-
const styleTag = `<style data-sv-drawn-styles="1">${DRAWN_ELEMENT_STYLES}</style>${aiLegendBanner()}`;
|
|
6520
|
-
if (/<body[^>]*>/i.test(out)) {
|
|
6521
|
-
return out.replace(/<body([^>]*)>/i, (m) => `${m}${styleTag}`);
|
|
6522
|
-
}
|
|
6523
|
-
return styleTag + out;
|
|
6524
|
-
}
|
|
6525
|
-
|
|
6526
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
|
+
}
|
|
6527
6104
|
async function buildTicketDetails(archive, ticketIds) {
|
|
6528
6105
|
const out = {};
|
|
6529
6106
|
for (const id of ticketIds) {
|
|
@@ -6532,118 +6109,170 @@ async function buildTicketDetails(archive, ticketIds) {
|
|
|
6532
6109
|
resolveApprovalElement(archive, id),
|
|
6533
6110
|
resolveMandateElement(archive, id)
|
|
6534
6111
|
]);
|
|
6535
|
-
out[id] = {
|
|
6112
|
+
out[id] = {
|
|
6113
|
+
ticket: withFields(ticket, ticketFields),
|
|
6114
|
+
approval: withFields(approval, approvalFields),
|
|
6115
|
+
mandate: withFields(mandate, mandateFields)
|
|
6116
|
+
};
|
|
6536
6117
|
}
|
|
6537
6118
|
return out;
|
|
6538
6119
|
}
|
|
6539
6120
|
|
|
6540
|
-
// src/lib/report/
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
const
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
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.`;
|
|
6555
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);
|
|
6556
6179
|
return {
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
tickets,
|
|
6566
|
-
authorizations
|
|
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
|
+
}
|
|
6567
6188
|
};
|
|
6568
6189
|
}
|
|
6569
|
-
function
|
|
6570
|
-
return
|
|
6571
|
-
}
|
|
6572
|
-
function
|
|
6573
|
-
const
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
const
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
}
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
function
|
|
6596
|
-
if (
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
.
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
.
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
}
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
}
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
const
|
|
6626
|
-
const
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
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);
|
|
6268
|
+
};
|
|
6269
|
+
return { archive: windowArchive(base.archive, window), runExport, window };
|
|
6270
|
+
}
|
|
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) };
|
|
6647
6276
|
}
|
|
6648
6277
|
|
|
6649
6278
|
// src/lib/profile-loader.ts
|
|
@@ -6714,10 +6343,19 @@ var CRM_RECORD_KINDS = ["contacts", "deals", "tasks", "activities", "changes", "
|
|
|
6714
6343
|
function errorText(text) {
|
|
6715
6344
|
return { content: [{ type: "text", text }], isError: true };
|
|
6716
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
|
+
}
|
|
6717
6355
|
function listTicketsTool(deps) {
|
|
6718
6356
|
return {
|
|
6719
6357
|
name: "list_tickets",
|
|
6720
|
-
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.",
|
|
6721
6359
|
inputSchema: {
|
|
6722
6360
|
type: "object",
|
|
6723
6361
|
properties: {
|
|
@@ -6726,30 +6364,22 @@ function listTicketsTool(deps) {
|
|
|
6726
6364
|
}
|
|
6727
6365
|
},
|
|
6728
6366
|
handler: async (args) => {
|
|
6367
|
+
const scoped = await scope(deps);
|
|
6368
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6729
6369
|
const since = typeof args.since === "number" ? args.since : void 0;
|
|
6730
6370
|
const until = typeof args.until === "number" ? args.until : void 0;
|
|
6731
|
-
const tickets =
|
|
6371
|
+
const tickets = scoped.sources.archive.getReceipts().map((entry) => {
|
|
6732
6372
|
const r = entry.receipt;
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
time,
|
|
6737
|
-
action: r.action,
|
|
6738
|
-
actionType: r.actionType ?? null,
|
|
6739
|
-
profile: r.profileId,
|
|
6740
|
-
authorizationId: r.authorizationId,
|
|
6741
|
-
limitsUsed: r.limits ?? r.executionContext ?? {},
|
|
6742
|
-
hasApproval: Boolean(entry.proposal)
|
|
6743
|
-
};
|
|
6744
|
-
}).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));
|
|
6745
|
-
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));
|
|
6746
6376
|
}
|
|
6747
6377
|
};
|
|
6748
6378
|
}
|
|
6749
6379
|
function getTicketTool(deps) {
|
|
6750
6380
|
return {
|
|
6751
6381
|
name: "get_ticket",
|
|
6752
|
-
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.',
|
|
6753
6383
|
inputSchema: {
|
|
6754
6384
|
type: "object",
|
|
6755
6385
|
properties: { id: { type: "string", description: "The ticket id, as listed by list_tickets." } },
|
|
@@ -6758,15 +6388,17 @@ function getTicketTool(deps) {
|
|
|
6758
6388
|
handler: async (args) => {
|
|
6759
6389
|
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
6760
6390
|
if (!id) return errorText("id is required.");
|
|
6761
|
-
const
|
|
6391
|
+
const scoped = await scope(deps);
|
|
6392
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6393
|
+
const details = await buildTicketDetails(scoped.sources.archive, [id]);
|
|
6762
6394
|
const detail = details[id];
|
|
6763
6395
|
if (!detail || detail.ticket.status !== "verified") {
|
|
6764
6396
|
return errorText(detail?.ticket.reason ?? `No ticket "${id}" in the local archive.`);
|
|
6765
6397
|
}
|
|
6766
6398
|
return builtinText(JSON.stringify({
|
|
6767
|
-
ticket: detail.ticket.data,
|
|
6768
|
-
approval: detail.approval.status === "verified" ? { verified: true, ...detail.approval.data } : { verified: false, reason: detail.approval.reason },
|
|
6769
|
-
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 }
|
|
6770
6402
|
}, null, 2));
|
|
6771
6403
|
}
|
|
6772
6404
|
};
|
|
@@ -6774,12 +6406,14 @@ function getTicketTool(deps) {
|
|
|
6774
6406
|
function listCasesTool(deps) {
|
|
6775
6407
|
return {
|
|
6776
6408
|
name: "list_cases",
|
|
6777
|
-
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.",
|
|
6778
6410
|
inputSchema: { type: "object", properties: {} },
|
|
6779
6411
|
handler: async () => {
|
|
6412
|
+
const scoped = await scope(deps);
|
|
6413
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6780
6414
|
let exported;
|
|
6781
6415
|
try {
|
|
6782
|
-
exported = await
|
|
6416
|
+
exported = await scoped.sources.runExport("email");
|
|
6783
6417
|
} catch (err) {
|
|
6784
6418
|
return errorText(`Could not read email records: ${err instanceof Error ? err.message : String(err)}`);
|
|
6785
6419
|
}
|
|
@@ -6800,6 +6434,7 @@ function listCasesTool(deps) {
|
|
|
6800
6434
|
sentAt: m.received_at
|
|
6801
6435
|
}));
|
|
6802
6436
|
return builtinText(JSON.stringify({
|
|
6437
|
+
window: windowInfo(scoped.window),
|
|
6803
6438
|
testDataLoadedAt: exp.simulation_load?.loaded_at ?? null,
|
|
6804
6439
|
cases,
|
|
6805
6440
|
sent
|
|
@@ -6815,7 +6450,7 @@ function recordKindsFor(system) {
|
|
|
6815
6450
|
function getRecordsTool(deps) {
|
|
6816
6451
|
return {
|
|
6817
6452
|
name: "get_records",
|
|
6818
|
-
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.`,
|
|
6819
6454
|
inputSchema: {
|
|
6820
6455
|
type: "object",
|
|
6821
6456
|
properties: {
|
|
@@ -6829,9 +6464,11 @@ function getRecordsTool(deps) {
|
|
|
6829
6464
|
if (system !== "email" && system !== "erp" && system !== "crm") {
|
|
6830
6465
|
return errorText('system must be one of "email", "erp", "crm".');
|
|
6831
6466
|
}
|
|
6467
|
+
const scoped = await scope(deps);
|
|
6468
|
+
if (!scoped.ok) return errorText(scoped.reason);
|
|
6832
6469
|
let exported;
|
|
6833
6470
|
try {
|
|
6834
|
-
exported = await
|
|
6471
|
+
exported = await scoped.sources.runExport(system);
|
|
6835
6472
|
} catch (err) {
|
|
6836
6473
|
return errorText(`Could not read ${system} records: ${err instanceof Error ? err.message : String(err)}`);
|
|
6837
6474
|
}
|
|
@@ -6848,11 +6485,11 @@ function getRecordsTool(deps) {
|
|
|
6848
6485
|
for (const kind of kinds) {
|
|
6849
6486
|
records[kind] = exp[kind] ?? [];
|
|
6850
6487
|
}
|
|
6851
|
-
return builtinText(JSON.stringify({ system, records }, null, 2));
|
|
6488
|
+
return builtinText(JSON.stringify({ system, window: windowInfo(scoped.window), records }, null, 2));
|
|
6852
6489
|
}
|
|
6853
6490
|
};
|
|
6854
6491
|
}
|
|
6855
|
-
function summarizeWrite(result) {
|
|
6492
|
+
function summarizeWrite(result, rawHtml) {
|
|
6856
6493
|
const verified = result.elements.filter((e) => e.status === "verified");
|
|
6857
6494
|
const warnings = result.elements.filter((e) => e.status === "warning");
|
|
6858
6495
|
const unverifiable = result.elements.filter((e) => e.status === "unverifiable");
|
|
@@ -6868,7 +6505,20 @@ function summarizeWrite(result) {
|
|
|
6868
6505
|
lines.push("Not verifiable \u2014 fix these references and write again:");
|
|
6869
6506
|
for (const u of unverifiable) lines.push(` - ${u.kind} (${u.id}): ${u.reason ?? "unspecified"}`);
|
|
6870
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
|
+
}
|
|
6871
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.`);
|
|
6872
6522
|
if (cov.emailExportError) {
|
|
6873
6523
|
lines.push(`Coverage could not be checked: ${cov.emailExportError}`);
|
|
6874
6524
|
} else {
|
|
@@ -6903,9 +6553,15 @@ function writeReportTool(deps) {
|
|
|
6903
6553
|
},
|
|
6904
6554
|
required: ["html"]
|
|
6905
6555
|
},
|
|
6906
|
-
// Empty/oversize html
|
|
6907
|
-
//
|
|
6908
|
-
|
|
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
|
+
},
|
|
6909
6565
|
handler: async (args) => {
|
|
6910
6566
|
const refusal = writeReportRefusal(args);
|
|
6911
6567
|
if (refusal) return errorText(`Refused: ${refusal}`);
|
|
@@ -6913,9 +6569,11 @@ function writeReportTool(deps) {
|
|
|
6913
6569
|
if (deps.state.reportStore.isLocked()) {
|
|
6914
6570
|
return errorText("The report store is locked (the vault is not unlocked) \u2014 cannot save a report right now.");
|
|
6915
6571
|
}
|
|
6572
|
+
const scoped = await scope(deps);
|
|
6573
|
+
if (!scoped.ok) return errorText(`Refused: ${scoped.reason}`);
|
|
6916
6574
|
try {
|
|
6917
|
-
const stored = await deps.state.reportStore.saveReport(html,
|
|
6918
|
-
return builtinText(summarizeWrite(stored.result));
|
|
6575
|
+
const stored = await deps.state.reportStore.saveReport(html, scoped.sources);
|
|
6576
|
+
return builtinText(summarizeWrite(stored.result, html));
|
|
6919
6577
|
} catch (err) {
|
|
6920
6578
|
return errorText(`Could not verify and store the report: ${err instanceof Error ? err.message : String(err)}`);
|
|
6921
6579
|
}
|
|
@@ -6932,6 +6590,11 @@ var reportBuiltin = (deps) => ({
|
|
|
6932
6590
|
// read-only export CLI; writes only the local, vault-encrypted report file)
|
|
6933
6591
|
// — safe during a simulation-mode test.
|
|
6934
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),
|
|
6935
6598
|
toolGating: {
|
|
6936
6599
|
overrides: {
|
|
6937
6600
|
list_tickets: READ_GATE,
|
|
@@ -7767,12 +7430,36 @@ app.get("/internal/evidence", internalOnly, (_req, res) => {
|
|
|
7767
7430
|
gates: state.gateStore.getAll()
|
|
7768
7431
|
});
|
|
7769
7432
|
});
|
|
7770
|
-
|
|
7771
|
-
|
|
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;
|
|
7772
7456
|
return {
|
|
7773
7457
|
savedAt: stored.savedAt,
|
|
7774
7458
|
checkedAt: stored.checkedAt,
|
|
7775
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 } } : {},
|
|
7776
7463
|
proof: stored.result.proof,
|
|
7777
7464
|
coverage: stored.result.coverage,
|
|
7778
7465
|
elements: stored.result.elements,
|
|
@@ -7801,9 +7488,14 @@ app.post("/internal/report", internalOnly, async (req, res) => {
|
|
|
7801
7488
|
res.status(400).json({ error: "Missing required field: html (non-empty string)" });
|
|
7802
7489
|
return;
|
|
7803
7490
|
}
|
|
7491
|
+
const scoped = await reportScope();
|
|
7492
|
+
if (!scoped.ok) {
|
|
7493
|
+
res.status(409).json({ error: scoped.reason });
|
|
7494
|
+
return;
|
|
7495
|
+
}
|
|
7804
7496
|
try {
|
|
7805
|
-
const stored = await state.reportStore.saveReport(html,
|
|
7806
|
-
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) });
|
|
7807
7499
|
} catch (err) {
|
|
7808
7500
|
console.error("[Suveren MCP] /internal/report save failed:", err);
|
|
7809
7501
|
res.status(500).json({ error: err instanceof Error ? err.message : "Report verification failed" });
|
|
@@ -7814,13 +7506,23 @@ app.post("/internal/report/recheck", internalOnly, async (_req, res) => {
|
|
|
7814
7506
|
res.status(503).json({ error: "Vault locked \u2014 cannot recheck until sign-in." });
|
|
7815
7507
|
return;
|
|
7816
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
|
+
}
|
|
7817
7519
|
try {
|
|
7818
|
-
const stored = await state.reportStore.recheck(
|
|
7520
|
+
const stored = await state.reportStore.recheck(scoped.sources);
|
|
7819
7521
|
if (!stored) {
|
|
7820
7522
|
res.json({ report: null });
|
|
7821
7523
|
return;
|
|
7822
7524
|
}
|
|
7823
|
-
res.json({ report: await reportResponsePayload(stored) });
|
|
7525
|
+
res.json({ report: await reportResponsePayload(stored, scoped.sources.archive) });
|
|
7824
7526
|
} catch (err) {
|
|
7825
7527
|
console.error("[Suveren MCP] /internal/report/recheck failed:", err);
|
|
7826
7528
|
res.status(500).json({ error: err instanceof Error ? err.message : "Recheck failed" });
|
|
@@ -7838,9 +7540,19 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
|
|
|
7838
7540
|
res.status(503).json({ error: "Vault locked \u2014 the report is encrypted until sign-in." });
|
|
7839
7541
|
return;
|
|
7840
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
|
+
}
|
|
7841
7553
|
let stored;
|
|
7842
7554
|
try {
|
|
7843
|
-
stored = await state.reportStore.recheck(
|
|
7555
|
+
stored = await state.reportStore.recheck(scoped.sources);
|
|
7844
7556
|
} catch (err) {
|
|
7845
7557
|
console.error("[Suveren MCP] /internal/report/export recheck failed:", err);
|
|
7846
7558
|
res.status(500).json({ error: err instanceof Error ? err.message : "Recheck failed" });
|
|
@@ -7860,12 +7572,11 @@ app.get("/internal/report/export", internalOnly, async (req, res) => {
|
|
|
7860
7572
|
const gatewayVersion = typeof req.query.gatewayVersion === "string" ? req.query.gatewayVersion : "unknown";
|
|
7861
7573
|
const bundle = buildExportBundle({
|
|
7862
7574
|
stored,
|
|
7863
|
-
archive:
|
|
7575
|
+
archive: scoped.sources.archive,
|
|
7864
7576
|
authorityServer,
|
|
7865
7577
|
gatewayVersion
|
|
7866
7578
|
});
|
|
7867
|
-
const
|
|
7868
|
-
const document = buildExportDocument({ bundle, renderedHtml });
|
|
7579
|
+
const document = buildExportDocument({ bundle });
|
|
7869
7580
|
res.setHeader("Content-Type", "text/html; charset=utf-8").setHeader("Content-Disposition", `attachment; filename="${suggestedFilename(bundle)}"`).setHeader("Cache-Control", "no-store").send(document);
|
|
7870
7581
|
});
|
|
7871
7582
|
app.get("/internal/brief", internalOnly, (_req, res) => {
|