@holonograph/client 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +321 -152
  2. package/dist/index.js +689 -13
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4691,7 +4691,9 @@ var LightSnapshotSchema = external_exports.object({
4691
4691
 
4692
4692
  vendorMetadata: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
4693
4693
 
4694
- rawRequestOmitted: external_exports.boolean().optional()
4694
+ rawRequestOmitted: external_exports.boolean().optional(),
4695
+
4696
+ rawResponsePresence: external_exports.enum(["present", "omitted-by-policy", "pending-attach", "failed-attach"]).optional()
4695
4697
  });
4696
4698
  var DimensionResultSchema = external_exports.object({
4697
4699
  dimensionId: DimensionIdSchema,
@@ -4765,7 +4767,9 @@ var EvaluationEventKindSchema = external_exports.enum([
4765
4767
 
4766
4768
  "analysis.eval-recommendation",
4767
4769
 
4768
- "analysis.conformance-fixture"
4770
+ "analysis.conformance-fixture",
4771
+
4772
+ "analysis.emission-coverage"
4769
4773
  ]);
4770
4774
  var AvailabilityTransitionDirectionSchema = external_exports.enum([
4771
4775
  "reachable->unreachable",
@@ -4941,6 +4945,18 @@ var ConformanceFixtureRecordSchema = external_exports.object({
4941
4945
 
4942
4946
  sourceSurfaceId: external_exports.string().min(1).optional()
4943
4947
  });
4948
+ var EmissionCoverageSchema = external_exports.object({
4949
+
4950
+ streamId: external_exports.string().min(1),
4951
+
4952
+ observedOrdinals: external_exports.array(external_exports.number().int().nonnegative()),
4953
+
4954
+ sentinelHighWater: external_exports.number().int().nonnegative().nullable(),
4955
+
4956
+ finalSeen: external_exports.boolean(),
4957
+
4958
+ observedAtMs: external_exports.number().int().nonnegative()
4959
+ });
4944
4960
  var EvaluationEventSchema = external_exports.object({
4945
4961
 
4946
4962
  eventId: external_exports.string().min(1),
@@ -4971,31 +4987,36 @@ var EvaluationEventSchema = external_exports.object({
4971
4987
 
4972
4988
  evalRecommendation: EvalRecommendationSchema.optional(),
4973
4989
 
4974
- conformanceFixtureRecord: ConformanceFixtureRecordSchema.optional()
4990
+ conformanceFixtureRecord: ConformanceFixtureRecordSchema.optional(),
4991
+
4992
+ emissionCoverage: EmissionCoverageSchema.optional()
4975
4993
  }).refine(
4976
4994
  (e) => {
4977
4995
  if (e.kind === "evaluation") {
4978
- return e.light !== void 0 && e.substrate !== void 0 && e.surface !== void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0;
4996
+ return e.light !== void 0 && e.substrate !== void 0 && e.surface !== void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0 && e.emissionCoverage === void 0;
4979
4997
  }
4980
4998
  if (e.kind === "availability.transition") {
4981
- return e.availability !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0;
4999
+ return e.availability !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0 && e.emissionCoverage === void 0;
4982
5000
  }
4983
5001
  if (e.kind === "analysis.noise-floor-calibration") {
4984
- return e.noiseFloorCalibration !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0;
5002
+ return e.noiseFloorCalibration !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0 && e.emissionCoverage === void 0;
4985
5003
  }
4986
5004
  if (e.kind === "analysis.variance-decomposition") {
4987
- return e.varianceDecomposition !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0;
5005
+ return e.varianceDecomposition !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0 && e.emissionCoverage === void 0;
4988
5006
  }
4989
5007
  if (e.kind === "analysis.lifecycle-transition") {
4990
- return e.lifecycleTransition !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0;
5008
+ return e.lifecycleTransition !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0 && e.emissionCoverage === void 0;
4991
5009
  }
4992
5010
  if (e.kind === "analysis.eval-recommendation") {
4993
- return e.evalRecommendation !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.conformanceFixtureRecord === void 0;
5011
+ return e.evalRecommendation !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.conformanceFixtureRecord === void 0 && e.emissionCoverage === void 0;
5012
+ }
5013
+ if (e.kind === "analysis.conformance-fixture") {
5014
+ return e.conformanceFixtureRecord !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.emissionCoverage === void 0;
4994
5015
  }
4995
- return e.conformanceFixtureRecord !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0;
5016
+ return e.emissionCoverage !== void 0 && e.light === void 0 && e.substrate === void 0 && e.surface === void 0 && e.availability === void 0 && e.noiseFloorCalibration === void 0 && e.varianceDecomposition === void 0 && e.lifecycleTransition === void 0 && e.evalRecommendation === void 0 && e.conformanceFixtureRecord === void 0;
4996
5017
  },
4997
5018
  {
4998
- message: "EvaluationEvent shape invariant: each kind requires exactly its corresponding payload and forbids the others. kind='evaluation' \u2192 light+substrate+surface; kind='availability.transition' \u2192 availability; kind='analysis.noise-floor-calibration' \u2192 noiseFloorCalibration; kind='analysis.variance-decomposition' \u2192 varianceDecomposition; kind='analysis.lifecycle-transition' \u2192 lifecycleTransition; kind='analysis.eval-recommendation' \u2192 evalRecommendation; kind='analysis.conformance-fixture' \u2192 conformanceFixtureRecord."
5019
+ message: "EvaluationEvent shape invariant: each kind requires exactly its corresponding payload and forbids the others. kind='evaluation' \u2192 light+substrate+surface; kind='availability.transition' \u2192 availability; kind='analysis.noise-floor-calibration' \u2192 noiseFloorCalibration; kind='analysis.variance-decomposition' \u2192 varianceDecomposition; kind='analysis.lifecycle-transition' \u2192 lifecycleTransition; kind='analysis.eval-recommendation' \u2192 evalRecommendation; kind='analysis.conformance-fixture' \u2192 conformanceFixtureRecord; kind='analysis.emission-coverage' \u2192 emissionCoverage."
4999
5020
  }
5000
5021
  );
5001
5022
  function newEventId(occurredAt = new Date()) {
@@ -5310,6 +5331,596 @@ var InProcessTransport = class {
5310
5331
  }
5311
5332
  };
5312
5333
 
5334
+ var BoundedEmitBuffer = class {
5335
+ capacity;
5336
+ items = [];
5337
+ _offeredTotal = 0;
5338
+ _droppedTotal = 0;
5339
+
5340
+ constructor(capacity) {
5341
+ if (!Number.isInteger(capacity) || capacity < 1) {
5342
+ throw new RangeError(
5343
+ `BoundedEmitBuffer capacity must be a positive integer, got ${String(capacity)}`
5344
+ );
5345
+ }
5346
+ this.capacity = capacity;
5347
+ }
5348
+
5349
+ offer(item) {
5350
+ this._offeredTotal += 1;
5351
+ if (this.items.length >= this.capacity) {
5352
+ this._droppedTotal += 1;
5353
+ return false;
5354
+ }
5355
+ this.items.push(item);
5356
+ return true;
5357
+ }
5358
+
5359
+ drain() {
5360
+ if (this.items.length === 0) return [];
5361
+ const out = this.items;
5362
+ this.items = [];
5363
+ return out;
5364
+ }
5365
+
5366
+ get size() {
5367
+ return this.items.length;
5368
+ }
5369
+
5370
+ get maxSize() {
5371
+ return this.capacity;
5372
+ }
5373
+
5374
+ get offeredTotal() {
5375
+ return this._offeredTotal;
5376
+ }
5377
+
5378
+ get droppedTotal() {
5379
+ return this._droppedTotal;
5380
+ }
5381
+ };
5382
+
5383
+ var CompletenessAccounting = class {
5384
+ constructor(lensVersion, config, hooks = {}) {
5385
+ this.lensVersion = lensVersion;
5386
+ this.config = config;
5387
+ this.hooks = hooks;
5388
+ }
5389
+ lensVersion;
5390
+ config;
5391
+ hooks;
5392
+ windowStartMs = null;
5393
+ offered = 0;
5394
+ dropped = 0;
5395
+ delivered = 0;
5396
+ failed = 0;
5397
+
5398
+ channels = new Map();
5399
+
5400
+ sustainedSinceMs = null;
5401
+
5402
+ alerted = false;
5403
+
5404
+ recordOffer(nowMs, accepted) {
5405
+ this.openWindowIfNeeded(nowMs);
5406
+ this.offered += 1;
5407
+ if (!accepted) this.dropped += 1;
5408
+ }
5409
+
5410
+ recordFlush(nowMs, channel, delivered, failed) {
5411
+ this.openWindowIfNeeded(nowMs);
5412
+ this.delivered += delivered;
5413
+ this.failed += failed;
5414
+ const entry = this.channels.get(channel) ?? { attempted: 0, delivered: 0, failed: 0 };
5415
+ entry.attempted += delivered + failed;
5416
+ entry.delivered += delivered;
5417
+ entry.failed += failed;
5418
+ this.channels.set(channel, entry);
5419
+ }
5420
+
5421
+ maybeRollover(nowMs) {
5422
+ if (this.windowStartMs === null) return;
5423
+ if (nowMs - this.windowStartMs < this.config.windowMs) return;
5424
+ this.finalize(this.windowStartMs + this.config.windowMs);
5425
+ }
5426
+
5427
+ snapshot(nowMs) {
5428
+ if (this.windowStartMs === null) return null;
5429
+ return this.buildWindow(this.windowStartMs, nowMs);
5430
+ }
5431
+
5432
+ flushOpenWindow(nowMs) {
5433
+ if (this.windowStartMs === null) return;
5434
+ this.finalize(Math.max(nowMs, this.windowStartMs));
5435
+ }
5436
+ openWindowIfNeeded(nowMs) {
5437
+ this.windowStartMs ??= nowMs;
5438
+ }
5439
+ finalize(windowEndMs) {
5440
+ const start = this.windowStartMs;
5441
+ if (start === null) return;
5442
+ const window = this.buildWindow(start, windowEndMs);
5443
+ this.windowStartMs = null;
5444
+ this.offered = 0;
5445
+ this.dropped = 0;
5446
+ this.delivered = 0;
5447
+ this.failed = 0;
5448
+ this.channels.clear();
5449
+ this.hooks.onWindow?.(window);
5450
+ this.evaluateSustainedLoad(window);
5451
+ }
5452
+ buildWindow(startMs, endMs) {
5453
+ const completenessLowerBound = this.offered === 0 ? 1 : (this.offered - this.dropped) / this.offered;
5454
+ const channels = {};
5455
+ for (const [channel, entry] of this.channels) {
5456
+ channels[channel] = { ...entry };
5457
+ }
5458
+ return {
5459
+ lensVersion: this.lensVersion,
5460
+ windowStartMs: startMs,
5461
+ windowEndMs: endMs,
5462
+ offered: this.offered,
5463
+ dropped: this.dropped,
5464
+ delivered: this.delivered,
5465
+ failed: this.failed,
5466
+ channels,
5467
+ completenessLowerBound,
5468
+ biasFlag: this.dropped > 0
5469
+ };
5470
+ }
5471
+ evaluateSustainedLoad(window) {
5472
+ const rate = window.offered === 0 ? 0 : window.dropped / window.offered;
5473
+ if (rate >= this.config.dropRateThreshold && window.dropped > 0) {
5474
+ this.sustainedSinceMs ??= window.windowStartMs;
5475
+ const sustainedForMs = window.windowEndMs - this.sustainedSinceMs;
5476
+ if (sustainedForMs >= this.config.sustainedForMs && !this.alerted) {
5477
+ this.alerted = true;
5478
+ this.hooks.onExtremeSustainedLoad?.({
5479
+ lensVersion: this.lensVersion,
5480
+ sinceMs: this.sustainedSinceMs,
5481
+ firedAtMs: window.windowEndMs,
5482
+ sustainedForMs,
5483
+ dropRate: rate,
5484
+ dropRateThreshold: this.config.dropRateThreshold
5485
+ });
5486
+ }
5487
+ } else {
5488
+ this.sustainedSinceMs = null;
5489
+ this.alerted = false;
5490
+ }
5491
+ }
5492
+ };
5493
+
5494
+ var DEFAULT_BUFFER_SIZE = 2048;
5495
+ var DEFAULT_FLUSH_INTERVAL_MS = 1e3;
5496
+ var DEFAULT_WINDOW_MS = 1e4;
5497
+ var DEFAULT_DROP_RATE_THRESHOLD = 0.01;
5498
+ var DEFAULT_SUSTAINED_FOR_MS = 6e4;
5499
+ var DEFAULT_SENTINEL_INTERVAL_MS = 3e4;
5500
+ var streamIdFallbackCounter = 0;
5501
+ function generateStreamId(nowMs) {
5502
+ const cryptoObj = globalThis.crypto;
5503
+ if (typeof cryptoObj?.randomUUID === "function") return cryptoObj.randomUUID();
5504
+ streamIdFallbackCounter += 1;
5505
+ return `stream-${String(nowMs)}-${String(streamIdFallbackCounter)}`;
5506
+ }
5507
+ function defaultScheduleInterval(fn, ms) {
5508
+ const handle = setInterval(fn, ms);
5509
+ handle.unref?.();
5510
+ return () => {
5511
+ clearInterval(handle);
5512
+ };
5513
+ }
5514
+ var EvaluationEventEmitter = class {
5515
+ buffer;
5516
+ flushTargets;
5517
+
5518
+ sentinelTargets;
5519
+ accounting;
5520
+ now;
5521
+ onFlushError;
5522
+ stopInterval;
5523
+ stopSentinelInterval;
5524
+
5525
+ streamId;
5526
+ nextSeq = 0;
5527
+
5528
+ lastSeq = -1;
5529
+
5530
+ inFlight = null;
5531
+ stopped = false;
5532
+ constructor(opts) {
5533
+ this.buffer = new BoundedEmitBuffer(opts.bufferSize ?? DEFAULT_BUFFER_SIZE);
5534
+ if (opts.flushTargets.length === 0) {
5535
+ throw new Error("EvaluationEventEmitter requires at least one flush target");
5536
+ }
5537
+ this.flushTargets = [...opts.flushTargets];
5538
+ this.sentinelTargets = this.flushTargets.filter((t) => typeof t.emitSentinel === "function");
5539
+ this.now = opts.now ?? Date.now;
5540
+ this.streamId = opts.streamId ?? generateStreamId(this.now());
5541
+ this.onFlushError = opts.onFlushError ?? ((error) => {
5542
+ console.warn("[holonograph] emit flush error:", error);
5543
+ });
5544
+ const config = {
5545
+ windowMs: opts.windowMs ?? DEFAULT_WINDOW_MS,
5546
+ dropRateThreshold: opts.extremeSustainedLoad?.dropRateThreshold ?? DEFAULT_DROP_RATE_THRESHOLD,
5547
+ sustainedForMs: opts.extremeSustainedLoad?.sustainedForMs ?? DEFAULT_SUSTAINED_FOR_MS
5548
+ };
5549
+ this.accounting = new CompletenessAccounting(opts.lensVersion, config, {
5550
+ ...opts.onWindow !== void 0 ? { onWindow: opts.onWindow } : {},
5551
+ ...opts.onExtremeSustainedLoad !== void 0 ? { onExtremeSustainedLoad: opts.onExtremeSustainedLoad } : {}
5552
+ });
5553
+ const schedule = opts.scheduleInterval ?? defaultScheduleInterval;
5554
+ this.stopInterval = schedule(() => {
5555
+ this.tick();
5556
+ }, opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS);
5557
+ this.stopSentinelInterval = schedule(() => {
5558
+ this.sentinelTick();
5559
+ }, opts.sentinelIntervalMs ?? DEFAULT_SENTINEL_INTERVAL_MS);
5560
+ }
5561
+
5562
+ enqueue(event) {
5563
+ const nowMs = this.now();
5564
+ const seq = this.nextSeq;
5565
+ this.nextSeq += 1;
5566
+ this.lastSeq = seq;
5567
+ const accepted = this.buffer.offer({ event, seq, streamId: this.streamId });
5568
+ this.accounting.recordOffer(nowMs, accepted);
5569
+ this.accounting.maybeRollover(nowMs);
5570
+ return event.eventId;
5571
+ }
5572
+
5573
+ tick() {
5574
+ void this.flushNow();
5575
+ this.accounting.maybeRollover(this.now());
5576
+ }
5577
+
5578
+ sentinelTick() {
5579
+ if (this.lastSeq < 0) return;
5580
+ void this.emitSentinelNow(false);
5581
+ }
5582
+
5583
+ async emitSentinelNow(final) {
5584
+ if (this.sentinelTargets.length === 0) return;
5585
+ const sentinel = { streamId: this.streamId, seq: this.lastSeq, final };
5586
+ await Promise.all(
5587
+ this.sentinelTargets.map(
5588
+ (t) => (t.emitSentinel?.(sentinel) ?? Promise.resolve()).catch((error) => {
5589
+ this.onFlushError(error);
5590
+ })
5591
+ )
5592
+ );
5593
+ }
5594
+
5595
+ flushNow() {
5596
+ if (this.inFlight) return this.inFlight;
5597
+ const batch = this.buffer.drain();
5598
+ if (batch.length === 0) return Promise.resolve();
5599
+ const flush = this.doFlush(batch).finally(() => {
5600
+ this.inFlight = null;
5601
+ });
5602
+ this.inFlight = flush;
5603
+ return flush;
5604
+ }
5605
+ async doFlush(batch) {
5606
+ await Promise.all(this.flushTargets.map((target) => this.flushToTarget(target, batch)));
5607
+ }
5608
+ async flushToTarget(target, batch) {
5609
+ const channel = target.channel;
5610
+ try {
5611
+ const result = await target.flushBatch(batch);
5612
+ this.accounting.recordFlush(this.now(), channel, result.delivered, result.failed);
5613
+ for (const error of result.errors) this.onFlushError(error);
5614
+ } catch (error) {
5615
+ this.accounting.recordFlush(this.now(), channel, 0, batch.length);
5616
+ this.onFlushError(error);
5617
+ }
5618
+ }
5619
+
5620
+ snapshotWindow() {
5621
+ return this.accounting.snapshot(this.now());
5622
+ }
5623
+
5624
+ async shutdown() {
5625
+ if (this.stopped) return;
5626
+ this.stopped = true;
5627
+ this.stopInterval();
5628
+ this.stopSentinelInterval();
5629
+ while (this.buffer.size > 0 || this.inFlight) {
5630
+ await this.flushNow();
5631
+ }
5632
+ if (this.lastSeq >= 0) await this.emitSentinelNow(true);
5633
+ await Promise.all(this.flushTargets.map((t) => t.shutdown?.() ?? Promise.resolve()));
5634
+ this.accounting.flushOpenWindow(this.now());
5635
+ }
5636
+ };
5637
+
5638
+ var HttpEventFlushTarget = class {
5639
+
5640
+ constructor(postEvent) {
5641
+ this.postEvent = postEvent;
5642
+ }
5643
+ postEvent;
5644
+
5645
+ channel = "http";
5646
+ async flushBatch(items) {
5647
+ let delivered = 0;
5648
+ let failed = 0;
5649
+ const errors = [];
5650
+ for (const item of items) {
5651
+ try {
5652
+ await this.postEvent(item.event);
5653
+ delivered += 1;
5654
+ } catch (error) {
5655
+ failed += 1;
5656
+ errors.push(error);
5657
+ }
5658
+ }
5659
+ return { delivered, failed, errors };
5660
+ }
5661
+ };
5662
+
5663
+ var EVALUATION_SPAN_NAME = "holonograph.evaluation";
5664
+ var DIMENSION_EVENT_NAME = "holonograph.dimension";
5665
+ var SENTINEL_SPAN_NAME = "holonograph.session.sentinel";
5666
+ var SPAN_ATTR = {
5667
+ eventId: "holonograph.event.id",
5668
+ correlationId: "holonograph.correlation_id",
5669
+ surfaceId: "holonograph.surface.id",
5670
+ fixtureId: "holonograph.surface.fixture_id",
5671
+ passed: "holonograph.surface.passed",
5672
+ cohortTags: "holonograph.surface.cohort_tags",
5673
+ lensVersion: "holonograph.lens.version",
5674
+ lightSourceId: "holonograph.light_source.id",
5675
+ runMode: "holonograph.run_mode",
5676
+ provenance: "holonograph.provenance",
5677
+ latencyMs: "holonograph.light.latency_ms",
5678
+ tokensInput: "holonograph.light.tokens.input",
5679
+ tokensOutput: "holonograph.light.tokens.output",
5680
+ ok: "holonograph.light.ok",
5681
+
5682
+ rawResponseLocation: "holonograph.raw_response.location",
5683
+
5684
+ substratePrefix: "holonograph.substrate.",
5685
+ dimensionId: "holonograph.dimension.id",
5686
+ dimensionPassed: "holonograph.dimension.passed",
5687
+ dimensionScore: "holonograph.dimension.score",
5688
+
5689
+ emissionSeq: "holonograph.emission.seq",
5690
+ emissionStream: "holonograph.emission.stream",
5691
+
5692
+ emissionFinal: "holonograph.emission.final"
5693
+ };
5694
+ function buildEvaluationSpanData(event, emission, options) {
5695
+ const light = event.light;
5696
+ const surface = event.surface;
5697
+ const substrate = event.substrate;
5698
+ const startTimeMs = Date.parse(event.occurredAt);
5699
+ const latencyMs = typeof light?.latencyMs === "number" ? light.latencyMs : 0;
5700
+ const endTimeMs = startTimeMs + latencyMs;
5701
+ const isError = light?.error !== void 0;
5702
+ const attributes = {
5703
+ [SPAN_ATTR.eventId]: event.eventId,
5704
+ [SPAN_ATTR.correlationId]: event.correlationId,
5705
+ [SPAN_ATTR.surfaceId]: event.surfaceId,
5706
+ [SPAN_ATTR.lensVersion]: event.lensVersion,
5707
+ [SPAN_ATTR.ok]: !isError,
5708
+
5709
+ [SPAN_ATTR.rawResponseLocation]: options?.rawResponseLocation ?? "store"
5710
+ };
5711
+ if (surface !== void 0) {
5712
+ attributes[SPAN_ATTR.fixtureId] = surface.fixtureId;
5713
+ attributes[SPAN_ATTR.passed] = surface.passed;
5714
+ attributes[SPAN_ATTR.cohortTags] = [...surface.cohortTags];
5715
+ }
5716
+ if (substrate !== void 0) {
5717
+ attributes[SPAN_ATTR.lightSourceId] = substrate.lightSourceIdentifier;
5718
+ attributes[SPAN_ATTR.runMode] = substrate.runMode;
5719
+ attributes[SPAN_ATTR.provenance] = substrate.provenance;
5720
+ for (const [key, value] of Object.entries(substrate.columns)) {
5721
+ attributes[`${SPAN_ATTR.substratePrefix}${key}`] = value;
5722
+ }
5723
+ }
5724
+ if (typeof light?.latencyMs === "number") attributes[SPAN_ATTR.latencyMs] = light.latencyMs;
5725
+ if (light?.tokens?.input !== void 0) attributes[SPAN_ATTR.tokensInput] = light.tokens.input;
5726
+ if (light?.tokens?.output !== void 0) attributes[SPAN_ATTR.tokensOutput] = light.tokens.output;
5727
+ if (emission !== void 0) {
5728
+ attributes[SPAN_ATTR.emissionSeq] = emission.seq;
5729
+ attributes[SPAN_ATTR.emissionStream] = emission.streamId;
5730
+ }
5731
+ const dimensionEvents = (surface?.dimensions ?? []).map((d) => {
5732
+ const attrs = {
5733
+ [SPAN_ATTR.dimensionId]: d.dimensionId,
5734
+ [SPAN_ATTR.dimensionPassed]: d.passed
5735
+ };
5736
+ if (d.score !== void 0) attrs[SPAN_ATTR.dimensionScore] = d.score;
5737
+ return { name: DIMENSION_EVENT_NAME, attributes: attrs };
5738
+ });
5739
+ return {
5740
+ name: EVALUATION_SPAN_NAME,
5741
+ startTimeMs,
5742
+ endTimeMs,
5743
+ attributes,
5744
+ dimensionEvents,
5745
+ isError,
5746
+ errorMessage: light?.error?.message
5747
+ };
5748
+ }
5749
+ function writeEvaluationSpan(tracer, event, otel, emission, options) {
5750
+ const data = buildEvaluationSpanData(event, emission, options);
5751
+ const span = tracer.startSpan(data.name, {
5752
+ startTime: data.startTimeMs,
5753
+ kind: otel.SpanKindClient,
5754
+ attributes: data.attributes
5755
+ });
5756
+ for (const ev of data.dimensionEvents) span.addEvent(ev.name, ev.attributes);
5757
+ if (data.isError) {
5758
+ span.setStatus(
5759
+ data.errorMessage !== void 0 ? { code: otel.statusError, message: data.errorMessage } : { code: otel.statusError }
5760
+ );
5761
+ } else {
5762
+ span.setStatus({ code: otel.statusOk });
5763
+ }
5764
+ span.end(data.endTimeMs);
5765
+ }
5766
+ function writeSentinelSpan(tracer, sentinel, otel) {
5767
+ const span = tracer.startSpan(SENTINEL_SPAN_NAME, {
5768
+ kind: otel.SpanKindClient,
5769
+ attributes: {
5770
+ [SPAN_ATTR.emissionStream]: sentinel.streamId,
5771
+ [SPAN_ATTR.emissionSeq]: sentinel.seq,
5772
+ [SPAN_ATTR.emissionFinal]: sentinel.final
5773
+ }
5774
+ });
5775
+ span.setStatus({ code: otel.statusOk });
5776
+ span.end();
5777
+ }
5778
+
5779
+ function reconcileBatchDelivery(batchSize, delivered, exportFailed) {
5780
+ const shed = Math.max(0, batchSize - delivered - exportFailed);
5781
+ return { delivered, failed: exportFailed + shed };
5782
+ }
5783
+ var CountingExporter = class {
5784
+ constructor(inner, tally, successCode) {
5785
+ this.inner = inner;
5786
+ this.tally = tally;
5787
+ this.successCode = successCode;
5788
+ }
5789
+ inner;
5790
+ tally;
5791
+ successCode;
5792
+ export(spans, resultCallback) {
5793
+ this.inner.export(spans, (result) => {
5794
+ if (result.code === this.successCode) {
5795
+ this.tally.delivered += spans.length;
5796
+ } else {
5797
+ this.tally.failed += spans.length;
5798
+ if (result.error) this.tally.errors.push(result.error);
5799
+ }
5800
+ resultCallback(result);
5801
+ });
5802
+ }
5803
+ shutdown() {
5804
+ return this.inner.shutdown();
5805
+ }
5806
+ forceFlush() {
5807
+ return this.inner.forceFlush?.() ?? Promise.resolve();
5808
+ }
5809
+ };
5810
+ var OtlpEventFlushTarget = class {
5811
+ channel = "otlp";
5812
+ options;
5813
+ tally = { delivered: 0, failed: 0, errors: [] };
5814
+ provider;
5815
+ tracer;
5816
+ otel;
5817
+ initPromise;
5818
+
5819
+ opLock = Promise.resolve();
5820
+ constructor(options) {
5821
+ this.options = options;
5822
+ }
5823
+
5824
+ serialize(fn) {
5825
+ const run = this.opLock.then(fn, fn);
5826
+ this.opLock = run.then(
5827
+ () => void 0,
5828
+ () => void 0
5829
+ );
5830
+ return run;
5831
+ }
5832
+ flushBatch(items) {
5833
+ return this.serialize(() => this.doFlushBatch(items));
5834
+ }
5835
+ async doFlushBatch(items) {
5836
+ await this.ensureInit();
5837
+ const tracer = this.tracer;
5838
+ const otel = this.otel;
5839
+ const provider = this.provider;
5840
+ if (!tracer || !otel || !provider) {
5841
+ return {
5842
+ delivered: 0,
5843
+ failed: items.length,
5844
+ errors: [new Error("OTLP target not initialized")]
5845
+ };
5846
+ }
5847
+ const beforeDelivered = this.tally.delivered;
5848
+ const beforeFailed = this.tally.failed;
5849
+ const beforeErrors = this.tally.errors.length;
5850
+ const spanOptions = {
5851
+ rawResponseLocation: this.options.rawResponseLocation ?? "store"
5852
+ };
5853
+ for (const item of items) {
5854
+ writeEvaluationSpan(
5855
+ tracer,
5856
+ item.event,
5857
+ otel,
5858
+ { seq: item.seq, streamId: item.streamId },
5859
+ spanOptions
5860
+ );
5861
+ }
5862
+ await provider.forceFlush();
5863
+ const delivered = this.tally.delivered - beforeDelivered;
5864
+ const exportFailed = this.tally.failed - beforeFailed;
5865
+ const reconciled = reconcileBatchDelivery(items.length, delivered, exportFailed);
5866
+ return { ...reconciled, errors: this.tally.errors.slice(beforeErrors) };
5867
+ }
5868
+
5869
+ emitSentinel(sentinel) {
5870
+ return this.serialize(async () => {
5871
+ await this.ensureInit();
5872
+ if (!this.tracer || !this.otel || !this.provider) return;
5873
+ writeSentinelSpan(this.tracer, sentinel, this.otel);
5874
+ await this.provider.forceFlush();
5875
+ });
5876
+ }
5877
+ async shutdown() {
5878
+ if (this.provider) await this.provider.shutdown();
5879
+ }
5880
+
5881
+ ensureInit() {
5882
+ this.initPromise ??= this.doInit();
5883
+ return this.initPromise;
5884
+ }
5885
+ async doInit() {
5886
+ const [api, sdk, resources, core] = await Promise.all([
5887
+ import('@opentelemetry/api'),
5888
+ import('@opentelemetry/sdk-trace-base'),
5889
+ import('@opentelemetry/resources'),
5890
+ import('@opentelemetry/core')
5891
+ ]);
5892
+ let exporter;
5893
+ if (this.options.exporter) {
5894
+ exporter = this.options.exporter;
5895
+ } else {
5896
+ const otlp = await import('@opentelemetry/exporter-trace-otlp-http');
5897
+ const exporterConfig = {};
5898
+ if (this.options.endpoint !== void 0) exporterConfig.url = this.options.endpoint;
5899
+ if (this.options.headers !== void 0) exporterConfig.headers = this.options.headers;
5900
+ exporter = new otlp.OTLPTraceExporter(exporterConfig);
5901
+ }
5902
+ const counting = new CountingExporter(exporter, this.tally, core.ExportResultCode.SUCCESS);
5903
+ const resource = resources.resourceFromAttributes({
5904
+ "service.name": "holonograph-client",
5905
+ "holonograph.lens.version": this.options.lensVersion
5906
+ });
5907
+ this.provider = new sdk.BasicTracerProvider({
5908
+ resource,
5909
+ spanProcessors: [
5910
+ new sdk.BatchSpanProcessor(counting, {
5911
+ maxQueueSize: this.options.maxQueueSize ?? 4096
5912
+ })
5913
+ ]
5914
+ });
5915
+ this.tracer = this.provider.getTracer("holonograph-client");
5916
+ this.otel = {
5917
+ SpanKindClient: api.SpanKind.CLIENT,
5918
+ statusOk: api.SpanStatusCode.OK,
5919
+ statusError: api.SpanStatusCode.ERROR
5920
+ };
5921
+ }
5922
+ };
5923
+
5313
5924
  var HolonographHttpError = class extends Error {
5314
5925
  constructor(status, code, message, details) {
5315
5926
  super(message);
@@ -5327,11 +5938,46 @@ var HttpTransport = class {
5327
5938
  token;
5328
5939
  fetchImpl;
5329
5940
  runMode;
5941
+
5942
+ emitter;
5330
5943
  constructor(opts) {
5331
5944
  this.endpoint = opts.endpoint.replace(/\/$/, "");
5332
5945
  this.token = opts.token;
5333
5946
  this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
5334
5947
  this.runMode = opts.runMode;
5948
+ const e = opts.emit;
5949
+ const mode = e?.mode;
5950
+ if (e !== void 0 && (mode === "buffered" || mode === "otlp" || mode === "dual")) {
5951
+ const lensVersion = opts.lensVersion ?? "unknown";
5952
+ const flushTargets = [];
5953
+ if (mode === "buffered" || mode === "dual") {
5954
+ flushTargets.push(new HttpEventFlushTarget((event) => this.postEvent(event)));
5955
+ }
5956
+ if (mode === "otlp" || mode === "dual") {
5957
+ const otlpOpts = { lensVersion };
5958
+ if (e.otlp?.endpoint !== void 0) otlpOpts.endpoint = e.otlp.endpoint;
5959
+ if (e.otlp?.headers !== void 0) otlpOpts.headers = e.otlp.headers;
5960
+ otlpOpts.maxQueueSize = e.otlp?.maxQueueSize ?? (e.bufferSize ?? 2048) * 2;
5961
+ otlpOpts.rawResponseLocation = mode === "dual" ? "http-channel" : "store";
5962
+ flushTargets.push(new OtlpEventFlushTarget(otlpOpts));
5963
+ }
5964
+ const emitterOpts = { flushTargets, lensVersion };
5965
+ if (e.bufferSize !== void 0) emitterOpts.bufferSize = e.bufferSize;
5966
+ if (e.flushIntervalMs !== void 0) emitterOpts.flushIntervalMs = e.flushIntervalMs;
5967
+ if (e.sentinelIntervalMs !== void 0) emitterOpts.sentinelIntervalMs = e.sentinelIntervalMs;
5968
+ if (e.windowMs !== void 0) emitterOpts.windowMs = e.windowMs;
5969
+ if (e.extremeSustainedLoad !== void 0) {
5970
+ emitterOpts.extremeSustainedLoad = e.extremeSustainedLoad;
5971
+ }
5972
+ if (e.onWindow !== void 0) emitterOpts.onWindow = e.onWindow;
5973
+ if (e.onExtremeSustainedLoad !== void 0) {
5974
+ emitterOpts.onExtremeSustainedLoad = e.onExtremeSustainedLoad;
5975
+ }
5976
+ if (e.onFlushError !== void 0) emitterOpts.onFlushError = e.onFlushError;
5977
+ this.emitter = new EvaluationEventEmitter(emitterOpts);
5978
+ } else {
5979
+ this.emitter = void 0;
5980
+ }
5335
5981
  }
5336
5982
  async dispatch(req) {
5337
5983
  const wire = await this.post(
@@ -5366,9 +6012,24 @@ var HttpTransport = class {
5366
6012
  return result;
5367
6013
  }
5368
6014
  async appendEvent(event) {
6015
+ if (this.emitter) {
6016
+ return this.emitter.enqueue(event);
6017
+ }
5369
6018
  const result = await this.post("/lens/events", event, event.correlationId);
5370
6019
  return result.eventId;
5371
6020
  }
6021
+
6022
+ async postEvent(event) {
6023
+ await this.post("/lens/events", event, event.correlationId);
6024
+ }
6025
+
6026
+ completenessWindow() {
6027
+ return this.emitter?.snapshotWindow() ?? null;
6028
+ }
6029
+
6030
+ async shutdown() {
6031
+ if (this.emitter) await this.emitter.shutdown();
6032
+ }
5372
6033
  async postContract(contract) {
5373
6034
  return this.post("/holonograph/contract", contract);
5374
6035
  }
@@ -5494,6 +6155,7 @@ function flattenSystemBlocks(blocks) {
5494
6155
  var HolonographClient = class {
5495
6156
  transport;
5496
6157
  inProcessTransport;
6158
+ httpTransport;
5497
6159
  lensVersion;
5498
6160
  substrate;
5499
6161
 
@@ -5513,12 +6175,17 @@ var HolonographClient = class {
5513
6175
  constructor(options) {
5514
6176
  if (isHttpOptions(options)) {
5515
6177
  const httpOpts = {
5516
- endpoint: options.endpoint
6178
+ endpoint: options.endpoint,
6179
+
6180
+ lensVersion: options.lensVersion
5517
6181
  };
5518
6182
  if (options.token !== void 0) httpOpts.token = options.token;
5519
6183
  if (options.fetch !== void 0) httpOpts.fetch = options.fetch;
5520
6184
  if (options.runMode !== void 0) httpOpts.runMode = options.runMode;
5521
- this.transport = new HttpTransport(httpOpts);
6185
+ if (options.emit !== void 0) httpOpts.emit = options.emit;
6186
+ const t = new HttpTransport(httpOpts);
6187
+ this.transport = t;
6188
+ this.httpTransport = t;
5522
6189
  this.inProcessTransport = void 0;
5523
6190
  } else {
5524
6191
  const inProcOpts = {
@@ -5529,6 +6196,7 @@ var HolonographClient = class {
5529
6196
  const t = new InProcessTransport(inProcOpts);
5530
6197
  this.transport = t;
5531
6198
  this.inProcessTransport = t;
6199
+ this.httpTransport = void 0;
5532
6200
  }
5533
6201
  this.lensVersion = options.lensVersion;
5534
6202
  this.substrate = options.substrate;
@@ -5588,6 +6256,14 @@ var HolonographClient = class {
5588
6256
  return this.inProcessTransport ? this.inProcessTransport.topology : null;
5589
6257
  }
5590
6258
 
6259
+ completenessWindow() {
6260
+ return this.httpTransport?.completenessWindow() ?? null;
6261
+ }
6262
+
6263
+ async shutdown() {
6264
+ if (this.httpTransport) await this.httpTransport.shutdown();
6265
+ }
6266
+
5591
6267
  fetchDeclaredColumns(force = false) {
5592
6268
  const getContract = this.transport.getContract?.bind(this.transport);
5593
6269
  if (getContract === void 0) return Promise.resolve(null);