@agent-finops/core 0.8.1 → 0.9.1
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 +5 -3
- package/dist/actionPlanner.d.ts +140 -0
- package/dist/actionPlanner.js +938 -0
- package/dist/actionVerification.d.ts +1240 -0
- package/dist/actionVerification.js +1028 -0
- package/dist/activitySnapshot.d.ts +142 -50
- package/dist/activitySnapshot.js +145 -6
- package/dist/activitySnapshotCache.d.ts +8 -1
- package/dist/activitySnapshotCache.js +103 -7
- package/dist/agentDraftToken.d.ts +80 -0
- package/dist/agentDraftToken.js +188 -0
- package/dist/agentEconomicsReceipt.d.ts +74 -74
- package/dist/agentLoopContract.d.ts +27 -0
- package/dist/agentLoopContract.js +36 -0
- package/dist/glance.d.ts +27 -1
- package/dist/glance.js +151 -12
- package/dist/guidedAnswer.d.ts +51 -0
- package/dist/guidedAnswer.js +352 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +13 -1
- package/dist/localAgentFormats/gemini.js +2 -2
- package/dist/localAgentFormats/registry.js +6 -2
- package/dist/localAgentFormats/runtimeRegistry.js +5 -2
- package/dist/localAgentFormats/types.d.ts +2 -1
- package/dist/localAgentLogs.d.ts +362 -3
- package/dist/localAgentLogs.js +1964 -165
- package/dist/modelPricing.d.ts +1 -1
- package/dist/modelPricing.js +1 -1
- package/dist/projectEconomics.d.ts +617 -0
- package/dist/projectEconomics.js +620 -0
- package/dist/projectEconomicsBuilder.d.ts +89 -0
- package/dist/projectEconomicsBuilder.js +473 -0
- package/dist/projectIndexStore.d.ts +545 -0
- package/dist/projectIndexStore.js +606 -0
- package/dist/providerConnectors.d.ts +161 -1
- package/dist/providerConnectors.js +406 -11
- package/dist/qualitativeIndexCache.d.ts +494 -0
- package/dist/qualitativeIndexCache.js +930 -0
- package/dist/resultCard.d.ts +350 -0
- package/dist/resultCard.js +604 -0
- package/dist/runtimeCommands.d.ts +36 -0
- package/dist/runtimeCommands.js +50 -0
- package/dist/scanGuard.d.ts +3 -1
- package/dist/scanGuard.js +164 -4
- package/dist/schema.d.ts +33 -31
- package/dist/schema.js +9 -1
- package/dist/sessionVitals.d.ts +145 -0
- package/dist/sessionVitals.js +521 -0
- package/dist/toolInvocations.d.ts +40 -1
- package/dist/toolInvocations.js +101 -20
- package/package.json +1 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const OWNERSHIP_SUGGESTION_V0_KIND = "aibill.ownership_suggestion";
|
|
4
|
+
export const CONFIRMED_OWNERSHIP_V0_KIND = "aibill.confirmed_ownership";
|
|
5
|
+
export const APPROVAL_EVENT_V0_KIND = "aibill.approval_event";
|
|
6
|
+
export const ACCEPTED_OUTCOME_V0_KIND = "aibill.accepted_outcome";
|
|
7
|
+
export const PROJECT_ECONOMICS_RECEIPT_V0_KIND = "aibill.project_economics_receipt";
|
|
8
|
+
export const PROJECT_ECONOMICS_V0_VERSION = "0.1.0";
|
|
9
|
+
export const MAX_APPROVAL_EVENTS_V0 = 256;
|
|
10
|
+
export const MAX_OUTCOME_CHECK_REFS_V0 = 64;
|
|
11
|
+
export const MAX_PROJECT_COST_LINES_V0 = 32;
|
|
12
|
+
const utcTimestampSchema = z.string()
|
|
13
|
+
.datetime({ offset: true })
|
|
14
|
+
.transform((value) => new Date(value).toISOString());
|
|
15
|
+
const finiteUsdSchema = z.number().finite().nonnegative()
|
|
16
|
+
.transform((value) => Object.is(value, -0) ? 0 : value);
|
|
17
|
+
const countSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
18
|
+
const percentageSchema = z.number().finite().min(0).max(100)
|
|
19
|
+
.transform((value) => roundPercent(value));
|
|
20
|
+
const boundedIdentifierSchema = z.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(96)
|
|
23
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/, "Expected a path-free, control-free identifier.")
|
|
24
|
+
.superRefine((value, context) => {
|
|
25
|
+
if (/^(?:sk-|sk_|gh[pousr]_|github_pat_|npm_|AIza|xox[baprs]-|glpat-|AKIA)/i
|
|
26
|
+
.test(value) || /^(?:env|keychain|secret|credential):/i.test(value)) {
|
|
27
|
+
context.addIssue({
|
|
28
|
+
code: "custom",
|
|
29
|
+
message: "Credential-like values are not project-economics identifiers."
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
/** Opaque privacy-reduced reference; the source-native value is never persisted. */
|
|
34
|
+
export const projectEconomicsReferenceSchema = z.string()
|
|
35
|
+
.regex(/^peref_[a-f0-9]{64}$/);
|
|
36
|
+
const actionVerificationReferenceSchema = z.string()
|
|
37
|
+
.regex(/^avref_[a-f0-9]{64}$/);
|
|
38
|
+
const wasteFindingReferenceSchema = z.string()
|
|
39
|
+
.regex(/^wf_v0_[a-f0-9]{64}$/);
|
|
40
|
+
const tokenExperimentReferenceSchema = z.string()
|
|
41
|
+
.regex(/^tre_v0_[a-f0-9]{64}$/);
|
|
42
|
+
const tokenExperimentRevisionReferenceSchema = z.string()
|
|
43
|
+
.regex(/^trev_v0_[a-f0-9]{64}$/);
|
|
44
|
+
const ownershipSuggestionIdSchema = z.string()
|
|
45
|
+
.regex(/^owns_v0_[a-f0-9]{64}$/);
|
|
46
|
+
const confirmedOwnershipIdSchema = z.string()
|
|
47
|
+
.regex(/^ownc_v0_[a-f0-9]{64}$/);
|
|
48
|
+
const approvalEventIdSchema = z.string()
|
|
49
|
+
.regex(/^ape_v0_[a-f0-9]{64}$/);
|
|
50
|
+
const acceptedOutcomeIdSchema = z.string()
|
|
51
|
+
.regex(/^aco_v0_[a-f0-9]{64}$/);
|
|
52
|
+
const projectEconomicsReceiptIdSchema = z.string()
|
|
53
|
+
.regex(/^per_v0_[a-f0-9]{64}$/);
|
|
54
|
+
export const projectEconomicsEvidenceValues = [
|
|
55
|
+
"verified",
|
|
56
|
+
"observed",
|
|
57
|
+
"calculated",
|
|
58
|
+
"user_declared",
|
|
59
|
+
"modeled",
|
|
60
|
+
"missing"
|
|
61
|
+
];
|
|
62
|
+
export const projectEconomicsEvidenceSchema = z.enum(projectEconomicsEvidenceValues);
|
|
63
|
+
const ownershipSuggestionBodySchema = z.object({
|
|
64
|
+
kind: z.literal(OWNERSHIP_SUGGESTION_V0_KIND),
|
|
65
|
+
schemaVersion: z.literal(PROJECT_ECONOMICS_V0_VERSION),
|
|
66
|
+
status: z.literal("suggested"),
|
|
67
|
+
generatedAt: utcTimestampSchema,
|
|
68
|
+
projectRef: projectEconomicsReferenceSchema,
|
|
69
|
+
suggestedHumanRef: projectEconomicsReferenceSchema.optional(),
|
|
70
|
+
suggestedTeamRef: projectEconomicsReferenceSchema.optional(),
|
|
71
|
+
suggestedClientRef: projectEconomicsReferenceSchema.optional(),
|
|
72
|
+
suggestedCostCenterRef: projectEconomicsReferenceSchema.optional(),
|
|
73
|
+
source: z.enum(["git_metadata", "codeowners", "repository_metadata"]),
|
|
74
|
+
evidence: z.literal("observed"),
|
|
75
|
+
requiresUserConfirmation: z.literal(true)
|
|
76
|
+
}).strict().superRefine((suggestion, context) => {
|
|
77
|
+
if (!suggestion.suggestedHumanRef && !suggestion.suggestedTeamRef &&
|
|
78
|
+
!suggestion.suggestedClientRef && !suggestion.suggestedCostCenterRef) {
|
|
79
|
+
context.addIssue({
|
|
80
|
+
code: "custom",
|
|
81
|
+
message: "An ownership suggestion must contain at least one suggested reference."
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
export const ownershipSuggestionV0Schema = ownershipSuggestionBodySchema.extend({
|
|
86
|
+
id: ownershipSuggestionIdSchema
|
|
87
|
+
});
|
|
88
|
+
const confirmedOwnershipBodySchema = z.object({
|
|
89
|
+
kind: z.literal(CONFIRMED_OWNERSHIP_V0_KIND),
|
|
90
|
+
schemaVersion: z.literal(PROJECT_ECONOMICS_V0_VERSION),
|
|
91
|
+
status: z.literal("confirmed"),
|
|
92
|
+
projectRef: projectEconomicsReferenceSchema,
|
|
93
|
+
humanOwnerRef: projectEconomicsReferenceSchema,
|
|
94
|
+
teamRef: projectEconomicsReferenceSchema,
|
|
95
|
+
clientRef: projectEconomicsReferenceSchema.optional(),
|
|
96
|
+
costCenterRef: projectEconomicsReferenceSchema.optional(),
|
|
97
|
+
confirmation: z.object({
|
|
98
|
+
evidence: z.literal("user_declared"),
|
|
99
|
+
confirmedAt: utcTimestampSchema,
|
|
100
|
+
confirmedByRef: projectEconomicsReferenceSchema,
|
|
101
|
+
locallyStored: z.literal(true)
|
|
102
|
+
}).strict()
|
|
103
|
+
}).strict();
|
|
104
|
+
export const confirmedOwnershipV0Schema = confirmedOwnershipBodySchema.extend({
|
|
105
|
+
id: confirmedOwnershipIdSchema
|
|
106
|
+
});
|
|
107
|
+
const approvalEventBodySchema = z.object({
|
|
108
|
+
kind: z.literal(APPROVAL_EVENT_V0_KIND),
|
|
109
|
+
schemaVersion: z.literal(PROJECT_ECONOMICS_V0_VERSION),
|
|
110
|
+
sequence: z.number().int().min(0).max(MAX_APPROVAL_EVENTS_V0 - 1),
|
|
111
|
+
previousEventId: approvalEventIdSchema.nullable(),
|
|
112
|
+
approvedAt: utcTimestampSchema,
|
|
113
|
+
decision: z.literal("approved"),
|
|
114
|
+
attestation: z.object({
|
|
115
|
+
scope: z.literal("local_self_attested"),
|
|
116
|
+
evidence: z.literal("user_declared"),
|
|
117
|
+
approverIdentityRef: projectEconomicsReferenceSchema,
|
|
118
|
+
approverRoleRef: projectEconomicsReferenceSchema,
|
|
119
|
+
rbacVerified: z.literal(false)
|
|
120
|
+
}).strict(),
|
|
121
|
+
references: z.object({
|
|
122
|
+
actionRef: actionVerificationReferenceSchema,
|
|
123
|
+
changeRef: actionVerificationReferenceSchema,
|
|
124
|
+
rollbackRef: actionVerificationReferenceSchema,
|
|
125
|
+
canaryRef: actionVerificationReferenceSchema
|
|
126
|
+
}).strict()
|
|
127
|
+
}).strict().superRefine((event, context) => {
|
|
128
|
+
if ((event.sequence === 0) !== (event.previousEventId === null)) {
|
|
129
|
+
context.addIssue({
|
|
130
|
+
code: "custom",
|
|
131
|
+
path: ["previousEventId"],
|
|
132
|
+
message: "Only the first approval event may omit a previous event ID."
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
export const approvalEventV0Schema = approvalEventBodySchema.extend({
|
|
137
|
+
id: approvalEventIdSchema
|
|
138
|
+
});
|
|
139
|
+
const businessDescriptionSchema = z.object({
|
|
140
|
+
value: z.string().trim().min(1).max(240)
|
|
141
|
+
.refine((value) => !/[\u0000-\u001f\u007f-\u009f]/u.test(value) &&
|
|
142
|
+
!hasUnpairedSurrogate(value), "Business descriptions must be bounded, control-free valid Unicode."),
|
|
143
|
+
evidence: z.literal("user_declared")
|
|
144
|
+
}).strict();
|
|
145
|
+
const acceptedOutcomeBodySchema = z.object({
|
|
146
|
+
kind: z.literal(ACCEPTED_OUTCOME_V0_KIND),
|
|
147
|
+
schemaVersion: z.literal(PROJECT_ECONOMICS_V0_VERSION),
|
|
148
|
+
platform: z.literal("github"),
|
|
149
|
+
outcomeType: z.enum(["pull_request", "task"]),
|
|
150
|
+
repositoryRef: projectEconomicsReferenceSchema,
|
|
151
|
+
workUnitRef: projectEconomicsReferenceSchema,
|
|
152
|
+
state: z.enum(["merged", "accepted"]),
|
|
153
|
+
stateEvidence: z.enum(["verified", "observed", "user_declared"]),
|
|
154
|
+
acceptedAt: utcTimestampSchema,
|
|
155
|
+
commit: z.object({
|
|
156
|
+
commitRef: projectEconomicsReferenceSchema,
|
|
157
|
+
evidence: z.enum(["verified", "observed"])
|
|
158
|
+
}).strict(),
|
|
159
|
+
// `passed` means every status check represented by these evidence refs
|
|
160
|
+
// passed. It does not assert that branch-protection requirements were read.
|
|
161
|
+
checks: z.object({
|
|
162
|
+
status: z.literal("passed"),
|
|
163
|
+
evidence: z.enum(["verified", "observed"]),
|
|
164
|
+
evidenceRefs: z.array(projectEconomicsReferenceSchema)
|
|
165
|
+
.min(1)
|
|
166
|
+
.max(MAX_OUTCOME_CHECK_REFS_V0)
|
|
167
|
+
}).strict(),
|
|
168
|
+
businessDescription: businessDescriptionSchema.optional()
|
|
169
|
+
}).strict().superRefine((outcome, context) => {
|
|
170
|
+
if (outcome.outcomeType === "pull_request" && outcome.state !== "merged") {
|
|
171
|
+
context.addIssue({
|
|
172
|
+
code: "custom",
|
|
173
|
+
path: ["state"],
|
|
174
|
+
message: "An accepted GitHub pull-request outcome must be merged."
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (outcome.outcomeType === "task" && outcome.state !== "accepted") {
|
|
178
|
+
context.addIssue({
|
|
179
|
+
code: "custom",
|
|
180
|
+
path: ["state"],
|
|
181
|
+
message: "An accepted GitHub task outcome must be accepted."
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (outcome.outcomeType === "pull_request" &&
|
|
185
|
+
outcome.stateEvidence === "user_declared") {
|
|
186
|
+
context.addIssue({
|
|
187
|
+
code: "custom",
|
|
188
|
+
path: ["stateEvidence"],
|
|
189
|
+
message: "A merged pull request requires observed or verified GitHub state."
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
export const acceptedOutcomeV0Schema = acceptedOutcomeBodySchema.extend({
|
|
194
|
+
id: acceptedOutcomeIdSchema
|
|
195
|
+
});
|
|
196
|
+
export const projectCostBasisValues = [
|
|
197
|
+
"provider_billed",
|
|
198
|
+
"subscription_included",
|
|
199
|
+
"api_equivalent_estimate",
|
|
200
|
+
"user_declared",
|
|
201
|
+
"missing"
|
|
202
|
+
];
|
|
203
|
+
export const projectCostBasisSchema = z.enum(projectCostBasisValues);
|
|
204
|
+
const costLineSchema = z.object({
|
|
205
|
+
sourceRef: projectEconomicsReferenceSchema,
|
|
206
|
+
basis: projectCostBasisSchema,
|
|
207
|
+
amountUsd: finiteUsdSchema.nullable(),
|
|
208
|
+
evidence: projectEconomicsEvidenceSchema
|
|
209
|
+
}).strict().superRefine((line, context) => {
|
|
210
|
+
if ((line.basis === "missing") !== (line.amountUsd === null)) {
|
|
211
|
+
context.addIssue({
|
|
212
|
+
code: "custom",
|
|
213
|
+
path: ["amountUsd"],
|
|
214
|
+
message: "Only a missing cost basis may omit its amount."
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const allowed = {
|
|
218
|
+
provider_billed: ["verified"],
|
|
219
|
+
subscription_included: ["observed", "user_declared"],
|
|
220
|
+
api_equivalent_estimate: ["calculated", "modeled"],
|
|
221
|
+
user_declared: ["user_declared"],
|
|
222
|
+
missing: ["missing"]
|
|
223
|
+
};
|
|
224
|
+
if (!allowed[line.basis].includes(line.evidence)) {
|
|
225
|
+
context.addIssue({
|
|
226
|
+
code: "custom",
|
|
227
|
+
path: ["evidence"],
|
|
228
|
+
message: `Evidence ${line.evidence} cannot support the ${line.basis} cost basis.`
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const costCoverageSchema = z.object({
|
|
233
|
+
status: z.enum(["complete", "partial", "missing"]),
|
|
234
|
+
coveredRecords: countSchema,
|
|
235
|
+
eligibleRecords: countSchema,
|
|
236
|
+
evidence: z.enum(["verified", "observed", "calculated", "missing"])
|
|
237
|
+
}).strict().superRefine((coverage, context) => {
|
|
238
|
+
if (coverage.coveredRecords > coverage.eligibleRecords) {
|
|
239
|
+
context.addIssue({
|
|
240
|
+
code: "custom",
|
|
241
|
+
path: ["coveredRecords"],
|
|
242
|
+
message: "Covered records cannot exceed eligible records."
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const expectedStatus = coverage.eligibleRecords === 0 || coverage.coveredRecords === 0
|
|
246
|
+
? "missing"
|
|
247
|
+
: coverage.coveredRecords === coverage.eligibleRecords ? "complete" : "partial";
|
|
248
|
+
if (coverage.status !== expectedStatus) {
|
|
249
|
+
context.addIssue({
|
|
250
|
+
code: "custom",
|
|
251
|
+
path: ["status"],
|
|
252
|
+
message: "Coverage status must be derived from covered and eligible records."
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if ((coverage.status === "missing") !== (coverage.evidence === "missing")) {
|
|
256
|
+
context.addIssue({
|
|
257
|
+
code: "custom",
|
|
258
|
+
path: ["evidence"],
|
|
259
|
+
message: "Missing coverage must use missing evidence, and present coverage must not."
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const measuredTokenResultSchema = z.object({
|
|
264
|
+
status: z.enum([
|
|
265
|
+
"measured_token_reduction",
|
|
266
|
+
"no_measured_change",
|
|
267
|
+
"regressed",
|
|
268
|
+
"inconclusive"
|
|
269
|
+
]),
|
|
270
|
+
baselineMedianTokens: z.number().finite().nonnegative().nullable(),
|
|
271
|
+
postChangeMedianTokens: z.number().finite().nonnegative().nullable(),
|
|
272
|
+
baselineSessions: countSchema,
|
|
273
|
+
postChangeSessions: countSchema,
|
|
274
|
+
reductionPercent: z.number().finite().nullable(),
|
|
275
|
+
metricEvidence: z.enum(["calculated", "missing"]),
|
|
276
|
+
matchingEvidence: z.enum(["verified", "observed", "user_declared", "missing"]),
|
|
277
|
+
qualityStatus: z.enum(["held", "regressed", "insufficient"]),
|
|
278
|
+
qualityEvidence: z.enum(["verified", "observed", "user_declared", "missing"])
|
|
279
|
+
}).strict().superRefine((result, context) => {
|
|
280
|
+
const measured = result.status !== "inconclusive";
|
|
281
|
+
const numeric = result.baselineMedianTokens !== null &&
|
|
282
|
+
result.postChangeMedianTokens !== null && result.reductionPercent !== null;
|
|
283
|
+
if (measured !== numeric || measured !== (result.metricEvidence === "calculated")) {
|
|
284
|
+
context.addIssue({
|
|
285
|
+
code: "custom",
|
|
286
|
+
message: "A conclusive token result requires calculated baseline, post-change, and percentage evidence."
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!measured) {
|
|
291
|
+
if (result.matchingEvidence !== "missing" || result.qualityStatus !== "insufficient" ||
|
|
292
|
+
result.qualityEvidence !== "missing") {
|
|
293
|
+
context.addIssue({
|
|
294
|
+
code: "custom",
|
|
295
|
+
message: "An inconclusive result must not imply matching or quality evidence."
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const baselineMedianTokens = result.baselineMedianTokens;
|
|
301
|
+
const postChangeMedianTokens = result.postChangeMedianTokens;
|
|
302
|
+
const reductionPercent = result.reductionPercent;
|
|
303
|
+
if (baselineMedianTokens === null || postChangeMedianTokens === null ||
|
|
304
|
+
reductionPercent === null)
|
|
305
|
+
return;
|
|
306
|
+
if (result.baselineSessions < 1 || result.postChangeSessions < 1 ||
|
|
307
|
+
result.matchingEvidence === "missing" || result.qualityEvidence === "missing") {
|
|
308
|
+
context.addIssue({
|
|
309
|
+
code: "custom",
|
|
310
|
+
message: "A conclusive result requires matched sessions and non-missing quality evidence."
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const expected = baselineMedianTokens === 0
|
|
314
|
+
? null
|
|
315
|
+
: roundPercent(100 *
|
|
316
|
+
(baselineMedianTokens - postChangeMedianTokens) /
|
|
317
|
+
baselineMedianTokens);
|
|
318
|
+
if (expected === null || reductionPercent !== expected) {
|
|
319
|
+
context.addIssue({
|
|
320
|
+
code: "custom",
|
|
321
|
+
path: ["reductionPercent"],
|
|
322
|
+
message: "The token reduction percentage must match the supplied medians."
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (result.status === "measured_token_reduction" &&
|
|
326
|
+
(!(reductionPercent > 0) || result.qualityStatus !== "held")) {
|
|
327
|
+
context.addIssue({
|
|
328
|
+
code: "custom",
|
|
329
|
+
path: ["status"],
|
|
330
|
+
message: "A reduction claim requires a positive measured percentage and held quality."
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (result.status === "no_measured_change" &&
|
|
334
|
+
(reductionPercent !== 0 || result.qualityStatus !== "held")) {
|
|
335
|
+
context.addIssue({
|
|
336
|
+
code: "custom",
|
|
337
|
+
path: ["status"],
|
|
338
|
+
message: "No measured change requires zero reduction and held quality."
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (result.status === "regressed" &&
|
|
342
|
+
!(reductionPercent < 0 || result.qualityStatus === "regressed")) {
|
|
343
|
+
context.addIssue({
|
|
344
|
+
code: "custom",
|
|
345
|
+
path: ["status"],
|
|
346
|
+
message: "A regression requires higher token use or regressed quality."
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
const billReconciliationSchema = z.discriminatedUnion("status", [
|
|
351
|
+
z.object({
|
|
352
|
+
status: z.literal("not_attempted"),
|
|
353
|
+
evidence: z.literal("missing")
|
|
354
|
+
}).strict(),
|
|
355
|
+
z.object({
|
|
356
|
+
status: z.literal("partial"),
|
|
357
|
+
evidence: z.literal("verified"),
|
|
358
|
+
providerBillRef: projectEconomicsReferenceSchema,
|
|
359
|
+
coveragePercent: percentageSchema,
|
|
360
|
+
matchedAmountUsd: finiteUsdSchema,
|
|
361
|
+
varianceUsd: z.number().finite().optional()
|
|
362
|
+
}).strict().superRefine((reconciliation, context) => {
|
|
363
|
+
if (reconciliation.coveragePercent >= 100) {
|
|
364
|
+
context.addIssue({
|
|
365
|
+
code: "custom",
|
|
366
|
+
path: ["coveragePercent"],
|
|
367
|
+
message: "Receipt V0 only represents partial provider-bill matching below complete coverage."
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
]);
|
|
372
|
+
const projectEconomicsReceiptBodySchema = z.object({
|
|
373
|
+
kind: z.literal(PROJECT_ECONOMICS_RECEIPT_V0_KIND),
|
|
374
|
+
schemaVersion: z.literal(PROJECT_ECONOMICS_V0_VERSION),
|
|
375
|
+
generatedAt: utcTimestampSchema,
|
|
376
|
+
scope: z.object({
|
|
377
|
+
projectRef: projectEconomicsReferenceSchema,
|
|
378
|
+
workUnitRef: projectEconomicsReferenceSchema
|
|
379
|
+
}).strict(),
|
|
380
|
+
ownership: confirmedOwnershipV0Schema,
|
|
381
|
+
costs: z.object({
|
|
382
|
+
lines: z.array(costLineSchema).max(MAX_PROJECT_COST_LINES_V0),
|
|
383
|
+
coverage: costCoverageSchema
|
|
384
|
+
}).strict(),
|
|
385
|
+
action: z.object({
|
|
386
|
+
wasteFindingRef: wasteFindingReferenceSchema,
|
|
387
|
+
tokenExperimentRef: tokenExperimentReferenceSchema,
|
|
388
|
+
tokenExperimentRevisionRef: tokenExperimentRevisionReferenceSchema,
|
|
389
|
+
approvalEvent: approvalEventV0Schema
|
|
390
|
+
}).strict(),
|
|
391
|
+
outcome: acceptedOutcomeV0Schema,
|
|
392
|
+
measuredTokenResult: measuredTokenResultSchema,
|
|
393
|
+
billReconciliation: billReconciliationSchema,
|
|
394
|
+
claims: z.object({
|
|
395
|
+
roi: z.literal("not_claimed"),
|
|
396
|
+
invoiceReconciled: z.literal(false),
|
|
397
|
+
rbacVerified: z.literal(false)
|
|
398
|
+
}).strict()
|
|
399
|
+
}).strict().superRefine((receipt, context) => {
|
|
400
|
+
if (receipt.ownership.projectRef !== receipt.scope.projectRef) {
|
|
401
|
+
context.addIssue({
|
|
402
|
+
code: "custom",
|
|
403
|
+
path: ["ownership", "projectRef"],
|
|
404
|
+
message: "Confirmed ownership must belong to the receipt project."
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (receipt.outcome.workUnitRef !== receipt.scope.workUnitRef) {
|
|
408
|
+
context.addIssue({
|
|
409
|
+
code: "custom",
|
|
410
|
+
path: ["outcome", "workUnitRef"],
|
|
411
|
+
message: "The accepted outcome must belong to the receipt work unit."
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (receipt.action.approvalEvent.approvedAt > receipt.generatedAt ||
|
|
415
|
+
receipt.outcome.acceptedAt > receipt.generatedAt) {
|
|
416
|
+
context.addIssue({
|
|
417
|
+
code: "custom",
|
|
418
|
+
path: ["generatedAt"],
|
|
419
|
+
message: "A receipt cannot predate its approval or accepted outcome."
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (receipt.costs.coverage.status !== "missing" && receipt.costs.lines.length === 0) {
|
|
423
|
+
context.addIssue({
|
|
424
|
+
code: "custom",
|
|
425
|
+
path: ["costs", "lines"],
|
|
426
|
+
message: "Present cost coverage requires at least one classified cost line."
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
export const projectEconomicsReceiptV0Schema = projectEconomicsReceiptBodySchema.extend({
|
|
431
|
+
id: projectEconomicsReceiptIdSchema
|
|
432
|
+
});
|
|
433
|
+
export function createProjectEconomicsReference(namespace, sourceNativeValue) {
|
|
434
|
+
const safeNamespace = boundedIdentifierSchema.parse(namespace);
|
|
435
|
+
if (sourceNativeValue.length < 1 || sourceNativeValue.length > 4_096 ||
|
|
436
|
+
hasUnpairedSurrogate(sourceNativeValue)) {
|
|
437
|
+
throw new TypeError("Project economics references require bounded valid Unicode source values.");
|
|
438
|
+
}
|
|
439
|
+
return `peref_${createHash("sha256")
|
|
440
|
+
.update(`${safeNamespace}\u0000${sourceNativeValue}`)
|
|
441
|
+
.digest("hex")}`;
|
|
442
|
+
}
|
|
443
|
+
export function createOwnershipSuggestionV0(input) {
|
|
444
|
+
const body = ownershipSuggestionBodySchema.parse(input);
|
|
445
|
+
return ownershipSuggestionV0Schema.parse({
|
|
446
|
+
...body,
|
|
447
|
+
id: digest("owns_v0", body)
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
export function parseOwnershipSuggestionV0(value) {
|
|
451
|
+
return parseContentAddressed(value, ownershipSuggestionV0Schema, ownershipSuggestionBodySchema, "owns_v0", "Ownership suggestion");
|
|
452
|
+
}
|
|
453
|
+
export function createConfirmedOwnershipV0(input) {
|
|
454
|
+
const body = confirmedOwnershipBodySchema.parse(input);
|
|
455
|
+
return confirmedOwnershipV0Schema.parse({
|
|
456
|
+
...body,
|
|
457
|
+
id: digest("ownc_v0", body)
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
export function parseConfirmedOwnershipV0(value) {
|
|
461
|
+
return parseContentAddressed(value, confirmedOwnershipV0Schema, confirmedOwnershipBodySchema, "ownc_v0", "Confirmed ownership");
|
|
462
|
+
}
|
|
463
|
+
export function createApprovalEventV0(input) {
|
|
464
|
+
const body = approvalEventBodySchema.parse(input);
|
|
465
|
+
return approvalEventV0Schema.parse({ ...body, id: digest("ape_v0", body) });
|
|
466
|
+
}
|
|
467
|
+
export function parseApprovalEventV0(value) {
|
|
468
|
+
return parseContentAddressed(value, approvalEventV0Schema, approvalEventBodySchema, "ape_v0", "Approval event");
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Append one event without mutating history. Sequence, previous digest, and
|
|
472
|
+
* monotonic time are derived from the already-validated append-only chain.
|
|
473
|
+
*/
|
|
474
|
+
export function appendApprovalEventV0(history, input) {
|
|
475
|
+
if (history.length >= MAX_APPROVAL_EVENTS_V0) {
|
|
476
|
+
throw new TypeError("Approval history exceeds the V0 event limit.");
|
|
477
|
+
}
|
|
478
|
+
const parsedHistory = history.map(parseApprovalEventV0);
|
|
479
|
+
for (let index = 0; index < parsedHistory.length; index += 1) {
|
|
480
|
+
const event = parsedHistory[index];
|
|
481
|
+
if (event.sequence !== index || event.previousEventId !==
|
|
482
|
+
(index === 0 ? null : parsedHistory[index - 1].id)) {
|
|
483
|
+
throw new TypeError("Approval history is not one append-only digest chain.");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const previous = parsedHistory.at(-1);
|
|
487
|
+
const next = createApprovalEventV0({
|
|
488
|
+
...input,
|
|
489
|
+
sequence: parsedHistory.length,
|
|
490
|
+
previousEventId: previous?.id ?? null
|
|
491
|
+
});
|
|
492
|
+
if (previous && Date.parse(next.approvedAt) < Date.parse(previous.approvedAt)) {
|
|
493
|
+
throw new TypeError("An appended approval event cannot predate its predecessor.");
|
|
494
|
+
}
|
|
495
|
+
return [...parsedHistory, next];
|
|
496
|
+
}
|
|
497
|
+
export function createAcceptedOutcomeV0(input) {
|
|
498
|
+
const body = canonicalOutcomeBody(acceptedOutcomeBodySchema.parse(input));
|
|
499
|
+
return acceptedOutcomeV0Schema.parse({ ...body, id: digest("aco_v0", body) });
|
|
500
|
+
}
|
|
501
|
+
export function parseAcceptedOutcomeV0(value) {
|
|
502
|
+
const parsed = acceptedOutcomeV0Schema.parse(value);
|
|
503
|
+
const { id, ...bodyInput } = parsed;
|
|
504
|
+
const body = canonicalOutcomeBody(acceptedOutcomeBodySchema.parse(bodyInput));
|
|
505
|
+
if (id !== digest("aco_v0", body)) {
|
|
506
|
+
throw new TypeError("Accepted outcome ID does not match its canonical body.");
|
|
507
|
+
}
|
|
508
|
+
return acceptedOutcomeV0Schema.parse({ ...body, id });
|
|
509
|
+
}
|
|
510
|
+
export function createProjectEconomicsReceiptV0(input) {
|
|
511
|
+
const body = canonicalReceiptBody(projectEconomicsReceiptBodySchema.parse(input));
|
|
512
|
+
return projectEconomicsReceiptV0Schema.parse({
|
|
513
|
+
...body,
|
|
514
|
+
id: digest("per_v0", body)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
export function parseProjectEconomicsReceiptV0(value) {
|
|
518
|
+
const parsed = projectEconomicsReceiptV0Schema.parse(value);
|
|
519
|
+
const { id, ...bodyInput } = parsed;
|
|
520
|
+
const body = canonicalReceiptBody(projectEconomicsReceiptBodySchema.parse(bodyInput));
|
|
521
|
+
if (id !== digest("per_v0", body)) {
|
|
522
|
+
throw new TypeError("Project economics receipt ID does not match its canonical body.");
|
|
523
|
+
}
|
|
524
|
+
return projectEconomicsReceiptV0Schema.parse({ ...body, id });
|
|
525
|
+
}
|
|
526
|
+
/** Stable one-line JSON for local persistence or transport. */
|
|
527
|
+
export function serializeProjectEconomicsReceiptV0(receipt) {
|
|
528
|
+
return canonicalJson(parseProjectEconomicsReceiptV0(receipt));
|
|
529
|
+
}
|
|
530
|
+
export function deserializeProjectEconomicsReceiptV0(serialized) {
|
|
531
|
+
if (serialized.length < 2 || serialized.length > 1_000_000 ||
|
|
532
|
+
hasUnpairedSurrogate(serialized)) {
|
|
533
|
+
throw new TypeError("Serialized project economics receipts must be bounded valid JSON.");
|
|
534
|
+
}
|
|
535
|
+
let value;
|
|
536
|
+
try {
|
|
537
|
+
value = JSON.parse(serialized);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
throw new TypeError("Serialized project economics receipt is not valid JSON.");
|
|
541
|
+
}
|
|
542
|
+
return parseProjectEconomicsReceiptV0(value);
|
|
543
|
+
}
|
|
544
|
+
function canonicalOutcomeBody(outcome) {
|
|
545
|
+
return {
|
|
546
|
+
...outcome,
|
|
547
|
+
checks: {
|
|
548
|
+
...outcome.checks,
|
|
549
|
+
evidenceRefs: [...outcome.checks.evidenceRefs].sort(compareText)
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function canonicalReceiptBody(receipt) {
|
|
554
|
+
return {
|
|
555
|
+
...receipt,
|
|
556
|
+
ownership: parseConfirmedOwnershipV0(receipt.ownership),
|
|
557
|
+
action: {
|
|
558
|
+
...receipt.action,
|
|
559
|
+
approvalEvent: parseApprovalEventV0(receipt.action.approvalEvent)
|
|
560
|
+
},
|
|
561
|
+
outcome: parseAcceptedOutcomeV0(receipt.outcome),
|
|
562
|
+
costs: {
|
|
563
|
+
...receipt.costs,
|
|
564
|
+
lines: [...receipt.costs.lines].sort((left, right) => compareText(canonicalJson(left), canonicalJson(right)))
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function parseContentAddressed(value, objectSchema, bodySchema, prefix, label) {
|
|
569
|
+
const parsed = objectSchema.parse(value);
|
|
570
|
+
const { id, ...bodyInput } = parsed;
|
|
571
|
+
const body = bodySchema.parse(bodyInput);
|
|
572
|
+
if (id !== digest(prefix, body)) {
|
|
573
|
+
throw new TypeError(`${label} ID does not match its canonical body.`);
|
|
574
|
+
}
|
|
575
|
+
return objectSchema.parse({ ...body, id });
|
|
576
|
+
}
|
|
577
|
+
function digest(prefix, value) {
|
|
578
|
+
return `${prefix}_${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
|
|
579
|
+
}
|
|
580
|
+
function roundPercent(value) {
|
|
581
|
+
const rounded = Math.round((value + Number.EPSILON) * 100) / 100;
|
|
582
|
+
return Object.is(rounded, -0) ? 0 : rounded;
|
|
583
|
+
}
|
|
584
|
+
function compareText(left, right) {
|
|
585
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
586
|
+
}
|
|
587
|
+
function canonicalJson(value) {
|
|
588
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
589
|
+
return JSON.stringify(value);
|
|
590
|
+
}
|
|
591
|
+
if (typeof value === "number") {
|
|
592
|
+
if (!Number.isFinite(value))
|
|
593
|
+
throw new TypeError("Canonical values must be finite.");
|
|
594
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
595
|
+
}
|
|
596
|
+
if (Array.isArray(value)) {
|
|
597
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
598
|
+
}
|
|
599
|
+
if (typeof value === "object" && value) {
|
|
600
|
+
const object = value;
|
|
601
|
+
return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort()
|
|
602
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
603
|
+
}
|
|
604
|
+
throw new TypeError("Canonical values must be JSON data.");
|
|
605
|
+
}
|
|
606
|
+
function hasUnpairedSurrogate(value) {
|
|
607
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
608
|
+
const code = value.charCodeAt(index);
|
|
609
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
610
|
+
const next = value.charCodeAt(index + 1);
|
|
611
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
612
|
+
return true;
|
|
613
|
+
index += 1;
|
|
614
|
+
}
|
|
615
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
//# sourceMappingURL=projectEconomics.js.map
|