@adjudicate/observability 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Rodolpho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OTLP-shaped audit span exporter.
3
+ *
4
+ * Wraps any `AuditSink` and, in addition to the underlying durable emit,
5
+ * pushes one `audit.span` event per AuditRecord into the configured
6
+ * Exporter. The span name is `adjudicate.audit.<intentKind>` and the
7
+ * attributes carry the headline fields used for trace correlation
8
+ * (intentHash, decision, basis-code count, …).
9
+ *
10
+ * The wrapper is NOT a sink replacement. Durability is the inner sink's
11
+ * job; this just sprays a parallel trace. If the inner emit rejects, the
12
+ * wrapper rethrows (so the kernel's existing durability story is unchanged)
13
+ * but the span has still been exported — observability is best-effort and
14
+ * never blocks the audit path.
15
+ *
16
+ * Wiring:
17
+ *
18
+ * const wrapped = createOtlpAuditSpanExporter({
19
+ * inner: postgresAuditSink,
20
+ * exporter,
21
+ * });
22
+ * await adjudicateAndAudit({ envelope, state, policy, sink: wrapped });
23
+ */
24
+ import type { AuditSink } from "@adjudicate/core";
25
+ import type { Exporter } from "./exporter.js";
26
+ export interface OtlpAuditSpanExporterOptions {
27
+ /** Inner durable sink. The wrapper delegates `.emit()` to this. */
28
+ readonly inner: AuditSink;
29
+ readonly exporter: Exporter;
30
+ /** Optional Pack identifier stamped on every span. */
31
+ readonly packId?: string;
32
+ /** Optional Pack policy version stamped on every span. */
33
+ readonly policyVersion?: string;
34
+ }
35
+ export declare function createOtlpAuditSpanExporter(opts: OtlpAuditSpanExporterOptions): AuditSink;
36
+ //# sourceMappingURL=audit-spans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-spans.d.ts","sourceRoot":"","sources":["../src/audit-spans.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAe,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,eAAe,CAAC;AAG7D,MAAM,WAAW,4BAA4B;IAC3C,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,4BAA4B,GACjC,SAAS,CAgBX"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * OTLP-shaped audit span exporter.
3
+ *
4
+ * Wraps any `AuditSink` and, in addition to the underlying durable emit,
5
+ * pushes one `audit.span` event per AuditRecord into the configured
6
+ * Exporter. The span name is `adjudicate.audit.<intentKind>` and the
7
+ * attributes carry the headline fields used for trace correlation
8
+ * (intentHash, decision, basis-code count, …).
9
+ *
10
+ * The wrapper is NOT a sink replacement. Durability is the inner sink's
11
+ * job; this just sprays a parallel trace. If the inner emit rejects, the
12
+ * wrapper rethrows (so the kernel's existing durability story is unchanged)
13
+ * but the span has still been exported — observability is best-effort and
14
+ * never blocks the audit path.
15
+ *
16
+ * Wiring:
17
+ *
18
+ * const wrapped = createOtlpAuditSpanExporter({
19
+ * inner: postgresAuditSink,
20
+ * exporter,
21
+ * });
22
+ * await adjudicateAndAudit({ envelope, state, policy, sink: wrapped });
23
+ */
24
+ import { SEMCONV } from "./semconv.js";
25
+ export function createOtlpAuditSpanExporter(opts) {
26
+ return {
27
+ async emit(record) {
28
+ // Emit the span first; if the inner sink throws, we still want a trace
29
+ // of the attempt. The exporter is best-effort and swallows its own
30
+ // errors so observability never disturbs the durability path.
31
+ const span = recordToSpan(record, opts);
32
+ try {
33
+ opts.exporter.export(span);
34
+ }
35
+ catch {
36
+ /* swallow */
37
+ }
38
+ // Durability path — the wrapper is transparent to its inner sink.
39
+ await opts.inner.emit(record);
40
+ },
41
+ };
42
+ }
43
+ function recordToSpan(record, opts) {
44
+ return {
45
+ kind: "audit.span",
46
+ name: `adjudicate.audit.${record.envelope.kind}`,
47
+ at: record.at,
48
+ attributes: {
49
+ [SEMCONV.INTENT_KIND]: record.envelope.kind,
50
+ [SEMCONV.DECISION_KIND]: record.decision.kind,
51
+ [SEMCONV.TAINT]: record.envelope.taint,
52
+ [SEMCONV.INTENT_HASH]: record.intentHash,
53
+ [SEMCONV.LATENCY_MS]: record.durationMs,
54
+ "adjudicate.basis.count": record.decision_basis.length,
55
+ ...(record.resourceVersion !== undefined
56
+ ? { "adjudicate.resource.version": record.resourceVersion }
57
+ : {}),
58
+ ...(record.plan?.planFingerprint !== undefined
59
+ ? { "adjudicate.plan.fingerprint": record.plan.planFingerprint }
60
+ : {}),
61
+ ...(record.kernelIdentity !== undefined
62
+ ? {
63
+ "adjudicate.kernel.id": record.kernelIdentity.id,
64
+ "adjudicate.kernel.version": record.kernelIdentity.version,
65
+ }
66
+ : {}),
67
+ ...(opts.packId !== undefined ? { [SEMCONV.PACK_ID]: opts.packId } : {}),
68
+ ...(opts.policyVersion !== undefined
69
+ ? { [SEMCONV.POLICY_VERSION]: opts.policyVersion }
70
+ : {}),
71
+ },
72
+ // Full record for ingestion pipelines that want everything. Exporters
73
+ // that don't care can ignore `body`.
74
+ body: record,
75
+ };
76
+ }
77
+ //# sourceMappingURL=audit-spans.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-spans.js","sourceRoot":"","sources":["../src/audit-spans.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAYvC,MAAM,UAAU,2BAA2B,CACzC,IAAkC;IAElC,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,MAAmB;YAC5B,uEAAuE;YACvE,mEAAmE;YACnE,8DAA8D;YAC9D,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YACxC,IAAI,CAAC;gBACH,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,aAAa;YACf,CAAC;YACD,kEAAkE;YAClE,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CACnB,MAAmB,EACnB,IAAkC;IAElC,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,oBAAoB,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE;QAChD,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,UAAU,EAAE;YACV,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI;YAC3C,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI;YAC7C,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK;YACtC,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,UAAU;YACxC,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,UAAU;YACvC,wBAAwB,EAAE,MAAM,CAAC,cAAc,CAAC,MAAM;YACtD,GAAG,CAAC,MAAM,CAAC,eAAe,KAAK,SAAS;gBACtC,CAAC,CAAC,EAAE,6BAA6B,EAAE,MAAM,CAAC,eAAe,EAAE;gBAC3D,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,KAAK,SAAS;gBAC5C,CAAC,CAAC,EAAE,6BAA6B,EAAE,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE;gBAChE,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,cAAc,KAAK,SAAS;gBACrC,CAAC,CAAC;oBACE,sBAAsB,EAAE,MAAM,CAAC,cAAc,CAAC,EAAE;oBAChD,2BAA2B,EAAE,MAAM,CAAC,cAAc,CAAC,OAAO;iBAC3D;gBACH,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,GAAG,CAAC,IAAI,CAAC,aAAa,KAAK,SAAS;gBAClC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE;gBAClD,CAAC,CAAC,EAAE,CAAC;SACR;QACD,sEAAsE;QACtE,qCAAqC;QACrC,IAAI,EAAE,MAAM;KACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Ecosystem telemetry — local-first, opt-in, deterministic.
3
+ *
4
+ * Post-v1 the framework must evolve from evidence. This module gives
5
+ * adopters a structured way to *locally* aggregate that evidence into
6
+ * snapshots they can later choose to share. It is the substrate behind
7
+ * "adoption telemetry hooks" without any of the substrate behind
8
+ * "phone-home telemetry."
9
+ *
10
+ * Design invariants (enforced by the public surface, not by trust):
11
+ *
12
+ * 1. Opt-in only. The kernel never instantiates these aggregators;
13
+ * adopters must construct them explicitly.
14
+ * 2. Local-first. The module contains zero network code, zero file
15
+ * I/O. A snapshot is a value the adopter chooses what to do with.
16
+ * 3. Privacy-preserving. The aggregators record taxonomy entries
17
+ * (codes, kinds, categories) — never adopter payloads, never
18
+ * intent payloads, never personally identifiable strings.
19
+ * 4. Deterministic. Same input sequence → identical snapshot. No
20
+ * clock, no RNG, no environmental capture inside the aggregator.
21
+ * Wall-clock attribution is the caller's responsibility.
22
+ * 5. Bounded cardinality. Each category caps its key-space so a
23
+ * runaway Pack cannot inflate the snapshot.
24
+ *
25
+ * What the aggregators DON'T do:
26
+ *
27
+ * - They do not export to any wire protocol.
28
+ * - They do not subscribe to kernel sinks automatically.
29
+ * - They do not coalesce across processes or hosts.
30
+ * - They never observe the envelope payload, the LLM history, or
31
+ * any string that originated from an LLM.
32
+ *
33
+ * If an adopter wires the snapshot to a metrics backend, that is their
34
+ * production decision — and the snapshot shape is explicitly designed
35
+ * to remain stable across minor versions so dashboards survive bumps.
36
+ */
37
+ import type { AuditRecord, Decision, ReplayMismatch, Refusal } from "@adjudicate/core";
38
+ /**
39
+ * Closed taxonomy of operational incident classes the runtime can
40
+ * encounter. Mirrors the operator runbook categories so an incident
41
+ * channel can route on a single key.
42
+ *
43
+ * Additions are MINOR. Removals are MAJOR (would invalidate downstream
44
+ * alert rules).
45
+ */
46
+ export type OperationalIncidentClass = "kill_switch_storm" | "kill_switch_split_brain" | "audit_sink_outage" | "audit_event_bus_failure" | "replay_drift_mass" | "replay_drift_single" | "integrity_failure" | "deferred_resume_stuck" | "confirmation_token_loss" | "pack_signature_invalid" | "pack_signature_missing" | "rate_limit_threshold_breach" | "guard_panic_storm" | "shadow_divergence_spike";
47
+ /**
48
+ * Closed taxonomy of replay-failure root causes. Distinct from
49
+ * `ReplayMismatchKind` — that is the *shape* of the divergence; this
50
+ * is the inferred *cause*. The classifier is purposefully conservative:
51
+ * unknown shapes fall into `unclassified` rather than being guessed.
52
+ *
53
+ * Additions are MINOR.
54
+ */
55
+ export type ReplayFailureClass = "decision_kind_changed" | "basis_added" | "basis_removed" | "basis_swapped" | "refusal_code_changed" | "unclassified";
56
+ /**
57
+ * Closed taxonomy of analyzer-diagnostic outcomes when adopters tag
58
+ * them. The kernel never tags — only the adopter, after triage. Used to
59
+ * separate true positives from noise without leaking the diagnostic
60
+ * detail (which can carry source identifiers).
61
+ *
62
+ * Additions are MINOR.
63
+ */
64
+ export type AnalyzerTriageOutcome = "true_positive" | "false_positive" | "by_design" | "wont_fix" | "deferred";
65
+ /**
66
+ * One slot of a Pack-ecosystem snapshot. Tracks how many *unique*
67
+ * Pack ids the local process has observed, bucketed by the Pack's
68
+ * `contract` field. Never records the Pack id itself — the bucket
69
+ * counter alone is the carry-out value.
70
+ */
71
+ export interface PackEcosystemSnapshot {
72
+ /** Number of distinct Pack ids observed, by contract version. */
73
+ readonly packsByContract: Readonly<Record<string, number>>;
74
+ /** Distribution of declared quality tiers across observed manifests. */
75
+ readonly qualityTiers: Readonly<Record<string, number>>;
76
+ /** Distribution of signature algorithms seen on verified packs. */
77
+ readonly signatureAlgorithms: Readonly<Record<string, number>>;
78
+ /** Pack trust verifications classified by outcome. */
79
+ readonly trustOutcomes: Readonly<Record<string, number>>;
80
+ }
81
+ /**
82
+ * One slot of a decision distribution snapshot. Maps `Decision.kind` to
83
+ * a count. Refusal codes (when present) are nested under a closed
84
+ * vocabulary bucket; basis codes are reported as a `category` bucket
85
+ * only (never the per-code key) to keep cardinality bounded.
86
+ */
87
+ export interface DecisionDistributionSnapshot {
88
+ readonly byKind: Readonly<Record<string, number>>;
89
+ readonly refusalByCode: Readonly<Record<string, number>>;
90
+ readonly basisByCategory: Readonly<Record<string, number>>;
91
+ }
92
+ export interface ReplayFailureSnapshot {
93
+ readonly byClass: Readonly<Record<ReplayFailureClass, number>>;
94
+ readonly total: number;
95
+ }
96
+ export interface AnalyzerTriageSnapshot {
97
+ readonly byDiagnosticCode: Readonly<Record<string, Readonly<Record<AnalyzerTriageOutcome, number>>>>;
98
+ readonly totals: Readonly<Record<AnalyzerTriageOutcome, number>>;
99
+ }
100
+ export interface SemconvAdoptionSnapshot {
101
+ /** Number of times each `adjudicate.*` attribute was emitted locally. */
102
+ readonly emissionsByAttribute: Readonly<Record<string, number>>;
103
+ /**
104
+ * Number of attributes that were emitted but are *not* in the local
105
+ * `SEMCONV` constant — a signal that an adopter or extension is
106
+ * minting their own keys. Bucket only; never the attribute string.
107
+ */
108
+ readonly unknownAttributeCount: number;
109
+ }
110
+ export interface MigrationPainSnapshot {
111
+ /** Codemod name → number of files visited. */
112
+ readonly codemodVisits: Readonly<Record<string, number>>;
113
+ /** Codemod name → number of files modified. */
114
+ readonly codemodModifications: Readonly<Record<string, number>>;
115
+ /** Codemod name → number of files where the rule did not apply (no-op). */
116
+ readonly codemodNoops: Readonly<Record<string, number>>;
117
+ }
118
+ export interface IncidentSnapshot {
119
+ readonly byClass: Readonly<Record<OperationalIncidentClass, number>>;
120
+ readonly total: number;
121
+ }
122
+ /**
123
+ * The top-level snapshot. JSON-serializable. Stable shape across
124
+ * minor versions. Adopters who ship snapshots to a backend should
125
+ * version-pin to the `schemaVersion`.
126
+ */
127
+ export interface EcosystemTelemetrySnapshot {
128
+ readonly schemaVersion: 1;
129
+ readonly pack: PackEcosystemSnapshot;
130
+ readonly decisions: DecisionDistributionSnapshot;
131
+ readonly replayFailures: ReplayFailureSnapshot;
132
+ readonly analyzerTriage: AnalyzerTriageSnapshot;
133
+ readonly semconvAdoption: SemconvAdoptionSnapshot;
134
+ readonly migrationPain: MigrationPainSnapshot;
135
+ readonly incidents: IncidentSnapshot;
136
+ }
137
+ /**
138
+ * Per-aggregator cardinality caps. When exceeded, additional keys are
139
+ * absorbed into a designated `__overflow__` bucket so the snapshot
140
+ * shape stays bounded. The defaults are intentionally generous for
141
+ * single-process aggregation; adopters running shared aggregators
142
+ * should tighten them.
143
+ */
144
+ export interface EcosystemTelemetryOptions {
145
+ readonly maxPackKeys?: number;
146
+ readonly maxBasisCategories?: number;
147
+ readonly maxRefusalCodes?: number;
148
+ readonly maxAttributeKeys?: number;
149
+ readonly maxDiagnosticCodes?: number;
150
+ readonly maxCodemodKeys?: number;
151
+ }
152
+ /**
153
+ * The aggregator. Adopters call observation methods; the aggregator
154
+ * accumulates bounded counters. `snapshot()` returns a JSON-stable
155
+ * value. `reset()` is provided for tests and for periodic flush-and-
156
+ * reset rotations (e.g., snapshot-per-hour).
157
+ *
158
+ * Every observation method is synchronous and total: no I/O, no
159
+ * throws, no allocations beyond the counter.
160
+ */
161
+ export interface EcosystemTelemetry {
162
+ observePackInstallation(packId: string, contract: string): void;
163
+ observePackManifest(qualityTier: string | null): void;
164
+ observePackTrust(outcome: "ok" | "missing" | "invalid" | "unverified"): void;
165
+ observePackSignature(algorithm: string): void;
166
+ observeDecision(decision: Decision): void;
167
+ observeRefusal(refusal: Refusal): void;
168
+ observeAuditRecord(record: AuditRecord): void;
169
+ observeReplayMismatch(mismatch: ReplayMismatch): void;
170
+ observeAnalyzerTriage(diagnosticCode: string, outcome: AnalyzerTriageOutcome): void;
171
+ observeSemconvEmission(attribute: string, knownAttributes: ReadonlySet<string>): void;
172
+ observeCodemod(name: string, outcome: "visited" | "modified" | "noop"): void;
173
+ observeIncident(klass: OperationalIncidentClass): void;
174
+ snapshot(): EcosystemTelemetrySnapshot;
175
+ reset(): void;
176
+ }
177
+ export declare function createEcosystemTelemetry(options?: EcosystemTelemetryOptions): EcosystemTelemetry;
178
+ /**
179
+ * Classify a single replay mismatch into a closed taxonomy entry. Pure;
180
+ * useful both inside the aggregator and standalone for adopters who
181
+ * want to bucket their own replay report into operational categories.
182
+ */
183
+ export declare function classifyReplayFailure(mismatch: ReplayMismatch): ReplayFailureClass;
184
+ /**
185
+ * Canonical JSON serialization of a snapshot. Useful when an adopter
186
+ * wants byte-identical snapshots across processes (e.g., for diffing
187
+ * weekly aggregates). Sorts every object key recursively.
188
+ */
189
+ export declare function serializeEcosystemSnapshot(snapshot: EcosystemTelemetrySnapshot): string;
190
+ //# sourceMappingURL=ecosystem-telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ecosystem-telemetry.d.ts","sourceRoot":"","sources":["../src/ecosystem-telemetry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Ecosystem telemetry — local-first, opt-in, deterministic.
3
+ *
4
+ * Post-v1 the framework must evolve from evidence. This module gives
5
+ * adopters a structured way to *locally* aggregate that evidence into
6
+ * snapshots they can later choose to share. It is the substrate behind
7
+ * "adoption telemetry hooks" without any of the substrate behind
8
+ * "phone-home telemetry."
9
+ *
10
+ * Design invariants (enforced by the public surface, not by trust):
11
+ *
12
+ * 1. Opt-in only. The kernel never instantiates these aggregators;
13
+ * adopters must construct them explicitly.
14
+ * 2. Local-first. The module contains zero network code, zero file
15
+ * I/O. A snapshot is a value the adopter chooses what to do with.
16
+ * 3. Privacy-preserving. The aggregators record taxonomy entries
17
+ * (codes, kinds, categories) — never adopter payloads, never
18
+ * intent payloads, never personally identifiable strings.
19
+ * 4. Deterministic. Same input sequence → identical snapshot. No
20
+ * clock, no RNG, no environmental capture inside the aggregator.
21
+ * Wall-clock attribution is the caller's responsibility.
22
+ * 5. Bounded cardinality. Each category caps its key-space so a
23
+ * runaway Pack cannot inflate the snapshot.
24
+ *
25
+ * What the aggregators DON'T do:
26
+ *
27
+ * - They do not export to any wire protocol.
28
+ * - They do not subscribe to kernel sinks automatically.
29
+ * - They do not coalesce across processes or hosts.
30
+ * - They never observe the envelope payload, the LLM history, or
31
+ * any string that originated from an LLM.
32
+ *
33
+ * If an adopter wires the snapshot to a metrics backend, that is their
34
+ * production decision — and the snapshot shape is explicitly designed
35
+ * to remain stable across minor versions so dashboards survive bumps.
36
+ */
37
+ const DEFAULTS = {
38
+ maxPackKeys: 256,
39
+ maxBasisCategories: 64,
40
+ maxRefusalCodes: 128,
41
+ maxAttributeKeys: 256,
42
+ maxDiagnosticCodes: 128,
43
+ maxCodemodKeys: 64,
44
+ };
45
+ const OVERFLOW = "__overflow__";
46
+ /**
47
+ * Bounded-key counter. Increment never throws; over-cap keys land in
48
+ * the overflow bucket so the snapshot's cardinality is provably
49
+ * bounded regardless of input.
50
+ */
51
+ class BoundedCounter {
52
+ cap;
53
+ map = new Map();
54
+ constructor(cap) {
55
+ this.cap = cap;
56
+ }
57
+ add(key, delta = 1) {
58
+ if (this.map.has(key)) {
59
+ this.map.set(key, (this.map.get(key) ?? 0) + delta);
60
+ return;
61
+ }
62
+ if (this.map.size >= this.cap) {
63
+ this.map.set(OVERFLOW, (this.map.get(OVERFLOW) ?? 0) + delta);
64
+ return;
65
+ }
66
+ this.map.set(key, delta);
67
+ }
68
+ snapshot() {
69
+ const out = {};
70
+ const keys = [...this.map.keys()].sort();
71
+ for (const key of keys) {
72
+ out[key] = this.map.get(key) ?? 0;
73
+ }
74
+ return out;
75
+ }
76
+ }
77
+ class PackUniqueSet {
78
+ cap;
79
+ seen = new Set();
80
+ byContract = new BoundedCounter(32);
81
+ constructor(cap) {
82
+ this.cap = cap;
83
+ }
84
+ observe(packId, contract) {
85
+ if (this.seen.has(packId))
86
+ return;
87
+ if (this.seen.size >= this.cap)
88
+ return;
89
+ this.seen.add(packId);
90
+ this.byContract.add(contract);
91
+ }
92
+ snapshot() {
93
+ return this.byContract.snapshot();
94
+ }
95
+ }
96
+ const INCIDENT_CLASSES = [
97
+ "kill_switch_storm",
98
+ "kill_switch_split_brain",
99
+ "audit_sink_outage",
100
+ "audit_event_bus_failure",
101
+ "replay_drift_mass",
102
+ "replay_drift_single",
103
+ "integrity_failure",
104
+ "deferred_resume_stuck",
105
+ "confirmation_token_loss",
106
+ "pack_signature_invalid",
107
+ "pack_signature_missing",
108
+ "rate_limit_threshold_breach",
109
+ "guard_panic_storm",
110
+ "shadow_divergence_spike",
111
+ ];
112
+ const TRIAGE_OUTCOMES = [
113
+ "true_positive",
114
+ "false_positive",
115
+ "by_design",
116
+ "wont_fix",
117
+ "deferred",
118
+ ];
119
+ function emptyIncidentMap() {
120
+ const out = {};
121
+ for (const klass of INCIDENT_CLASSES)
122
+ out[klass] = 0;
123
+ return out;
124
+ }
125
+ function emptyTriageMap() {
126
+ const out = {};
127
+ for (const outcome of TRIAGE_OUTCOMES)
128
+ out[outcome] = 0;
129
+ return out;
130
+ }
131
+ export function createEcosystemTelemetry(options = {}) {
132
+ const opts = { ...DEFAULTS, ...options };
133
+ let pack = makeState();
134
+ function makeState() {
135
+ return {
136
+ packs: new PackUniqueSet(opts.maxPackKeys),
137
+ qualityTiers: new BoundedCounter(16),
138
+ signatureAlgorithms: new BoundedCounter(8),
139
+ trustOutcomes: new BoundedCounter(8),
140
+ decisionKinds: new BoundedCounter(8),
141
+ refusalCodes: new BoundedCounter(opts.maxRefusalCodes),
142
+ basisCategories: new BoundedCounter(opts.maxBasisCategories),
143
+ replayByClass: emptyReplayMap(),
144
+ replayTotal: 0,
145
+ analyzerByCode: new Map(),
146
+ analyzerTotals: emptyTriageMap(),
147
+ semconvEmissions: new BoundedCounter(opts.maxAttributeKeys),
148
+ semconvUnknown: 0,
149
+ codemodVisits: new BoundedCounter(opts.maxCodemodKeys),
150
+ codemodModifications: new BoundedCounter(opts.maxCodemodKeys),
151
+ codemodNoops: new BoundedCounter(opts.maxCodemodKeys),
152
+ incidents: emptyIncidentMap(),
153
+ incidentTotal: 0,
154
+ };
155
+ }
156
+ function emptyReplayMap() {
157
+ return {
158
+ decision_kind_changed: 0,
159
+ basis_added: 0,
160
+ basis_removed: 0,
161
+ basis_swapped: 0,
162
+ refusal_code_changed: 0,
163
+ unclassified: 0,
164
+ };
165
+ }
166
+ return {
167
+ observePackInstallation(packId, contract) {
168
+ pack.packs.observe(packId, contract);
169
+ },
170
+ observePackManifest(qualityTier) {
171
+ pack.qualityTiers.add(qualityTier ?? "unset");
172
+ },
173
+ observePackTrust(outcome) {
174
+ pack.trustOutcomes.add(outcome);
175
+ },
176
+ observePackSignature(algorithm) {
177
+ pack.signatureAlgorithms.add(algorithm);
178
+ },
179
+ observeDecision(decision) {
180
+ pack.decisionKinds.add(decision.kind);
181
+ for (const b of decision.basis) {
182
+ pack.basisCategories.add(b.category);
183
+ }
184
+ if (decision.kind === "REFUSE") {
185
+ pack.refusalCodes.add(decision.refusal.code);
186
+ }
187
+ },
188
+ observeRefusal(refusal) {
189
+ pack.refusalCodes.add(refusal.code);
190
+ },
191
+ observeAuditRecord(record) {
192
+ pack.decisionKinds.add(record.decision.kind);
193
+ for (const b of record.decision.basis) {
194
+ pack.basisCategories.add(b.category);
195
+ }
196
+ if (record.decision.kind === "REFUSE") {
197
+ pack.refusalCodes.add(record.decision.refusal.code);
198
+ }
199
+ },
200
+ observeReplayMismatch(mismatch) {
201
+ pack.replayTotal++;
202
+ pack.replayByClass[classifyReplayFailure(mismatch)]++;
203
+ },
204
+ observeAnalyzerTriage(diagnosticCode, outcome) {
205
+ pack.analyzerTotals[outcome]++;
206
+ let bucket = pack.analyzerByCode.get(diagnosticCode);
207
+ if (!bucket) {
208
+ if (pack.analyzerByCode.size >= opts.maxDiagnosticCodes) {
209
+ bucket = pack.analyzerByCode.get(OVERFLOW);
210
+ if (!bucket) {
211
+ bucket = emptyTriageMap();
212
+ pack.analyzerByCode.set(OVERFLOW, bucket);
213
+ }
214
+ }
215
+ else {
216
+ bucket = emptyTriageMap();
217
+ pack.analyzerByCode.set(diagnosticCode, bucket);
218
+ }
219
+ }
220
+ bucket[outcome]++;
221
+ },
222
+ observeSemconvEmission(attribute, knownAttributes) {
223
+ pack.semconvEmissions.add(attribute);
224
+ if (!knownAttributes.has(attribute)) {
225
+ pack.semconvUnknown++;
226
+ }
227
+ },
228
+ observeCodemod(name, outcome) {
229
+ if (outcome === "visited")
230
+ pack.codemodVisits.add(name);
231
+ if (outcome === "modified")
232
+ pack.codemodModifications.add(name);
233
+ if (outcome === "noop")
234
+ pack.codemodNoops.add(name);
235
+ },
236
+ observeIncident(klass) {
237
+ pack.incidents[klass]++;
238
+ pack.incidentTotal++;
239
+ },
240
+ snapshot() {
241
+ const analyzerByDiagnosticCode = {};
242
+ const codeKeys = [...pack.analyzerByCode.keys()].sort();
243
+ for (const key of codeKeys) {
244
+ const value = pack.analyzerByCode.get(key);
245
+ if (value !== undefined)
246
+ analyzerByDiagnosticCode[key] = { ...value };
247
+ }
248
+ return {
249
+ schemaVersion: 1,
250
+ pack: {
251
+ packsByContract: pack.packs.snapshot(),
252
+ qualityTiers: pack.qualityTiers.snapshot(),
253
+ signatureAlgorithms: pack.signatureAlgorithms.snapshot(),
254
+ trustOutcomes: pack.trustOutcomes.snapshot(),
255
+ },
256
+ decisions: {
257
+ byKind: pack.decisionKinds.snapshot(),
258
+ refusalByCode: pack.refusalCodes.snapshot(),
259
+ basisByCategory: pack.basisCategories.snapshot(),
260
+ },
261
+ replayFailures: {
262
+ byClass: { ...pack.replayByClass },
263
+ total: pack.replayTotal,
264
+ },
265
+ analyzerTriage: {
266
+ byDiagnosticCode: analyzerByDiagnosticCode,
267
+ totals: { ...pack.analyzerTotals },
268
+ },
269
+ semconvAdoption: {
270
+ emissionsByAttribute: pack.semconvEmissions.snapshot(),
271
+ unknownAttributeCount: pack.semconvUnknown,
272
+ },
273
+ migrationPain: {
274
+ codemodVisits: pack.codemodVisits.snapshot(),
275
+ codemodModifications: pack.codemodModifications.snapshot(),
276
+ codemodNoops: pack.codemodNoops.snapshot(),
277
+ },
278
+ incidents: {
279
+ byClass: { ...pack.incidents },
280
+ total: pack.incidentTotal,
281
+ },
282
+ };
283
+ },
284
+ reset() {
285
+ pack = makeState();
286
+ },
287
+ };
288
+ }
289
+ /**
290
+ * Classify a single replay mismatch into a closed taxonomy entry. Pure;
291
+ * useful both inside the aggregator and standalone for adopters who
292
+ * want to bucket their own replay report into operational categories.
293
+ */
294
+ export function classifyReplayFailure(mismatch) {
295
+ if (mismatch.kind === "DECISION_KIND")
296
+ return "decision_kind_changed";
297
+ if (mismatch.kind === "REFUSAL_CODE_DRIFT")
298
+ return "refusal_code_changed";
299
+ if (mismatch.kind === "BASIS_DRIFT") {
300
+ const missing = mismatch.basisDelta?.missing.length ?? 0;
301
+ const extra = mismatch.basisDelta?.extra.length ?? 0;
302
+ if (missing > 0 && extra === 0)
303
+ return "basis_removed";
304
+ if (missing === 0 && extra > 0)
305
+ return "basis_added";
306
+ if (missing > 0 && extra > 0)
307
+ return "basis_swapped";
308
+ }
309
+ return "unclassified";
310
+ }
311
+ /**
312
+ * Canonical JSON serialization of a snapshot. Useful when an adopter
313
+ * wants byte-identical snapshots across processes (e.g., for diffing
314
+ * weekly aggregates). Sorts every object key recursively.
315
+ */
316
+ export function serializeEcosystemSnapshot(snapshot) {
317
+ return canonicalJson(snapshot);
318
+ }
319
+ function canonicalJson(value) {
320
+ if (value === null || typeof value !== "object") {
321
+ return JSON.stringify(value);
322
+ }
323
+ if (Array.isArray(value)) {
324
+ return `[${value.map((v) => canonicalJson(v)).join(",")}]`;
325
+ }
326
+ const obj = value;
327
+ const keys = Object.keys(obj).sort();
328
+ const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`);
329
+ return `{${parts.join(",")}}`;
330
+ }
331
+ //# sourceMappingURL=ecosystem-telemetry.js.map