@artemiskit/core 0.5.0 → 0.5.2

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.
@@ -65,6 +65,33 @@ export interface WorkloadIdentity {
65
65
  rubric: ContentIdentity;
66
66
  }
67
67
 
68
+ /** Bounded target identity captured from a case execution. */
69
+ export interface CaseTargetEvidence {
70
+ provider: string;
71
+ requested_model?: string;
72
+ /** Model identifiers returned by the target provider during this case. */
73
+ observed_models?: string[];
74
+ }
75
+
76
+ /** Requested and observed execution configuration for a complete run. */
77
+ export interface ExecutionProvenance {
78
+ schema_version: '1';
79
+ target: {
80
+ provider: string;
81
+ requested_models?: string[];
82
+ observed_models?: string[];
83
+ generation?: {
84
+ temperature?: number;
85
+ max_tokens?: number;
86
+ seed?: number;
87
+ };
88
+ };
89
+ /** Judge/evaluator model identities, never combined with target identity. */
90
+ evaluator?: {
91
+ models?: string[];
92
+ };
93
+ }
94
+
68
95
  // ============================================================================
69
96
  // Case Result Types
70
97
  // ============================================================================
@@ -97,6 +124,39 @@ export interface CaseEvaluationEvidence {
97
124
  };
98
125
  }
99
126
 
127
+ /** A bounded record of one execution in a retry chain. */
128
+ export interface CaseAttemptEvidence {
129
+ attempt_id: string;
130
+ retry_chain_id: string;
131
+ /** One-based coordinate within a deliberately independent repetition. */
132
+ repetition_index: number;
133
+ /** One-based coordinate within this retry chain. */
134
+ attempt_number: number;
135
+ status: CaseEvaluationStatus;
136
+ /** Only the terminal measurement can contribute to an outcome rate. */
137
+ included_in_outcome: boolean;
138
+ latency_ms: number;
139
+ /** Sanitized classification, never arbitrary provider error text. */
140
+ error_code?: 'timeout' | 'target_error' | 'tool_error';
141
+ }
142
+
143
+ /** Declared retry and repetition context for a run. */
144
+ export interface RunAttemptEvidence {
145
+ schema_version: '1';
146
+ repetition: {
147
+ index: number;
148
+ total: number;
149
+ };
150
+ retry_policy: {
151
+ default_max_retries: number;
152
+ backoff: 'exponential';
153
+ initial_delay_ms: number;
154
+ };
155
+ timeout?: {
156
+ default_ms: number;
157
+ };
158
+ }
159
+
100
160
  /**
101
161
  * Individual test case result
102
162
  */
@@ -108,6 +168,8 @@ export interface CaseResult {
108
168
  status?: CaseEvaluationStatus;
109
169
  /** Number of execution attempts represented by this terminal result. */
110
170
  attempts?: number;
171
+ /** Bounded retry-chain evidence for this terminal case result. */
172
+ attempt_evidence?: CaseAttemptEvidence[];
111
173
  score: number;
112
174
  matcherType: string;
113
175
  reason?: string;
@@ -124,6 +186,8 @@ export interface CaseResult {
124
186
  error?: string;
125
187
  /** Sanitized evaluator evidence; arbitrary evaluator details are never stored here. */
126
188
  evidence?: CaseEvaluationEvidence;
189
+ /** Requested and observed target identity for this case. */
190
+ target?: CaseTargetEvidence;
127
191
  /** Redaction information for this case */
128
192
  redaction?: CaseRedactionInfo;
129
193
  /** Ordered tool activity captured for an enabled tool loop. */
@@ -151,6 +215,28 @@ export interface CostEstimateInfo {
151
215
  };
152
216
  }
153
217
 
218
+ /** Whether a monetary value is attested, supplied by an operator, or unavailable. */
219
+ export type CostProvenanceStatus = 'known' | 'user_supplied' | 'unavailable';
220
+
221
+ /**
222
+ * Cost evidence suitable for assurance reporting. Generic token-price estimates
223
+ * are intentionally not cost evidence.
224
+ */
225
+ export interface CostProvenance {
226
+ schema_version: '1';
227
+ status: CostProvenanceStatus;
228
+ /** Required for known and user-supplied amounts. */
229
+ amount?: number;
230
+ /** ISO 4217 currency required with an amount. */
231
+ currency?: string;
232
+ /** Origin of a recorded monetary value. */
233
+ source?: 'provider_billing' | 'operator_input';
234
+ /** ISO timestamp for a recorded monetary value. */
235
+ recorded_at?: string;
236
+ /** Stable reason code when no attested amount is available. */
237
+ unavailable_reason?: 'provider_billing_not_recorded' | 'unsupported_provider' | 'not_requested';
238
+ }
239
+
154
240
  /**
155
241
  * Run metrics
156
242
  */
@@ -173,7 +259,10 @@ export interface RunMetrics {
173
259
  total_prompt_tokens: number;
174
260
  total_completion_tokens: number;
175
261
  /** Estimated cost information */
262
+ /** @deprecated Generic pricing estimates are not assurance cost evidence. */
176
263
  cost?: CostEstimateInfo;
264
+ /** Explicit monetary evidence for assurance reporting. */
265
+ cost_provenance?: CostProvenance;
177
266
  }
178
267
 
179
268
  /**
@@ -302,6 +391,10 @@ export interface RunManifest {
302
391
  resolved_config?: ResolvedConfig;
303
392
  /** Versioned identities for the declared workload and evaluation rubric. */
304
393
  workload_identity?: WorkloadIdentity;
394
+ /** Requested and observed target/evaluator configuration for this run. */
395
+ execution_provenance?: ExecutionProvenance;
396
+ /** Retry-chain and repetition context. Present in manifest v1.4+. */
397
+ attempt_evidence?: RunAttemptEvidence;
305
398
  metrics: RunMetrics;
306
399
  git: GitInfo;
307
400
  provenance: ProvenanceInfo;
@@ -351,6 +444,15 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
351
444
  if (manifest.workload_identity !== undefined) {
352
445
  assertWorkloadIdentity(manifest.workload_identity);
353
446
  }
447
+ if (manifest.execution_provenance !== undefined) {
448
+ assertExecutionProvenance(manifest.execution_provenance);
449
+ }
450
+ if (manifest.attempt_evidence !== undefined) {
451
+ assertRunAttemptEvidence(manifest.attempt_evidence);
452
+ }
453
+ if (isRecord(manifest.metrics) && manifest.metrics.cost_provenance !== undefined) {
454
+ assertCostProvenance(manifest.metrics.cost_provenance);
455
+ }
354
456
 
355
457
  for (const [index, caseResult] of manifest.cases.entries()) {
356
458
  if (!isRecord(caseResult)) {
@@ -370,9 +472,169 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
370
472
  if (caseResult.evidence !== undefined) {
371
473
  assertCaseEvaluationEvidence(caseResult.evidence, index);
372
474
  }
475
+ if (caseResult.target !== undefined) {
476
+ assertCaseTargetEvidence(caseResult.target);
477
+ }
478
+ if (caseResult.attempt_evidence !== undefined) {
479
+ assertCaseAttemptEvidence(caseResult.attempt_evidence, index);
480
+ }
373
481
  }
374
482
  }
375
483
 
484
+ function assertRunAttemptEvidence(evidence: unknown): void {
485
+ if (
486
+ !isRecord(evidence) ||
487
+ evidence.schema_version !== '1' ||
488
+ !isRecord(evidence.repetition) ||
489
+ !isPositiveSafeInteger(evidence.repetition.index) ||
490
+ !isPositiveSafeInteger(evidence.repetition.total) ||
491
+ evidence.repetition.index > evidence.repetition.total ||
492
+ !isRecord(evidence.retry_policy) ||
493
+ !isNonnegativeSafeInteger(evidence.retry_policy.default_max_retries) ||
494
+ evidence.retry_policy.backoff !== 'exponential' ||
495
+ !isNonnegativeFiniteNumber(evidence.retry_policy.initial_delay_ms) ||
496
+ (evidence.timeout !== undefined &&
497
+ (!isRecord(evidence.timeout) || !isPositiveFiniteNumber(evidence.timeout.default_ms)))
498
+ ) {
499
+ throw new Error('Invalid run manifest: malformed attempt evidence');
500
+ }
501
+ }
502
+
503
+ function assertCaseAttemptEvidence(evidence: unknown, index: number): void {
504
+ if (!Array.isArray(evidence) || evidence.length === 0 || evidence.length > 100) {
505
+ throw new Error(`Invalid run manifest: case ${index} has malformed attempt evidence`);
506
+ }
507
+ for (const attempt of evidence) {
508
+ if (
509
+ !isRecord(attempt) ||
510
+ !isBoundedNonemptyString(attempt.attempt_id, 200) ||
511
+ !isBoundedNonemptyString(attempt.retry_chain_id, 200) ||
512
+ !isPositiveSafeInteger(attempt.repetition_index) ||
513
+ !isPositiveSafeInteger(attempt.attempt_number) ||
514
+ !isCaseEvaluationStatus(attempt.status) ||
515
+ typeof attempt.included_in_outcome !== 'boolean' ||
516
+ !isNonnegativeFiniteNumber(attempt.latency_ms) ||
517
+ (attempt.error_code !== undefined &&
518
+ attempt.error_code !== 'timeout' &&
519
+ attempt.error_code !== 'target_error' &&
520
+ attempt.error_code !== 'tool_error')
521
+ ) {
522
+ throw new Error(`Invalid run manifest: case ${index} has malformed attempt evidence`);
523
+ }
524
+ }
525
+ }
526
+
527
+ function assertCostProvenance(cost: unknown): void {
528
+ if (!isRecord(cost) || cost.schema_version !== '1') {
529
+ throw new Error('Invalid run manifest: malformed cost provenance');
530
+ }
531
+ if (cost.status === 'unavailable') {
532
+ if (
533
+ cost.amount !== undefined ||
534
+ cost.currency !== undefined ||
535
+ cost.source !== undefined ||
536
+ cost.recorded_at !== undefined ||
537
+ (cost.unavailable_reason !== 'provider_billing_not_recorded' &&
538
+ cost.unavailable_reason !== 'unsupported_provider' &&
539
+ cost.unavailable_reason !== 'not_requested')
540
+ ) {
541
+ throw new Error('Invalid run manifest: malformed cost provenance');
542
+ }
543
+ return;
544
+ }
545
+ if (
546
+ (cost.status !== 'known' && cost.status !== 'user_supplied') ||
547
+ !isNonnegativeFiniteNumber(cost.amount) ||
548
+ !isBoundedNonemptyString(cost.currency, 3) ||
549
+ (cost.status === 'known' && cost.source !== 'provider_billing') ||
550
+ (cost.status === 'user_supplied' && cost.source !== 'operator_input') ||
551
+ !isIsoTimestamp(cost.recorded_at) ||
552
+ cost.unavailable_reason !== undefined
553
+ ) {
554
+ throw new Error('Invalid run manifest: malformed cost provenance');
555
+ }
556
+ }
557
+
558
+ function isCaseEvaluationStatus(value: unknown): value is CaseEvaluationStatus {
559
+ return value === 'passed' || value === 'failed' || value === 'invalid' || value === 'error';
560
+ }
561
+
562
+ function isBoundedNonemptyString(value: unknown, maxLength: number): value is string {
563
+ return typeof value === 'string' && value.length > 0 && value.length <= maxLength;
564
+ }
565
+
566
+ function isNonnegativeFiniteNumber(value: unknown): value is number {
567
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
568
+ }
569
+
570
+ function isPositiveFiniteNumber(value: unknown): value is number {
571
+ return isNonnegativeFiniteNumber(value) && value > 0;
572
+ }
573
+
574
+ function isNonnegativeSafeInteger(value: unknown): value is number {
575
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
576
+ }
577
+
578
+ function isPositiveSafeInteger(value: unknown): value is number {
579
+ return isNonnegativeSafeInteger(value) && value > 0;
580
+ }
581
+
582
+ function isIsoTimestamp(value: unknown): value is string {
583
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
584
+ }
585
+
586
+ function assertCaseTargetEvidence(target: unknown): void {
587
+ if (
588
+ !isRecord(target) ||
589
+ typeof target.provider !== 'string' ||
590
+ target.provider.length === 0 ||
591
+ target.provider.length > 100 ||
592
+ (target.requested_model !== undefined &&
593
+ (typeof target.requested_model !== 'string' || target.requested_model.length > 200)) ||
594
+ !isBoundedStringList(target.observed_models)
595
+ ) {
596
+ throw new Error('Invalid run manifest: malformed target evidence');
597
+ }
598
+ }
599
+
600
+ function assertExecutionProvenance(provenance: unknown): void {
601
+ if (!isRecord(provenance) || provenance.schema_version !== '1' || !isRecord(provenance.target)) {
602
+ throw new Error('Invalid run manifest: malformed execution provenance');
603
+ }
604
+ const target = provenance.target;
605
+ if (
606
+ typeof target.provider !== 'string' ||
607
+ target.provider.length === 0 ||
608
+ target.provider.length > 100 ||
609
+ !isBoundedStringList(target.requested_models) ||
610
+ !isBoundedStringList(target.observed_models) ||
611
+ (target.generation !== undefined && !isGenerationConfig(target.generation))
612
+ ) {
613
+ throw new Error('Invalid run manifest: malformed execution provenance');
614
+ }
615
+ if (provenance.evaluator !== undefined) {
616
+ if (!isRecord(provenance.evaluator) || !isBoundedStringList(provenance.evaluator.models)) {
617
+ throw new Error('Invalid run manifest: malformed execution provenance');
618
+ }
619
+ }
620
+ }
621
+
622
+ function isBoundedStringList(value: unknown): boolean {
623
+ return (
624
+ value === undefined ||
625
+ (Array.isArray(value) &&
626
+ value.length <= 100 &&
627
+ value.every((item) => typeof item === 'string' && item.length > 0 && item.length <= 200))
628
+ );
629
+ }
630
+
631
+ function isGenerationConfig(value: unknown): boolean {
632
+ if (!isRecord(value)) return false;
633
+ return [value.temperature, value.max_tokens, value.seed].every(
634
+ (item) => item === undefined || (typeof item === 'number' && Number.isFinite(item))
635
+ );
636
+ }
637
+
376
638
  function assertWorkloadIdentity(identity: unknown): void {
377
639
  if (
378
640
  !isRecord(identity) ||
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createExecutionProvenance } from './execution-provenance';
3
+
4
+ describe('createExecutionProvenance', () => {
5
+ test('keeps requested, observed target, and evaluator model identities separate', () => {
6
+ const provenance = createExecutionProvenance({
7
+ provider: 'openai',
8
+ requestedModel: 'gpt-requested',
9
+ temperature: 0.2,
10
+ maxTokens: 100,
11
+ seed: 7,
12
+ cases: [
13
+ {
14
+ id: 'one',
15
+ ok: true,
16
+ score: 1,
17
+ matcherType: 'exact',
18
+ latencyMs: 1,
19
+ tokens: { prompt: 1, completion: 1, total: 2 },
20
+ prompt: 'prompt',
21
+ response: 'response',
22
+ expected: { type: 'exact', value: 'response', caseSensitive: true },
23
+ tags: [],
24
+ target: {
25
+ provider: 'openai',
26
+ requested_model: 'gpt-requested',
27
+ observed_models: ['gpt-observed'],
28
+ },
29
+ evidence: { evaluator: 'llm_grader', model: 'judge-model' },
30
+ },
31
+ ],
32
+ });
33
+
34
+ expect(provenance).toEqual({
35
+ schema_version: '1',
36
+ target: {
37
+ provider: 'openai',
38
+ requested_models: ['gpt-requested'],
39
+ observed_models: ['gpt-observed'],
40
+ generation: { temperature: 0.2, max_tokens: 100, seed: 7 },
41
+ },
42
+ evaluator: { models: ['judge-model'] },
43
+ });
44
+ });
45
+
46
+ test('records an unavailable observed identity without inventing one', () => {
47
+ const provenance = createExecutionProvenance({
48
+ provider: 'custom',
49
+ requestedModel: 'requested-model',
50
+ cases: [],
51
+ });
52
+
53
+ expect(provenance.target).toEqual({
54
+ provider: 'custom',
55
+ requested_models: ['requested-model'],
56
+ });
57
+ expect(provenance.evaluator).toBeUndefined();
58
+ });
59
+ });
@@ -0,0 +1,53 @@
1
+ /** Bounded execution provenance derived from declared and observed run evidence. */
2
+
3
+ import type { CaseResult, ExecutionProvenance } from '../artifacts/types';
4
+
5
+ export function createExecutionProvenance(options: {
6
+ provider: string;
7
+ requestedModel?: string;
8
+ temperature?: number;
9
+ maxTokens?: number;
10
+ seed?: number;
11
+ cases: CaseResult[];
12
+ }): ExecutionProvenance {
13
+ const requestedModels = uniqueStrings([
14
+ options.requestedModel,
15
+ ...options.cases.map((caseResult) => caseResult.target?.requested_model),
16
+ ]);
17
+ const observedModels = uniqueStrings(
18
+ options.cases.flatMap((caseResult) => caseResult.target?.observed_models ?? [])
19
+ );
20
+ const evaluatorModels = uniqueStrings(
21
+ options.cases.map((caseResult) => caseResult.evidence?.model)
22
+ );
23
+ const generation = omitUndefined({
24
+ temperature: options.temperature,
25
+ max_tokens: options.maxTokens,
26
+ seed: options.seed,
27
+ });
28
+
29
+ return {
30
+ schema_version: '1',
31
+ target: {
32
+ provider: boundedString(options.provider, 100) ?? 'unknown',
33
+ ...(requestedModels.length ? { requested_models: requestedModels } : {}),
34
+ ...(observedModels.length ? { observed_models: observedModels } : {}),
35
+ ...(Object.keys(generation).length ? { generation } : {}),
36
+ },
37
+ ...(evaluatorModels.length ? { evaluator: { models: evaluatorModels } } : {}),
38
+ };
39
+ }
40
+
41
+ function uniqueStrings(values: unknown[]): string[] {
42
+ return [...new Set(values.map((value) => boundedString(value, 200)).filter(Boolean))] as string[];
43
+ }
44
+
45
+ function boundedString(value: unknown, maxLength: number): string | undefined {
46
+ return typeof value === 'string' && value.length > 0 ? value.slice(0, maxLength) : undefined;
47
+ }
48
+
49
+ function omitUndefined<T extends Record<string, number | undefined>>(value: T): Partial<T> {
50
+ return Object.fromEntries(
51
+ Object.entries(value).filter(([, item]) => item !== undefined)
52
+ ) as Partial<T>;
53
+ }
@@ -5,3 +5,4 @@
5
5
  export { getGitInfo } from './git';
6
6
  export { getEnvironmentInfo } from './environment';
7
7
  export { createWorkloadIdentity } from './workload-identity';
8
+ export { createExecutionProvenance } from './execution-provenance';
@@ -170,6 +170,11 @@ describe('executeCase tool loop', () => {
170
170
  expect(result.error).toBe('TOOL_EXECUTOR_REQUIRED');
171
171
  expect(result.latencyMs).toBe(4);
172
172
  expect(result.tokens).toEqual({ prompt: 7, completion: 2, total: 9 });
173
+ expect(result.target).toEqual({
174
+ provider: 'ling',
175
+ requested_model: 'Ling-3.0-flash',
176
+ observed_models: ['Ling-3.0-flash'],
177
+ });
173
178
  });
174
179
 
175
180
  it('retains prior generation metrics when a later generation rejects', async () => {
@@ -201,6 +206,7 @@ describe('executeCase tool loop', () => {
201
206
  steps: 1,
202
207
  terminationReason: 'tool_error',
203
208
  });
209
+ expect(result.target?.observed_models).toEqual(['Ling-3.0-flash']);
204
210
  });
205
211
 
206
212
  it('retains prior generation metrics when a later generation times out', async () => {
@@ -304,7 +310,61 @@ describe('executeCase measurement integrity', () => {
304
310
  status: 'error',
305
311
  response: '',
306
312
  error: 'provider unavailable',
313
+ target: {
314
+ provider: 'test',
315
+ },
307
316
  });
317
+ expect(result.target?.observed_models).toBeUndefined();
318
+ });
319
+
320
+ it('records retried target failures as excluded retry-chain attempts', async () => {
321
+ let calls = 0;
322
+ const retryClient: ModelClient = {
323
+ ...client,
324
+ generate: async () => {
325
+ calls++;
326
+ if (calls === 1) throw new Error('temporary provider failure');
327
+ return {
328
+ id: 'response',
329
+ model: 'target-model',
330
+ text: 'target response',
331
+ tokens: { prompt: 1, completion: 1, total: 2 },
332
+ latencyMs: 1,
333
+ finishReason: 'stop',
334
+ };
335
+ },
336
+ };
337
+ registerEvaluator('custom', {
338
+ type: 'custom',
339
+ evaluate: async () => ({ passed: true, score: 1 }),
340
+ });
341
+
342
+ const result = await executeCase(scenario.cases[0], {
343
+ client: retryClient,
344
+ scenario,
345
+ retries: 1,
346
+ runId: 'assurance-run',
347
+ repetition: { index: 2, total: 3 },
348
+ });
349
+
350
+ expect(result.attempts).toBe(2);
351
+ expect(result.attempt_evidence).toEqual([
352
+ expect.objectContaining({
353
+ attempt_id: 'assurance-run:custom-evaluation:1',
354
+ retry_chain_id: 'assurance-run:custom-evaluation',
355
+ repetition_index: 2,
356
+ attempt_number: 1,
357
+ status: 'error',
358
+ included_in_outcome: false,
359
+ error_code: 'target_error',
360
+ }),
361
+ expect.objectContaining({
362
+ attempt_id: 'assurance-run:custom-evaluation:2',
363
+ attempt_number: 2,
364
+ status: 'passed',
365
+ included_in_outcome: true,
366
+ }),
367
+ ]);
308
368
  });
309
369
 
310
370
  it('retains only the bounded evidence contract rather than evaluator details', async () => {