@holonograph/client 0.1.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 +397 -212
  2. package/dist/index.js +795 -15
  3. package/package.json +2 -10
package/dist/index.js CHANGED
@@ -4008,6 +4008,8 @@ var ALL_VISIBILITIES = ["transparent", "announced", "opaque"];
4008
4008
  var VisibilityAttributeSchema = external_exports.enum(ALL_VISIBILITIES);
4009
4009
  var ALL_SUBSTRATE_COLUMN_SEMANTICS = ["deployment-state", "diagnostic"];
4010
4010
  var SubstrateColumnSemanticSchema = external_exports.enum(ALL_SUBSTRATE_COLUMN_SEMANTICS);
4011
+ var ALL_EMISSION_EXPECTATIONS = ["always", "conditional"];
4012
+ var EmissionExpectationSchema = external_exports.enum(ALL_EMISSION_EXPECTATIONS);
4011
4013
  var SubstrateColumnSchema = external_exports.object({
4012
4014
  id: external_exports.string().min(1).max(128),
4013
4015
  control: ControlAttributeSchema,
@@ -4016,7 +4018,9 @@ var SubstrateColumnSchema = external_exports.object({
4016
4018
 
4017
4019
  semantic: SubstrateColumnSemanticSchema.default("deployment-state"),
4018
4020
 
4019
- values: external_exports.array(external_exports.string().min(1)).min(1).optional()
4021
+ values: external_exports.array(external_exports.string().min(1)).min(1).optional(),
4022
+
4023
+ emissionExpectation: EmissionExpectationSchema.optional()
4020
4024
  });
4021
4025
  var SubstrateSnapshotSchema = external_exports.object({
4022
4026
  lensVersion: LensVersionIdSchema,
@@ -4585,7 +4589,9 @@ external_exports.object({
4585
4589
 
4586
4590
  tools: external_exports.array(CapabilityToolDefSchema).default([]),
4587
4591
 
4588
- proseRules: external_exports.array(external_exports.string().min(1)).default([])
4592
+ proseRules: external_exports.array(external_exports.string().min(1)).default([]),
4593
+
4594
+ availableColumns: external_exports.array(external_exports.string().min(1)).optional()
4589
4595
  });
4590
4596
  external_exports.object({
4591
4597
 
@@ -4685,7 +4691,9 @@ var LightSnapshotSchema = external_exports.object({
4685
4691
 
4686
4692
  vendorMetadata: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
4687
4693
 
4688
- 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()
4689
4697
  });
4690
4698
  var DimensionResultSchema = external_exports.object({
4691
4699
  dimensionId: DimensionIdSchema,
@@ -4759,7 +4767,9 @@ var EvaluationEventKindSchema = external_exports.enum([
4759
4767
 
4760
4768
  "analysis.eval-recommendation",
4761
4769
 
4762
- "analysis.conformance-fixture"
4770
+ "analysis.conformance-fixture",
4771
+
4772
+ "analysis.emission-coverage"
4763
4773
  ]);
4764
4774
  var AvailabilityTransitionDirectionSchema = external_exports.enum([
4765
4775
  "reachable->unreachable",
@@ -4935,6 +4945,18 @@ var ConformanceFixtureRecordSchema = external_exports.object({
4935
4945
 
4936
4946
  sourceSurfaceId: external_exports.string().min(1).optional()
4937
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
+ });
4938
4960
  var EvaluationEventSchema = external_exports.object({
4939
4961
 
4940
4962
  eventId: external_exports.string().min(1),
@@ -4965,31 +4987,36 @@ var EvaluationEventSchema = external_exports.object({
4965
4987
 
4966
4988
  evalRecommendation: EvalRecommendationSchema.optional(),
4967
4989
 
4968
- conformanceFixtureRecord: ConformanceFixtureRecordSchema.optional()
4990
+ conformanceFixtureRecord: ConformanceFixtureRecordSchema.optional(),
4991
+
4992
+ emissionCoverage: EmissionCoverageSchema.optional()
4969
4993
  }).refine(
4970
4994
  (e) => {
4971
4995
  if (e.kind === "evaluation") {
4972
- 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;
4973
4997
  }
4974
4998
  if (e.kind === "availability.transition") {
4975
- 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;
4976
5000
  }
4977
5001
  if (e.kind === "analysis.noise-floor-calibration") {
4978
- 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;
4979
5003
  }
4980
5004
  if (e.kind === "analysis.variance-decomposition") {
4981
- 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;
4982
5006
  }
4983
5007
  if (e.kind === "analysis.lifecycle-transition") {
4984
- 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;
4985
5009
  }
4986
5010
  if (e.kind === "analysis.eval-recommendation") {
4987
- 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;
4988
5012
  }
4989
- 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;
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;
5015
+ }
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;
4990
5017
  },
4991
5018
  {
4992
- 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."
4993
5020
  }
4994
5021
  );
4995
5022
  function newEventId(occurredAt = new Date()) {
@@ -5304,6 +5331,596 @@ var InProcessTransport = class {
5304
5331
  }
5305
5332
  };
5306
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
+
5307
5924
  var HolonographHttpError = class extends Error {
5308
5925
  constructor(status, code, message, details) {
5309
5926
  super(message);
@@ -5321,11 +5938,46 @@ var HttpTransport = class {
5321
5938
  token;
5322
5939
  fetchImpl;
5323
5940
  runMode;
5941
+
5942
+ emitter;
5324
5943
  constructor(opts) {
5325
5944
  this.endpoint = opts.endpoint.replace(/\/$/, "");
5326
5945
  this.token = opts.token;
5327
5946
  this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
5328
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
+ }
5329
5981
  }
5330
5982
  async dispatch(req) {
5331
5983
  const wire = await this.post(
@@ -5360,12 +6012,30 @@ var HttpTransport = class {
5360
6012
  return result;
5361
6013
  }
5362
6014
  async appendEvent(event) {
6015
+ if (this.emitter) {
6016
+ return this.emitter.enqueue(event);
6017
+ }
5363
6018
  const result = await this.post("/lens/events", event, event.correlationId);
5364
6019
  return result.eventId;
5365
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
+ }
5366
6033
  async postContract(contract) {
5367
6034
  return this.post("/holonograph/contract", contract);
5368
6035
  }
6036
+ async getContract() {
6037
+ return this.get("/holonograph/contract");
6038
+ }
5369
6039
  async markAvailability(req) {
5370
6040
  return this.post("/lens/availability/mark", req);
5371
6041
  }
@@ -5413,6 +6083,45 @@ var HttpTransport = class {
5413
6083
  }
5414
6084
  return parsed;
5415
6085
  }
6086
+
6087
+ async get(path) {
6088
+ const url = `${this.endpoint}${path}`;
6089
+ const headers = { "content-type": "application/json" };
6090
+ if (this.token !== void 0) headers.authorization = `Bearer ${this.token}`;
6091
+ if (this.runMode !== void 0) headers["x-holonograph-run-mode"] = this.runMode;
6092
+ let response;
6093
+ try {
6094
+ response = await this.fetchImpl(url, { method: "GET", headers });
6095
+ } catch (cause) {
6096
+ throw new HolonographHttpError(
6097
+ 0,
6098
+ "transport_error",
6099
+ `the lens fetch failed: ${cause.message}`,
6100
+ { cause: String(cause) }
6101
+ );
6102
+ }
6103
+ const text = await response.text();
6104
+ let parsed;
6105
+ try {
6106
+ parsed = text.length ? JSON.parse(text) : {};
6107
+ } catch {
6108
+ throw new HolonographHttpError(
6109
+ response.status,
6110
+ "parse_error",
6111
+ `the lens returned non-JSON (status ${String(response.status)}): ${text.slice(0, 200)}`
6112
+ );
6113
+ }
6114
+ if (!response.ok) {
6115
+ const envelope = parsed;
6116
+ throw new HolonographHttpError(
6117
+ response.status,
6118
+ envelope.code ?? "server_error",
6119
+ envelope.error ?? `the lens returned ${String(response.status)}`,
6120
+ envelope.details
6121
+ );
6122
+ }
6123
+ return parsed;
6124
+ }
5416
6125
  };
5417
6126
  function lightSnapshotFromResult(result) {
5418
6127
  const snapshot = {
@@ -5446,9 +6155,18 @@ function flattenSystemBlocks(blocks) {
5446
6155
  var HolonographClient = class {
5447
6156
  transport;
5448
6157
  inProcessTransport;
6158
+ httpTransport;
5449
6159
  lensVersion;
5450
6160
  substrate;
5451
6161
 
6162
+ onUnknownColumn;
6163
+
6164
+ declaredColumns;
6165
+
6166
+ warnedColumns = new Set();
6167
+
6168
+ refreshedColumnsOnce = false;
6169
+
5452
6170
  messages;
5453
6171
 
5454
6172
  contract;
@@ -5457,12 +6175,17 @@ var HolonographClient = class {
5457
6175
  constructor(options) {
5458
6176
  if (isHttpOptions(options)) {
5459
6177
  const httpOpts = {
5460
- endpoint: options.endpoint
6178
+ endpoint: options.endpoint,
6179
+
6180
+ lensVersion: options.lensVersion
5461
6181
  };
5462
6182
  if (options.token !== void 0) httpOpts.token = options.token;
5463
6183
  if (options.fetch !== void 0) httpOpts.fetch = options.fetch;
5464
6184
  if (options.runMode !== void 0) httpOpts.runMode = options.runMode;
5465
- 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;
5466
6189
  this.inProcessTransport = void 0;
5467
6190
  } else {
5468
6191
  const inProcOpts = {
@@ -5473,9 +6196,11 @@ var HolonographClient = class {
5473
6196
  const t = new InProcessTransport(inProcOpts);
5474
6197
  this.transport = t;
5475
6198
  this.inProcessTransport = t;
6199
+ this.httpTransport = void 0;
5476
6200
  }
5477
6201
  this.lensVersion = options.lensVersion;
5478
6202
  this.substrate = options.substrate;
6203
+ this.onUnknownColumn = isHttpOptions(options) ? options.onUnknownColumn ?? "warn" : "ignore";
5479
6204
  this.messages = { create: (req) => this.messagesCreate(req) };
5480
6205
  this.contract = { register: (c) => this.registerContract(c) };
5481
6206
  this.availability = { mark: (req, opts) => this.markAvailability(req, opts) };
@@ -5531,6 +6256,60 @@ var HolonographClient = class {
5531
6256
  return this.inProcessTransport ? this.inProcessTransport.topology : null;
5532
6257
  }
5533
6258
 
6259
+ completenessWindow() {
6260
+ return this.httpTransport?.completenessWindow() ?? null;
6261
+ }
6262
+
6263
+ async shutdown() {
6264
+ if (this.httpTransport) await this.httpTransport.shutdown();
6265
+ }
6266
+
6267
+ fetchDeclaredColumns(force = false) {
6268
+ const getContract = this.transport.getContract?.bind(this.transport);
6269
+ if (getContract === void 0) return Promise.resolve(null);
6270
+ if (this.declaredColumns === void 0 || force) {
6271
+ this.declaredColumns = getContract().then(
6272
+ (c) => {
6273
+ const union = new Set();
6274
+ for (const surface of c.surfaces) {
6275
+ for (const id of surface.substrateColumnIds ?? []) union.add(id);
6276
+ }
6277
+ return union;
6278
+ },
6279
+ () => null
6280
+ );
6281
+ }
6282
+ return this.declaredColumns;
6283
+ }
6284
+
6285
+ async validateSubstrateColumns(columns) {
6286
+ if (columns === void 0 || this.onUnknownColumn === "ignore" || this.transport.getContract === void 0) {
6287
+ return;
6288
+ }
6289
+ const keys = Object.keys(columns);
6290
+ if (keys.length === 0) return;
6291
+ const declared = await this.fetchDeclaredColumns();
6292
+ if (declared === null || declared.size === 0) return;
6293
+ let unknown = keys.filter((k) => !declared.has(k));
6294
+ if (unknown.length > 0 && !this.refreshedColumnsOnce) {
6295
+ this.refreshedColumnsOnce = true;
6296
+ const refreshed = await this.fetchDeclaredColumns(true);
6297
+ if (refreshed === null || refreshed.size === 0) return;
6298
+ unknown = keys.filter((k) => !refreshed.has(k));
6299
+ }
6300
+ if (unknown.length === 0) return;
6301
+ if (this.onUnknownColumn === "throw") {
6302
+ throw new Error(
6303
+ `Holonograph: substrate column(s) declared in no surface contract: ${unknown.join(", ")}. Declare them on the surface contract or fix the typo. (onUnknownColumn: 'throw')`
6304
+ );
6305
+ }
6306
+ const toWarn = unknown.filter((k) => !this.warnedColumns.has(k));
6307
+ if (toWarn.length === 0) return;
6308
+ for (const k of toWarn) this.warnedColumns.add(k);
6309
+ console.warn(
6310
+ `Holonograph: substrate column(s) declared in no surface contract: ${toWarn.join(", ")}. They are still emitted, but the lens won't recognize them \u2014 declare them on the contract or fix the typo. (onUnknownColumn: 'throw' to block, 'ignore' to silence.)`
6311
+ );
6312
+ }
5534
6313
  async registerContract(contract) {
5535
6314
  if (typeof this.transport.postContract !== "function") {
5536
6315
  throw new Error(
@@ -5627,6 +6406,7 @@ var HolonographClient = class {
5627
6406
  substrate: { ...this.substrate, lightSourceIdentifier: callResult.record.lightSourceId },
5628
6407
  surface
5629
6408
  });
6409
+ await this.validateSubstrateColumns(event.substrate?.columns);
5630
6410
  await this.transport.appendEvent(event);
5631
6411
  if (callResult.observerRecords && callResult.observerRecords.length > 0) {
5632
6412
  const siblingCohortTags = baseCohortTags;