pi-workflow-engine 0.1.1 → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { Type } from "typebox";
3
- import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
3
+ import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
4
4
 
5
5
  export const meta: WorkflowMeta = {
6
6
  name: "code-review",
@@ -41,6 +41,8 @@ const ReportSchema = Type.Object({
41
41
  severity: Type.Union([Type.Literal("bug"), Type.Literal("cleanup")]),
42
42
  verdict: Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE")]),
43
43
  summary: Type.String(),
44
+ evidence: Type.Optional(Type.String()),
45
+ failure_scenario: Type.Optional(Type.String()),
44
46
  }),
45
47
  ),
46
48
  });
@@ -113,8 +115,18 @@ export function inDiff(changed: Map<string, Set<number>>, file: string, line?: n
113
115
  }
114
116
 
115
117
  export default async function run(api: WorkflowApi): Promise<unknown> {
116
- const { agent, parallel, pipeline, phase, log, args, cwd } = api;
118
+ const { agent, parallel, pipeline, phase, log, progress, args, cwd } = api;
117
119
  const target = args.trim();
120
+ let fileCount = 0;
121
+ let rawCandidateCount = 0;
122
+ let droppedCandidateCount = 0;
123
+ const makeStats = (verified: number, kept: number): WorkflowRunStats => ({
124
+ files: fileCount,
125
+ candidates: rawCandidateCount,
126
+ verified,
127
+ kept,
128
+ dropped: droppedCandidateCount,
129
+ });
118
130
 
119
131
  // ─── Phase 0: Scope ───
120
132
  phase("Scope");
@@ -137,8 +149,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
137
149
  { phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
138
150
  );
139
151
 
140
- if (!scope || scope.files.length === 0) {
141
- return { summary: "No changes found to review.", findings: [], stats: { candidates: 0, verified: 0 } };
152
+ if (!scope) {
153
+ return { summary: "No changes found to review.", findings: [], stats: makeStats(0, 0) };
154
+ }
155
+
156
+ fileCount = scope.files.length;
157
+ progress({ type: "summary", key: "files", value: scope.files.join(", ") || "(none)" });
158
+ progress({ type: "summary", key: "diffCommand", value: scope.diffCommand });
159
+ progress({ type: "counter", key: "files", label: "files", value: fileCount });
160
+
161
+ if (scope.files.length === 0) {
162
+ return { summary: "No changes found to review.", findings: [], stats: makeStats(0, 0) };
142
163
  }
143
164
  log(`${scope.files.length} changed files`);
144
165
 
@@ -192,10 +213,27 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
192
213
  { phase: "Find", label: `find:${angle.label}`, tools: TOOLS, thinkingLevel: "low", schema: CandidatesSchema },
193
214
  );
194
215
  const raw = (found?.candidates ?? []).slice(0, PER_ANGLE);
216
+ rawCandidateCount += raw.length;
217
+ progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
195
218
  const gate = changed;
196
219
  const bounded = gate ? raw.filter((candidate) => inDiff(gate, candidate.file, candidate.line)) : raw;
197
- if (gate && raw.length - bounded.length > 0) {
198
- log(`find:${angle.label}: dropped ${raw.length - bounded.length} out-of-diff candidate(s)`);
220
+ const dropped = raw.length - bounded.length;
221
+ if (dropped > 0) {
222
+ droppedCandidateCount += dropped;
223
+ progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
224
+ }
225
+ if (gate && dropped > 0) {
226
+ log(`find:${angle.label}: dropped ${dropped} out-of-diff candidate(s)`);
227
+ }
228
+ for (const candidate of bounded) {
229
+ progress({
230
+ type: "lane_item",
231
+ lane: "Candidates",
232
+ title: candidate.summary,
233
+ subtitle: formatLocation(candidate),
234
+ status: "pending",
235
+ details: candidate.failure_scenario,
236
+ });
199
237
  }
200
238
  return { angle, candidates: bounded };
201
239
  },
@@ -218,7 +256,17 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
218
256
  "with evidence quoting the line(s). Default toward REFUTED if you cannot substantiate it. Structured output only.",
219
257
  { phase: "Verify", label: `verify:${candidate.file.split("/").pop() ?? candidate.file}`, tools: TOOLS, thinkingLevel: "low", schema: VerdictSchema },
220
258
  );
221
- return judged ? { ...candidate, verdict: judged.verdict, evidence: judged.evidence, kind: angle.kind } : null;
259
+ if (!judged) return null;
260
+ progress({ type: "counter_delta", key: `verdict.${judged.verdict.toLowerCase()}`, label: judged.verdict, delta: 1 });
261
+ progress({
262
+ type: "lane_item",
263
+ lane: verdictLane(judged.verdict),
264
+ title: candidate.summary,
265
+ subtitle: formatLocation(candidate),
266
+ status: verdictStatus(judged.verdict),
267
+ details: judged.evidence,
268
+ });
269
+ return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, kind: angle.kind };
222
270
  }),
223
271
  );
224
272
  return verdicts.filter((value): value is Verified => value !== null);
@@ -227,10 +275,15 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
227
275
 
228
276
  const verified = (perAngle as Verified[][]).flat();
229
277
  const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
278
+ const stats = makeStats(verified.length, surviving.length);
279
+ progress({ type: "counter", key: "verified", label: "verified", value: verified.length });
280
+ progress({ type: "counter", key: "kept", label: "kept", value: surviving.length });
281
+ progress({ type: "summary", key: "verified", value: verified.length });
282
+ progress({ type: "summary", key: "kept", value: surviving.length });
230
283
  log(`${verified.length} verified → ${surviving.length} kept`);
231
284
 
232
285
  if (surviving.length === 0) {
233
- return { summary: "No findings survived verification.", findings: [], stats: { candidates: seen.size, verified: verified.length } };
286
+ return { summary: "No findings survived verification.", findings: [], stats };
234
287
  }
235
288
 
236
289
  // ─── Synthesize: rank, merge, report ───
@@ -247,9 +300,49 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
247
300
 
248
301
  const report = await agent(
249
302
  `## Synthesis: final code-review report\n\n${ranked.length} findings survived independent verification.\n\n${block}\n\n` +
250
- "Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final report. Structured output only.",
303
+ "Merge findings with the same root cause, rank most-severe first (correctness bugs above cleanups), and produce the final report. Include evidence and failure_scenario for each finding when available. Structured output only.",
251
304
  { phase: "Synthesize", label: "synthesize", thinkingLevel: "medium", schema: ReportSchema },
252
305
  );
253
306
 
254
- return report ?? { summary: "Synthesis produced no output.", findings: [] };
307
+ if (!report) return { summary: "Synthesis produced no output.", findings: [], stats };
308
+
309
+ const findings = report.findings.map((finding) => {
310
+ const source = ranked.find((candidate) => sameFinding(candidate, finding));
311
+ return {
312
+ ...finding,
313
+ evidence: finding.evidence ?? source?.evidence,
314
+ failure_scenario: finding.failure_scenario ?? source?.failure_scenario,
315
+ };
316
+ });
317
+ return { ...report, findings, stats };
318
+ }
319
+
320
+ function formatLocation(candidate: Pick<Candidate, "file" | "line">): string {
321
+ return `${candidate.file}${candidate.line != null ? `:${candidate.line}` : ""}`;
322
+ }
323
+
324
+ function verdictLane(verdict: Verified["verdict"]): string {
325
+ switch (verdict) {
326
+ case "CONFIRMED":
327
+ return "Confirmed";
328
+ case "PLAUSIBLE":
329
+ return "Plausible";
330
+ case "REFUTED":
331
+ return "Refuted";
332
+ }
333
+ }
334
+
335
+ function verdictStatus(verdict: Verified["verdict"]): "success" | "warning" | "error" {
336
+ switch (verdict) {
337
+ case "CONFIRMED":
338
+ return "success";
339
+ case "PLAUSIBLE":
340
+ return "warning";
341
+ case "REFUTED":
342
+ return "error";
343
+ }
344
+ }
345
+
346
+ function sameFinding(candidate: Verified, finding: { file: string; line?: number; summary: string }): boolean {
347
+ return normalizePath(candidate.file) === normalizePath(finding.file) && candidate.line === finding.line;
255
348
  }