qa-engineer 0.9.2 → 0.11.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 (147) hide show
  1. package/COMPATIBILITY.md +26 -12
  2. package/README.md +182 -84
  3. package/package.json +11 -20
  4. package/packages/engine/bin/qa-engine.mjs +560 -0
  5. package/packages/engine/lib/analysis/branding.mjs +187 -0
  6. package/packages/engine/lib/analysis/context.mjs +294 -0
  7. package/packages/engine/lib/analysis/contracts.mjs +149 -0
  8. package/packages/engine/lib/analysis/diff-guard.mjs +425 -0
  9. package/packages/engine/lib/analysis/discovery.mjs +220 -0
  10. package/packages/engine/lib/analysis/evidence.mjs +116 -0
  11. package/packages/engine/lib/analysis/har.mjs +124 -0
  12. package/packages/engine/lib/analysis/junit.mjs +126 -0
  13. package/packages/engine/lib/analysis/network.mjs +237 -0
  14. package/packages/engine/lib/analysis/redaction.mjs +127 -0
  15. package/packages/engine/lib/analysis/report-html.mjs +76 -0
  16. package/packages/engine/lib/analysis/taxonomy.mjs +109 -0
  17. package/packages/engine/lib/analysis/xml.mjs +153 -0
  18. package/packages/engine/lib/analysis/zip.mjs +107 -0
  19. package/packages/engine/lib/artifacts/manager.mjs +453 -0
  20. package/packages/engine/lib/artifacts/mime.mjs +109 -0
  21. package/packages/engine/lib/artifacts/zip-write.mjs +143 -0
  22. package/packages/engine/lib/diagnostics/engine.mjs +165 -0
  23. package/packages/engine/lib/diagnostics/internal-contracts.mjs +50 -0
  24. package/packages/engine/lib/diagnostics/prioritization.mjs +99 -0
  25. package/packages/engine/lib/diagnostics/repair.mjs +73 -0
  26. package/packages/engine/lib/diagnostics/root-cause.mjs +92 -0
  27. package/packages/engine/lib/diagnostics/timeline.mjs +101 -0
  28. package/packages/engine/lib/frameworks/junit-frameworks.mjs +48 -0
  29. package/packages/engine/lib/frameworks/playwright.mjs +158 -0
  30. package/packages/engine/lib/report/components/charts.mjs +424 -0
  31. package/packages/engine/lib/report/components/evidence.mjs +207 -0
  32. package/packages/engine/lib/report/components/findings.mjs +258 -0
  33. package/packages/engine/lib/report/components/nav.mjs +99 -0
  34. package/packages/engine/lib/report/components/primitives.mjs +246 -0
  35. package/packages/engine/lib/report/components/runtime.mjs +246 -0
  36. package/packages/engine/lib/report/components/timeline.mjs +65 -0
  37. package/packages/engine/lib/report/core/model.mjs +270 -0
  38. package/packages/engine/lib/report/core/normalize.mjs +226 -0
  39. package/packages/engine/lib/report/core/sections.mjs +978 -0
  40. package/packages/engine/lib/report/export/bundle.mjs +293 -0
  41. package/packages/engine/lib/report/export/html.mjs +183 -0
  42. package/packages/engine/lib/report/export/machine.mjs +290 -0
  43. package/packages/engine/lib/report/export/markdown.mjs +323 -0
  44. package/packages/engine/lib/report/schemas/qa-report.schema.json +555 -0
  45. package/packages/engine/lib/report/theme/css.mjs +529 -0
  46. package/packages/engine/lib/report/theme/tokens.mjs +137 -0
  47. package/packages/engine/lib/report/version.mjs +78 -0
  48. package/packages/engine/package.json +14 -0
  49. package/packages/installer/lib/agents/targets.mjs +90 -0
  50. package/packages/installer/lib/agents/user-level.mjs +80 -0
  51. package/packages/installer/lib/cli/flags.mjs +20 -2
  52. package/packages/installer/lib/commands/doctor.mjs +13 -20
  53. package/packages/installer/lib/commands/install.mjs +160 -91
  54. package/packages/installer/lib/commands/repair.mjs +10 -6
  55. package/packages/installer/lib/commands/self-test.mjs +4 -3
  56. package/packages/installer/lib/commands/uninstall.mjs +14 -8
  57. package/packages/installer/lib/commands/update.mjs +9 -4
  58. package/packages/installer/lib/commands/verify.mjs +13 -12
  59. package/packages/installer/lib/constants.mjs +13 -0
  60. package/packages/installer/lib/core/bundle.mjs +69 -92
  61. package/packages/installer/lib/core/conflict.mjs +5 -4
  62. package/packages/installer/lib/core/fs-safe.mjs +146 -6
  63. package/packages/installer/lib/core/integrity.mjs +59 -0
  64. package/packages/installer/lib/core/lockfile.mjs +19 -3
  65. package/packages/installer/lib/core/manifest.mjs +48 -57
  66. package/packages/installer/lib/core/plan.mjs +213 -0
  67. package/packages/installer/lib/core/qa-home.mjs +145 -0
  68. package/packages/installer/lib/core/scope.mjs +274 -0
  69. package/packages/installer/lib/core/validate-install.mjs +49 -31
  70. package/packages/installer/package.json +1 -1
  71. package/packages/installer/schemas/qa-lock.schema.json +119 -21
  72. package/shared/tooling/qa-tool.mjs +161 -0
  73. package/skills/qa-api/SKILL.md +5 -5
  74. package/skills/qa-api/references/deterministic-tooling.md +34 -32
  75. package/skills/qa-api/references/evidence-and-reporting.md +5 -5
  76. package/skills/qa-api/scripts/qa-tool.mjs +162 -0
  77. package/skills/qa-audit/SKILL.md +5 -5
  78. package/skills/qa-audit/references/deterministic-tooling.md +34 -32
  79. package/skills/qa-audit/references/evidence-and-reporting.md +5 -5
  80. package/skills/qa-audit/scripts/qa-tool.mjs +162 -0
  81. package/skills/qa-debug/SKILL.md +6 -6
  82. package/skills/qa-debug/references/deterministic-tooling.md +34 -32
  83. package/skills/qa-debug/references/diagnostic-engine.md +3 -3
  84. package/skills/qa-debug/references/evidence-and-reporting.md +5 -5
  85. package/skills/qa-debug/scripts/qa-tool.mjs +162 -0
  86. package/skills/qa-explore/SKILL.md +31 -15
  87. package/skills/qa-explore/contracts/explore-result.schema.json +517 -11
  88. package/skills/qa-explore/references/api-replay.md +40 -1
  89. package/skills/qa-explore/references/deterministic-tooling.md +34 -32
  90. package/skills/qa-explore/references/evidence-and-reporting.md +5 -5
  91. package/skills/qa-explore/references/report-pipeline.md +266 -96
  92. package/skills/qa-explore/scripts/qa-tool.mjs +162 -0
  93. package/skills/qa-fix/SKILL.md +4 -4
  94. package/skills/qa-fix/references/deterministic-tooling.md +34 -32
  95. package/skills/qa-fix/references/diagnostic-engine.md +3 -3
  96. package/skills/qa-fix/references/evidence-and-reporting.md +5 -5
  97. package/skills/qa-fix/scripts/qa-tool.mjs +162 -0
  98. package/skills/qa-flaky/SKILL.md +4 -4
  99. package/skills/qa-flaky/references/deterministic-tooling.md +34 -32
  100. package/skills/qa-flaky/references/evidence-and-reporting.md +5 -5
  101. package/skills/qa-flaky/scripts/qa-tool.mjs +162 -0
  102. package/skills/qa-generate/references/evidence-and-reporting.md +5 -5
  103. package/skills/qa-init/SKILL.md +4 -4
  104. package/skills/qa-init/references/deterministic-tooling.md +34 -32
  105. package/skills/qa-init/references/evidence-and-reporting.md +5 -5
  106. package/skills/qa-init/scripts/qa-tool.mjs +162 -0
  107. package/skills/qa-report/SKILL.md +7 -7
  108. package/skills/qa-report/references/deterministic-tooling.md +34 -32
  109. package/skills/qa-report/references/diagnostic-engine.md +3 -3
  110. package/skills/qa-report/references/evidence-and-reporting.md +5 -5
  111. package/skills/qa-report/scripts/qa-tool.mjs +162 -0
  112. package/skills/qa-review/references/evidence-and-reporting.md +5 -5
  113. package/skills/qa-run/SKILL.md +6 -6
  114. package/skills/qa-run/references/deterministic-tooling.md +34 -32
  115. package/skills/qa-run/references/evidence-and-reporting.md +5 -5
  116. package/skills/qa-run/scripts/qa-tool.mjs +162 -0
  117. package/shared/analysis/lib/qa_analysis/__init__.py +0 -11
  118. package/shared/analysis/lib/qa_analysis/branding.py +0 -175
  119. package/shared/analysis/lib/qa_analysis/cli.py +0 -144
  120. package/shared/analysis/lib/qa_analysis/context.py +0 -233
  121. package/shared/analysis/lib/qa_analysis/contracts.py +0 -158
  122. package/shared/analysis/lib/qa_analysis/diff_guard.py +0 -327
  123. package/shared/analysis/lib/qa_analysis/discovery.py +0 -113
  124. package/shared/analysis/lib/qa_analysis/evidence.py +0 -128
  125. package/shared/analysis/lib/qa_analysis/har.py +0 -87
  126. package/shared/analysis/lib/qa_analysis/junit.py +0 -104
  127. package/shared/analysis/lib/qa_analysis/redaction.py +0 -107
  128. package/shared/analysis/lib/qa_analysis/report_html.py +0 -781
  129. package/shared/analysis/lib/qa_analysis/taxonomy.py +0 -98
  130. package/shared/diagnostics/lib/qa_diagnostics/__init__.py +0 -13
  131. package/shared/diagnostics/lib/qa_diagnostics/cli.py +0 -124
  132. package/shared/diagnostics/lib/qa_diagnostics/engine.py +0 -143
  133. package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +0 -75
  134. package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +0 -101
  135. package/shared/diagnostics/lib/qa_diagnostics/repair.py +0 -72
  136. package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +0 -89
  137. package/shared/diagnostics/lib/qa_diagnostics/timeline.py +0 -71
  138. package/shared/frameworks/cypress/lib/cypress_analysis.py +0 -27
  139. package/shared/frameworks/playwright/lib/playwright_analysis.py +0 -167
  140. package/shared/frameworks/selenium/lib/selenium_analysis.py +0 -28
  141. package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +0 -24
  142. package/shared/tooling/qa_tool.py +0 -127
  143. /package/{shared/analysis/lib/qa_analysis → packages/engine/lib/analysis}/branding.json +0 -0
  144. /package/{shared → packages/engine/lib}/analysis/schemas/context.schema.json +0 -0
  145. /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/analysis-result.schema.json +0 -0
  146. /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/diagnosis.schema.json +0 -0
  147. /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/execution-result-min.schema.json +0 -0
@@ -0,0 +1,101 @@
1
+ // Deterministic timeline reconstruction.
2
+ //
3
+ // Reconstructs the ordered sequence of a run — start, browser launch, navigation,
4
+ // requests and responses, console errors, assertions, failure, cleanup — from the
5
+ // execution result and the analysis findings. It records only stages for which there
6
+ // is evidence; it never invents an event to fill the shape. Reusable by qa-debug and
7
+ // qa-report.
8
+
9
+ // Canonical phase order, used to sort events that share (or lack) a timestamp.
10
+ export const PHASE_ORDER = [
11
+ 'execution-start', 'browser-launch', 'navigation', 'request', 'response',
12
+ 'console-error', 'assertion', 'failure', 'cleanup', 'execution-finish',
13
+ ];
14
+ const PHASE_INDEX = new Map(PHASE_ORDER.map((phase, index) => [phase, index]));
15
+
16
+ // Evidence type -> the timeline phase it contributes.
17
+ const EVIDENCE_PHASE = {
18
+ network: 'response',
19
+ console: 'console-error',
20
+ trace: 'navigation',
21
+ junit: 'assertion',
22
+ report: 'assertion',
23
+ };
24
+
25
+ /**
26
+ * Build an ordered timeline: `[{order, phase, detail, source, timestamp?}]`.
27
+ *
28
+ * Deterministic: same inputs, same timeline. Only evidenced stages appear.
29
+ */
30
+ export function buildTimeline(executionResult, findings = null) {
31
+ const events = [];
32
+ const execution = (executionResult ?? {}).execution ?? {};
33
+
34
+ const started = execution.startedAt;
35
+ if (started || (executionResult !== null && executionResult !== undefined && truthy(executionResult))) {
36
+ events.push(event('execution-start', 'Run started', 'execution-result', started ?? null));
37
+ }
38
+
39
+ // Contribute a stage per evidence entry we actually have.
40
+ for (const finding of findings ?? []) {
41
+ for (const item of finding.evidence ?? []) {
42
+ const phase = EVIDENCE_PHASE[item.type];
43
+ if (phase) {
44
+ events.push(event(phase, item.description ?? '', item.source ?? 'analysis', null));
45
+ }
46
+ }
47
+ }
48
+
49
+ // A failure stage per failed test recorded in the result.
50
+ for (const test of (executionResult ?? {}).executed ?? []) {
51
+ if (test.status === 'failed') {
52
+ events.push(event('failure', `Test failed: ${test.title ?? ''}`,
53
+ test.file ?? 'execution-result', null));
54
+ }
55
+ }
56
+
57
+ const finished = execution.finishedAt;
58
+ if (finished) {
59
+ events.push(event('cleanup', 'Run cleaned up', 'execution-result', null));
60
+ events.push(event('execution-finish', 'Run finished', 'execution-result', finished));
61
+ }
62
+
63
+ // Deterministic order: by canonical phase first (it encodes the logical
64
+ // sequence), then by timestamp within a phase, then by insertion order. Phase
65
+ // order is primary because per-event wall-clock times are often unavailable.
66
+ const decorated = events.map((entry, seq) => ({ entry, seq }));
67
+ decorated.sort((a, b) => {
68
+ const phaseDelta = (PHASE_INDEX.get(a.entry.phase) ?? 99) - (PHASE_INDEX.get(b.entry.phase) ?? 99);
69
+ if (phaseDelta !== 0) return phaseDelta;
70
+ const left = a.entry.timestamp || '';
71
+ const right = b.entry.timestamp || '';
72
+ if (left !== right) return left < right ? -1 : 1;
73
+ return a.seq - b.seq;
74
+ });
75
+
76
+ // Key order matters: these events are serialized and compared byte-for-byte
77
+ // against the Python implementation, which builds phase/detail/source/timestamp
78
+ // and then appends `order`. Assigning `order` before `timestamp` would produce
79
+ // the same data in a different JSON shape.
80
+ return decorated.map(({ entry }, order) => {
81
+ const result = { phase: entry.phase, detail: entry.detail, source: entry.source };
82
+ if (entry.timestamp !== null && entry.timestamp !== undefined) result.timestamp = entry.timestamp;
83
+ result.order = order;
84
+ return result;
85
+ });
86
+ }
87
+
88
+ function event(phase, detail, source, timestamp) {
89
+ return { phase, detail, source, timestamp };
90
+ }
91
+
92
+ /**
93
+ * Python's `if started or execution_result:` treats an empty dict as falsey, so a
94
+ * bare `{}` produces no start event while `{"tests": {...}}` does. JavaScript
95
+ * considers every object truthy, so the emptiness has to be asked about directly.
96
+ */
97
+ function truthy(value) {
98
+ if (Array.isArray(value)) return value.length > 0;
99
+ if (value && typeof value === 'object') return Object.keys(value).length > 0;
100
+ return Boolean(value);
101
+ }
@@ -0,0 +1,48 @@
1
+ // The JUnit-XML frameworks: Selenium, Cypress, WebdriverIO.
2
+ //
3
+ // All three emit JUnit XML, so their normalization *is* the framework-agnostic JUnit
4
+ // parser — each adapter only points at the right artifact and tags provenance. That
5
+ // thinness is the proof of the pack's multi-framework claim: the second, third and
6
+ // fourth frameworks cost a few lines each, because the contract and the parsing are
7
+ // shared and only the artifact location differs (ADR-0013).
8
+ //
9
+ // The three lived in separate Python modules that were identical but for a name and
10
+ // a glob list. Here they are one table, because three copies of the same four lines
11
+ // is three places for them to drift.
12
+
13
+ import { parseJUnit } from '../analysis/junit.mjs';
14
+ import { classify } from '../analysis/taxonomy.mjs';
15
+
16
+ /** framework -> where that framework's runners conventionally write JUnit XML. */
17
+ export const RESULT_GLOBS = {
18
+ selenium: ['**/target/surefire-reports/*.xml', '**/test-results/*.xml', '**/junit*.xml'],
19
+ cypress: ['**/results/*.xml', '**/cypress/results/*.xml', '**/junit*.xml'],
20
+ webdriverio: ['**/junit*.xml', '**/results/*.xml', '**/test-results/*.xml'],
21
+ };
22
+
23
+ export const FRAMEWORKS = Object.keys(RESULT_GLOBS);
24
+
25
+ /**
26
+ * Normalize a run into the shared result shape.
27
+ *
28
+ * Identical output for every framework — that is the point. The `framework`
29
+ * argument selects nothing in the parsing; it exists so a caller can be explicit
30
+ * about provenance and so an unknown name is refused rather than silently treated
31
+ * as one of the three.
32
+ */
33
+ export function normalize(framework, junitPath) {
34
+ if (!Object.prototype.hasOwnProperty.call(RESULT_GLOBS, framework)) {
35
+ throw new Error(`unknown JUnit framework: ${framework} (known: ${FRAMEWORKS.join(', ')})`);
36
+ }
37
+ return parseJUnit(junitPath);
38
+ }
39
+
40
+ /**
41
+ * Classify a failure using the shared taxonomy.
42
+ *
43
+ * There is no per-framework classifier, because failure classes are
44
+ * framework-agnostic: a missing element is a locator failure whoever reported it.
45
+ */
46
+ export function classifyFailure(message, httpStatus = null) {
47
+ return classify(message, httpStatus);
48
+ }
@@ -0,0 +1,158 @@
1
+ // Playwright-specific analyzers: trace and JSON report.
2
+ //
3
+ // This is the Playwright *adapter* for the analysis platform. It depends on the
4
+ // framework-agnostic analysis core for the evidence model, redaction, and the
5
+ // failure taxonomy; it adds only what is Playwright-specific — the shape of a
6
+ // trace.zip and of Playwright's JSON reporter. Everything it emits is in the shared
7
+ // normalized shape, so nothing downstream knows it was Playwright.
8
+ //
9
+ // Framework knowledge stays inside the adapter: the core never grows a
10
+ // `--framework` flag (ADR-0013).
11
+
12
+ import fs from 'node:fs';
13
+
14
+ import { MalformedArtifact } from '../analysis/junit.mjs';
15
+ import { redactText } from '../analysis/redaction.mjs';
16
+ import * as taxonomy from '../analysis/taxonomy.mjs';
17
+ import { openZip, isZip, ZipError } from '../analysis/zip.mjs';
18
+
19
+ export const FRAMEWORK = 'playwright';
20
+
21
+ /**
22
+ * Parse Playwright's JSON reporter output into the normalized result shape.
23
+ *
24
+ * Mirrors the JUnit parser's output (`{tests, executed}`) so a Playwright run
25
+ * normalizes identically whether it emitted JSON or JUnit.
26
+ */
27
+ export function parseReport(path) {
28
+ let data;
29
+ try {
30
+ data = JSON.parse(fs.readFileSync(path, 'utf8'));
31
+ } catch (error) {
32
+ throw new MalformedArtifact(`could not parse Playwright report at ${path}: ${error.message}`);
33
+ }
34
+
35
+ const executed = [];
36
+ for (const suite of data.suites ?? []) walkSuite(suite, executed);
37
+
38
+ return {
39
+ tests: {
40
+ total: executed.length,
41
+ passed: executed.filter((e) => e.status === 'passed').length,
42
+ failed: executed.filter((e) => e.status === 'failed').length,
43
+ skipped: executed.filter((e) => e.status === 'skipped').length,
44
+ },
45
+ executed,
46
+ };
47
+ }
48
+
49
+ function walkSuite(suite, executed) {
50
+ for (const spec of suite.specs ?? []) {
51
+ for (const test of spec.tests ?? []) {
52
+ const results = test.results ?? [];
53
+ const status = statusOf(results);
54
+ let message = '';
55
+ if (status === 'failed' && results.length > 0) {
56
+ const errors = results[results.length - 1].errors ?? [];
57
+ if (errors.length > 0) {
58
+ message = redactText((errors[0].message ?? '').trim());
59
+ }
60
+ }
61
+ const entry = {
62
+ title: spec.title ?? '',
63
+ file: suite.file ?? '',
64
+ status,
65
+ durationMs: results.length > 0 ? (results[results.length - 1].duration ?? 0) : 0,
66
+ retries: Math.max(0, results.length - 1),
67
+ };
68
+ if (message) entry.message = message;
69
+ executed.push(entry);
70
+ }
71
+ }
72
+ for (const child of suite.suites ?? []) walkSuite(child, executed);
73
+ }
74
+
75
+ function statusOf(results) {
76
+ if (results.length === 0) return 'skipped';
77
+ const final = results[results.length - 1].status;
78
+ if (final === 'passed' || final === 'expected') return results.length > 1 ? 'flaky' : 'passed';
79
+ if (final === 'skipped') return 'skipped';
80
+ return 'failed';
81
+ }
82
+
83
+ /**
84
+ * Extract a deterministic summary from a Playwright trace.zip.
85
+ *
86
+ * A trace is a zip of newline-delimited JSON event files. This lists the actions,
87
+ * surfaces the last error, and counts console and network events — enough for a
88
+ * diagnostic skill to reason over, without decoding the full binary.
89
+ */
90
+ export function analyzeTrace(path) {
91
+ let archive;
92
+ try {
93
+ const buffer = fs.readFileSync(path);
94
+ if (!isZip(buffer)) throw new MalformedArtifact(`not a valid trace zip: ${path}`);
95
+ archive = openZip(path);
96
+ } catch (error) {
97
+ if (error instanceof MalformedArtifact) throw error;
98
+ throw new MalformedArtifact(`not a valid trace zip: ${path}`);
99
+ }
100
+
101
+ const actions = [];
102
+ const errors = [];
103
+ let console = 0;
104
+ let network = 0;
105
+
106
+ try {
107
+ const named = archive.entries.filter(
108
+ (entry) => entry.name.endsWith('.trace') || entry.name.endsWith('.jsonl') || entry.name.includes('trace'),
109
+ );
110
+ for (const entry of named.length > 0 ? named : archive.entries) {
111
+ const text = archive.read(entry).toString('utf8');
112
+ for (const line of text.split('\n')) {
113
+ const raw = line.trim();
114
+ if (!raw || !raw.startsWith('{')) continue;
115
+ let event;
116
+ try {
117
+ event = JSON.parse(raw);
118
+ } catch {
119
+ continue;
120
+ }
121
+ const kind = event.type ?? event.kind;
122
+ if (kind === 'action' || kind === 'before') {
123
+ actions.push(redactText(String(event.apiName ?? event.method ?? '')));
124
+ } else if (kind === 'console') {
125
+ console += 1;
126
+ } else if (kind === 'resource' || kind === 'network' || kind === 'http') {
127
+ network += 1;
128
+ }
129
+ if (event.error || kind === 'error') {
130
+ const message = event.error && typeof event.error === 'object'
131
+ ? event.error.message
132
+ : event.message;
133
+ if (message) errors.push(redactText(String(message)));
134
+ }
135
+ }
136
+ }
137
+ } catch (error) {
138
+ if (error instanceof ZipError) {
139
+ throw new MalformedArtifact(`could not read trace ${path}: ${error.message}`);
140
+ }
141
+ throw error;
142
+ }
143
+
144
+ const lastError = errors.length > 0 ? errors[errors.length - 1] : '';
145
+ const classified = lastError
146
+ ? taxonomy.classify(lastError)
147
+ : { classification: taxonomy.UNKNOWN, confidence: 0.2, reason: 'No error found in trace.' };
148
+
149
+ return {
150
+ actions: actions.filter(Boolean),
151
+ consoleEvents: console,
152
+ networkEvents: network,
153
+ errors,
154
+ classification: classified.classification,
155
+ confidence: classified.confidence,
156
+ reason: classified.reason,
157
+ };
158
+ }