@zanii/blackbox 0.11.0 → 0.13.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/README.md CHANGED
@@ -83,6 +83,14 @@ verifyAnswerCredential(credential, lines, bodies, packs); // { ok, problems }: t
83
83
 
84
84
  The same checks run offline as pure functions: `toolHallucinations`, `groundingFindings`, `factsIn`, `loadReferencePacks`, `answerRisk`, `detectorAccuracy`.
85
85
 
86
+ ## New in 0.13.0
87
+
88
+ - `fromOtlp(body)`: OpenTelemetry spans (GenAI semconv or OpenInference) as session events, the mapping behind gateway v0.15's `POST /v1/traces`. Point any framework's OTLP exporter at the gateway with the session's token, and its spans join the record.
89
+
90
+ ## New in 0.12.0
91
+
92
+ - Tax credit notes: `taxCreditNote` / `renderTaxCreditNote`, the document and printable copy behind gateway v0.14's `POST /v1/invoices/:number/credit-notes`.
93
+
86
94
  ## New in 0.11.0
87
95
 
88
96
  - Facts against the record compare currencies and units: `AED 500` in a tool result no longer grounds `USD 500` in the answer, nor `500 g` grounds `500 mg` (`currencyCodes`, `unitOf`). Dates written with an English or Arabic month (`3 March 2026`, `3 مارس 2026`) are facts too.
@@ -95,5 +95,51 @@ export declare function taxInvoice(input: TaxInvoiceInput): {
95
95
  export type TaxInvoice = ReturnType<typeof taxInvoice>;
96
96
  /** spec/billing.md §5: the printable copy. */
97
97
  export declare function renderTaxInvoice(d: TaxInvoice): string;
98
+ export interface TaxCreditNoteInput {
99
+ number: string;
100
+ issued_at: string;
101
+ /** The invoice it corrects, as issued. */
102
+ invoice: TaxInvoice;
103
+ reason: string;
104
+ /** What this note credits, before VAT. */
105
+ subtotal_fils: number;
106
+ /** What earlier notes on the invoice credited already. */
107
+ credited?: {
108
+ subtotal_fils: number;
109
+ vat_fils: number;
110
+ };
111
+ }
112
+ /** spec/billing.md §5a: a tax credit note. The note that credits what's left of the invoice takes
113
+ * the rest of its VAT exactly, so the notes never credit more VAT than was invoiced. */
114
+ export declare function taxCreditNote(input: TaxCreditNoteInput): {
115
+ v: 1;
116
+ title: "Tax Credit Note";
117
+ number: string;
118
+ issued_at: string;
119
+ invoice: {
120
+ number: string;
121
+ issued_at: string;
122
+ };
123
+ reason: string;
124
+ supplier: {
125
+ name: string;
126
+ address: string;
127
+ trn: string;
128
+ };
129
+ customer: {
130
+ legal_name: string;
131
+ address: string;
132
+ trn: string | null;
133
+ };
134
+ currency: "AED";
135
+ subtotal_fils: number;
136
+ vat_percent: number;
137
+ vat_fils: number;
138
+ total_fils: number;
139
+ total_aed: string;
140
+ };
141
+ export type TaxCreditNote = ReturnType<typeof taxCreditNote>;
142
+ /** spec/billing.md §5a: the printable copy. */
143
+ export declare function renderTaxCreditNote(d: TaxCreditNote): string;
98
144
  /** spec/billing.md §6: a `Stripe-Signature` header checked against the raw body. */
99
145
  export declare function stripeSignatureOk(header: string, rawBody: Uint8Array, secret: string, nowSeconds: number, toleranceSeconds?: number): boolean;
@@ -151,6 +151,64 @@ export function renderTaxInvoice(d) {
151
151
  ];
152
152
  return out.join("\n");
153
153
  }
154
+ /** spec/billing.md §5a: a tax credit note. The note that credits what's left of the invoice takes
155
+ * the rest of its VAT exactly, so the notes never credit more VAT than was invoiced. */
156
+ export function taxCreditNote(input) {
157
+ const inv = input.invoice;
158
+ const before = input.credited ?? { subtotal_fils: 0, vat_fils: 0 };
159
+ const last = input.subtotal_fils === inv.subtotal_fils - before.subtotal_fils;
160
+ const vat = last
161
+ ? inv.vat_fils - before.vat_fils
162
+ : Math.floor((input.subtotal_fils * inv.vat_percent + 50) / 100);
163
+ return {
164
+ v: 1,
165
+ title: "Tax Credit Note",
166
+ number: input.number,
167
+ issued_at: input.issued_at,
168
+ invoice: { number: inv.number, issued_at: inv.issued_at },
169
+ reason: input.reason,
170
+ supplier: inv.supplier,
171
+ customer: inv.customer,
172
+ currency: "AED",
173
+ subtotal_fils: input.subtotal_fils,
174
+ vat_percent: inv.vat_percent,
175
+ vat_fils: vat,
176
+ total_fils: input.subtotal_fils + vat,
177
+ total_aed: formatAed(input.subtotal_fils + vat),
178
+ };
179
+ }
180
+ /** spec/billing.md §5a: the printable copy. */
181
+ export function renderTaxCreditNote(d) {
182
+ return [
183
+ "# Tax Credit Note",
184
+ "",
185
+ "| Field | Value |",
186
+ "|---|---|",
187
+ `| Credit note number | ${cell(d.number)} |`,
188
+ `| Date of issue | ${d.issued_at} |`,
189
+ `| Corrects invoice | ${cell(d.invoice.number)} of ${d.invoice.issued_at} |`,
190
+ `| Reason | ${cell(oneLine(d.reason))} |`,
191
+ "",
192
+ "## Supplier",
193
+ "",
194
+ `${oneLine(d.supplier.name)} `,
195
+ `${oneLine(d.supplier.address)} `,
196
+ `TRN: ${d.supplier.trn}`,
197
+ "",
198
+ "## Customer",
199
+ "",
200
+ `${oneLine(d.customer.legal_name)} `,
201
+ `${oneLine(d.customer.address)} `,
202
+ `TRN: ${d.customer.trn ?? "not registered"}`,
203
+ "",
204
+ "| Credited | AED |",
205
+ "|---|---:|",
206
+ `| Subtotal | ${aed(d.subtotal_fils)} |`,
207
+ `| VAT ${d.vat_percent}% | ${aed(d.vat_fils)} |`,
208
+ `| **Total credited** | **${aed(d.total_fils)}** |`,
209
+ "",
210
+ ].join("\n");
211
+ }
154
212
  // ---------------------------------------------------------------- Stripe webhook signature (§6)
155
213
  /** spec/billing.md §6: a `Stripe-Signature` header checked against the raw body. */
156
214
  export function stripeSignatureOk(header, rawBody, secret, nowSeconds, toleranceSeconds = 300) {
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { archiveParquet } from "./archive/parquet.ts";
11
11
  export { type Attestation, attest, isReadOnly, normalise, parseCommand } from "./attest/index.ts";
12
12
  export { type AuthorityFinding, authorityAt, authorityTimeline, automationSurprise, type Mode as AuthorityMode, type Span as AuthoritySpan, } from "./authority/index.ts";
13
13
  export { badgeSvg, RECORD_STATES, type RecordState, recordBadge, recordState, } from "./badge/index.ts";
14
- export { type Invoice, type InvoiceLine, invoice, loadPlans, type Plan, type Plans, renderTaxInvoice, stripeSignatureOk, type TaxInvoice, type TaxInvoiceInput, taxInvoice, } from "./billing/index.ts";
14
+ export { type Invoice, type InvoiceLine, invoice, loadPlans, type Plan, type Plans, renderTaxCreditNote, renderTaxInvoice, stripeSignatureOk, type TaxCreditNote, type TaxCreditNoteInput, type TaxInvoice, type TaxInvoiceInput, taxCreditNote, taxInvoice, } from "./billing/index.ts";
15
15
  export { type AgentIdentity, checkAgent, sessionBom } from "./bom/index.ts";
16
16
  export { BlackboxApiError, type Client, type ClientOptions, client, type SessionQuery, } from "./client/index.ts";
17
17
  export { ART12_MIN_RETENTION_DAYS, type Art12Check, type Art12Report, art12Check, } from "./compliance/art12.ts";
@@ -42,7 +42,7 @@ export { commitContent, memoryChain, memoryEntry, verifyMemoryContent, verifyMem
42
42
  export { type AmountUnit, anthropicStatement, type Baseline, baselines, type Connector, checkConnector, checkStatement, csvStatement, customStatement, decimalToMicroUsd, fuel, modelSpend, openaiStatement, pageParams, parseXml, reconcileBilling, requestFor, resolvePath, type Statement, } from "./money/index.ts";
43
43
  export { deadlinesDue, filingPack, type OccurrenceFacts, type OccurrenceFramework, type OccurrenceReport, occurrenceFacts, occurrenceReport, renderOccurrence, } from "./occurrence/index.ts";
44
44
  export { ingestOcsf, type OcsfEvent, type OcsfVersion, ocsfLine, toOcsf } from "./ocsf/index.ts";
45
- export { toOtlp } from "./otlp/index.ts";
45
+ export { fromOtlp, type OtlpEvent, toOtlp } from "./otlp/index.ts";
46
46
  export { checkIdentifier, checkPack, corePack, type PackEntry, packData, packFrameworks, } from "./packs/index.ts";
47
47
  export { type PolicyChange, type PolicyDelta, policyDelta } from "./policy/delta.ts";
48
48
  export { type Draft, type DraftGroup, policyDrafts } from "./policy/drafts.ts";
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export { archiveParquet } from "./archive/parquet.js";
13
13
  export { attest, isReadOnly, normalise, parseCommand } from "./attest/index.js";
14
14
  export { authorityAt, authorityTimeline, automationSurprise, } from "./authority/index.js";
15
15
  export { badgeSvg, RECORD_STATES, recordBadge, recordState, } from "./badge/index.js";
16
- export { invoice, loadPlans, renderTaxInvoice, stripeSignatureOk, taxInvoice, } from "./billing/index.js";
16
+ export { invoice, loadPlans, renderTaxCreditNote, renderTaxInvoice, stripeSignatureOk, taxCreditNote, taxInvoice, } from "./billing/index.js";
17
17
  export { checkAgent, sessionBom } from "./bom/index.js";
18
18
  export { BlackboxApiError, client, } from "./client/index.js";
19
19
  export { ART12_MIN_RETENTION_DAYS, art12Check, } from "./compliance/art12.js";
@@ -44,7 +44,7 @@ export { commitContent, memoryChain, memoryEntry, verifyMemoryContent, verifyMem
44
44
  export { anthropicStatement, baselines, checkConnector, checkStatement, csvStatement, customStatement, decimalToMicroUsd, fuel, modelSpend, openaiStatement, pageParams, parseXml, reconcileBilling, requestFor, resolvePath, } from "./money/index.js";
45
45
  export { deadlinesDue, filingPack, occurrenceFacts, occurrenceReport, renderOccurrence, } from "./occurrence/index.js";
46
46
  export { ingestOcsf, ocsfLine, toOcsf } from "./ocsf/index.js";
47
- export { toOtlp } from "./otlp/index.js";
47
+ export { fromOtlp, toOtlp } from "./otlp/index.js";
48
48
  export { checkIdentifier, checkPack, corePack, packData, packFrameworks, } from "./packs/index.js";
49
49
  export { policyDelta } from "./policy/delta.js";
50
50
  export { policyDrafts } from "./policy/drafts.js";
@@ -28,4 +28,21 @@ export declare function toOtlp(lines: readonly string[], options?: {
28
28
  }[];
29
29
  }[];
30
30
  };
31
+ /** One event for `POST /v1/sessions/:id/sdk-events`, numbered by the server (spec/sdk.md §1). */
32
+ export interface OtlpEvent {
33
+ event_id: string;
34
+ type: string;
35
+ name: string;
36
+ ts: string;
37
+ data: Record<string, unknown>;
38
+ }
39
+ /**
40
+ * spec/otlp.md §4: OTLP/JSON traces as session events: one per span, typed from its GenAI or
41
+ * OpenInference attributes, its event_id `otel:<trace>:<span>` so a resent span adds nothing.
42
+ */
43
+ export declare function fromOtlp(body: unknown): {
44
+ events: OtlpEvent[];
45
+ } | {
46
+ error: string;
47
+ };
31
48
  export {};
@@ -398,3 +398,122 @@ function toOtlpV2(lines) {
398
398
  ],
399
399
  };
400
400
  }
401
+ const LLM_OPS = new Set(["chat", "text_completion", "generate_content", "embeddings"]);
402
+ const MAX_SPANS = 5000;
403
+ const MAX_DATA = 60_000;
404
+ function anyValue(v) {
405
+ if (!v || typeof v !== "object")
406
+ return null;
407
+ if (typeof v.stringValue === "string")
408
+ return v.stringValue;
409
+ if (typeof v.boolValue === "boolean")
410
+ return v.boolValue;
411
+ if (v.intValue !== undefined) {
412
+ const n = Number(v.intValue);
413
+ return Number.isSafeInteger(n) ? n : String(v.intValue);
414
+ }
415
+ if (typeof v.doubleValue === "number")
416
+ return v.doubleValue;
417
+ if (v.arrayValue)
418
+ return (v.arrayValue.values ?? []).map(anyValue);
419
+ if (v.kvlistValue)
420
+ return attributes(v.kvlistValue.values);
421
+ return null;
422
+ }
423
+ function attributes(list) {
424
+ const out = {};
425
+ if (!Array.isArray(list))
426
+ return out;
427
+ for (const kv of list)
428
+ if (typeof kv?.key === "string")
429
+ out[kv.key] = anyValue(kv.value);
430
+ return out;
431
+ }
432
+ /** The event type for a span, from GenAI semconv and OpenInference attributes. */
433
+ function spanType(a) {
434
+ const op = a["gen_ai.operation.name"];
435
+ const kind = a["openinference.span.kind"];
436
+ if ((typeof op === "string" && LLM_OPS.has(op)) || kind === "LLM" || kind === "EMBEDDING")
437
+ return "llm.call";
438
+ if (op === "execute_tool" || kind === "TOOL")
439
+ return "tool.call";
440
+ if (kind === "RETRIEVER")
441
+ return "retrieval";
442
+ if (op === "invoke_agent" || op === "create_agent" || kind === "AGENT" || kind === "CHAIN")
443
+ return "step";
444
+ return "otel.span";
445
+ }
446
+ const isoOfNanos = (n) => {
447
+ const s = typeof n === "string" || typeof n === "number" ? String(n) : "";
448
+ if (!/^[0-9]{1,20}$/.test(s))
449
+ return "";
450
+ return new Date(Number(BigInt(s) / 1000000n)).toISOString();
451
+ };
452
+ const size = (o) => new TextEncoder().encode(JSON.stringify(o)).length;
453
+ /**
454
+ * spec/otlp.md §4: OTLP/JSON traces as session events: one per span, typed from its GenAI or
455
+ * OpenInference attributes, its event_id `otel:<trace>:<span>` so a resent span adds nothing.
456
+ */
457
+ export function fromOtlp(body) {
458
+ const rs = body?.resourceSpans;
459
+ if (!body || typeof body !== "object" || !Array.isArray(rs))
460
+ return { error: "not OTLP/JSON traces (no resourceSpans)" };
461
+ const events = [];
462
+ for (const r of rs) {
463
+ const service = attributes(r?.resource?.attributes)["service.name"];
464
+ const scopes = Array.isArray(r?.scopeSpans) ? r.scopeSpans : [];
465
+ for (const ss of scopes) {
466
+ const spans = Array.isArray(ss?.spans) ? ss.spans : [];
467
+ for (const sp of spans) {
468
+ const trace = sp?.traceId;
469
+ const span = sp?.spanId;
470
+ if (typeof trace !== "string" || !/^[0-9a-f]{32}$/i.test(trace))
471
+ return { error: "a span without a 32-hex traceId" };
472
+ if (typeof span !== "string" || !/^[0-9a-f]{16}$/i.test(span))
473
+ return { error: "a span without a 16-hex spanId" };
474
+ if (events.length === MAX_SPANS)
475
+ return { error: `more than ${MAX_SPANS} spans` };
476
+ const attrs = attributes(sp.attributes);
477
+ const status = sp.status;
478
+ const data = {
479
+ source: "otlp",
480
+ ...(typeof service === "string" ? { service } : {}),
481
+ scope: {
482
+ name: typeof ss?.scope?.name === "string" ? ss.scope.name : "",
483
+ ...(typeof ss?.scope?.version === "string" ? { version: ss.scope.version } : {}),
484
+ },
485
+ trace_id: trace.toLowerCase(),
486
+ span_id: span.toLowerCase(),
487
+ ...(typeof sp.parentSpanId === "string" && sp.parentSpanId
488
+ ? { parent_span_id: sp.parentSpanId.toLowerCase() }
489
+ : {}),
490
+ kind: typeof sp.kind === "number" ? sp.kind : 0,
491
+ start_unix_nano: String(sp.startTimeUnixNano ?? ""),
492
+ end_unix_nano: String(sp.endTimeUnixNano ?? ""),
493
+ ...(status && typeof status.code === "number" && status.code !== 0
494
+ ? {
495
+ status: {
496
+ code: status.code,
497
+ ...(typeof status.message === "string" ? { message: status.message } : {}),
498
+ },
499
+ }
500
+ : {}),
501
+ attributes: attrs,
502
+ };
503
+ if (size(data) > MAX_DATA) {
504
+ // keep the small attributes (ids, models, counts), drop the long ones (prompts, outputs)
505
+ data.attributes = Object.fromEntries(Object.entries(attrs).filter(([, v]) => size(v) <= 1000));
506
+ data.truncated = true;
507
+ }
508
+ events.push({
509
+ event_id: `otel:${trace.toLowerCase()}:${span.toLowerCase()}`,
510
+ type: spanType(attrs),
511
+ name: typeof sp.name === "string" ? sp.name.slice(0, 256) : "span",
512
+ ts: isoOfNanos(sp.startTimeUnixNano),
513
+ data,
514
+ });
515
+ }
516
+ }
517
+ }
518
+ return { events };
519
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.11.0";
1
+ export declare const VERSION = "0.13.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // The SDK version, on its own so the CLI can print it without loading the whole SDK.
2
- export const VERSION = "0.11.0";
2
+ export const VERSION = "0.13.0";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zanii/blackbox",
3
3
  "license": "Apache-2.0",
4
- "version": "0.11.0",
4
+ "version": "0.13.0",
5
5
  "description": "The flight recorder for AI agents: sessions, a zero-loss spool, framework hooks, approvals, and offline verification of the gateway's hash-chained record.",
6
6
  "keywords": [
7
7
  "ai-agents",