Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions conformance/parity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Projection boundary corpus

Both SDK test suites consume projection-boundaries.json. This slice covers six canonical timestamps, thirteen noncanonical spellings, and five scalar projection cases. Nonfinite values use local special-value tags because they have no JSON representation. These projection inputs are not assertions that arbitrary event fields pass the event schema.

This companion is based on telemetry #119. Other adapter timestamp/duration and EventFactory corrections remain in #116–120; it neither replaces those fixes nor claims their acceptance. The 20-digit tuple is an explicit positive boundary; logs, delivery, collector behavior and hardware are separate.
123 changes: 123 additions & 0 deletions conformance/parity/projection-boundaries.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"timestamps": [
{
"input": "0",
"tuple": [
0,
0
]
},
{
"input": "1",
"tuple": [
0,
1
]
},
{
"input": "999999999",
"tuple": [
0,
999999999
]
},
{
"input": "1000000000",
"tuple": [
1,
0
]
},
{
"input": "1000000001",
"tuple": [
1,
1
]
},
{
"input": "99999999999999999999",
"tuple": [
99999999999,
999999999
]
}
],
"invalid_timestamps": [
"",
" ",
"-1",
"+1",
"01",
"0x10",
"1.0",
"1e9",
"1\n",
"1\r",
"1
",
"١",
"111111111111111111111"
],
"projections": [
{
"id": "finite-scalars",
"event": {
"run_id": "run-1",
"task_id": 0,
"decision": false
},
"expected": {
"agentrust.run.id": "run-1",
"agentrust.task.id": 0,
"agentrust.policy.decision": false
}
},
{
"id": "structured-and-null-omit",
"event": {
"run_id": "run-1",
"task_id": null,
"workflow_id": [],
"agent_id": {}
},
"expected": {
"agentrust.run.id": "run-1"
}
},
{
"id": "unmapped-payload-omit",
"event": {
"run_id": "run-1",
"payload": "synthetic"
},
"expected": {
"agentrust.run.id": "run-1"
}
},
{
"id": "nonfinite-omit",
"event": {
"run_id": "run-1"
},
"special": {
"task_id": "nan",
"workflow_id": "positive_infinity",
"agent_id": "negative_infinity"
},
"expected": {
"agentrust.run.id": "run-1"
}
},
{
"id": "fractional-scalar",
"event": {
"run_id": "run-1",
"task_id": 1.5
},
"expected": {
"agentrust.run.id": "run-1",
"agentrust.task.id": 1.5
}
}
]
}
7 changes: 6 additions & 1 deletion packages/typescript/src/projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function spanAttributes(event: NormalizedEvent): Record<string, Scalar> {
const result: Record<string, Scalar> = {};
for (const [source, destination] of Object.entries(DIRECT_ATTRIBUTES)) {
const value = event[source];
if (typeof value === "number" && !Number.isFinite(value)) continue;
if (["string", "number", "boolean"].includes(typeof value)) result[destination] = value as Scalar;
}
return result;
Expand All @@ -22,5 +23,9 @@ export function logRecord(event: NormalizedEvent, context?: ContextIds): Record<
return {event_name: event.event_type, timestamp_ns: event.time_unix_nano, body: structuredClone(event), ...(context ? {trace_id: context.traceId, span_id: context.spanId} : {})};
}
export function hrTime(value: string): [number, number] {
const timestamp = BigInt(value); return [Number(timestamp / 1_000_000_000n), Number(timestamp % 1_000_000_000n)];
if (typeof value !== "string" || !/^(0|[1-9][0-9]{0,19})$/.test(value)) throw new Error("Unix nanosecond timestamps must be canonical non-negative decimals of at most 20 digits");
const timestamp = BigInt(value);
const seconds = timestamp / 1_000_000_000n;
if (seconds > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("timestamp seconds exceed safe integer range");
return [Number(seconds), Number(timestamp % 1_000_000_000n)];
}
9 changes: 9 additions & 0 deletions packages/typescript/test/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ test("policy, approval, and action metrics expose only bounded semantic dimensio
for (const name of ["policy-decision.json", "approval.json", "action.json"]) assert.equal(emitter.emit(fixture(name)), true);
assert.deepEqual(Object.keys(meter.instruments.get("agentrust.action.executions")!.calls[0]!.attributes!).sort(), ["agentrust.action.kind", "agentrust.action.outcome"]);
});

test("hrTime rejects malformed timestamps and spanAttributes skips non-finite numbers", async () => {
const {hrTime, spanAttributes} = await import("../src/projection.js");
for (const bad of ["", " ", "-1", "0x10", "01", "1".repeat(21)]) {
assert.throws(() => hrTime(bad), /canonical non-negative decimals/);
}
const attrs = spanAttributes({run_id: "r1", task_id: Number.NaN, workflow_id: Number.POSITIVE_INFINITY} as unknown as NormalizedEvent);
assert.deepEqual(attrs, {"agentrust.run.id": "r1"});
});
30 changes: 30 additions & 0 deletions packages/typescript/test/projection-parity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import {readFileSync} from "node:fs";
import test from "node:test";
import {hrTime, spanAttributes} from "../src/projection.js";
import type {NormalizedEvent} from "../src/types.js";

const corpus = JSON.parse(readFileSync(
new URL("../../../conformance/parity/projection-boundaries.json", import.meta.url), "utf8",
));
for (const row of corpus.timestamps) {
test("shared timestamp " + row.input, () => assert.deepEqual(hrTime(row.input), row.tuple));
}
for (const value of corpus.invalid_timestamps) {
test("shared noncanonical timestamp " + JSON.stringify(value), () => {
assert.throws(() => hrTime(value));
});
}
const special: Record<string, number> = {
nan: Number.NaN, positive_infinity: Number.POSITIVE_INFINITY,
negative_infinity: Number.NEGATIVE_INFINITY,
};
for (const row of corpus.projections) {
test("shared scalar projection " + row.id, () => {
const event = {...row.event};
for (const [key, value] of Object.entries(row.special ?? {})) {
event[key] = special[value as string];
}
assert.deepEqual(spanAttributes(event as NormalizedEvent), row.expected);
});
}
3 changes: 3 additions & 0 deletions src/agentrust_telemetry/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from copy import deepcopy
import math
from typing import Any

from .context import ContextIds
Expand Down Expand Up @@ -32,6 +33,8 @@ def span_attributes(event: dict[str, Any]) -> dict[str, str | int | float | bool
projected: dict[str, str | int | float | bool] = {}
for source, destination in DIRECT_ATTRIBUTES.items():
value = event.get(source)
if isinstance(value, float) and not math.isfinite(value):
continue
if isinstance(value, (str, int, float, bool)):
projected[destination] = value
return projected
Expand Down
12 changes: 12 additions & 0 deletions tests/test_otel_projectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,18 @@ def test_action_metrics_exclude_action_and_agent_identifiers(self):
{"agentrust.action.kind", "agentrust.action.outcome"},
)

def test_span_attributes_excludes_nonfinite_floats(self):
from agentrust_telemetry.projection import span_attributes

attrs = span_attributes(
{
"run_id": "r-1",
"task_id": float("nan"),
"workflow_id": float("inf"),
}
)
self.assertEqual(attrs, {"agentrust.run.id": "r-1"})


if __name__ == "__main__":
unittest.main()
35 changes: 35 additions & 0 deletions tests/test_projection_parity_corpus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Shared Python/TypeScript normalization and projection cases, not schema acceptance."""
import json
import math
import unittest
from pathlib import Path

from agentrust_telemetry.adapters.base import _unix_nano
from agentrust_telemetry.projection import span_attributes

CORPUS = json.loads(
(Path(__file__).resolve().parents[1] / "conformance/parity/projection-boundaries.json")
.read_text(encoding="utf-8")
)


class TestProjectionParityCorpus(unittest.TestCase):
def test_timestamp_normalization(self):
for case in CORPUS["timestamps"]:
with self.subTest(value=case["input"]):
normalized = _unix_nano(case["input"])
self.assertEqual(normalized, case["input"])
self.assertEqual(list(divmod(int(normalized), 1_000_000_000)), case["tuple"])

def test_noncanonical_timestamps(self):
for value in CORPUS["invalid_timestamps"]:
with self.subTest(value=value), self.assertRaises((ValueError, TypeError)):
_unix_nano(value)

def test_scalar_projection(self):
special = {"nan": math.nan, "positive_infinity": math.inf, "negative_infinity": -math.inf}
for case in CORPUS["projections"]:
with self.subTest(case=case["id"]):
event = dict(case["event"])
event.update({key: special[value] for key, value in case.get("special", {}).items()})
self.assertEqual(span_attributes(event), case["expected"])