@humain/humain-code-contracts 0.3.2 → 0.3.3
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/CHANGELOG.md +24 -0
- package/README.md +10 -1
- package/dist/trace-artifact.d.ts +1 -1
- package/dist/trace-artifact.d.ts.map +1 -1
- package/dist/trace-artifact.js +3 -1
- package/dist/trace-artifact.js.map +1 -1
- package/dist/trace-http.d.ts.map +1 -1
- package/dist/trace-http.js +3 -0
- package/dist/trace-http.js.map +1 -1
- package/dist/trace-payload.d.ts +25 -1
- package/dist/trace-payload.d.ts.map +1 -1
- package/dist/trace-payload.js +2 -0
- package/dist/trace-payload.js.map +1 -1
- package/dist/trace-records.d.ts +985 -0
- package/dist/trace-records.d.ts.map +1 -0
- package/dist/trace-records.js +535 -0
- package/dist/trace-records.js.map +1 -0
- package/dist/trace.d.ts +1 -0
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js +1 -0
- package/dist/trace.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-records.d.ts","sourceRoot":"","sources":["../src/trace-records.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { DH_COVERAGE_REASONS, DH_COVERAGE_STATES, DH_OUTCOMES, DhAgentIdSchema, DhAttemptIdSchema, DhEventIdSchema, DhGenerationIdSchema, } from "./trace.js";
|
|
3
|
+
import { assertDhPlainData, DhContractError, dhCanonicalText } from "./trace-canonical.js";
|
|
4
|
+
import { dhStrictDtoParser } from "./trace-http.js";
|
|
5
|
+
import { assertDhUsageMeasurement, dhUsageMeasurementSchema } from "./trace-payload.js";
|
|
6
|
+
/*
|
|
7
|
+
* Usage response/correction DTOs and records-policy (generation delete, legal hold) DTOs. Additions only:
|
|
8
|
+
* origin contractVersion 1, every existing /2 DTO and dh.batch/1-proposal are untouched, and DH_ROUTES is
|
|
9
|
+
* not changed (see DH_RECORDS_ROUTE_SCHEMAS for the route mapping).
|
|
10
|
+
*
|
|
11
|
+
* Usage rules (C0 decision 16): usage is submission-owned. Failed attempts keep their usage and every
|
|
12
|
+
* attempt is summed. Inclusive Flow aggregates are listed but contribute zero. Unknown usage or price is
|
|
13
|
+
* an explicit unknown/partial state, never zero. Corrections are append-only: a new record names the
|
|
14
|
+
* record it supersedes; nothing is overwritten.
|
|
15
|
+
*
|
|
16
|
+
* Hold precedence: an active legal hold always wins over deletion. A hold blocks the deletion claim; a
|
|
17
|
+
* claimed or tombstoned generation cannot accept a new hold.
|
|
18
|
+
*
|
|
19
|
+
* Like trace-payload.ts this module is loaded through the trace.ts import cycle: only typebox values are
|
|
20
|
+
* read at module top level; schemas and parsers are built lazily.
|
|
21
|
+
*/
|
|
22
|
+
export const DH_RECORDS_SCHEMA_IDS = Object.freeze({
|
|
23
|
+
generationDeleteRequest: "dh.generation-delete-request/1",
|
|
24
|
+
generationDeletion: "dh.generation-deletion/1",
|
|
25
|
+
legalHold: "dh.legal-hold/1",
|
|
26
|
+
legalHoldReleaseRequest: "dh.legal-hold-release-request/1",
|
|
27
|
+
legalHoldRequest: "dh.legal-hold-request/1",
|
|
28
|
+
usageCorrection: "dh.usage-correction/1",
|
|
29
|
+
usageResponse: "dh.usage-response/1",
|
|
30
|
+
usageSearchPage: "dh.usage-search-page/1",
|
|
31
|
+
});
|
|
32
|
+
/** Route to schema mapping. DH_ROUTES stays byte-identical; DELETE routes carry no body, so their request DTO is path-derived. */
|
|
33
|
+
export const DH_RECORDS_ROUTE_SCHEMAS = Object.freeze({
|
|
34
|
+
usage: Object.freeze({ response: DH_RECORDS_SCHEMA_IDS.usageResponse }),
|
|
35
|
+
search: Object.freeze({ response: DH_RECORDS_SCHEMA_IDS.usageSearchPage }),
|
|
36
|
+
generationDelete: Object.freeze({
|
|
37
|
+
request: DH_RECORDS_SCHEMA_IDS.generationDeleteRequest,
|
|
38
|
+
response: DH_RECORDS_SCHEMA_IDS.generationDeletion,
|
|
39
|
+
}),
|
|
40
|
+
legalHoldCreate: Object.freeze({
|
|
41
|
+
request: DH_RECORDS_SCHEMA_IDS.legalHoldRequest,
|
|
42
|
+
response: DH_RECORDS_SCHEMA_IDS.legalHold,
|
|
43
|
+
}),
|
|
44
|
+
legalHoldRelease: Object.freeze({ request: DH_RECORDS_SCHEMA_IDS.legalHoldReleaseRequest }),
|
|
45
|
+
});
|
|
46
|
+
export const DH_USAGE_PAGE_MAX_ITEMS = 100;
|
|
47
|
+
export const DH_USAGE_RESPONSE_MAX_ATTEMPTS = 4096;
|
|
48
|
+
export const DH_USAGE_MAX_CURRENCIES = 16;
|
|
49
|
+
export const DH_LEGAL_HOLD_REASONS = ["litigation", "regulatory-inquiry", "internal-investigation", "audit"];
|
|
50
|
+
export const DH_LEGAL_HOLD_STATES = ["active", "released"];
|
|
51
|
+
export const DH_DELETION_STATES = ["blocked-by-hold", "accepted", "claimed", "tombstoned"];
|
|
52
|
+
export const DH_USAGE_CORRECTION_REASONS = [
|
|
53
|
+
"provider-reconciliation",
|
|
54
|
+
"price-correction",
|
|
55
|
+
"capture-error",
|
|
56
|
+
"duplicate-owner",
|
|
57
|
+
];
|
|
58
|
+
export const DH_DELETION_MAX_HOLD_IDS = 64;
|
|
59
|
+
/** Upper bound for one currency amount; keeps float sums finite and the comparison tolerance meaningful. */
|
|
60
|
+
export const DH_USAGE_MAX_AMOUNT = 1e15;
|
|
61
|
+
const strict = (properties) => Type.Object(properties, { additionalProperties: false });
|
|
62
|
+
const count = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
63
|
+
const positive = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
|
|
64
|
+
const digest = Type.String({ pattern: "^[a-f0-9]{64}$" });
|
|
65
|
+
const timestamp = Type.String({
|
|
66
|
+
minLength: 20,
|
|
67
|
+
maxLength: 30,
|
|
68
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?Z$",
|
|
69
|
+
});
|
|
70
|
+
/** Opaque producer-side usage and submission identifier; same grammar as dh.payload.usage/1. */
|
|
71
|
+
const ident = Type.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$" });
|
|
72
|
+
const prefixed = (prefix) => Type.String({ pattern: `^${prefix}_[A-Za-z0-9_-]{8,128}$`, maxLength: prefix.length + 129 });
|
|
73
|
+
const refSchema = Type.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_.-]+$" });
|
|
74
|
+
const cursorSchema = Type.String({ minLength: 1, maxLength: 1024, pattern: "^[\\x21-\\x7e]+$" });
|
|
75
|
+
const currency = Type.String({ pattern: "^[A-Z]{3}$" });
|
|
76
|
+
function bad(label, reason) {
|
|
77
|
+
throw new DhContractError("schema_invalid", `Invalid ${label}: ${reason}`);
|
|
78
|
+
}
|
|
79
|
+
const unique = (ids) => new Set(ids).size === ids.length;
|
|
80
|
+
/** Fixed-width key so fractional-second instants compare lexicographically. */
|
|
81
|
+
const instantKey = (text) => {
|
|
82
|
+
const [whole = "", fraction = ""] = text.slice(0, -1).split(".");
|
|
83
|
+
return `${whole}.${fraction.padEnd(9, "0")}`;
|
|
84
|
+
};
|
|
85
|
+
function coverageSchema() {
|
|
86
|
+
// No detail: it can carry transcript text.
|
|
87
|
+
return strict({ state: Type.Enum(DH_COVERAGE_STATES), reason: Type.Optional(Type.Enum(DH_COVERAGE_REASONS)) });
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Usage totals
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
const USAGE_DIMENSIONS = [
|
|
93
|
+
"input",
|
|
94
|
+
"output",
|
|
95
|
+
"cacheRead",
|
|
96
|
+
"cacheWrite",
|
|
97
|
+
"reasoning",
|
|
98
|
+
"totalTokens",
|
|
99
|
+
"durationMs",
|
|
100
|
+
];
|
|
101
|
+
/** unknown: no counted submission reports it. known: every one does. partial: value is a lower bound. */
|
|
102
|
+
function dimensionSchema() {
|
|
103
|
+
return Type.Union([
|
|
104
|
+
strict({ state: Type.Literal("unknown"), unknownSubmissions: count }),
|
|
105
|
+
strict({ state: Type.Literal("known"), value: count, knownSubmissions: positive }),
|
|
106
|
+
strict({
|
|
107
|
+
state: Type.Literal("partial"),
|
|
108
|
+
value: count,
|
|
109
|
+
knownSubmissions: positive,
|
|
110
|
+
unknownSubmissions: positive,
|
|
111
|
+
}),
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
function currencyTotalSchema() {
|
|
115
|
+
return strict({
|
|
116
|
+
currency,
|
|
117
|
+
amount: Type.Number({ minimum: 0, maximum: DH_USAGE_MAX_AMOUNT }),
|
|
118
|
+
submissions: positive,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Price is never converted across currencies and never zero when unknown. */
|
|
122
|
+
function costTotalSchema() {
|
|
123
|
+
const currencies = Type.Array(currencyTotalSchema(), { minItems: 1, maxItems: DH_USAGE_MAX_CURRENCIES });
|
|
124
|
+
return Type.Union([
|
|
125
|
+
strict({ state: Type.Literal("unknown"), unknownSubmissions: count }),
|
|
126
|
+
strict({ state: Type.Literal("known"), byCurrency: currencies }),
|
|
127
|
+
strict({ state: Type.Literal("partial"), byCurrency: currencies, unknownSubmissions: positive }),
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
function totalsSchema() {
|
|
131
|
+
return strict({
|
|
132
|
+
/** Counted submission-owned records only: superseded records and inclusive Flow aggregates are excluded. */
|
|
133
|
+
submissions: count,
|
|
134
|
+
input: dimensionSchema(),
|
|
135
|
+
output: dimensionSchema(),
|
|
136
|
+
cacheRead: dimensionSchema(),
|
|
137
|
+
cacheWrite: dimensionSchema(),
|
|
138
|
+
reasoning: dimensionSchema(),
|
|
139
|
+
totalTokens: dimensionSchema(),
|
|
140
|
+
durationMs: dimensionSchema(),
|
|
141
|
+
cost: costTotalSchema(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const accOf = (d) => d.state === "unknown"
|
|
145
|
+
? { value: 0, known: 0, unknown: d.unknownSubmissions }
|
|
146
|
+
: { value: d.value, known: d.knownSubmissions, unknown: d.state === "partial" ? d.unknownSubmissions : 0 };
|
|
147
|
+
const dimensionOf = ({ value, known, unknown }) => known === 0
|
|
148
|
+
? { state: "unknown", unknownSubmissions: unknown }
|
|
149
|
+
: unknown === 0
|
|
150
|
+
? { state: "known", value, knownSubmissions: known }
|
|
151
|
+
: { state: "partial", value, knownSubmissions: known, unknownSubmissions: unknown };
|
|
152
|
+
/** Sums totals. Unknown never contributes a value; any unknown submission turns known into partial. */
|
|
153
|
+
export function sumDhUsageTotals(parts) {
|
|
154
|
+
const dimensions = Object.fromEntries(USAGE_DIMENSIONS.map((key) => {
|
|
155
|
+
const sum = { value: 0, known: 0, unknown: 0 };
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
const next = accOf(part[key]);
|
|
158
|
+
sum.value += next.value;
|
|
159
|
+
sum.known += next.known;
|
|
160
|
+
sum.unknown += next.unknown;
|
|
161
|
+
}
|
|
162
|
+
return [key, dimensionOf(sum)];
|
|
163
|
+
}));
|
|
164
|
+
const byCurrency = new Map();
|
|
165
|
+
let unknown = 0;
|
|
166
|
+
for (const { cost } of parts) {
|
|
167
|
+
if (cost.state !== "known")
|
|
168
|
+
unknown += cost.unknownSubmissions;
|
|
169
|
+
if (cost.state === "unknown")
|
|
170
|
+
continue;
|
|
171
|
+
for (const entry of cost.byCurrency) {
|
|
172
|
+
const total = byCurrency.get(entry.currency) ?? { amount: 0, submissions: 0 };
|
|
173
|
+
byCurrency.set(entry.currency, {
|
|
174
|
+
amount: total.amount + entry.amount,
|
|
175
|
+
submissions: total.submissions + entry.submissions,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const list = [...byCurrency]
|
|
180
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
181
|
+
.map(([code, total]) => ({ currency: code, ...total }));
|
|
182
|
+
const cost = list.length === 0
|
|
183
|
+
? { state: "unknown", unknownSubmissions: unknown }
|
|
184
|
+
: unknown === 0
|
|
185
|
+
? { state: "known", byCurrency: list }
|
|
186
|
+
: { state: "partial", byCurrency: list, unknownSubmissions: unknown };
|
|
187
|
+
return { submissions: parts.reduce((n, part) => n + part.submissions, 0), ...dimensions, cost };
|
|
188
|
+
}
|
|
189
|
+
/** Totals for counted submission-owned measurements. No measurements is unknown, not zero. */
|
|
190
|
+
export function dhUsageTotalsOf(measurements) {
|
|
191
|
+
return sumDhUsageTotals(measurements.map((measurement) => {
|
|
192
|
+
const reported = measurement.state === "reported" ? measurement : undefined;
|
|
193
|
+
const dimensions = Object.fromEntries(USAGE_DIMENSIONS.map((key) => {
|
|
194
|
+
const value = reported?.[key];
|
|
195
|
+
return [
|
|
196
|
+
key,
|
|
197
|
+
value === undefined
|
|
198
|
+
? { state: "unknown", unknownSubmissions: 1 }
|
|
199
|
+
: { state: "known", value, knownSubmissions: 1 },
|
|
200
|
+
];
|
|
201
|
+
}));
|
|
202
|
+
const cost = reported?.cost.state === "known"
|
|
203
|
+
? {
|
|
204
|
+
state: "known",
|
|
205
|
+
byCurrency: [{ currency: reported.cost.currency, amount: reported.cost.amount, submissions: 1 }],
|
|
206
|
+
}
|
|
207
|
+
: { state: "unknown", unknownSubmissions: 1 };
|
|
208
|
+
return { submissions: 1, ...dimensions, cost };
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
function assertTotals(label, totals) {
|
|
212
|
+
for (const key of USAGE_DIMENSIONS) {
|
|
213
|
+
const { known, unknown } = accOf(totals[key]);
|
|
214
|
+
if (known + unknown !== totals.submissions)
|
|
215
|
+
bad(label, `${key} does not account for every submission`);
|
|
216
|
+
}
|
|
217
|
+
const { cost } = totals;
|
|
218
|
+
if (cost.state === "unknown") {
|
|
219
|
+
if (cost.unknownSubmissions !== totals.submissions)
|
|
220
|
+
bad(label, "cost does not account for every submission");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const codes = cost.byCurrency.map((entry) => entry.currency);
|
|
224
|
+
if (codes.some((code, index) => index > 0 && code <= codes[index - 1]))
|
|
225
|
+
bad(label, "cost currencies must be unique and sorted");
|
|
226
|
+
const priced = cost.byCurrency.reduce((n, entry) => n + entry.submissions, 0);
|
|
227
|
+
if (priced + (cost.state === "partial" ? cost.unknownSubmissions : 0) !== totals.submissions)
|
|
228
|
+
bad(label, "cost does not account for every submission");
|
|
229
|
+
}
|
|
230
|
+
/** Amounts are decimal sums whose float order varies, so they compare with a relative tolerance; everything else is exact. */
|
|
231
|
+
function sameTotals(a, b) {
|
|
232
|
+
const zeroed = (totals) => dhCanonicalText((totals.cost.state === "unknown"
|
|
233
|
+
? totals
|
|
234
|
+
: {
|
|
235
|
+
...totals,
|
|
236
|
+
cost: { ...totals.cost, byCurrency: totals.cost.byCurrency.map((e) => ({ ...e, amount: 0 })) },
|
|
237
|
+
}));
|
|
238
|
+
const amounts = (totals) => totals.cost.state === "unknown" ? [] : totals.cost.byCurrency.map((entry) => entry.amount);
|
|
239
|
+
const other = amounts(b);
|
|
240
|
+
return ([...amounts(a), ...other].every(Number.isFinite) &&
|
|
241
|
+
zeroed(a) === zeroed(b) &&
|
|
242
|
+
amounts(a).every((x, i) => Math.abs(x - other[i]) <= 1e-9 * Math.max(1, Math.abs(x), Math.abs(other[i]))));
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Usage response, search page, correction
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
function usageResponseSchema() {
|
|
248
|
+
return strict({
|
|
249
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.usageResponse),
|
|
250
|
+
generationId: DhGenerationIdSchema,
|
|
251
|
+
captureCutoff: timestamp,
|
|
252
|
+
/** Sum of every attempt below, failed attempts included. */
|
|
253
|
+
totals: totalsSchema(),
|
|
254
|
+
attempts: Type.Array(strict({
|
|
255
|
+
agentId: DhAgentIdSchema,
|
|
256
|
+
attemptId: DhAttemptIdSchema,
|
|
257
|
+
/** Absent while the attempt has no recorded terminal outcome. */
|
|
258
|
+
outcome: Type.Optional(Type.Enum(DH_OUTCOMES)),
|
|
259
|
+
totals: totalsSchema(),
|
|
260
|
+
}), { maxItems: DH_USAGE_RESPONSE_MAX_ATTEMPTS }),
|
|
261
|
+
/** Appended correction records that replaced an earlier record; they are retained and contribute zero. */
|
|
262
|
+
supersededRecords: count,
|
|
263
|
+
/** Inclusive Flow aggregate records: listed by search, contribute zero to totals. */
|
|
264
|
+
inclusiveAggregateRecords: count,
|
|
265
|
+
coverage: coverageSchema(),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export const parseDhUsageResponse = dhStrictDtoParser("usage response", usageResponseSchema, (response) => {
|
|
269
|
+
const label = "usage response";
|
|
270
|
+
assertTotals(label, response.totals);
|
|
271
|
+
for (const attempt of response.attempts)
|
|
272
|
+
assertTotals(label, attempt.totals);
|
|
273
|
+
if (!unique(response.attempts.map((attempt) => attempt.attemptId)))
|
|
274
|
+
bad(label, "duplicate attempt");
|
|
275
|
+
if (response.coverage.state !== "captured" && response.coverage.reason === undefined)
|
|
276
|
+
bad(label, "non-captured coverage requires a reason");
|
|
277
|
+
if (!sameTotals(response.totals, sumDhUsageTotals(response.attempts.map((attempt) => attempt.totals))))
|
|
278
|
+
bad(label, "totals do not equal the sum of attempts");
|
|
279
|
+
});
|
|
280
|
+
const usageOriginSchema = () => Type.Union([
|
|
281
|
+
strict({ owner: Type.Literal("submission"), submissionId: ident }),
|
|
282
|
+
strict({ owner: Type.Literal("flow-aggregate"), inclusive: Type.Literal(true) }),
|
|
283
|
+
]);
|
|
284
|
+
function usageRecordSchema() {
|
|
285
|
+
return strict({
|
|
286
|
+
usageId: ident,
|
|
287
|
+
eventId: DhEventIdSchema,
|
|
288
|
+
generationId: DhGenerationIdSchema,
|
|
289
|
+
agentId: DhAgentIdSchema,
|
|
290
|
+
/** Required for submission-owned records so failed and retried attempts stay separable. */
|
|
291
|
+
attemptId: Type.Optional(DhAttemptIdSchema),
|
|
292
|
+
origin: usageOriginSchema(),
|
|
293
|
+
measurement: dhUsageMeasurementSchema(),
|
|
294
|
+
/** True only for a submission-owned record that no correction supersedes. */
|
|
295
|
+
counted: Type.Boolean(),
|
|
296
|
+
supersedesUsageId: Type.Optional(ident),
|
|
297
|
+
/** Query-time pointer to the correction that replaced this record; the stored record is never mutated. */
|
|
298
|
+
supersededByUsageId: Type.Optional(ident),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function assertUsageRecord(record) {
|
|
302
|
+
const label = "usage record";
|
|
303
|
+
assertDhUsageMeasurement(label, record.measurement);
|
|
304
|
+
const submission = record.origin.owner === "submission";
|
|
305
|
+
if (submission && record.attemptId === undefined)
|
|
306
|
+
bad(label, "submission-owned usage needs attemptId");
|
|
307
|
+
if (record.counted !== (submission && record.supersededByUsageId === undefined))
|
|
308
|
+
bad(label, "counted must be true only for a submission-owned record that is not superseded");
|
|
309
|
+
if (!submission && (record.supersedesUsageId !== undefined || record.supersededByUsageId !== undefined))
|
|
310
|
+
bad(label, "inclusive aggregates are never corrected");
|
|
311
|
+
if (record.supersedesUsageId === record.usageId || record.supersededByUsageId === record.usageId)
|
|
312
|
+
bad(label, "a record cannot supersede itself");
|
|
313
|
+
if (record.supersedesUsageId !== undefined && record.supersedesUsageId === record.supersededByUsageId)
|
|
314
|
+
bad(label, "supersession cycle");
|
|
315
|
+
}
|
|
316
|
+
export const parseDhUsageRecord = dhStrictDtoParser("usage record", usageRecordSchema, assertUsageRecord);
|
|
317
|
+
function usageSearchPageSchema() {
|
|
318
|
+
return strict({
|
|
319
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.usageSearchPage),
|
|
320
|
+
generationId: DhGenerationIdSchema,
|
|
321
|
+
captureCutoff: timestamp,
|
|
322
|
+
filterSha256: digest,
|
|
323
|
+
nextCursor: Type.Union([Type.Null(), cursorSchema]),
|
|
324
|
+
items: Type.Array(usageRecordSchema(), { maxItems: DH_USAGE_PAGE_MAX_ITEMS }),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
export const parseDhUsageSearchPage = dhStrictDtoParser("usage search page", usageSearchPageSchema, (page) => {
|
|
328
|
+
const label = "usage search page";
|
|
329
|
+
const byId = new Map();
|
|
330
|
+
for (const item of page.items) {
|
|
331
|
+
assertUsageRecord(item);
|
|
332
|
+
if (item.generationId !== page.generationId)
|
|
333
|
+
bad(label, "record belongs to a different generation");
|
|
334
|
+
if (byId.has(item.usageId))
|
|
335
|
+
bad(label, "duplicate usageId");
|
|
336
|
+
byId.set(item.usageId, item);
|
|
337
|
+
}
|
|
338
|
+
const owners = page.items.flatMap((item) => item.counted && item.origin.owner === "submission" ? [item.origin.submissionId] : []);
|
|
339
|
+
if (!unique(owners))
|
|
340
|
+
bad(label, "more than one counted owner for a submission");
|
|
341
|
+
const predecessors = page.items.flatMap((item) => item.supersedesUsageId === undefined ? [] : [item.supersedesUsageId]);
|
|
342
|
+
if (!unique(predecessors))
|
|
343
|
+
bad(label, "more than one record supersedes the same record");
|
|
344
|
+
const successors = page.items.flatMap((item) => item.supersededByUsageId === undefined ? [] : [item.supersededByUsageId]);
|
|
345
|
+
if (!unique(successors))
|
|
346
|
+
bad(label, "more than one record is superseded by the same record");
|
|
347
|
+
const sameSubmission = (a, b) => a.agentId === b.agentId &&
|
|
348
|
+
a.attemptId === b.attemptId &&
|
|
349
|
+
a.origin.owner === "submission" &&
|
|
350
|
+
b.origin.owner === "submission" &&
|
|
351
|
+
a.origin.submissionId === b.origin.submissionId;
|
|
352
|
+
for (const item of page.items) {
|
|
353
|
+
const older = item.supersedesUsageId === undefined ? undefined : byId.get(item.supersedesUsageId);
|
|
354
|
+
if (older !== undefined && older.supersededByUsageId !== item.usageId)
|
|
355
|
+
bad(label, "supersession pointers disagree");
|
|
356
|
+
const newer = item.supersededByUsageId === undefined ? undefined : byId.get(item.supersededByUsageId);
|
|
357
|
+
if (newer !== undefined && newer.supersedesUsageId !== item.usageId)
|
|
358
|
+
bad(label, "supersession pointers disagree");
|
|
359
|
+
if ((older !== undefined && !sameSubmission(older, item)) ||
|
|
360
|
+
(newer !== undefined && !sameSubmission(item, newer)))
|
|
361
|
+
bad(label, "supersession identity differs between agent, attempt and submission");
|
|
362
|
+
const seen = new Set([item.usageId]);
|
|
363
|
+
for (let next = newer; next !== undefined; next = byId.get(next.supersededByUsageId ?? "")) {
|
|
364
|
+
if (seen.has(next.usageId))
|
|
365
|
+
bad(label, "supersession cycle");
|
|
366
|
+
seen.add(next.usageId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
function usageCorrectionSchema() {
|
|
371
|
+
return strict({
|
|
372
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.usageCorrection),
|
|
373
|
+
/** The new record's id. Append-only: it never reuses the superseded id. */
|
|
374
|
+
usageId: ident,
|
|
375
|
+
supersedesUsageId: ident,
|
|
376
|
+
generationId: DhGenerationIdSchema,
|
|
377
|
+
agentId: DhAgentIdSchema,
|
|
378
|
+
attemptId: DhAttemptIdSchema,
|
|
379
|
+
submissionId: ident,
|
|
380
|
+
reason: Type.Enum(DH_USAGE_CORRECTION_REASONS),
|
|
381
|
+
recordedAt: timestamp,
|
|
382
|
+
/** Replacement measurement. An explicit unknown retracts a value; zero is never implied. */
|
|
383
|
+
measurement: dhUsageMeasurementSchema(),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
export const parseDhUsageCorrection = dhStrictDtoParser("usage correction", usageCorrectionSchema, (correction) => {
|
|
387
|
+
assertDhUsageMeasurement("usage correction", correction.measurement);
|
|
388
|
+
if (correction.usageId === correction.supersedesUsageId)
|
|
389
|
+
bad("usage correction", "a record cannot supersede itself");
|
|
390
|
+
});
|
|
391
|
+
/** One stored record id (and the id it supersedes, if any) from the caller's store; parsed through the plain-data gate. */
|
|
392
|
+
const parseStoredUsageId = dhStrictDtoParser("stored usage id", () => strict({ usageId: ident, supersedesUsageId: Type.Optional(ident) }));
|
|
393
|
+
/**
|
|
394
|
+
* Checks a correction against the stored record it supersedes and the records already stored.
|
|
395
|
+
* `existing` lists every stored usageId (originals and corrections; supersedesUsageId for corrections).
|
|
396
|
+
* Same submission, no fork (one correction per superseded record), no reuse of any stored id or of the
|
|
397
|
+
* superseded record's predecessor, only submission-owned targets.
|
|
398
|
+
*/
|
|
399
|
+
export function assertDhUsageCorrection(correctionInput, supersededInput, existing = []) {
|
|
400
|
+
const correction = parseDhUsageCorrection(correctionInput);
|
|
401
|
+
const superseded = parseDhUsageRecord(supersededInput);
|
|
402
|
+
const stored = existing.map(parseStoredUsageId);
|
|
403
|
+
if (superseded.origin.owner !== "submission" ||
|
|
404
|
+
superseded.usageId !== correction.supersedesUsageId ||
|
|
405
|
+
superseded.generationId !== correction.generationId ||
|
|
406
|
+
superseded.agentId !== correction.agentId ||
|
|
407
|
+
superseded.attemptId !== correction.attemptId ||
|
|
408
|
+
superseded.origin.submissionId !== correction.submissionId)
|
|
409
|
+
throw new DhContractError("usage_correction_mismatch", "Usage correction does not match the superseded record");
|
|
410
|
+
const storedIds = new Set([
|
|
411
|
+
superseded.usageId,
|
|
412
|
+
...(superseded.supersedesUsageId === undefined ? [] : [superseded.supersedesUsageId]),
|
|
413
|
+
...stored.flatMap((entry) => [
|
|
414
|
+
entry.usageId,
|
|
415
|
+
...(entry.supersedesUsageId === undefined ? [] : [entry.supersedesUsageId]),
|
|
416
|
+
]),
|
|
417
|
+
]);
|
|
418
|
+
if (superseded.supersededByUsageId !== undefined ||
|
|
419
|
+
stored.some((entry) => entry.supersedesUsageId === correction.supersedesUsageId) ||
|
|
420
|
+
storedIds.has(correction.usageId))
|
|
421
|
+
throw new DhContractError("usage_correction_conflict", "Usage correction conflicts with an appended correction");
|
|
422
|
+
}
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Records policy: generation delete and legal holds (hold precedence)
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
const holdId = () => prefixed("hld");
|
|
427
|
+
/** DELETE has no body, so this is the path-derived request context, not a wire body. */
|
|
428
|
+
export const parseDhGenerationDeleteRequest = dhStrictDtoParser("generation delete request", () => strict({ schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.generationDeleteRequest), generationId: DhGenerationIdSchema }));
|
|
429
|
+
/** Path-derived like the delete request; release is explicit and authorized, never implied by deletion. */
|
|
430
|
+
export const parseDhLegalHoldReleaseRequest = dhStrictDtoParser("legal hold release request", () => strict({
|
|
431
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.legalHoldReleaseRequest),
|
|
432
|
+
generationId: DhGenerationIdSchema,
|
|
433
|
+
holdId: holdId(),
|
|
434
|
+
}));
|
|
435
|
+
/** Reason is an enum and matterRef an opaque reference: no free text, so a hold cannot carry content. */
|
|
436
|
+
export const parseDhLegalHoldRequest = dhStrictDtoParser("legal hold request", () => strict({
|
|
437
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.legalHoldRequest),
|
|
438
|
+
requestId: prefixed("hlr"),
|
|
439
|
+
generationId: DhGenerationIdSchema,
|
|
440
|
+
reasonCode: Type.Enum(DH_LEGAL_HOLD_REASONS),
|
|
441
|
+
matterRef: Type.Optional(refSchema),
|
|
442
|
+
}));
|
|
443
|
+
function legalHoldSchema() {
|
|
444
|
+
return strict({
|
|
445
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.legalHold),
|
|
446
|
+
holdId: holdId(),
|
|
447
|
+
requestId: prefixed("hlr"),
|
|
448
|
+
generationId: DhGenerationIdSchema,
|
|
449
|
+
state: Type.Enum(DH_LEGAL_HOLD_STATES),
|
|
450
|
+
reasonCode: Type.Enum(DH_LEGAL_HOLD_REASONS),
|
|
451
|
+
matterRef: Type.Optional(refSchema),
|
|
452
|
+
placedAt: timestamp,
|
|
453
|
+
releasedAt: Type.Optional(timestamp),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
export const parseDhLegalHold = dhStrictDtoParser("legal hold", legalHoldSchema, (hold) => {
|
|
457
|
+
if (hold.state === "active" ? hold.releasedAt !== undefined : hold.releasedAt === undefined)
|
|
458
|
+
bad("legal hold", "releasedAt is required exactly when the hold is released");
|
|
459
|
+
if (hold.releasedAt !== undefined && instantKey(hold.releasedAt) < instantKey(hold.placedAt))
|
|
460
|
+
bad("legal hold", "releasedAt precedes placedAt");
|
|
461
|
+
});
|
|
462
|
+
function generationDeletionSchema() {
|
|
463
|
+
return strict({
|
|
464
|
+
schema: Type.Literal(DH_RECORDS_SCHEMA_IDS.generationDeletion),
|
|
465
|
+
generationId: DhGenerationIdSchema,
|
|
466
|
+
state: Type.Enum(DH_DELETION_STATES),
|
|
467
|
+
/** Evidence from the same locked snapshot as the state: held iff blocked-by-hold. */
|
|
468
|
+
holdCheck: Type.Union([
|
|
469
|
+
strict({ state: Type.Literal("clear") }),
|
|
470
|
+
strict({
|
|
471
|
+
state: Type.Literal("held"),
|
|
472
|
+
holdIds: Type.Array(holdId(), { minItems: 1, maxItems: DH_DELETION_MAX_HOLD_IDS, uniqueItems: true }),
|
|
473
|
+
}),
|
|
474
|
+
]),
|
|
475
|
+
/** Durable deletion claim time; present exactly when claimed or tombstoned. */
|
|
476
|
+
claimedAt: Type.Optional(timestamp),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
export const parseDhGenerationDeletion = dhStrictDtoParser("generation deletion", generationDeletionSchema, (deletion) => {
|
|
480
|
+
const label = "generation deletion";
|
|
481
|
+
if ((deletion.holdCheck.state === "held") !== (deletion.state === "blocked-by-hold"))
|
|
482
|
+
bad(label, "an active hold blocks deletion and only a hold blocks it");
|
|
483
|
+
if ((deletion.claimedAt !== undefined) !== (deletion.state === "claimed" || deletion.state === "tombstoned"))
|
|
484
|
+
bad(label, "claimedAt is required exactly for claimed or tombstoned");
|
|
485
|
+
});
|
|
486
|
+
/** A generation with a durable deletion claim cannot accept a new hold. */
|
|
487
|
+
export function assertDhHoldPlacementAllowed(deletionInput) {
|
|
488
|
+
const { state } = parseDhGenerationDeletion(deletionInput);
|
|
489
|
+
if (state === "claimed" || state === "tombstoned")
|
|
490
|
+
throw new DhContractError("deletion_claimed", "Generation deletion is already claimed");
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Hold wins. Given one snapshot's deletion status and holds: an active hold forbids any state but
|
|
494
|
+
* blocked-by-hold and must be named in holdCheck. A hold placed at or before the claim that was released
|
|
495
|
+
* only after the claim (or never) blocks the deletion; a hold placed after the claim is also rejected.
|
|
496
|
+
*/
|
|
497
|
+
export function assertDhHoldPrecedence(deletionInput, holdInputs) {
|
|
498
|
+
const deletion = parseDhGenerationDeletion(deletionInput);
|
|
499
|
+
const holds = holdInputs.map(parseDhLegalHold);
|
|
500
|
+
if (holds.some((hold) => hold.generationId !== deletion.generationId))
|
|
501
|
+
throw new DhContractError("hold_precedence", "Hold belongs to a different generation");
|
|
502
|
+
const active = holds.filter((hold) => hold.state === "active");
|
|
503
|
+
const named = deletion.holdCheck.state === "held" ? deletion.holdCheck.holdIds : [];
|
|
504
|
+
if (active.length > 0 &&
|
|
505
|
+
(deletion.state !== "blocked-by-hold" || active.some((hold) => !named.includes(hold.holdId))))
|
|
506
|
+
throw new DhContractError("hold_precedence", "An active legal hold takes precedence over deletion");
|
|
507
|
+
if (named.some((id) => !active.some((hold) => hold.holdId === id)))
|
|
508
|
+
throw new DhContractError("hold_precedence", "Deletion names a hold that is not active in this snapshot");
|
|
509
|
+
const claimedAt = deletion.claimedAt === undefined ? undefined : instantKey(deletion.claimedAt);
|
|
510
|
+
if (claimedAt !== undefined && holds.some((hold) => instantKey(hold.placedAt) > claimedAt))
|
|
511
|
+
throw new DhContractError("hold_precedence", "A hold cannot attach after the deletion claim");
|
|
512
|
+
if (claimedAt !== undefined &&
|
|
513
|
+
holds.some((hold) => hold.releasedAt !== undefined && instantKey(hold.releasedAt) > claimedAt))
|
|
514
|
+
throw new DhContractError("hold_precedence", "An active legal hold takes precedence over deletion");
|
|
515
|
+
}
|
|
516
|
+
/** The body must match the route's path generation (and hold). Route params are validated by buildDhRoutePath. */
|
|
517
|
+
export function assertDhRecordsRequestForPath(requestInput, params) {
|
|
518
|
+
assertDhPlainData(requestInput);
|
|
519
|
+
if (typeof requestInput !== "object" || requestInput === null || Array.isArray(requestInput))
|
|
520
|
+
throw new DhContractError("schema_invalid", "Records request must be an object");
|
|
521
|
+
const schema = requestInput.schema;
|
|
522
|
+
const parse = schema === DH_RECORDS_SCHEMA_IDS.generationDeleteRequest
|
|
523
|
+
? parseDhGenerationDeleteRequest
|
|
524
|
+
: schema === DH_RECORDS_SCHEMA_IDS.legalHoldRequest
|
|
525
|
+
? parseDhLegalHoldRequest
|
|
526
|
+
: schema === DH_RECORDS_SCHEMA_IDS.legalHoldReleaseRequest
|
|
527
|
+
? parseDhLegalHoldReleaseRequest
|
|
528
|
+
: undefined;
|
|
529
|
+
if (parse === undefined)
|
|
530
|
+
throw new DhContractError("schema_invalid", "Invalid records request schema");
|
|
531
|
+
const request = parse(requestInput);
|
|
532
|
+
if (request.generationId !== params.generationId || ("holdId" in request && request.holdId !== params.holdId))
|
|
533
|
+
throw new DhContractError("schema_invalid", "Records request does not match the route path");
|
|
534
|
+
}
|
|
535
|
+
//# sourceMappingURL=trace-records.js.map
|