@allurereport/plugin-agent 3.5.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.
@@ -0,0 +1,264 @@
1
+ import type { Statistic, TestLabel, TestStatus } from "@allurereport/core-api";
2
+ import { type EnrichmentActionCategory } from "./guidance.js";
3
+ export type AgentFindingSeverity = "info" | "warning" | "high";
4
+ export type AgentFindingCategory = "bootstrap" | "scope" | "metadata" | "evidence" | "smells";
5
+ export type AgentScopeMatch = "match" | "unexpected" | "forbidden" | "unknown";
6
+ export type AgentAcceptanceStatus = "accept" | "iterate" | "reject";
7
+ export type AgentAcceptanceImpact = "advisory" | "iterate" | "reject";
8
+ export type AgentEnrichmentActionCategory = EnrichmentActionCategory;
9
+ export type AgentExpectationSelector = {
10
+ environments?: string[];
11
+ full_names?: string[];
12
+ full_name_prefixes?: string[];
13
+ label_values?: Record<string, string | string[]>;
14
+ };
15
+ export type AgentExpectations = {
16
+ goal?: string;
17
+ task_id?: string;
18
+ expected?: AgentExpectationSelector;
19
+ forbidden?: AgentExpectationSelector;
20
+ notes?: string[];
21
+ };
22
+ export type AgentHarnessScopeInput = {
23
+ environments?: string[];
24
+ fullNames?: string[];
25
+ fullNamePrefixes?: string[];
26
+ labelValues?: Record<string, string | string[]>;
27
+ };
28
+ export type AgentHarnessRequest = {
29
+ goal?: string;
30
+ taskId?: string;
31
+ target?: AgentHarnessScopeInput;
32
+ expected?: AgentHarnessScopeInput;
33
+ forbidden?: AgentHarnessScopeInput;
34
+ notes?: string | string[];
35
+ repoContext?: {
36
+ framework?: string;
37
+ workspace?: string;
38
+ };
39
+ };
40
+ export type AgentRunManifest = {
41
+ schema_version: string;
42
+ report_uuid: string;
43
+ generated_at: string;
44
+ phase?: "running" | "done";
45
+ command: string | null;
46
+ actual_exit_code: number | null;
47
+ original_exit_code: number | null;
48
+ exit_code: {
49
+ original: number;
50
+ actual: number | null;
51
+ } | null;
52
+ summary: {
53
+ stats: Statistic;
54
+ modeled_stats?: {
55
+ total: number;
56
+ failed: number;
57
+ broken: number;
58
+ skipped: number;
59
+ unknown: number;
60
+ passed: number;
61
+ };
62
+ unmodeled_from_stats?: {
63
+ total: number;
64
+ failed: number;
65
+ broken: number;
66
+ skipped: number;
67
+ unknown: number;
68
+ passed: number;
69
+ };
70
+ compact?: {
71
+ visible_results: number;
72
+ logical_tests: number;
73
+ unmodeled_visible_results: number;
74
+ runner_failures_outside_logical_tests: number;
75
+ completeness: "complete" | "partial";
76
+ findings?: number;
77
+ };
78
+ duration_ms: {
79
+ total: number;
80
+ average: number;
81
+ max: number;
82
+ };
83
+ environments: Array<{
84
+ environmentId: string;
85
+ total: number;
86
+ failed: number;
87
+ broken: number;
88
+ skipped: number;
89
+ unknown: number;
90
+ passed: number;
91
+ }>;
92
+ };
93
+ paths: {
94
+ index_md: string;
95
+ agents_md: string;
96
+ tests_manifest: string;
97
+ findings_manifest: string;
98
+ test_events_manifest?: string;
99
+ expected_manifest: string | null;
100
+ project_guide: string | null;
101
+ process_logs: {
102
+ stdout: string | null;
103
+ stderr: string | null;
104
+ };
105
+ };
106
+ modeling?: {
107
+ completeness: "complete" | "partial";
108
+ reasons: string[];
109
+ modeledStats: {
110
+ total: number;
111
+ failed: number;
112
+ broken: number;
113
+ skipped: number;
114
+ unknown: number;
115
+ passed: number;
116
+ };
117
+ unmodeledFromStats: {
118
+ total: number;
119
+ failed: number;
120
+ broken: number;
121
+ skipped: number;
122
+ unknown: number;
123
+ passed: number;
124
+ };
125
+ runnerFailures: {
126
+ total: number;
127
+ globalErrors: number;
128
+ stderrActionable: number;
129
+ samples: Array<{
130
+ source: "stderr" | "global_error";
131
+ kind: "import" | "suite-load" | "setup" | "global-error";
132
+ message: string;
133
+ count: number;
134
+ }>;
135
+ };
136
+ stderr: {
137
+ actionableCount: number;
138
+ actionableSamples: string[];
139
+ noisyWarningCount: number;
140
+ noisyWarningSamples: string[];
141
+ };
142
+ compact: {
143
+ visible_results: number;
144
+ logical_tests: number;
145
+ unmodeled_visible_results: number;
146
+ runner_failures_outside_logical_tests: number;
147
+ completeness: "complete" | "partial";
148
+ };
149
+ };
150
+ expectations_present: boolean;
151
+ check_summary: {
152
+ total: number;
153
+ countsBySeverity: Record<AgentFindingSeverity, number>;
154
+ countsByCategory: Record<AgentFindingCategory, number>;
155
+ };
156
+ agent_context: {
157
+ agent_name: string | null;
158
+ loop_id: string | null;
159
+ task_id: string | null;
160
+ conversation_id: string | null;
161
+ };
162
+ };
163
+ export type AgentTestManifestLine = {
164
+ environment_id: string;
165
+ history_id: string | null;
166
+ test_result_id: string;
167
+ full_name: string;
168
+ package: string | null;
169
+ labels: TestLabel[];
170
+ status: TestStatus;
171
+ duration_ms: number;
172
+ retries: number;
173
+ flaky: boolean;
174
+ scope_match: AgentScopeMatch;
175
+ scope_reasons?: string[];
176
+ finding_counts: {
177
+ total: number;
178
+ high: number;
179
+ warning: number;
180
+ info: number;
181
+ };
182
+ markdown_path: string;
183
+ assets_dir: string;
184
+ };
185
+ export type AgentFindingManifestLine = {
186
+ finding_id: string;
187
+ subject: string;
188
+ severity: AgentFindingSeverity;
189
+ category: AgentFindingCategory;
190
+ check_name: string;
191
+ message: string;
192
+ explanation: string;
193
+ evidence_paths: string[];
194
+ remediation_hint: string;
195
+ expected_reference?: string;
196
+ confidence?: number;
197
+ };
198
+ export type AgentOutputBundle = {
199
+ outputDir: string;
200
+ run: AgentRunManifest;
201
+ tests: AgentTestManifestLine[];
202
+ findings: AgentFindingManifestLine[];
203
+ expected?: AgentExpectations;
204
+ };
205
+ export type AgentEnrichmentAction = {
206
+ checkName: string;
207
+ category: AgentEnrichmentActionCategory;
208
+ title: string;
209
+ guidance: string;
210
+ };
211
+ export type AgentEnrichmentPlanItem = AgentEnrichmentAction & {
212
+ subject: string;
213
+ subjectType: "run" | "test";
214
+ severity: AgentFindingSeverity;
215
+ message: string;
216
+ explanation: string;
217
+ remediationHint: string;
218
+ evidencePaths: string[];
219
+ expectedReference?: string;
220
+ confidence?: number;
221
+ acceptanceImpact: AgentAcceptanceImpact;
222
+ fullName?: string;
223
+ markdownPath?: string;
224
+ scopeMatch?: AgentScopeMatch;
225
+ };
226
+ export type AgentEnrichmentReview = {
227
+ status: AgentAcceptanceStatus;
228
+ outputDir: string;
229
+ expectationsPresent: boolean;
230
+ expected?: AgentExpectations;
231
+ summary: {
232
+ totalTests: number;
233
+ totalFindings: number;
234
+ planItems: number;
235
+ rejectingItems: number;
236
+ iterationItems: number;
237
+ advisoryItems: number;
238
+ countsByActionCategory: Record<AgentEnrichmentActionCategory, number>;
239
+ };
240
+ notes: string[];
241
+ plan: AgentEnrichmentPlanItem[];
242
+ rejecting: AgentEnrichmentPlanItem[];
243
+ iterate: AgentEnrichmentPlanItem[];
244
+ advisory: AgentEnrichmentPlanItem[];
245
+ rerun: {
246
+ requiresExpectations: boolean;
247
+ useExistingExpectations: boolean;
248
+ targetedTests: string[];
249
+ };
250
+ };
251
+ export type AgentReviewOptions = {
252
+ antiDummyConfidenceThreshold?: number;
253
+ requireExpectationsForAcceptance?: boolean;
254
+ };
255
+ export declare const DEFAULT_ANTI_DUMMY_CONFIDENCE = 0.75;
256
+ export declare const AGENT_ENRICHMENT_ACTIONS: Record<string, AgentEnrichmentAction>;
257
+ export declare const SCOPE_REJECTING_CHECKS: readonly ["missing-expected-test", "missing-expected-prefix", "missing-expected-environment", "unexpected-environment", "forbidden-selector-match", "unexpected-test"];
258
+ export declare const ITERATION_REQUIRED_CHECKS: readonly ["invalid-expectations-file", "no-visible-tests", "runner-failures-outside-logical-results", "missing-expected-label-selector", "metadata-mismatch", "history-id-collision", "failed-without-useful-steps", "failed-without-attachments", "nontrivial-run-with-empty-trace", "retries-without-new-evidence", "passed-without-observable-evidence"];
259
+ export declare const ANTI_DUMMY_CHECKS: readonly ["noop-dominated-steps"];
260
+ export declare const buildAgentExpectations: (input: AgentHarnessRequest) => AgentExpectations;
261
+ export declare const mapFindingToEnrichmentAction: (finding: AgentFindingManifestLine | string) => AgentEnrichmentAction;
262
+ export declare const loadAgentOutput: (outputDir: string) => Promise<AgentOutputBundle>;
263
+ export declare const planAgentEnrichmentReview: (output: AgentOutputBundle, options?: AgentReviewOptions) => AgentEnrichmentReview;
264
+ export declare const reviewAgentOutput: (outputDir: string, options?: AgentReviewOptions) => Promise<AgentEnrichmentReview>;
@@ -0,0 +1,236 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { ENRICHMENT_ACTIONS_BY_CHECK_NAME } from "./guidance.js";
4
+ export const DEFAULT_ANTI_DUMMY_CONFIDENCE = 0.75;
5
+ const FALLBACK_ACTION = {
6
+ checkName: "*",
7
+ category: "review-manually",
8
+ title: "Review the finding directly",
9
+ guidance: "Inspect the linked markdown and follow the remediation hint before rerunning.",
10
+ };
11
+ export const AGENT_ENRICHMENT_ACTIONS = Object.fromEntries(Object.entries(ENRICHMENT_ACTIONS_BY_CHECK_NAME).map(([checkName, action]) => [checkName, { checkName, ...action }]));
12
+ export const SCOPE_REJECTING_CHECKS = [
13
+ "missing-expected-test",
14
+ "missing-expected-prefix",
15
+ "missing-expected-environment",
16
+ "unexpected-environment",
17
+ "forbidden-selector-match",
18
+ "unexpected-test",
19
+ ];
20
+ export const ITERATION_REQUIRED_CHECKS = [
21
+ "invalid-expectations-file",
22
+ "no-visible-tests",
23
+ "runner-failures-outside-logical-results",
24
+ "missing-expected-label-selector",
25
+ "metadata-mismatch",
26
+ "history-id-collision",
27
+ "failed-without-useful-steps",
28
+ "failed-without-attachments",
29
+ "nontrivial-run-with-empty-trace",
30
+ "retries-without-new-evidence",
31
+ "passed-without-observable-evidence",
32
+ ];
33
+ export const ANTI_DUMMY_CHECKS = ["noop-dominated-steps"];
34
+ const SEVERITY_ORDER = {
35
+ high: 0,
36
+ warning: 1,
37
+ info: 2,
38
+ };
39
+ const IMPACT_ORDER = {
40
+ reject: 0,
41
+ iterate: 1,
42
+ advisory: 2,
43
+ };
44
+ const uniqueValues = (values) => Array.from(new Set(values));
45
+ const normalizeStringArray = (value) => {
46
+ if (typeof value === "string") {
47
+ return value.length ? [value] : [];
48
+ }
49
+ if (!Array.isArray(value)) {
50
+ return [];
51
+ }
52
+ return uniqueValues(value.filter((item) => typeof item === "string" && item.length > 0));
53
+ };
54
+ const normalizeLabelValues = (value) => Object.fromEntries(Object.entries(value ?? {}).flatMap(([name, rawValue]) => {
55
+ const values = normalizeStringArray(rawValue);
56
+ return values.length ? [[name, values]] : [];
57
+ }));
58
+ const toExpectationSelector = (input) => {
59
+ if (!input) {
60
+ return undefined;
61
+ }
62
+ const environments = normalizeStringArray(input.environments);
63
+ const fullNames = normalizeStringArray(input.fullNames);
64
+ const fullNamePrefixes = normalizeStringArray(input.fullNamePrefixes);
65
+ const labelValues = normalizeLabelValues(input.labelValues);
66
+ if (!environments.length && !fullNames.length && !fullNamePrefixes.length && !Object.keys(labelValues).length) {
67
+ return undefined;
68
+ }
69
+ return {
70
+ ...(environments.length ? { environments } : {}),
71
+ ...(fullNames.length ? { full_names: fullNames } : {}),
72
+ ...(fullNamePrefixes.length ? { full_name_prefixes: fullNamePrefixes } : {}),
73
+ ...(Object.keys(labelValues).length ? { label_values: labelValues } : {}),
74
+ };
75
+ };
76
+ const readJson = async (path) => JSON.parse(await readFile(path, "utf-8"));
77
+ const readJsonl = async (path) => (await readFile(path, "utf-8"))
78
+ .trim()
79
+ .split("\n")
80
+ .filter(Boolean)
81
+ .map((line) => JSON.parse(line));
82
+ const countByActionCategory = (items) => items.reduce((counts, item) => {
83
+ counts[item.category] += 1;
84
+ return counts;
85
+ }, {
86
+ "bootstrap-allure": 0,
87
+ "narrow-test-scope": 0,
88
+ "repair-test-metadata": 0,
89
+ "add-meaningful-steps": 0,
90
+ "add-test-attachments": 0,
91
+ "add-retry-diagnostics": 0,
92
+ "collapse-low-signal-trace": 0,
93
+ "review-manually": 0,
94
+ });
95
+ const sortPlan = (items) => [...items].sort((left, right) => {
96
+ const byImpact = IMPACT_ORDER[left.acceptanceImpact] - IMPACT_ORDER[right.acceptanceImpact];
97
+ if (byImpact !== 0) {
98
+ return byImpact;
99
+ }
100
+ const bySeverity = SEVERITY_ORDER[left.severity] - SEVERITY_ORDER[right.severity];
101
+ if (bySeverity !== 0) {
102
+ return bySeverity;
103
+ }
104
+ const byCategory = left.category.localeCompare(right.category);
105
+ if (byCategory !== 0) {
106
+ return byCategory;
107
+ }
108
+ const bySubject = left.subject.localeCompare(right.subject);
109
+ if (bySubject !== 0) {
110
+ return bySubject;
111
+ }
112
+ return left.checkName.localeCompare(right.checkName);
113
+ });
114
+ const impactForFinding = (finding, antiDummyConfidenceThreshold) => {
115
+ if (SCOPE_REJECTING_CHECKS.includes(finding.check_name)) {
116
+ return "reject";
117
+ }
118
+ if (ANTI_DUMMY_CHECKS.includes(finding.check_name) &&
119
+ (finding.confidence ?? 0) >= antiDummyConfidenceThreshold) {
120
+ return "reject";
121
+ }
122
+ if (ITERATION_REQUIRED_CHECKS.includes(finding.check_name)) {
123
+ return "iterate";
124
+ }
125
+ if (finding.severity === "high") {
126
+ return "iterate";
127
+ }
128
+ return "advisory";
129
+ };
130
+ export const buildAgentExpectations = (input) => {
131
+ const expected = toExpectationSelector(input.target ?? input.expected);
132
+ const forbidden = toExpectationSelector(input.forbidden);
133
+ const notes = normalizeStringArray(input.notes);
134
+ return {
135
+ ...(input.goal ? { goal: input.goal } : {}),
136
+ ...(input.taskId ? { task_id: input.taskId } : {}),
137
+ ...(expected ? { expected } : {}),
138
+ ...(forbidden ? { forbidden } : {}),
139
+ ...(notes.length ? { notes } : {}),
140
+ };
141
+ };
142
+ export const mapFindingToEnrichmentAction = (finding) => {
143
+ const checkName = typeof finding === "string" ? finding : finding.check_name;
144
+ const mapped = AGENT_ENRICHMENT_ACTIONS[checkName];
145
+ return mapped ?? { ...FALLBACK_ACTION, checkName };
146
+ };
147
+ export const loadAgentOutput = async (outputDir) => {
148
+ const absoluteOutputDir = resolve(outputDir);
149
+ const run = await readJson(join(absoluteOutputDir, "manifest", "run.json"));
150
+ const tests = await readJsonl(join(absoluteOutputDir, "manifest", "tests.jsonl"));
151
+ const findings = await readJsonl(join(absoluteOutputDir, "manifest", "findings.jsonl"));
152
+ const expected = run.paths.expected_manifest && run.expectations_present
153
+ ? await readJson(join(absoluteOutputDir, run.paths.expected_manifest))
154
+ : undefined;
155
+ return {
156
+ outputDir: absoluteOutputDir,
157
+ run,
158
+ tests,
159
+ findings,
160
+ expected,
161
+ };
162
+ };
163
+ export const planAgentEnrichmentReview = (output, options = {}) => {
164
+ const antiDummyConfidenceThreshold = options.antiDummyConfidenceThreshold ?? DEFAULT_ANTI_DUMMY_CONFIDENCE;
165
+ const requireExpectationsForAcceptance = options.requireExpectationsForAcceptance ?? true;
166
+ const testsByPath = new Map(output.tests.map((test) => [test.markdown_path, test]));
167
+ const plan = sortPlan(output.findings.map((finding) => {
168
+ const action = mapFindingToEnrichmentAction(finding);
169
+ const matchedTest = testsByPath.get(finding.subject);
170
+ return {
171
+ ...action,
172
+ subject: finding.subject,
173
+ subjectType: finding.subject === "run" ? "run" : "test",
174
+ severity: finding.severity,
175
+ message: finding.message,
176
+ explanation: finding.explanation,
177
+ remediationHint: finding.remediation_hint,
178
+ evidencePaths: finding.evidence_paths,
179
+ expectedReference: finding.expected_reference,
180
+ confidence: finding.confidence,
181
+ acceptanceImpact: impactForFinding(finding, antiDummyConfidenceThreshold),
182
+ fullName: matchedTest?.full_name,
183
+ markdownPath: matchedTest?.markdown_path,
184
+ scopeMatch: matchedTest?.scope_match,
185
+ };
186
+ }));
187
+ const rejecting = plan.filter((item) => item.acceptanceImpact === "reject");
188
+ const iterate = plan.filter((item) => item.acceptanceImpact === "iterate");
189
+ const advisory = plan.filter((item) => item.acceptanceImpact === "advisory");
190
+ const notes = [];
191
+ if (!output.run.expectations_present) {
192
+ notes.push("Generate ALLURE_AGENT_EXPECTATIONS before the next enrichment iteration so scope checks are comparable.");
193
+ }
194
+ if (rejecting.some((item) => item.checkName === "noop-dominated-steps")) {
195
+ notes.push("Reject noop-dominated enrichment: keep only steps tied to real actions or checks, and use real runtime attachments instead of placeholders.");
196
+ }
197
+ if (rejecting.some((item) => SCOPE_REJECTING_CHECKS.includes(item.checkName))) {
198
+ notes.push("Reject scope drift: rerun only the intended tests and keep forbidden scope in the expectations file.");
199
+ }
200
+ if (!notes.length && !iterate.length && output.run.expectations_present) {
201
+ notes.push("Scope matched expectations and no blocking evidence gaps or anti-dummy findings remain.");
202
+ }
203
+ const status = rejecting.length
204
+ ? "reject"
205
+ : requireExpectationsForAcceptance && !output.run.expectations_present
206
+ ? "iterate"
207
+ : iterate.length
208
+ ? "iterate"
209
+ : "accept";
210
+ return {
211
+ status,
212
+ outputDir: output.outputDir,
213
+ expectationsPresent: output.run.expectations_present,
214
+ expected: output.expected,
215
+ summary: {
216
+ totalTests: output.tests.length,
217
+ totalFindings: output.findings.length,
218
+ planItems: plan.length,
219
+ rejectingItems: rejecting.length,
220
+ iterationItems: iterate.length,
221
+ advisoryItems: advisory.length,
222
+ countsByActionCategory: countByActionCategory(plan),
223
+ },
224
+ notes,
225
+ plan,
226
+ rejecting,
227
+ iterate,
228
+ advisory,
229
+ rerun: {
230
+ requiresExpectations: requireExpectationsForAcceptance,
231
+ useExistingExpectations: output.run.expectations_present,
232
+ targetedTests: uniqueValues(plan.flatMap((item) => (item.fullName ? [item.fullName] : []))),
233
+ },
234
+ };
235
+ };
236
+ export const reviewAgentOutput = async (outputDir, options) => planAgentEnrichmentReview(await loadAgentOutput(outputDir), options);
@@ -0,0 +1,3 @@
1
+ export { type AgentPluginOptions } from "./model.js";
2
+ export * from "./harness.js";
3
+ export { AgentPlugin as default } from "./plugin.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./harness.js";
2
+ export { AgentPlugin as default } from "./plugin.js";
@@ -0,0 +1,3 @@
1
+ export type AgentPluginOptions = {
2
+ outputDir?: string;
3
+ };
package/dist/model.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { AllureStore, Plugin, PluginContext, RealtimeSubscriber } from "@allurereport/plugin-api";
2
+ import type { AgentPluginOptions } from "./model.js";
3
+ export declare class AgentPlugin implements Plugin {
4
+ #private;
5
+ readonly options: AgentPluginOptions;
6
+ constructor(options?: AgentPluginOptions);
7
+ start: (context: PluginContext, store: AllureStore, realtime: RealtimeSubscriber) => Promise<void>;
8
+ done: (context: PluginContext, store: AllureStore) => Promise<void>;
9
+ }