@kweaver-ai/kweaver-sdk 0.8.1 → 0.8.2

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 (183) hide show
  1. package/README.md +19 -5
  2. package/README.zh.md +19 -5
  3. package/dist/agent-providers/index.d.ts +7 -0
  4. package/dist/agent-providers/index.js +5 -0
  5. package/dist/agent-providers/prompt-template.d.ts +62 -0
  6. package/dist/agent-providers/prompt-template.js +105 -0
  7. package/dist/agent-providers/prompts/rubric-judge-v1.prompt.md +51 -0
  8. package/dist/agent-providers/prompts/within-trace-synthesizer-v1.prompt.md +60 -0
  9. package/dist/agent-providers/providers/claude-code-subprocess.d.ts +74 -0
  10. package/dist/agent-providers/providers/claude-code-subprocess.js +259 -0
  11. package/dist/agent-providers/providers/stub.d.ts +47 -0
  12. package/dist/agent-providers/providers/stub.js +77 -0
  13. package/dist/agent-providers/registry.d.ts +45 -0
  14. package/dist/agent-providers/registry.js +77 -0
  15. package/dist/agent-providers/types.d.ts +91 -0
  16. package/dist/agent-providers/types.js +25 -0
  17. package/dist/api/agent-chat.js +8 -6
  18. package/dist/api/context-loader.d.ts +1 -0
  19. package/dist/api/semantic-search.d.ts +5 -0
  20. package/dist/api/semantic-search.js +5 -0
  21. package/dist/api/skills.d.ts +75 -2
  22. package/dist/api/skills.js +108 -12
  23. package/dist/api/trace.d.ts +5 -0
  24. package/dist/api/trace.js +4 -0
  25. package/dist/cli.js +7 -5
  26. package/dist/commands/agent/mode.d.ts +6 -0
  27. package/dist/commands/agent/mode.js +75 -0
  28. package/dist/commands/agent.js +101 -29
  29. package/dist/commands/context-loader.js +608 -38
  30. package/dist/commands/skill.d.ts +21 -1
  31. package/dist/commands/skill.js +389 -1
  32. package/dist/commands/trace.d.ts +26 -1
  33. package/dist/commands/trace.js +515 -15
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.js +1 -1
  36. package/dist/resources/bkn.d.ts +5 -0
  37. package/dist/resources/bkn.js +5 -0
  38. package/dist/resources/skills.d.ts +17 -1
  39. package/dist/resources/skills.js +32 -1
  40. package/dist/trace-ai/diagnose/agent-binding.d.ts +67 -0
  41. package/dist/trace-ai/diagnose/agent-binding.js +257 -0
  42. package/dist/trace-ai/diagnose/builtin-rules/tool-retry-intent-mismatch.yaml +68 -0
  43. package/dist/trace-ai/diagnose/index.d.ts +32 -0
  44. package/dist/trace-ai/diagnose/index.js +246 -0
  45. package/dist/trace-ai/diagnose/output-schema-converter.d.ts +24 -0
  46. package/dist/trace-ai/diagnose/output-schema-converter.js +81 -0
  47. package/dist/trace-ai/diagnose/query-extractor.d.ts +14 -0
  48. package/dist/trace-ai/diagnose/query-extractor.js +45 -0
  49. package/dist/trace-ai/diagnose/report-assembler.d.ts +31 -0
  50. package/dist/{trace-core → trace-ai}/diagnose/report-assembler.js +19 -9
  51. package/dist/trace-ai/diagnose/report-markdown.d.ts +18 -0
  52. package/dist/trace-ai/diagnose/report-markdown.js +192 -0
  53. package/dist/{trace-core → trace-ai}/diagnose/rule-loader.js +42 -8
  54. package/dist/{trace-core → trace-ai}/diagnose/schemas.d.ts +77 -2
  55. package/dist/trace-ai/diagnose/schemas.js +154 -0
  56. package/dist/trace-ai/diagnose/signal-probe.d.ts +17 -0
  57. package/dist/trace-ai/diagnose/signal-probe.js +39 -0
  58. package/dist/trace-ai/diagnose/synthesizer-agent.d.ts +40 -0
  59. package/dist/trace-ai/diagnose/synthesizer-agent.js +158 -0
  60. package/dist/{trace-core → trace-ai}/diagnose/trace-shaper.js +1 -0
  61. package/dist/{trace-core → trace-ai}/diagnose/types.d.ts +55 -6
  62. package/dist/trace-ai/eval-set/assertion-evaluator.d.ts +29 -0
  63. package/dist/trace-ai/eval-set/assertion-evaluator.js +100 -0
  64. package/dist/trace-ai/eval-set/builder.d.ts +36 -0
  65. package/dist/trace-ai/eval-set/builder.js +126 -0
  66. package/dist/trace-ai/eval-set/index.d.ts +15 -0
  67. package/dist/trace-ai/eval-set/index.js +10 -0
  68. package/dist/trace-ai/eval-set/output-writer.d.ts +27 -0
  69. package/dist/trace-ai/eval-set/output-writer.js +126 -0
  70. package/dist/trace-ai/eval-set/query-picker.d.ts +37 -0
  71. package/dist/trace-ai/eval-set/query-picker.js +147 -0
  72. package/dist/trace-ai/eval-set/redactor.d.ts +42 -0
  73. package/dist/trace-ai/eval-set/redactor.js +133 -0
  74. package/dist/trace-ai/eval-set/rubric-templates/answer-match-reference.prompt.md +19 -0
  75. package/dist/trace-ai/eval-set/schemas.d.ts +136 -0
  76. package/dist/trace-ai/eval-set/schemas.js +130 -0
  77. package/dist/trace-ai/eval-set/semantic-match-provider.d.ts +33 -0
  78. package/dist/trace-ai/eval-set/semantic-match-provider.js +51 -0
  79. package/dist/trace-ai/eval-set/test-runner.d.ts +34 -0
  80. package/dist/trace-ai/eval-set/test-runner.js +153 -0
  81. package/dist/trace-ai/eval-set/types.d.ts +46 -0
  82. package/dist/trace-ai/eval-set/types.js +8 -0
  83. package/dist/trace-ai/exp/bundle-writer.d.ts +10 -0
  84. package/dist/trace-ai/exp/bundle-writer.js +54 -0
  85. package/dist/trace-ai/exp/claude-binary.d.ts +5 -0
  86. package/dist/trace-ai/exp/claude-binary.js +30 -0
  87. package/dist/trace-ai/exp/coordinator.d.ts +45 -0
  88. package/dist/trace-ai/exp/coordinator.js +203 -0
  89. package/dist/trace-ai/exp/eval-runner.d.ts +14 -0
  90. package/dist/trace-ai/exp/eval-runner.js +47 -0
  91. package/dist/trace-ai/exp/exp-store/abort-signal.d.ts +3 -0
  92. package/dist/trace-ai/exp/exp-store/abort-signal.js +27 -0
  93. package/dist/trace-ai/exp/exp-store/candidate-lineage-yaml.d.ts +4 -0
  94. package/dist/trace-ai/exp/exp-store/candidate-lineage-yaml.js +37 -0
  95. package/dist/trace-ai/exp/exp-store/events-jsonl.d.ts +17 -0
  96. package/dist/trace-ai/exp/exp-store/events-jsonl.js +60 -0
  97. package/dist/trace-ai/exp/exp-store/exp-registry.d.ts +6 -0
  98. package/dist/trace-ai/exp/exp-store/exp-registry.js +41 -0
  99. package/dist/trace-ai/exp/exp-store/index.d.ts +46 -0
  100. package/dist/trace-ai/exp/exp-store/index.js +59 -0
  101. package/dist/trace-ai/exp/exp-store/lock.d.ts +3 -0
  102. package/dist/trace-ai/exp/exp-store/lock.js +73 -0
  103. package/dist/trace-ai/exp/exp-store/mission-md.d.ts +3 -0
  104. package/dist/trace-ai/exp/exp-store/mission-md.js +37 -0
  105. package/dist/trace-ai/exp/exp-store/readme-template.d.ts +5 -0
  106. package/dist/trace-ai/exp/exp-store/readme-template.js +25 -0
  107. package/dist/trace-ai/exp/exp-store/round-yaml.d.ts +3 -0
  108. package/dist/trace-ai/exp/exp-store/round-yaml.js +33 -0
  109. package/dist/trace-ai/exp/index.d.ts +8 -0
  110. package/dist/trace-ai/exp/index.js +238 -0
  111. package/dist/trace-ai/exp/info.d.ts +35 -0
  112. package/dist/trace-ai/exp/info.js +120 -0
  113. package/dist/trace-ai/exp/patch/agent-config.d.ts +1 -0
  114. package/dist/trace-ai/exp/patch/agent-config.js +26 -0
  115. package/dist/trace-ai/exp/patch/index.d.ts +2 -0
  116. package/dist/trace-ai/exp/patch/index.js +13 -0
  117. package/dist/trace-ai/exp/patch/skill.d.ts +1 -0
  118. package/dist/trace-ai/exp/patch/skill.js +24 -0
  119. package/dist/trace-ai/exp/providers/synthesizer-client.d.ts +14 -0
  120. package/dist/trace-ai/exp/providers/synthesizer-client.js +39 -0
  121. package/dist/trace-ai/exp/providers/triage-client.d.ts +19 -0
  122. package/dist/trace-ai/exp/providers/triage-client.js +51 -0
  123. package/dist/trace-ai/exp/schemas.d.ts +147 -0
  124. package/dist/trace-ai/exp/schemas.js +50 -0
  125. package/dist/trace-ai/exp/scoring.d.ts +2 -0
  126. package/dist/trace-ai/exp/scoring.js +46 -0
  127. package/dist/trace-ai/scan/aggregator.d.ts +20 -0
  128. package/dist/trace-ai/scan/aggregator.js +26 -0
  129. package/dist/trace-ai/scan/artifacts/paths.d.ts +12 -0
  130. package/dist/trace-ai/scan/artifacts/paths.js +18 -0
  131. package/dist/trace-ai/scan/artifacts/writer.d.ts +67 -0
  132. package/dist/trace-ai/scan/artifacts/writer.js +96 -0
  133. package/dist/trace-ai/scan/batched-rubric.d.ts +55 -0
  134. package/dist/trace-ai/scan/batched-rubric.js +159 -0
  135. package/dist/trace-ai/scan/cross-trace-synthesizer.d.ts +24 -0
  136. package/dist/trace-ai/scan/cross-trace-synthesizer.js +93 -0
  137. package/dist/trace-ai/scan/index.d.ts +31 -0
  138. package/dist/trace-ai/scan/index.js +390 -0
  139. package/dist/trace-ai/scan/prompts/builtin/cross-trace-synthesizer-v1.prompt.md +44 -0
  140. package/dist/trace-ai/scan/prompts/builtin/rubric-judge-batch-v1.prompt.md +44 -0
  141. package/dist/trace-ai/scan/runner.d.ts +25 -0
  142. package/dist/trace-ai/scan/runner.js +42 -0
  143. package/dist/trace-ai/scan/sampler.d.ts +18 -0
  144. package/dist/trace-ai/scan/sampler.js +81 -0
  145. package/dist/trace-ai/scan/scan-summary-markdown.d.ts +2 -0
  146. package/dist/trace-ai/scan/scan-summary-markdown.js +71 -0
  147. package/dist/trace-ai/scan/scan-summary-schema.d.ts +73 -0
  148. package/dist/trace-ai/scan/scan-summary-schema.js +61 -0
  149. package/dist/trace-ai/scan/single-agent-validator.d.ts +23 -0
  150. package/dist/trace-ai/scan/single-agent-validator.js +42 -0
  151. package/dist/trace-ai/scan/traces-list-parser.d.ts +15 -0
  152. package/dist/trace-ai/scan/traces-list-parser.js +46 -0
  153. package/package.json +2 -2
  154. package/dist/trace-core/diagnose/index.d.ts +0 -9
  155. package/dist/trace-core/diagnose/index.js +0 -104
  156. package/dist/trace-core/diagnose/report-assembler.d.ts +0 -12
  157. package/dist/trace-core/diagnose/schemas.js +0 -94
  158. package/dist/trace-core/diagnose/signal-probe.d.ts +0 -5
  159. package/dist/trace-core/diagnose/signal-probe.js +0 -21
  160. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/excessive-tool-calls-per-turn.d.ts +0 -0
  161. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/excessive-tool-calls-per-turn.js +0 -0
  162. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/excessive-tool-calls-per-turn.yaml +0 -0
  163. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/llm-response-truncated-no-continue.d.ts +0 -0
  164. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/llm-response-truncated-no-continue.js +0 -0
  165. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/llm-response-truncated-no-continue.yaml +0 -0
  166. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/register.d.ts +0 -0
  167. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/register.js +0 -0
  168. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/retrieval-empty-no-fallback.d.ts +0 -0
  169. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/retrieval-empty-no-fallback.js +0 -0
  170. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/retrieval-empty-no-fallback.yaml +0 -0
  171. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/tool-error-swallowed.d.ts +0 -0
  172. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/tool-error-swallowed.js +0 -0
  173. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/tool-error-swallowed.yaml +0 -0
  174. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/tool-loop-no-state-change.d.ts +0 -0
  175. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/tool-loop-no-state-change.js +0 -0
  176. /package/dist/{trace-core → trace-ai}/diagnose/builtin-rules/tool-loop-no-state-change.yaml +0 -0
  177. /package/dist/{trace-core → trace-ai}/diagnose/predicate-registry.d.ts +0 -0
  178. /package/dist/{trace-core → trace-ai}/diagnose/predicate-registry.js +0 -0
  179. /package/dist/{trace-core → trace-ai}/diagnose/rule-loader.d.ts +0 -0
  180. /package/dist/{trace-core → trace-ai}/diagnose/synthesizer-template.d.ts +0 -0
  181. /package/dist/{trace-core → trace-ai}/diagnose/synthesizer-template.js +0 -0
  182. /package/dist/{trace-core → trace-ai}/diagnose/trace-shaper.d.ts +0 -0
  183. /package/dist/{trace-core → trace-ai}/diagnose/types.js +0 -0
@@ -0,0 +1,246 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getSpansByConversationId } from "../../api/trace.js";
6
+ import { assembleTraceTree } from "./trace-shaper.js";
7
+ import { loadRules, RuleLoadError } from "./rule-loader.js";
8
+ import { runRules, RuleProbeError, rubricRules } from "./signal-probe.js";
9
+ import { agentSynthesize } from "./synthesizer-agent.js";
10
+ import { evaluateRubricRules } from "./agent-binding.js";
11
+ import { assembleReport, reportToYamlObject, symbolicHitsToFindings } from "./report-assembler.js";
12
+ import { renderReportMarkdown } from "./report-markdown.js";
13
+ import { defaultRegistry } from "../../agent-providers/registry.js";
14
+ import { defaultPromptRegistry, } from "../../agent-providers/prompt-template.js";
15
+ import { ArtifactWriter } from "../scan/artifacts/writer.js";
16
+ import { resolveArtifactsBase } from "../scan/artifacts/paths.js";
17
+ import { extractUserQueryFromTrace } from "./query-extractor.js";
18
+ import "./builtin-rules/register.js"; // side effect: registers all builtin predicates
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const BUILTIN_DIR = path.join(__dirname, "builtin-rules");
21
+ // Prompts moved to top-level agent-providers/ when the trace-core/ container
22
+ // was split (refactor 2026-05-12). diagnose/ now sits two levels under src/,
23
+ // so we go up two and across.
24
+ const SHARED_PROMPT_DIR = path.join(__dirname, "..", "..", "agent-providers", "prompts");
25
+ export class TraceNotFoundError extends Error {
26
+ constructor(conversationId) {
27
+ super(`no spans found for conversation: ${conversationId}`);
28
+ this.name = "TraceNotFoundError";
29
+ }
30
+ }
31
+ let sharedPromptsLoaded = false;
32
+ async function ensureBuiltinPromptsLoaded(reg) {
33
+ if (reg !== defaultPromptRegistry) {
34
+ // Caller-provided registry: load on every call so test-specific
35
+ // overrides see their content (cheap; ENOENT is no-op).
36
+ await reg.loadBuiltinDir(SHARED_PROMPT_DIR);
37
+ return;
38
+ }
39
+ if (sharedPromptsLoaded)
40
+ return;
41
+ await reg.loadBuiltinDir(SHARED_PROMPT_DIR);
42
+ sharedPromptsLoaded = true;
43
+ }
44
+ export async function diagnose(conversationId, opts, internal = {}) {
45
+ const t_start = Date.now();
46
+ const cwdRulesDir = opts.rulesDir ?? path.join(process.cwd(), "diagnosis-rules");
47
+ const registry = internal.registry ?? defaultRegistry;
48
+ const promptRegistry = internal.promptRegistry ?? defaultPromptRegistry;
49
+ await ensureBuiltinPromptsLoaded(promptRegistry);
50
+ // ── Artifact writer setup ────────────────────────────────────────────────
51
+ const artifactsEnabled = !(opts.noArtifacts ?? false) && opts.out !== null;
52
+ const artifactsBase = artifactsEnabled
53
+ ? resolveArtifactsBase({ mode: "single", out: opts.out })
54
+ : "";
55
+ const artifacts = new ArtifactWriter({ base: artifactsBase, enabled: artifactsEnabled });
56
+ // ── 1. Fetch + shape spans ──────────────────────────────────────────────
57
+ const fetched = await getSpansByConversationId({
58
+ baseUrl: opts.baseUrl,
59
+ token: opts.token,
60
+ businessDomain: opts.businessDomain,
61
+ conversationId,
62
+ });
63
+ const rawSpans = fetched.spans;
64
+ if (rawSpans.length === 0)
65
+ throw new TraceNotFoundError(conversationId);
66
+ const observedTraceIds = fetched.traceIds.length > 0
67
+ ? fetched.traceIds
68
+ : [...new Set(rawSpans.map((s) => s.traceId).filter((t) => Boolean(t)))];
69
+ const primaryTraceId = observedTraceIds[0] ?? conversationId;
70
+ if (observedTraceIds.length > 1) {
71
+ process.stderr.write(`warning: conversation ${conversationId} has ${observedTraceIds.length} traces; diagnosing the first (${primaryTraceId})\n`);
72
+ }
73
+ const spansForPrimary = observedTraceIds.length > 0
74
+ ? rawSpans.filter((s) => !s.traceId || s.traceId === primaryTraceId)
75
+ : rawSpans;
76
+ const tree = assembleTraceTree(primaryTraceId, spansForPrimary);
77
+ // ── 1b. Extract user query for suggested_eval_case population ───────────
78
+ const userQuery = extractUserQueryFromTrace(tree);
79
+ const queryId = conversationId;
80
+ // ── 2. Load rules + run Stage-1 (symbolic) ──────────────────────────────
81
+ const rules = await loadRules({
82
+ builtinDir: BUILTIN_DIR,
83
+ cwdRulesDir,
84
+ extraRulesDir: null,
85
+ noBuiltin: opts.noBuiltin,
86
+ });
87
+ const hits = await runRules(rules, tree);
88
+ const symbolicFindings = symbolicHitsToFindings(rules, hits, userQuery, queryId);
89
+ // ── 3. Stage-2 (rubric) — skip everything when --no-llm ─────────────────
90
+ const haveRubric = rubricRules(rules).length > 0;
91
+ let rubricFindings = [];
92
+ let rulesSkipped = [];
93
+ if (haveRubric) {
94
+ const r = await evaluateRubricRules({
95
+ rules,
96
+ tree,
97
+ registry,
98
+ promptRegistry,
99
+ noLlm: opts.noLlm,
100
+ timeoutMs: opts.timeoutMs,
101
+ lang: opts.lang,
102
+ artifacts,
103
+ userQuery,
104
+ queryId,
105
+ });
106
+ rubricFindings = r.findings;
107
+ rulesSkipped = r.skipped;
108
+ }
109
+ const allFindings = [...symbolicFindings, ...rubricFindings];
110
+ // ── 4. Stage-3 — agent synthesizer (template fallback) ──────────────────
111
+ const synthProvider = opts.noLlm
112
+ ? null
113
+ : registry.resolve({ preferred: opts.agentProvider ?? undefined });
114
+ const synth = await agentSynthesize({
115
+ findings: allFindings,
116
+ traceId: primaryTraceId,
117
+ agentId: extractAgentId(tree),
118
+ provider: synthProvider,
119
+ promptRegistry,
120
+ timeoutMs: opts.timeoutMs,
121
+ lang: opts.lang,
122
+ artifacts,
123
+ });
124
+ // ── 5. Assemble report ──────────────────────────────────────────────────
125
+ const haveSymbolic = rules.some((r) => r.predicateRef !== null);
126
+ const ranRubric = haveRubric && !opts.noLlm;
127
+ const mode = haveSymbolic && ranRubric
128
+ ? "hybrid"
129
+ : ranRubric
130
+ ? "rubric-only"
131
+ : "symbolic-only";
132
+ const version = await cliVersion();
133
+ const report = assembleReport({
134
+ traceId: primaryTraceId,
135
+ agentId: extractAgentId(tree),
136
+ tenant: extractTenant(tree),
137
+ cliVersion: version,
138
+ rules,
139
+ hits,
140
+ extraFindings: rubricFindings,
141
+ summary: synth.summary,
142
+ mode,
143
+ rulesSkipped,
144
+ synthesizerMode: synth.mode,
145
+ userQuery,
146
+ queryId,
147
+ });
148
+ // ── 6. Write run-metadata artifact ─────────────────────────────────────
149
+ const t_total = Date.now() - t_start;
150
+ await artifacts.writeRunMetadata({
151
+ cli_args: { conv_id: conversationId, out: opts.out, lang: opts.lang ?? "en" },
152
+ agent_id: extractAgentId(tree) ?? "",
153
+ rule_load_summary: {
154
+ rules_applied: rules.map((r) => r.id),
155
+ rules_skipped_at_load: [],
156
+ rules_dir: opts.rulesDir ?? "builtin",
157
+ },
158
+ single_agent_validation: { checked_conv_ids: 1, agent_id_resolved: extractAgentId(tree) ?? "" },
159
+ timing: { stage_1_ms: 0, stage_2_ms: 0, stage_3_ms: 0, stage_4_ms: 0, total_ms: t_total },
160
+ llm_calls: {
161
+ stage_2_chunks: rubricFindings.length > 0 ? 1 : 0,
162
+ stage_3: synth.mode === "agent" ? 1 : 0,
163
+ stage_4: 0,
164
+ total: (rubricFindings.length > 0 ? 1 : 0) + (synth.mode === "agent" ? 1 : 0),
165
+ },
166
+ cost_estimate_usd: { stage_2: 0, stage_4: 0, total: 0, model_price_table_version: "2026-05" },
167
+ });
168
+ // ── 7. Emit ──────────────────────────────────────────────────────────────
169
+ const yamlText = yaml.dump(reportToYamlObject(report));
170
+ // Markdown renderer also receives the conversation_id + business_domain so
171
+ // the "How to verify" section can emit runnable CLI commands. These two
172
+ // values are NOT in the yaml schema (yaml stays CLI-agnostic) — they live
173
+ // only in the md projection.
174
+ const mdOpts = { conversationId, businessDomain: opts.businessDomain };
175
+ const format = opts.format ?? (opts.out !== null ? "both" : "yaml");
176
+ if (opts.out !== null) {
177
+ await fs.mkdir(path.dirname(opts.out), { recursive: true });
178
+ const { yamlPath, mdPath } = derivePaths(opts.out, format);
179
+ if (yamlPath !== null)
180
+ await fs.writeFile(yamlPath, yamlText, "utf8");
181
+ if (mdPath !== null)
182
+ await fs.writeFile(mdPath, renderReportMarkdown(report, mdOpts), "utf8");
183
+ }
184
+ else {
185
+ // stdout — markdown to stdout would corrupt downstream `yq` / yaml consumers, so
186
+ // 'both' degrades to yaml-only. Users who want md on stdout pass --format=markdown.
187
+ if (format === "markdown") {
188
+ process.stdout.write(renderReportMarkdown(report, mdOpts));
189
+ }
190
+ else {
191
+ process.stdout.write(yamlText);
192
+ }
193
+ }
194
+ if (report.findings.length === 0) {
195
+ process.stderr.write("no findings\n");
196
+ }
197
+ return report;
198
+ }
199
+ /** Resolve which file paths to write given the user-supplied --out and format.
200
+ * Both: derive the missing extension from the given one; if --out had no
201
+ * recognized extension, append .yaml / .md. Single-format: write to --out
202
+ * verbatim (caller's extension is honored as-is). */
203
+ export function derivePaths(out, format) {
204
+ if (format === "yaml")
205
+ return { yamlPath: out, mdPath: null };
206
+ if (format === "markdown")
207
+ return { yamlPath: null, mdPath: out };
208
+ // both
209
+ const lower = out.toLowerCase();
210
+ if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
211
+ const stem = out.slice(0, out.lastIndexOf("."));
212
+ return { yamlPath: out, mdPath: `${stem}.md` };
213
+ }
214
+ if (lower.endsWith(".md") || lower.endsWith(".markdown")) {
215
+ const stem = out.slice(0, out.lastIndexOf("."));
216
+ return { yamlPath: `${stem}.yaml`, mdPath: out };
217
+ }
218
+ return { yamlPath: `${out}.yaml`, mdPath: `${out}.md` };
219
+ }
220
+ function extractAgentId(tree) {
221
+ for (const s of tree.spans) {
222
+ const v = s.attributes["gen_ai.agent.id"];
223
+ if (typeof v === "string")
224
+ return v;
225
+ }
226
+ return null;
227
+ }
228
+ function extractTenant(tree) {
229
+ for (const s of tree.spans) {
230
+ const v = s.attributes["tenant"];
231
+ if (typeof v === "string")
232
+ return v;
233
+ }
234
+ return null;
235
+ }
236
+ async function cliVersion() {
237
+ try {
238
+ const pkgPath = path.join(__dirname, "..", "..", "..", "package.json");
239
+ const txt = await fs.readFile(pkgPath, "utf8");
240
+ return JSON.parse(txt).version ?? "0.0.0";
241
+ }
242
+ catch {
243
+ return "0.0.0";
244
+ }
245
+ }
246
+ export { TraceNotFoundError as DiagnoseTraceNotFound, RuleLoadError, RuleProbeError };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Convert a rubric YAML's `output_schema` (a JSON-Schema-ish blob) into a
3
+ * zod schema the agent provider validates LLM responses against.
4
+ *
5
+ * We don't pull in a full JSON-Schema-to-Zod converter — rubric YAMLs use
6
+ * a deliberately narrow subset: `type: object` with `required[]` and
7
+ * `properties{type, enum, items}`. Anything richer is rejected at load
8
+ * time so authors don't accidentally rely on full JSON Schema semantics
9
+ * we haven't implemented.
10
+ *
11
+ * Supported per-property `type` values: `string`, `number`, `boolean`,
12
+ * `array` (homogeneous items by `items.type`), `object` (recursive).
13
+ * `enum` (string-only) is supported on `string` properties.
14
+ *
15
+ * Unsupported / rejected at conversion time: `type: integer` (use number),
16
+ * `anyOf`/`oneOf`, `$ref`, `additionalProperties: false`, `format`.
17
+ */
18
+ import { z } from "zod";
19
+ import type { RubricYaml } from "./schemas.js";
20
+ export declare class OutputSchemaConversionError extends Error {
21
+ readonly path: string;
22
+ constructor(message: string, path: string);
23
+ }
24
+ export declare function rubricOutputToZod(rubric: RubricYaml): z.ZodTypeAny;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Convert a rubric YAML's `output_schema` (a JSON-Schema-ish blob) into a
3
+ * zod schema the agent provider validates LLM responses against.
4
+ *
5
+ * We don't pull in a full JSON-Schema-to-Zod converter — rubric YAMLs use
6
+ * a deliberately narrow subset: `type: object` with `required[]` and
7
+ * `properties{type, enum, items}`. Anything richer is rejected at load
8
+ * time so authors don't accidentally rely on full JSON Schema semantics
9
+ * we haven't implemented.
10
+ *
11
+ * Supported per-property `type` values: `string`, `number`, `boolean`,
12
+ * `array` (homogeneous items by `items.type`), `object` (recursive).
13
+ * `enum` (string-only) is supported on `string` properties.
14
+ *
15
+ * Unsupported / rejected at conversion time: `type: integer` (use number),
16
+ * `anyOf`/`oneOf`, `$ref`, `additionalProperties: false`, `format`.
17
+ */
18
+ import { z } from "zod";
19
+ export class OutputSchemaConversionError extends Error {
20
+ path;
21
+ constructor(message, path) {
22
+ super(`${message} (at ${path})`);
23
+ this.path = path;
24
+ this.name = "OutputSchemaConversionError";
25
+ }
26
+ }
27
+ function convertProp(spec, path) {
28
+ const t = spec.type;
29
+ if (typeof t !== "string") {
30
+ throw new OutputSchemaConversionError(`property is missing 'type' string`, path);
31
+ }
32
+ switch (t) {
33
+ case "string": {
34
+ if (Array.isArray(spec.enum)) {
35
+ if (spec.enum.length === 0) {
36
+ throw new OutputSchemaConversionError(`empty enum`, path);
37
+ }
38
+ for (const v of spec.enum) {
39
+ if (typeof v !== "string") {
40
+ throw new OutputSchemaConversionError(`enum supports string values only`, path);
41
+ }
42
+ }
43
+ return z.enum(spec.enum);
44
+ }
45
+ return z.string();
46
+ }
47
+ case "number": return z.number();
48
+ case "boolean": return z.boolean();
49
+ case "array": {
50
+ const items = spec.items;
51
+ if (!items) {
52
+ throw new OutputSchemaConversionError(`array property requires 'items'`, path);
53
+ }
54
+ return z.array(convertProp(items, `${path}.items`));
55
+ }
56
+ case "object": {
57
+ const subProps = spec.properties ?? {};
58
+ const subRequired = spec.required ?? [];
59
+ return buildObject(subProps, subRequired, path);
60
+ }
61
+ default:
62
+ throw new OutputSchemaConversionError(`unsupported type '${t}'`, path);
63
+ }
64
+ }
65
+ function buildObject(properties, required, path) {
66
+ const shape = {};
67
+ const requiredSet = new Set(required);
68
+ for (const [key, spec] of Object.entries(properties)) {
69
+ const sub = convertProp(spec, `${path}.${key}`);
70
+ shape[key] = requiredSet.has(key) ? sub : sub.optional();
71
+ }
72
+ for (const req of required) {
73
+ if (!(req in properties)) {
74
+ throw new OutputSchemaConversionError(`required key '${req}' is not present in properties`, path);
75
+ }
76
+ }
77
+ return z.object(shape);
78
+ }
79
+ export function rubricOutputToZod(rubric) {
80
+ return buildObject(rubric.output_schema.properties, rubric.output_schema.required, "output_schema");
81
+ }
@@ -0,0 +1,14 @@
1
+ import type { TraceTree } from "./types.js";
2
+ /**
3
+ * Extract the most recent user-role message from a trace's input.messages.
4
+ *
5
+ * Scans spans for `gen_ai.input.messages` (a JSON-stringified array of
6
+ * {role, content}), checking two locations in order:
7
+ * 1. span.events[*].attributes — emitted by dolphin otel_listener as the
8
+ * "gen_ai.client.inference.operation.details" event (primary path)
9
+ * 2. span.attributes — fallback for runtimes that promote the
10
+ * field directly onto the span
11
+ *
12
+ * Returns the last `role === "user"` message content, or null if not found.
13
+ */
14
+ export declare function extractUserQueryFromTrace(tree: TraceTree): string | null;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Extract the most recent user-role message from a trace's input.messages.
3
+ *
4
+ * Scans spans for `gen_ai.input.messages` (a JSON-stringified array of
5
+ * {role, content}), checking two locations in order:
6
+ * 1. span.events[*].attributes — emitted by dolphin otel_listener as the
7
+ * "gen_ai.client.inference.operation.details" event (primary path)
8
+ * 2. span.attributes — fallback for runtimes that promote the
9
+ * field directly onto the span
10
+ *
11
+ * Returns the last `role === "user"` message content, or null if not found.
12
+ */
13
+ export function extractUserQueryFromTrace(tree) {
14
+ for (const span of tree.spans) {
15
+ const candidates = [];
16
+ // Primary: event attributes (dolphin otel_listener path)
17
+ for (const ev of span.events ?? []) {
18
+ const v = ev.attributes?.["gen_ai.input.messages"];
19
+ if (typeof v === "string")
20
+ candidates.push(v);
21
+ }
22
+ // Fallback: span attributes
23
+ const spanAttr = span.attributes?.["gen_ai.input.messages"];
24
+ if (typeof spanAttr === "string")
25
+ candidates.push(spanAttr);
26
+ for (const raw of candidates) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(raw);
30
+ }
31
+ catch {
32
+ continue;
33
+ }
34
+ if (!Array.isArray(parsed))
35
+ continue;
36
+ for (let i = parsed.length - 1; i >= 0; i--) {
37
+ const m = parsed[i];
38
+ if (m?.role === "user" && typeof m.content === "string" && m.content.length > 0) {
39
+ return m.content;
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return null;
45
+ }
@@ -0,0 +1,31 @@
1
+ import type { Finding, Hit, Report, Rule, Summary } from "./types.js";
2
+ export interface AssembleReportOpts {
3
+ traceId: string;
4
+ agentId: string | null;
5
+ tenant: string | null;
6
+ cliVersion: string;
7
+ rules: Rule[];
8
+ hits: Map<string, Hit[]>;
9
+ /** Additional pre-built findings (rubric judgments come from agent-binding). */
10
+ extraFindings?: Finding[];
11
+ summary: Summary;
12
+ /** Run mode. Default `symbolic-only` for backward compat. */
13
+ mode?: 'symbolic-only' | 'rubric-only' | 'hybrid';
14
+ /** Rubric rules skipped due to --no-llm / unavailable provider / etc. */
15
+ rulesSkipped?: {
16
+ ruleId: string;
17
+ reason: string;
18
+ }[];
19
+ /** Stage-3 synthesizer that produced `summary`. */
20
+ synthesizerMode?: 'template' | 'agent';
21
+ /** User query extracted from trace input.messages (2026-05-13). */
22
+ userQuery?: string | null;
23
+ /** Conversation/query ID for suggested_eval_case correlation (2026-05-13). */
24
+ queryId?: string | null;
25
+ }
26
+ /** Build symbolic-pillar findings from rule+hit pairs.
27
+ * Exported so callers (e.g. tests, index.ts) can compose findings from
28
+ * multiple sources before handing them to a custom summary path. */
29
+ export declare function symbolicHitsToFindings(rules: Rule[], hits: Map<string, Hit[]>, userQuery?: string | null, queryId?: string | null): Finding[];
30
+ export declare function assembleReport(opts: AssembleReportOpts): Report;
31
+ export declare function reportToYamlObject(r: Report): unknown;
@@ -4,17 +4,22 @@ function renderTemplate(tpl, bindings) {
4
4
  return v === undefined ? `{{${key}}}` : String(v);
5
5
  });
6
6
  }
7
- export function assembleReport(opts) {
7
+ /** Build symbolic-pillar findings from rule+hit pairs.
8
+ * Exported so callers (e.g. tests, index.ts) can compose findings from
9
+ * multiple sources before handing them to a custom summary path. */
10
+ export function symbolicHitsToFindings(rules, hits, userQuery = null, queryId = null) {
8
11
  const findings = [];
9
- for (const rule of opts.rules) {
10
- const ruleHits = opts.hits.get(rule.id) ?? [];
12
+ for (const rule of rules) {
13
+ if (rule.predicateRef === null)
14
+ continue;
15
+ const ruleHits = hits.get(rule.id) ?? [];
11
16
  for (const hit of ruleHits) {
12
17
  findings.push({
13
18
  ruleId: rule.id,
14
19
  judgmentKind: "symbolic",
15
20
  severity: rule.severity,
16
21
  symptom: rule.symptom,
17
- likelyCause: rule.symptom, // PR-A: no LLM, so we mirror symptom; PR-B agent overrides this
22
+ likelyCause: rule.symptom, // symbolic: no LLM, so mirror symptom; rubric agent overrides
18
23
  evidence: { spans: hit.evidenceSpans, excerpt: hit.excerpt },
19
24
  suggestedFix: {
20
25
  target: rule.suggestedFix.target,
@@ -23,24 +28,29 @@ export function assembleReport(opts) {
23
28
  confidence: "low",
24
29
  verifyWith: {
25
30
  suggestedEvalCase: {
26
- queryId: null, // PR-A: no query extraction yet (deferred per spec)
27
- query: null,
31
+ queryId,
32
+ query: userQuery,
28
33
  assertions: rule.verifyWith.assertionTemplates.map((t) => renderTemplate(t, hit.bindings)),
29
34
  },
30
35
  },
31
36
  });
32
37
  }
33
38
  }
39
+ return findings;
40
+ }
41
+ export function assembleReport(opts) {
42
+ const symbolicFindings = symbolicHitsToFindings(opts.rules, opts.hits, opts.userQuery ?? null, opts.queryId ?? null);
43
+ const findings = [...symbolicFindings, ...(opts.extraFindings ?? [])];
34
44
  return {
35
45
  schemaVersion: "trace-diagnose-report/v1",
36
46
  trace: { traceId: opts.traceId, agentId: opts.agentId, tenant: opts.tenant },
37
47
  run: {
38
48
  diagnosedAt: new Date().toISOString(),
39
49
  cliVersion: opts.cliVersion,
40
- mode: "symbolic-only",
50
+ mode: opts.mode ?? "symbolic-only",
41
51
  rulesApplied: opts.rules.map((r) => r.id),
42
- rulesSkipped: [],
43
- synthesizerMode: "template",
52
+ rulesSkipped: opts.rulesSkipped ?? [],
53
+ synthesizerMode: opts.synthesizerMode ?? "template",
44
54
  },
45
55
  summary: opts.summary,
46
56
  findings,
@@ -0,0 +1,18 @@
1
+ import type { Report } from "./types.js";
2
+ /**
3
+ * Optional context the md renderer uses to build runnable verification
4
+ * commands. None of these are in the yaml schema (which stays v1-locked and
5
+ * CLI-agnostic) — they live only in the markdown view so users who paste the
6
+ * md into a ticket / PR have copy-pasteable shell commands without needing to
7
+ * remember the trace's conversation context.
8
+ */
9
+ export interface MarkdownRenderOpts {
10
+ /** The conversation_id passed to `kweaver trace diagnose`. Used to render
11
+ * the "re-run diagnosis" command. When undefined, that command is rendered
12
+ * with a `<conversation_id>` placeholder. */
13
+ conversationId?: string;
14
+ /** Business domain (`-bd` flag). When undefined, commands omit the flag and
15
+ * inherit kweaver's default (`bd_public`). */
16
+ businessDomain?: string;
17
+ }
18
+ export declare function renderReportMarkdown(r: Report, opts?: MarkdownRenderOpts): string;