autotel-schema 0.2.0 → 2.0.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/dist/index.cjs +139 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/snapshots/agent-security.snapshot.json +238 -0
- package/src/attrs.ts +0 -23
- package/src/cli.ts +0 -117
- package/src/contract.test.ts +0 -67
- package/src/contract.ts +0 -231
- package/src/diff.ts +0 -282
- package/src/index.ts +0 -88
- package/src/processor.test.ts +0 -74
- package/src/processor.ts +0 -152
- package/src/redaction.ts +0 -64
- package/src/snapshot.test.ts +0 -88
- package/src/snapshot.ts +0 -119
- package/src/validate.test.ts +0 -100
- package/src/validate.ts +0 -237
package/src/validate.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure span-vs-contract validation. No SDK, no side effects — the same engine
|
|
3
|
-
* the runtime processor ({@link ./processor}) and any test harness can call.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
allowsAdditionalAttributes,
|
|
8
|
-
resolveAttributeSpec,
|
|
9
|
-
type AttributeSpec,
|
|
10
|
-
type AttributeType,
|
|
11
|
-
type TelemetryContract,
|
|
12
|
-
} from './contract.js';
|
|
13
|
-
|
|
14
|
-
/** Severity of a contract violation. `error` = a breaking-shaped problem. */
|
|
15
|
-
export type ViolationSeverity = 'error' | 'warning';
|
|
16
|
-
|
|
17
|
-
export type ViolationCode =
|
|
18
|
-
| 'unknown_span'
|
|
19
|
-
| 'unknown_attribute'
|
|
20
|
-
| 'type_mismatch'
|
|
21
|
-
| 'missing_required'
|
|
22
|
-
| 'deprecated_attribute'
|
|
23
|
-
| 'enum_violation';
|
|
24
|
-
|
|
25
|
-
/** A single discrepancy between an emitted span and the contract. */
|
|
26
|
-
export interface SchemaViolation {
|
|
27
|
-
code: ViolationCode;
|
|
28
|
-
severity: ViolationSeverity;
|
|
29
|
-
spanName: string;
|
|
30
|
-
/** Attribute key involved, when the violation is attribute-scoped. */
|
|
31
|
-
attribute?: string;
|
|
32
|
-
message: string;
|
|
33
|
-
/** Nearest declared key, for likely typos (`unknown_attribute` only). */
|
|
34
|
-
suggestion?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Minimal emitted-span shape — avoids a hard dependency on the OTel SDK. */
|
|
38
|
-
export interface SpanShape {
|
|
39
|
-
name: string;
|
|
40
|
-
attributes: Record<string, unknown>;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ValidateOptions {
|
|
44
|
-
/** Report `unknown_span` for span names not in the contract. Default `false`. */
|
|
45
|
-
strictSpanNames?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** `'empty[]'` is a distinct marker: an empty array satisfies any array type. */
|
|
49
|
-
function actualType(value: unknown): AttributeType | 'empty[]' | 'unknown' {
|
|
50
|
-
if (typeof value === 'string') return 'string';
|
|
51
|
-
if (typeof value === 'number') return 'number';
|
|
52
|
-
if (typeof value === 'boolean') return 'boolean';
|
|
53
|
-
if (Array.isArray(value)) {
|
|
54
|
-
const first = value.find((v) => v !== null && v !== undefined);
|
|
55
|
-
if (first === undefined) return 'empty[]';
|
|
56
|
-
if (typeof first === 'string') return 'string[]';
|
|
57
|
-
if (typeof first === 'number') return 'number[]';
|
|
58
|
-
if (typeof first === 'boolean') return 'boolean[]';
|
|
59
|
-
}
|
|
60
|
-
return 'unknown';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function typeMatches(expected: AttributeType, value: unknown): boolean {
|
|
64
|
-
const actual = actualType(value);
|
|
65
|
-
if (actual === 'unknown') return false;
|
|
66
|
-
if (actual === 'empty[]') return expected.endsWith('[]');
|
|
67
|
-
return actual === expected;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Levenshtein distance — small, allocation-light, good enough for key typos. */
|
|
71
|
-
function editDistance(a: string, b: string): number {
|
|
72
|
-
const m = a.length;
|
|
73
|
-
const n = b.length;
|
|
74
|
-
if (m === 0) return n;
|
|
75
|
-
if (n === 0) return m;
|
|
76
|
-
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
77
|
-
let curr = Array.from<number>({ length: n + 1 });
|
|
78
|
-
for (let i = 1; i <= m; i++) {
|
|
79
|
-
curr[0] = i;
|
|
80
|
-
for (let j = 1; j <= n; j++) {
|
|
81
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
82
|
-
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
83
|
-
}
|
|
84
|
-
[prev, curr] = [curr, prev];
|
|
85
|
-
}
|
|
86
|
-
return prev[n];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Closest declared key to `key`, when one is within a small edit distance.
|
|
91
|
-
* Turns "you emitted an attribute I don't know" into "did you mean `user.id`?".
|
|
92
|
-
*/
|
|
93
|
-
function nearestKey(key: string, candidates: string[]): string | undefined {
|
|
94
|
-
let best: string | undefined;
|
|
95
|
-
let bestDistance = Infinity;
|
|
96
|
-
const threshold = Math.max(1, Math.floor(key.length / 4) + 1);
|
|
97
|
-
for (const candidate of candidates) {
|
|
98
|
-
const d = editDistance(key, candidate);
|
|
99
|
-
if (d < bestDistance && d <= threshold) {
|
|
100
|
-
best = candidate;
|
|
101
|
-
bestDistance = d;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return best;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function declaredKeysFor(
|
|
108
|
-
contract: TelemetryContract,
|
|
109
|
-
spanName: string,
|
|
110
|
-
): string[] {
|
|
111
|
-
return [
|
|
112
|
-
...Object.keys(contract.spans[spanName]?.attributes ?? {}),
|
|
113
|
-
...Object.keys(contract.commonAttributes ?? {}),
|
|
114
|
-
];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function checkValue(
|
|
118
|
-
spanName: string,
|
|
119
|
-
key: string,
|
|
120
|
-
value: unknown,
|
|
121
|
-
spec: AttributeSpec,
|
|
122
|
-
out: SchemaViolation[],
|
|
123
|
-
): void {
|
|
124
|
-
if (!typeMatches(spec.type, value)) {
|
|
125
|
-
out.push({
|
|
126
|
-
code: 'type_mismatch',
|
|
127
|
-
severity: 'error',
|
|
128
|
-
spanName,
|
|
129
|
-
attribute: key,
|
|
130
|
-
message: `attribute "${key}" should be ${spec.type} but got ${actualType(value)}`,
|
|
131
|
-
});
|
|
132
|
-
return; // a wrong type makes enum/deprecation checks noise
|
|
133
|
-
}
|
|
134
|
-
if (spec.enum && (typeof value === 'string' || typeof value === 'number') && !spec.enum.includes(value)) {
|
|
135
|
-
out.push({
|
|
136
|
-
code: 'enum_violation',
|
|
137
|
-
severity: 'error',
|
|
138
|
-
spanName,
|
|
139
|
-
attribute: key,
|
|
140
|
-
message: `attribute "${key}" value ${JSON.stringify(value)} is not one of ${JSON.stringify(spec.enum)}`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
if (spec.stability === 'deprecated') {
|
|
144
|
-
const hint = spec.replacedBy
|
|
145
|
-
? ` — use "${spec.replacedBy}" instead`
|
|
146
|
-
: spec.deprecatedReason
|
|
147
|
-
? ` — ${spec.deprecatedReason}`
|
|
148
|
-
: '';
|
|
149
|
-
out.push({
|
|
150
|
-
code: 'deprecated_attribute',
|
|
151
|
-
severity: 'warning',
|
|
152
|
-
spanName,
|
|
153
|
-
attribute: key,
|
|
154
|
-
message: `attribute "${key}" is deprecated${hint}`,
|
|
155
|
-
suggestion: spec.replacedBy,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Validate one emitted span against the contract, returning every discrepancy.
|
|
162
|
-
* Order is deterministic: required-but-missing first, then per-attribute checks
|
|
163
|
-
* in attribute insertion order.
|
|
164
|
-
*/
|
|
165
|
-
export function validateSpan(
|
|
166
|
-
span: SpanShape,
|
|
167
|
-
contract: TelemetryContract,
|
|
168
|
-
options: ValidateOptions = {},
|
|
169
|
-
): SchemaViolation[] {
|
|
170
|
-
const out: SchemaViolation[] = [];
|
|
171
|
-
const spanSpec = contract.spans[span.name];
|
|
172
|
-
|
|
173
|
-
if (!spanSpec) {
|
|
174
|
-
if (options.strictSpanNames) {
|
|
175
|
-
out.push({
|
|
176
|
-
code: 'unknown_span',
|
|
177
|
-
severity: 'warning',
|
|
178
|
-
spanName: span.name,
|
|
179
|
-
message: `span "${span.name}" is not declared in the contract`,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
return out; // unknown span → no attribute contract to check against
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Required attributes that never showed up.
|
|
186
|
-
const required = [
|
|
187
|
-
...Object.entries(spanSpec.attributes ?? {}),
|
|
188
|
-
...Object.entries(contract.commonAttributes ?? {}),
|
|
189
|
-
].filter(([, spec]) => spec.required);
|
|
190
|
-
for (const [key] of required) {
|
|
191
|
-
if (!(key in span.attributes)) {
|
|
192
|
-
out.push({
|
|
193
|
-
code: 'missing_required',
|
|
194
|
-
severity: 'error',
|
|
195
|
-
spanName: span.name,
|
|
196
|
-
attribute: key,
|
|
197
|
-
message: `required attribute "${key}" is missing`,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const allowExtra = allowsAdditionalAttributes(contract, span.name);
|
|
203
|
-
const declared = allowExtra ? [] : declaredKeysFor(contract, span.name);
|
|
204
|
-
|
|
205
|
-
for (const [key, value] of Object.entries(span.attributes)) {
|
|
206
|
-
if (value === null || value === undefined) continue;
|
|
207
|
-
const spec = resolveAttributeSpec(contract, span.name, key);
|
|
208
|
-
if (!spec) {
|
|
209
|
-
if (!allowExtra) {
|
|
210
|
-
out.push({
|
|
211
|
-
code: 'unknown_attribute',
|
|
212
|
-
severity: 'warning',
|
|
213
|
-
spanName: span.name,
|
|
214
|
-
attribute: key,
|
|
215
|
-
message: `attribute "${key}" is not declared on span "${span.name}"`,
|
|
216
|
-
suggestion: nearestKey(key, declared),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
checkValue(span.name, key, value, spec, out);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return out;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** `true` when any violation is `error` severity. */
|
|
228
|
-
export function hasErrors(violations: SchemaViolation[]): boolean {
|
|
229
|
-
return violations.some((v) => v.severity === 'error');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** One-line human/agent-readable rendering of a violation. */
|
|
233
|
-
export function formatViolation(v: SchemaViolation): string {
|
|
234
|
-
const where = v.attribute ? `${v.spanName}.${v.attribute}` : v.spanName;
|
|
235
|
-
const suffix = v.suggestion ? ` (did you mean "${v.suggestion}"?)` : '';
|
|
236
|
-
return `[${v.severity}] ${v.code} @ ${where}: ${v.message}${suffix}`;
|
|
237
|
-
}
|