@tangle-network/traces 0.23.1 → 1.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.
Files changed (74) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-XLS3VANY.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-3I4PJ5Q7.js +25 -0
  6. package/dist/chunk-3T62R7ON.js +5695 -0
  7. package/dist/chunk-4VKPN432.js +138 -0
  8. package/dist/chunk-6CLC7DKP.js +492 -0
  9. package/dist/chunk-7CBV5RJ2.js +535 -0
  10. package/dist/chunk-B62DOFGI.js +273 -0
  11. package/dist/chunk-CL3OQOWX.js +407 -0
  12. package/dist/chunk-DLGAKCQJ.js +782 -0
  13. package/dist/chunk-DU534HAX.js +263 -0
  14. package/dist/chunk-GLUDGUGJ.js +232 -0
  15. package/dist/chunk-J2PM5SVN.js +503 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-NH7HCDLB.js +759 -0
  18. package/dist/chunk-NZ5Y7UEL.js +1217 -0
  19. package/dist/chunk-O52XK7OB.js +19 -0
  20. package/dist/chunk-PYH3PL3N.js +62 -0
  21. package/dist/chunk-RZL5GAWH.js +60 -0
  22. package/dist/chunk-TR6O4S4T.js +208 -0
  23. package/dist/chunk-TW43MR5D.js +259 -0
  24. package/dist/chunk-TYLFUX42.js +228 -0
  25. package/dist/chunk-UOLRCQHC.js +120 -0
  26. package/dist/chunk-UVE5ANVV.js +3921 -0
  27. package/dist/chunk-V2OCGX57.js +53 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-ZZ6KYBPP.js +16 -0
  30. package/dist/improvement-OJQ6365E.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-Y64AJ6MA.js +14 -0
  34. package/dist/mcp-ZPGTJQYQ.js +85 -0
  35. package/dist/report-JAMXRTCW.js +338 -0
  36. package/dist/run-watch-PLWTIRSV.js +649 -0
  37. package/dist/supervisor-report-RMPLLBGZ.js +29 -0
  38. package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
  39. package/dist/upload-R6NMKSGQ.js +20 -0
  40. package/dist/verify-safe-C5ZXZOZ2.js +8 -0
  41. package/docs/analyze.md +76 -0
  42. package/docs/ask.md +88 -0
  43. package/docs/bundle.md +40 -0
  44. package/docs/check.md +77 -0
  45. package/docs/convert.md +44 -0
  46. package/docs/develop.md +32 -0
  47. package/docs/diff.md +38 -0
  48. package/docs/facts.md +114 -0
  49. package/docs/mcp.md +36 -0
  50. package/docs/migrate-1.0.md +33 -0
  51. package/docs/parsers.md +66 -0
  52. package/docs/release-1.0.0.md +147 -0
  53. package/docs/sdk.md +77 -0
  54. package/docs/select.md +70 -0
  55. package/docs/skills.md +46 -0
  56. package/docs/upload.md +47 -0
  57. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  58. package/docs/watch.md +66 -0
  59. package/examples/README.md +9 -0
  60. package/examples/custom-analyst.ts +65 -74
  61. package/install.sh +22 -2
  62. package/package.json +8 -7
  63. package/skills/inspect-agent-traces/SKILL.md +47 -110
  64. package/dist/chunk-7DVZCCZI.js +0 -20983
  65. package/docs/replay-verify.md +0 -123
  66. package/docs/stream-findings.svg +0 -39
  67. package/docs/trace-analysts.md +0 -643
  68. package/examples/custom-backend.ts +0 -21
  69. package/examples/external-engines.ts +0 -62
  70. package/examples/improvement-config.mjs +0 -63
  71. package/examples/observe-and-alert.ts +0 -19
  72. package/examples/register-harness.ts +0 -26
  73. package/skills/build-trace-analyst/SKILL.md +0 -76
  74. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
@@ -0,0 +1,19 @@
1
+ // src/ask-defaults.ts
2
+ var DEFAULT_ANALYST_MODEL = "gpt-5.6-luna";
3
+ var DEFAULT_QUESTION_MAX_COST_USD = 2;
4
+ var DEFAULT_QUESTION_TIMEOUT_MS = 18e5;
5
+ var DEFAULT_ASK_CONCURRENCY = 4;
6
+ var QUESTION_FIELD_MAX_CHARS = 900;
7
+ var ASK_QUESTION_HEADER = "Answer QUESTION with the trace tools and follow TRACES ASK RULES in analyst_instructions. The trace list is JSON after PREPARED CONTEXT: there. Cite trace://<trace_id>/span/<span_id>.";
8
+ var QUESTION_LABEL = "\n\nQUESTION: ";
9
+ var MAX_TRACE_QUESTION_CHARS = QUESTION_FIELD_MAX_CHARS - ASK_QUESTION_HEADER.length - QUESTION_LABEL.length;
10
+
11
+ export {
12
+ DEFAULT_ANALYST_MODEL,
13
+ DEFAULT_QUESTION_MAX_COST_USD,
14
+ DEFAULT_QUESTION_TIMEOUT_MS,
15
+ DEFAULT_ASK_CONCURRENCY,
16
+ ASK_QUESTION_HEADER,
17
+ QUESTION_LABEL,
18
+ MAX_TRACE_QUESTION_CHARS
19
+ };
@@ -0,0 +1,62 @@
1
+ // src/finding-rejections.ts
2
+ var REJECTION_MESSAGE = /^(?:\[([^\]]+)\] )?finding rejected: (.+)$/;
3
+ function findingRejection(message, fields) {
4
+ const match = REJECTION_MESSAGE.exec(message);
5
+ if (!match) return void 0;
6
+ const kind = match[2].trim();
7
+ const reason = kind === "unresolved evidence" && typeof fields?.reason === "string" && fields.reason ? fields.reason : kind;
8
+ return match[1] ? { analystId: match[1], reason } : { reason };
9
+ }
10
+ function rejectionCause(fields) {
11
+ if (typeof fields?.reason === "string" && fields.reason) return fields.reason;
12
+ const issues = fields?.issues;
13
+ if (typeof issues === "string" && issues) return issues;
14
+ if (Array.isArray(issues) && issues.length > 0) return issues.map((issue) => String(issue)).join("; ");
15
+ return void 0;
16
+ }
17
+ function findingRejectionDetail(message, fields) {
18
+ const rejection = findingRejection(message, fields);
19
+ if (!rejection) return void 0;
20
+ const lead = rejectionCause(fields) ?? rejection.reason;
21
+ const parts = message.endsWith(lead) ? [] : [lead];
22
+ if (typeof fields?.uri === "string" && fields.uri) parts.push(`uri ${fields.uri}`);
23
+ if (typeof fields?.required === "number" && typeof fields?.distinct === "number") {
24
+ parts.push(`${fields.distinct} of ${fields.required} required distinct citation(s)`);
25
+ }
26
+ if (typeof fields?.subject === "string" && fields.subject) parts.push(`subject ${fields.subject}`);
27
+ return parts.join("; ");
28
+ }
29
+ function createFindingRejectionTally(defaultAnalystId = "unknown") {
30
+ const byAnalyst = /* @__PURE__ */ new Map();
31
+ return {
32
+ record(message, fields) {
33
+ const rejection = findingRejection(message, fields);
34
+ if (!rejection) return;
35
+ const analystId = rejection.analystId ?? defaultAnalystId;
36
+ const reasons = byAnalyst.get(analystId) ?? /* @__PURE__ */ new Map();
37
+ reasons.set(rejection.reason, (reasons.get(rejection.reason) ?? 0) + 1);
38
+ byAnalyst.set(analystId, reasons);
39
+ },
40
+ counts() {
41
+ return Object.fromEntries(
42
+ [...byAnalyst].map(([analystId, reasons]) => [analystId, Object.fromEntries(reasons)])
43
+ );
44
+ }
45
+ };
46
+ }
47
+ function totalFindingRejections(reasons) {
48
+ return Object.values(reasons ?? {}).reduce((sum, count) => sum + count, 0);
49
+ }
50
+ function formatFindingRejections(reasons) {
51
+ const total = totalFindingRejections(reasons);
52
+ if (total === 0) return "";
53
+ const ordered = Object.entries(reasons ?? {}).filter(([, count]) => count > 0).sort(([a, left], [b, right]) => right - left || a.localeCompare(b)).map(([reason, count]) => `${reason} \xD7${count}`);
54
+ return `${total} finding(s) rejected: ${ordered.join("; ")}`;
55
+ }
56
+
57
+ export {
58
+ findingRejectionDetail,
59
+ createFindingRejectionTally,
60
+ totalFindingRejections,
61
+ formatFindingRejections
62
+ };
@@ -0,0 +1,60 @@
1
+ import {
2
+ normalizeToolIoAttributes,
3
+ stripSourceAttributes
4
+ } from "./chunk-DU534HAX.js";
5
+
6
+ // src/redact.ts
7
+ import {
8
+ assessShareSafety,
9
+ combineVerdicts,
10
+ redact
11
+ } from "@tangle-network/agent-eval/traces";
12
+ function redactSpans(spans, options = {}) {
13
+ const parts = spans.map((span) => ({
14
+ name: span.name,
15
+ attributes: stripSourceAttributes(span.attributes),
16
+ message: span.status.message,
17
+ links: span.links?.map((link) => ({
18
+ ...link,
19
+ ...link.attributes ? { attributes: stripSourceAttributes(link.attributes) } : {}
20
+ }))
21
+ }));
22
+ const { value, report } = redact(parts, options);
23
+ const out = spans.map((span, index) => {
24
+ const part = value[index];
25
+ normalizeToolIoAttributes(part.attributes);
26
+ const status = part.message !== void 0 && part.message !== span.status.message ? { ...span.status, message: part.message } : span.status;
27
+ return {
28
+ ...span,
29
+ name: part.name,
30
+ attributes: part.attributes,
31
+ status,
32
+ ...part.links ? { links: part.links } : {}
33
+ };
34
+ });
35
+ return { spans: out, report };
36
+ }
37
+ function assessSpans(spans, profile = "default") {
38
+ return combineVerdicts(
39
+ profile,
40
+ spans.map((span) => {
41
+ const verdict = assessShareSafety(
42
+ { name: span.name, attributes: span.attributes, status: span.status, links: span.links },
43
+ { profile }
44
+ );
45
+ return {
46
+ ...verdict,
47
+ findings: verdict.findings.map((finding) => ({
48
+ ...finding,
49
+ paths: finding.paths.map((path) => `span:${span.span_id}${path}`)
50
+ })),
51
+ unreadable: verdict.unreadable.map((entry) => `span:${span.span_id}${entry}`)
52
+ };
53
+ })
54
+ );
55
+ }
56
+
57
+ export {
58
+ redactSpans,
59
+ assessSpans
60
+ };
@@ -0,0 +1,208 @@
1
+ import {
2
+ span
3
+ } from "./chunk-NZ5Y7UEL.js";
4
+
5
+ // src/attributes.ts
6
+ var ATTR = {
7
+ /** = the trace/session id. Wire contract (dedup linkage). */
8
+ SESSION_ID: "tangle.sessionId",
9
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
10
+ * that also streamed live. Wire contract. */
11
+ INGEST_SOURCE: "tangle.ingest_source",
12
+ HARNESS: "tangle.harness",
13
+ CWD: "tangle.cwd",
14
+ /** THE per-session grouping key the spine derives subjects from
15
+ * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
16
+ * remote (host/owner/repo) when readable, else the cwd path basename. */
17
+ SUBJECT_KEY: "tangle.subject.key",
18
+ /** Normalized git remote (e.g. `github.com/tangle-network/agent-container`). */
19
+ GIT_REPOSITORY: "git.repository",
20
+ /** Current branch at conversion time. */
21
+ GIT_BRANCH_NAME: "git.branch",
22
+ /** HEAD short sha at conversion time. */
23
+ GIT_COMMIT: "git.commit",
24
+ /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
25
+ REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
26
+ GIT_BRANCH: "tangle.git_branch",
27
+ HOST: "tangle.host",
28
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
29
+ * which collided with `tangle.ingest_source`.) */
30
+ SESSION_FILE: "tangle.session_file",
31
+ UPLOADED_AT: "tangle.uploaded_at",
32
+ UPLOADER: "tangle.uploader",
33
+ REDACTION_VERSION: "redaction.version",
34
+ REDACTION_COUNT: "redaction.count",
35
+ /** Upload contract: bounded root summary plus location-only receipt child spans.
36
+ * Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
37
+ SESSION_INTEGRITY: "traces.session.integrity",
38
+ CORRUPTION_COUNT: "traces.session.corruption_count",
39
+ CORRUPTION_DIGEST: "traces.session.corruption_digest",
40
+ CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
41
+ CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
42
+ CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
43
+ CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
44
+ CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
45
+ CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
46
+ CORRUPTION_SHA256: "traces.session.corruption.sha256",
47
+ RAW_SOURCE_RETENTION: "traces.session.raw_source_retention",
48
+ /** Container image reference the session executed in (OTel semconv key). */
49
+ CONTAINER_IMAGE: "container.image.name",
50
+ /** Immutable image digest (`sha256:…`) — the replay-grade environment pin. */
51
+ CONTAINER_IMAGE_DIGEST: "container.image.digest",
52
+ /** Sandbox/container instance id that executed the session. */
53
+ SANDBOX_ID: "tangle.sandbox.id",
54
+ /** Recorded execution cwd, verbatim. Unlike `tangle.cwd` it is never
55
+ * repaired against the host filesystem — replay verification needs the
56
+ * in-container path even when no such path exists on this host. */
57
+ ENVIRONMENT_CWD: "tangle.environment.cwd"
58
+ };
59
+ var SESSION_CWD_ATTR = "traces.session.cwd";
60
+ var SESSION_TOTAL_TOKENS_ATTR = "traces.session.total_tokens";
61
+ var SESSION_TOTAL_INPUT_ATTR = "traces.session.total_input_tokens";
62
+ var SESSION_TOTAL_OUTPUT_ATTR = "traces.session.total_output_tokens";
63
+ var SESSION_TOTAL_CACHED_INPUT_ATTR = "traces.session.total_cached_input_tokens";
64
+ var SESSION_TOTAL_CACHE_WRITE_ATTR = "traces.session.total_cache_write_input_tokens";
65
+ var SESSION_TOTAL_REASONING_ATTR = "traces.session.total_reasoning_tokens";
66
+ var INGEST_SOURCE_CLI = "cli";
67
+ function environmentAttributes(environment) {
68
+ if (!environment) return {};
69
+ const attrs = {};
70
+ if (environment.image) attrs[ATTR.CONTAINER_IMAGE] = environment.image;
71
+ if (environment.imageDigest) attrs[ATTR.CONTAINER_IMAGE_DIGEST] = environment.imageDigest;
72
+ if (environment.sandboxId) attrs[ATTR.SANDBOX_ID] = environment.sandboxId;
73
+ if (environment.cwd) attrs[ATTR.ENVIRONMENT_CWD] = environment.cwd;
74
+ if (environment.gitCommit) attrs[ATTR.GIT_COMMIT] = environment.gitCommit;
75
+ return attrs;
76
+ }
77
+ function stampEnvironmentAttrs(spans, environment) {
78
+ const attrs = environmentAttributes(environment);
79
+ const keys = Object.keys(attrs);
80
+ if (keys.length === 0) return;
81
+ for (const span2 of spans) {
82
+ for (const key of keys) {
83
+ if (span2.attributes[key] === void 0) span2.attributes[key] = attrs[key];
84
+ }
85
+ }
86
+ }
87
+ var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
88
+ function sessionIdFromAttributes(attributes) {
89
+ return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
90
+ (value) => typeof value === "string" && value.length > 0
91
+ );
92
+ }
93
+ function indexSessionIdsByTrace(spans) {
94
+ const candidatesByTrace = /* @__PURE__ */ new Map();
95
+ for (const span2 of spans) {
96
+ const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
97
+ for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
98
+ const value = span2.attributes[key];
99
+ if (typeof value === "string" && value.length > 0) candidates.add(value);
100
+ }
101
+ if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
102
+ }
103
+ const sessionByTrace = /* @__PURE__ */ new Map();
104
+ const conflicts = [];
105
+ for (const [traceId, candidates] of candidatesByTrace) {
106
+ const sessionIds = [...candidates].sort();
107
+ if (sessionIds.length === 1) {
108
+ sessionByTrace.set(traceId, sessionIds[0]);
109
+ } else {
110
+ conflicts.push({ traceId, sessionIds });
111
+ }
112
+ }
113
+ conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
114
+ return { sessionByTrace, conflicts };
115
+ }
116
+ function stampSessionIdentity(spans, fallbackSessionId) {
117
+ const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
118
+ const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
119
+ const traceIds = new Set(spans.map((span2) => span2.trace_id));
120
+ const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
121
+ for (const span2 of spans) {
122
+ if (conflictingTraceIds.has(span2.trace_id)) continue;
123
+ const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
124
+ if (!sessionIdFromAttributes(span2.attributes)) {
125
+ span2.attributes[ATTR.SESSION_ID] = sessionId;
126
+ }
127
+ }
128
+ }
129
+
130
+ // src/adapters/conversation.ts
131
+ var CONTENT_CAP = 8e3;
132
+ function capText(raw, limit = CONTENT_CAP) {
133
+ const t = raw.trim();
134
+ if (t.length <= limit) return t;
135
+ return `${t.slice(0, limit)}\u2026 [+${t.length - limit} chars]`;
136
+ }
137
+ var ACTOR_ATTR = "tangle.actor";
138
+ function userPromptSpan(o) {
139
+ return span({
140
+ traceId: o.traceId,
141
+ spanId: o.spanId,
142
+ parentSpanId: o.parentSpanId,
143
+ name: "user.prompt",
144
+ kind: "CHAIN",
145
+ startTime: o.startTime,
146
+ service: o.service ?? null,
147
+ agent: o.agent ?? null,
148
+ step: o.step,
149
+ content: o.content,
150
+ contentSource: o.contentSource,
151
+ extra: { [ACTOR_ATTR]: o.actor ?? "human" }
152
+ });
153
+ }
154
+
155
+ // src/adapters/provenance.ts
156
+ var SYNTHESIZED_SPAN_ATTR = "traces.span.synthesized";
157
+ var SYNTHESIZED_SOURCE_ATTR = "traces.span.synthesized_from";
158
+ var INHERITED_SPAN_ATTR = "traces.session.inherited";
159
+ var INHERITED_SOURCE_ATTR = "traces.session.inherited_source";
160
+ var INHERITED_SPAN_COUNT_ATTR = "traces.session.inherited_span_count";
161
+ var INHERITED_SPANS_OMITTED_ATTR = "traces.session.inherited_spans_omitted";
162
+ var SUBAGENT_SPAN_ATTR = "traces.span.subagent";
163
+ var SUBAGENT_TASK_ATTR = "traces.span.subagent_task";
164
+ var SUBAGENT_SPAN_COUNT_ATTR = "traces.session.subagent_span_count";
165
+ var SUBAGENT_SPAWN_ATTR = "traces.agent.spawn";
166
+ var SUBAGENT_SPAWN_TASK_ATTR = "traces.agent.spawn_task";
167
+ function isSynthesizedSpan(attributes) {
168
+ return attributes[SYNTHESIZED_SPAN_ATTR] === true;
169
+ }
170
+ function isInheritedSpan(attributes) {
171
+ return attributes[INHERITED_SPAN_ATTR] === true;
172
+ }
173
+ function isSubagentSpan(attributes) {
174
+ return attributes[SUBAGENT_SPAN_ATTR] === true;
175
+ }
176
+
177
+ export {
178
+ ATTR,
179
+ SESSION_CWD_ATTR,
180
+ SESSION_TOTAL_TOKENS_ATTR,
181
+ SESSION_TOTAL_INPUT_ATTR,
182
+ SESSION_TOTAL_OUTPUT_ATTR,
183
+ SESSION_TOTAL_CACHED_INPUT_ATTR,
184
+ SESSION_TOTAL_CACHE_WRITE_ATTR,
185
+ SESSION_TOTAL_REASONING_ATTR,
186
+ INGEST_SOURCE_CLI,
187
+ stampEnvironmentAttrs,
188
+ sessionIdFromAttributes,
189
+ indexSessionIdsByTrace,
190
+ stampSessionIdentity,
191
+ capText,
192
+ ACTOR_ATTR,
193
+ userPromptSpan,
194
+ SYNTHESIZED_SPAN_ATTR,
195
+ SYNTHESIZED_SOURCE_ATTR,
196
+ INHERITED_SPAN_ATTR,
197
+ INHERITED_SOURCE_ATTR,
198
+ INHERITED_SPAN_COUNT_ATTR,
199
+ INHERITED_SPANS_OMITTED_ATTR,
200
+ SUBAGENT_SPAN_ATTR,
201
+ SUBAGENT_TASK_ATTR,
202
+ SUBAGENT_SPAN_COUNT_ATTR,
203
+ SUBAGENT_SPAWN_ATTR,
204
+ SUBAGENT_SPAWN_TASK_ATTR,
205
+ isSynthesizedSpan,
206
+ isInheritedSpan,
207
+ isSubagentSpan
208
+ };
@@ -0,0 +1,259 @@
1
+ import {
2
+ TRACE_ID_MINTED_ATTR,
3
+ toOpenInferenceSpan
4
+ } from "./chunk-NZ5Y7UEL.js";
5
+
6
+ // src/conformance.ts
7
+ import {
8
+ validateTraceSpans
9
+ } from "@tangle-network/agent-trace-contract";
10
+ var SPAN_ID_SAMPLE = 5;
11
+ var ISSUE_SAMPLE = 10;
12
+ var SEVERITY_BADGE = {
13
+ error: "\u{1F534} ERROR",
14
+ warn: "\u{1F7E0} WARN",
15
+ info: "\u2139\uFE0F INFO"
16
+ };
17
+ var CAPABILITY_ANALYSES = {
18
+ "token-accounting": "execution facts \u2192 Direct model usage (input/output/reasoning/cache token totals)",
19
+ "cost-attribution": "execution facts \u2192 Cost coverage (observed vs estimated USD)",
20
+ "tool-usage": "tool-usage table + per-tool duplicate/retry/error rates + stuck-loop detection",
21
+ "loop-convergence": "round-over-round convergence (did round N+1 improve on N)",
22
+ "tree-comparison": "arm-vs-arm comparison: `traces diff <file>#branch=<a> <file>#branch=<b>` marks where two arms first diverge",
23
+ "steering-chain": "steering chain (which verdict caused which retry)",
24
+ "latency-analysis": "latency + critical-path timing distributions"
25
+ };
26
+ function summarizeSpanStructure(spans) {
27
+ const ids = new Set(spans.map((span) => JSON.stringify([span.trace_id, span.span_id])));
28
+ const rootsByTrace = /* @__PURE__ */ new Map();
29
+ let rootless = 0;
30
+ let orphans = 0;
31
+ let links = 0;
32
+ for (const span of spans) {
33
+ links += span.links?.length ?? 0;
34
+ if (!rootsByTrace.has(span.trace_id)) rootsByTrace.set(span.trace_id, 0);
35
+ if (span.parent_span_id === null) {
36
+ rootless += 1;
37
+ rootsByTrace.set(span.trace_id, (rootsByTrace.get(span.trace_id) ?? 0) + 1);
38
+ } else if (!ids.has(JSON.stringify([span.trace_id, span.parent_span_id]))) orphans += 1;
39
+ }
40
+ let extraRoots = 0;
41
+ let tracesWithoutRoot = 0;
42
+ for (const roots of rootsByTrace.values()) {
43
+ if (roots === 0) tracesWithoutRoot += 1;
44
+ else extraRoots += roots - 1;
45
+ }
46
+ return {
47
+ spans: spans.length,
48
+ traces: rootsByTrace.size,
49
+ rootless,
50
+ extraRoots,
51
+ tracesWithoutRoot,
52
+ orphans,
53
+ links
54
+ };
55
+ }
56
+ function renderStructure(lines, structure) {
57
+ const share = structure.spans === 0 ? 0 : structure.rootless / structure.spans * 100;
58
+ lines.push(
59
+ `**Span structure:** ${structure.spans} spans across ${structure.traces} trace(s); ${structure.rootless}/${structure.spans} (${share.toFixed(1)}%) carry no parent edge (${structure.traces} of those are the trace roots); ${structure.orphans} name a parent absent from the export; ${structure.links} causal link(s).`
60
+ );
61
+ lines.push("");
62
+ if (structure.extraRoots === structure.spans - structure.traces && structure.extraRoots > 0) {
63
+ lines.push(
64
+ "> Every span is its own root. Nothing rolls up: there is no run, round or session to attribute cost, tokens or outcomes to, only a list of events."
65
+ );
66
+ lines.push("");
67
+ } else if (structure.extraRoots > 0) {
68
+ lines.push(
69
+ `> ${structure.extraRoots} span(s) attribute to nothing: beyond the one root each trace is entitled to, their model calls, tool calls and verdicts roll up to no enclosing unit of work, so only the ${structure.spans - structure.extraRoots} attached span(s) can be charged to the run that caused them.`
70
+ );
71
+ lines.push("");
72
+ }
73
+ if (structure.tracesWithoutRoot > 0) {
74
+ lines.push(
75
+ `> ${structure.tracesWithoutRoot} trace(s) have no root at all: every span names a parent, and the top of the tree is absent from the export, so nothing in them can be charged to a run.`
76
+ );
77
+ lines.push("");
78
+ }
79
+ if (structure.links === 0) {
80
+ lines.push(
81
+ "> No causal links. Containment (`parent_span_id`) was recorded; causality (round N's verdict CAUSING round N+1) was not, so a steering or retry chain cannot be followed."
82
+ );
83
+ lines.push("");
84
+ }
85
+ }
86
+ function conformanceOfSpans(spans) {
87
+ return validateTraceSpans(spans.map(toOpenInferenceSpan));
88
+ }
89
+ function skippedAnalyses(validation) {
90
+ return validation.capabilities.filter((capability) => !capability.available).map((capability) => ({
91
+ capability: capability.name,
92
+ analysis: CAPABILITY_ANALYSES[capability.name] ?? "analyses keyed off this capability",
93
+ reason: capability.reason ?? "no reason recorded"
94
+ }));
95
+ }
96
+ function tableCell(value) {
97
+ return value.replace(/\s+/g, " ").replaceAll("|", "\\|").trim();
98
+ }
99
+ function spanIdSample(finding) {
100
+ const ids = finding.spanIds ?? [];
101
+ if (ids.length === 0) return "";
102
+ const shown = ids.slice(0, SPAN_ID_SAMPLE).map((id) => `\`${tableCell(id)}\``).join(", ");
103
+ return ids.length > SPAN_ID_SAMPLE ? `${shown} +${ids.length - SPAN_ID_SAMPLE} more` : shown;
104
+ }
105
+ function capabilityRow(capability) {
106
+ const analysis = CAPABILITY_ANALYSES[capability.name] ?? "\u2014";
107
+ return capability.available ? `| \`${capability.name}\` | \u2705 available | ${tableCell(analysis)} | \u2014 |` : `| \`${capability.name}\` | \u274C unavailable | ${tableCell(analysis)} | ${tableCell(capability.reason ?? "no reason recorded")} |`;
108
+ }
109
+ function renderInheritedLoss(lines, unreadable) {
110
+ if (unreadable === void 0 || unreadable.inherited === 0) return;
111
+ lines.push(
112
+ `**${unreadable.inherited} row(s) of the ORIGINAL source are not in this file.** It is a re-export: ${unreadable.kinds.join(", ")} row(s) could not be represented as spans and were left behind by an earlier hop, so nothing below is a finding about them. The findings here describe what survived, and this file is NOT a substitute for validating the source.`
113
+ );
114
+ lines.push("");
115
+ }
116
+ function renderMintedFields(lines, spans) {
117
+ const minted = spans?.filter((span) => span.attributes[TRACE_ID_MINTED_ATTR] === true) ?? [];
118
+ if (minted.length === 0) return;
119
+ const ids = [...new Set(minted.map((span) => span.trace_id))];
120
+ lines.push(
121
+ `**${minted.length} span(s) carry a trace_id this tool minted** (${ids.slice(0, 3).map((id) => `\`${tableCell(id)}\``).join(", ")}${ids.length > 3 ? `, +${ids.length - 3} more` : ""}): their producer wrote none, and a file whose rows carry no trace id cannot be read back at all. So \`missing-trace-id\` is NOT reported below even though the source earns it \u2014 the minted id is recorded per span under \`traces.raw_attribute.trace_id\` / \`traces.substituted_fields\`, and this file is NOT a substitute for validating the source.`
122
+ );
123
+ lines.push("");
124
+ }
125
+ function renderIngest(lines, files, skipped, issues, withheld) {
126
+ if (files && files.length > 0) {
127
+ const rows = files.reduce((total, file) => total + file.rows, 0);
128
+ const spans = files.reduce((total, file) => total + file.spans, 0);
129
+ const repeats = files.reduce((total, file) => total + file.repeats, 0);
130
+ lines.push(
131
+ `**Ingested:** ${spans} span(s) from ${rows} row(s) across ${files.length} OTLP file(s)` + (repeats > 0 ? `; ${repeats} row(s) re-declared a span already read, identically.` : ".")
132
+ );
133
+ lines.push("");
134
+ lines.push("| File | Rows | Spans | Identical repeats |");
135
+ lines.push("|---|---:|---:|---:|");
136
+ for (const file of files) {
137
+ lines.push(`| \`${tableCell(file.path)}\` | ${file.rows} | ${file.spans} | ${file.repeats} |`);
138
+ }
139
+ lines.push("");
140
+ if (repeats > 0) {
141
+ lines.push(
142
+ "> Identical repeats are not a defect. A multi-shot run writes one file per shot and each file re-declares the run and cell spans it hangs under, so every file is analysable on its own; reading them together collapses those back to one span. Only a repeat with DIFFERENT content is reported as a problem."
143
+ );
144
+ lines.push("");
145
+ }
146
+ }
147
+ if (skipped && skipped.length > 0) {
148
+ lines.push(
149
+ `**${skipped.length} JSONL file(s) in this directory are not OTLP** and were not read. Their lines are not counted above and produce no findings: they were never claimed to be spans.`
150
+ );
151
+ lines.push("");
152
+ lines.push("| File | What it holds |");
153
+ lines.push("|---|---|");
154
+ for (const file of skipped.slice(0, ISSUE_SAMPLE)) {
155
+ lines.push(`| \`${tableCell(file.path)}\` | ${tableCell(file.reason)} |`);
156
+ }
157
+ if (skipped.length > ISSUE_SAMPLE) {
158
+ lines.push(`| ... | ${skipped.length - ISSUE_SAMPLE} more omitted |`);
159
+ }
160
+ lines.push("");
161
+ }
162
+ if (issues && issues.length > 0) {
163
+ lines.push(
164
+ `**${issues.length} row(s) could not be analysed.** They are excluded from every count above rather than repaired, because a synthesized timestamp or id would put invented work into the totals.`
165
+ );
166
+ lines.push("");
167
+ lines.push("| Reason | File | Line | Detail |");
168
+ lines.push("|---|---|---:|---|");
169
+ for (const issue of issues.slice(0, ISSUE_SAMPLE)) {
170
+ lines.push(`| ${issue.kind} | \`${tableCell(issue.file)}\` | ${issue.line} | ${tableCell(issue.detail)} |`);
171
+ }
172
+ if (issues.length > ISSUE_SAMPLE) {
173
+ lines.push(`| ... | ${issues.length - ISSUE_SAMPLE} more omitted | - | - |`);
174
+ }
175
+ lines.push("");
176
+ }
177
+ if (withheld && withheld.length > 0) {
178
+ lines.push(
179
+ `**${withheld.length} span(s) were analysed with one field withheld.** They ARE counted above and in every table that does not read the named field: a span with an unusable timestamp still has a real identity, name, tool and status, and dropping it would delete real work from every total to avoid one wrong number. The field itself is withheld, never repaired.`
180
+ );
181
+ lines.push("");
182
+ lines.push("| Reason | Field | File | Line | Detail |");
183
+ lines.push("|---|---|---|---:|---|");
184
+ for (const entry of withheld.slice(0, ISSUE_SAMPLE)) {
185
+ lines.push(
186
+ `| ${entry.kind} | \`${tableCell(entry.field)}\` | \`${tableCell(entry.file)}\` | ${entry.line} | ${tableCell(entry.detail)} |`
187
+ );
188
+ }
189
+ if (withheld.length > ISSUE_SAMPLE) {
190
+ lines.push(`| ... | - | ${withheld.length - ISSUE_SAMPLE} more omitted | - | - |`);
191
+ }
192
+ lines.push("");
193
+ }
194
+ }
195
+ function conformanceBody(validation, opts) {
196
+ const heading = "#".repeat(opts.headingLevel ?? 2);
197
+ const lines = [];
198
+ const errors = validation.findings.filter((finding) => finding.severity === "error").length;
199
+ const warnings = validation.findings.filter((finding) => finding.severity === "warn").length;
200
+ const infos = validation.findings.filter((finding) => finding.severity === "info").length;
201
+ const unavailable = validation.capabilities.filter((capability) => !capability.available);
202
+ lines.push(
203
+ `${errors} error, ${warnings} warn, ${infos} info finding(s); ${validation.capabilities.length - unavailable.length}/${validation.capabilities.length} analysis capabilities available.`
204
+ );
205
+ lines.push("");
206
+ renderInheritedLoss(lines, opts.unreadable);
207
+ renderMintedFields(lines, opts.spans);
208
+ renderIngest(lines, opts.files, opts.skipped, opts.issues, opts.withheld);
209
+ if (opts.spans && opts.spans.length > 0) renderStructure(lines, summarizeSpanStructure(opts.spans));
210
+ if (validation.findings.length > 0) {
211
+ lines.push("| Severity | Code | Consequence | Blocks | Spans |");
212
+ lines.push("|---|---|---|---|---|");
213
+ for (const finding of validation.findings) {
214
+ const badge = SEVERITY_BADGE[finding.severity] ?? finding.severity;
215
+ const blocks = finding.blocks?.length ? finding.blocks.map((name) => `\`${name}\``).join(", ") : "\u2014";
216
+ lines.push(
217
+ `| ${badge} | \`${tableCell(finding.code)}\` | ${tableCell(finding.message)} | ${blocks} | ${spanIdSample(finding) || "\u2014"} |`
218
+ );
219
+ }
220
+ lines.push("");
221
+ } else {
222
+ lines.push("No conformance findings: the spans carry every field this analysis reads.");
223
+ lines.push("");
224
+ }
225
+ lines.push("| Capability | State | Analysis it drives | Why not |");
226
+ lines.push("|---|---|---|---|");
227
+ for (const capability of validation.capabilities) lines.push(capabilityRow(capability));
228
+ lines.push("");
229
+ const skipped = skippedAnalyses(validation);
230
+ if (skipped.length > 0) {
231
+ lines.push(`${heading}# analyses skipped, and why`);
232
+ lines.push("");
233
+ lines.push(
234
+ "These reported nothing because the trace never carried what they read \u2014 not because the run was uneventful. Every section they feed is marked at the table, not only here."
235
+ );
236
+ lines.push("");
237
+ for (const entry of skipped) {
238
+ lines.push(`- **${entry.analysis}** \u2014 skipped: ${entry.reason} (\`${entry.capability}\`).`);
239
+ }
240
+ lines.push("");
241
+ }
242
+ return lines.join("\n");
243
+ }
244
+ function renderValidation(validation, opts) {
245
+ const errors = validation.findings.filter((finding) => finding.severity === "error");
246
+ const available = validation.capabilities.filter((capability) => capability.available);
247
+ const verdict = !validation.ok ? `NOT A TRACE \u2014 ${errors.length} error finding(s): not one entry in this export could be read as a span, so there is nothing to analyse and every capability below is unavailable. An empty export and one whose every line was unreadable both land here. This is the only state that exits non-zero.` : available.length === 0 ? `ANALYSABLE, WITH NOTHING TO ANALYSE \u2014 spans WERE read, so this is a trace, but 0/${validation.capabilities.length} capabilities are available: every analysis this package runs reads a field none of these spans carry. The rows below name what each one is missing. Exits 0, because the export is readable \u2014 a caller deciding whether to spend a run should read the capability table, not the exit code.` : "CONFORMS \u2014 analysable. Every finding below is a degradation of a readable trace, not a bar to reading it; each one names the capability it costs.";
248
+ return `# trace conformance \u2014 ${opts.subject}
249
+
250
+ **${verdict}**
251
+
252
+ ${conformanceBody(validation, { ...opts, headingLevel: 2 })}`;
253
+ }
254
+
255
+ export {
256
+ conformanceOfSpans,
257
+ skippedAnalyses,
258
+ renderValidation
259
+ };