@boxxy/closeout 0.2.1 → 0.2.3

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.
package/README.md CHANGED
@@ -11,8 +11,9 @@
11
11
  </p>
12
12
  </div>
13
13
 
14
- boxxy's phase kinds: triage, design, build and review. Each one gives the input of an attempt,
15
- checks the close-out the agent sends at the end of it, and settles the attempt from that close-out.
14
+ boxxy's phase kinds: triage, design, build, review and verify. Each one gives the input of an
15
+ attempt, checks the close-out the agent sends at the end of it, and settles the attempt from that
16
+ close-out.
16
17
 
17
18
  The package does no I/O: the caller passes in the item, its attempts, the attempt's evidence rows
18
19
  and what a close-out may name.
@@ -29,11 +30,13 @@ The package runs on [Bun](https://bun.sh) 1.3 or newer.
29
30
 
30
31
  ## Features
31
32
 
32
- - `triage`, `design`, `build` and `review`, the four phase kinds, and `phaseKinds`, which lists them
33
- for the rules loader. Each parser returns the stored close-out, or every problem with it in one
34
- pass.
35
- - `closeOutLimits`, the byte limits of a close-out, and `measureJsonBytes`, which measures a value
36
- as stored.
33
+ - `triage`, `design`, `build` and `verify`, and `phaseKinds`, which lists them for the rules loader.
34
+ Each parser returns the stored close-out, or every problem with it in one pass.
35
+ - `createReviewPhase`, which builds the review phase kind on the review gate's `buildCommand`. The
36
+ server adds it to `phaseKinds`.
37
+ - `countUnrelatedLines`, the changed lines of a later review in files that had no open finding.
38
+ - `closeOutLimits`, the byte limit of a close-out, and `measureJsonBytes`, which measures a value as
39
+ stored.
37
40
  - `dedupeOpenItems`, which matches each open item the agent reports to an open item with the same
38
41
  title.
39
42
 
@@ -2,7 +2,7 @@ import type { CloseOutContext, CloseOutParser, CloseOutProblem } from '@boxxy/mo
2
2
  import { z } from 'zod';
3
3
  interface SchemaParserInput<Raw, Body> {
4
4
  readonly schema: z.ZodType<Raw>;
5
- readonly toBody: (raw: Raw, frictionDropped: number) => Body;
5
+ readonly toBody: (raw: Raw, frictionDropped: number, ctx: CloseOutContext) => Body;
6
6
  readonly check: (body: Body, ctx: CloseOutContext) => readonly CloseOutProblem[];
7
7
  }
8
8
  export declare function buildSchemaParser<Raw, Body>(input: SchemaParserInput<Raw, Body>): CloseOutParser<Body>;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export { measureJsonBytes } from './measure-json-bytes.ts';
4
4
  export { phaseKinds } from './phase-kinds.ts';
5
5
  export { build } from './phases/build.ts';
6
6
  export { design } from './phases/design.ts';
7
- export { review } from './phases/review.ts';
7
+ export { createReviewPhase } from './phases/create-review-phase.ts';
8
8
  export { triage } from './phases/triage.ts';
9
- export type { CloseOutLimits, NoslopReport } from './types.ts';
9
+ export { verify } from './phases/verify.ts';
10
+ export { countUnrelatedLines } from './review/count-unrelated-lines.ts';
11
+ export type { CloseOutLimits } from './types.ts';
@@ -0,0 +1,2 @@
1
+ import type { ReviewGate } from '@boxxy/model';
2
+ export declare function createReviewPhase(deps: Pick<ReviewGate, 'buildCommand'>): import("@boxxy/model").PhaseKind<unknown, import("@boxxy/content-hash").JsonValue, readonly string[]>;
@@ -1 +1 @@
1
- export declare const review: import("@boxxy/model").PhaseKind<unknown, import("@boxxy/content-hash").JsonValue, readonly string[]>;
1
+ export declare const verify: import("@boxxy/model").PhaseKind<unknown, import("@boxxy/content-hash").JsonValue, readonly string[]>;
@@ -0,0 +1,2 @@
1
+ import type { RoundCause, Settled } from '@boxxy/model';
2
+ export declare function buildRoundCause(history: readonly Settled[]): RoundCause;
@@ -0,0 +1,2 @@
1
+ import type { ChangedFile } from '@boxxy/model';
2
+ export declare function countUnrelatedLines(changed: readonly ChangedFile[], findingPaths: readonly string[]): number;
package/dist/types.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- import type { z } from 'zod';
2
- import type { noslopReportSchema } from './noslop/noslop-report-schema.ts';
3
1
  export interface CloseOutLimits {
4
2
  readonly closeOutBytes: 120_000;
5
- readonly noslopReportBytes: 32_768;
6
3
  }
7
- export type NoslopReport = z.output<typeof noslopReportSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxxy/closeout",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "boxxy's phase kinds: the input, the close-out parser and the settle rule of triage, design, build and review",
5
5
  "homepage": "https://github.com/zgeoff/boxxy/tree/main/modules/closeout#readme",
6
6
  "bugs": "https://github.com/zgeoff/boxxy/issues",
@@ -20,8 +20,7 @@
20
20
  ".": {
21
21
  "types": "./dist/index.d.ts",
22
22
  "default": "./src/index.ts"
23
- },
24
- "./test-utils/*": "./src/test-utils/*.ts"
23
+ }
25
24
  },
26
25
  "publishConfig": {
27
26
  "access": "public"
@@ -31,14 +30,14 @@
31
30
  "typecheck": "tsrs --noEmit"
32
31
  },
33
32
  "dependencies": {
34
- "@boxxy/model": "0.2.1",
33
+ "@boxxy/model": "0.2.3",
35
34
  "tiny-invariant": "1.3.3",
36
35
  "ts-pattern": "5.9.2",
37
36
  "zod": "4.6.7"
38
37
  },
39
38
  "devDependencies": {
40
- "@boxxy/planning": "0.2.1",
41
- "@boxxy/rules": "0.2.1",
39
+ "@boxxy/planning": "0.2.3",
40
+ "@boxxy/rules": "0.2.3",
42
41
  "@faker-js/faker": "10.6.0"
43
42
  },
44
43
  "peerDependencies": {
@@ -8,7 +8,7 @@ import { measureJsonBytes } from './measure-json-bytes.ts';
8
8
  interface SchemaParserInput<Raw, Body> {
9
9
  // the body as the agent sends it, with no `frictionDropped`: the parser counts that itself
10
10
  readonly schema: z.ZodType<Raw>;
11
- readonly toBody: (raw: Raw, frictionDropped: number) => Body;
11
+ readonly toBody: (raw: Raw, frictionDropped: number, ctx: CloseOutContext) => Body;
12
12
 
13
13
  // the rules of the phase that the schema cannot hold; each problem comes back
14
14
  readonly check: (body: Body, ctx: CloseOutContext) => readonly CloseOutProblem[];
@@ -41,7 +41,7 @@ function parseCloseOut<Raw, Body>(
41
41
  };
42
42
  }
43
43
 
44
- const body = input.toBody(parsed.data, split.dropped);
44
+ const body = input.toBody(parsed.data, split.dropped, ctx);
45
45
  const problems = input.check(body, ctx);
46
46
 
47
47
  if (problems.length > 0) {
@@ -71,6 +71,12 @@ function splitFriction(raw: unknown): FrictionSplit {
71
71
  }
72
72
 
73
73
  const { frictionDropped: _, ...rest } = raw;
74
+
75
+ // a body with no friction list, such as a review's, keeps no `friction` key for a strict schema
76
+ if (!('friction' in rest)) {
77
+ return { candidate: rest, keptRawIndexes: [], dropped: 0 };
78
+ }
79
+
74
80
  const entries: readonly unknown[] = Array.isArray(rest['friction']) ? rest['friction'] : [];
75
81
  const keptRawIndexes = entries.flatMap((entry, index) => (isUntargeted(entry) ? [] : [index]));
76
82
 
@@ -5,5 +5,4 @@ import type { CloseOutLimits } from './types.ts';
5
5
  // so a layer that escapes the JSON once more still fits.
6
6
  export const closeOutLimits: CloseOutLimits = {
7
7
  closeOutBytes: 120_000,
8
- noslopReportBytes: 32_768,
9
8
  };
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ export { measureJsonBytes } from './measure-json-bytes.ts';
4
4
  export { phaseKinds } from './phase-kinds.ts';
5
5
  export { build } from './phases/build.ts';
6
6
  export { design } from './phases/design.ts';
7
- export { review } from './phases/review.ts';
7
+ export { createReviewPhase } from './phases/create-review-phase.ts';
8
8
  export { triage } from './phases/triage.ts';
9
- export type { CloseOutLimits, NoslopReport } from './types.ts';
9
+ export { verify } from './phases/verify.ts';
10
+ export { countUnrelatedLines } from './review/count-unrelated-lines.ts';
11
+ export type { CloseOutLimits } from './types.ts';
@@ -1,8 +1,9 @@
1
1
  import type { PhaseKind } from '@boxxy/model';
2
2
  import { build } from './phases/build.ts';
3
3
  import { design } from './phases/design.ts';
4
- import { review } from './phases/review.ts';
5
4
  import { triage } from './phases/triage.ts';
5
+ import { verify } from './phases/verify.ts';
6
6
 
7
- // The phase kinds that the server passes to the rules loader, in pipeline order
8
- export const phaseKinds: readonly PhaseKind[] = [triage, design, build, review];
7
+ // The phase kinds that need no port, in pipeline order. The server adds the review phase kind,
8
+ // which it builds on its review gate.
9
+ export const phaseKinds: readonly PhaseKind[] = [triage, design, build, verify];
@@ -7,12 +7,13 @@ import type {
7
7
  Effect,
8
8
  ItemView,
9
9
  PhaseInputContext,
10
+ SentBack,
10
11
  } from '@boxxy/model';
11
12
  import {
12
13
  brandSchemas,
13
14
  buildCloseOutSchema,
14
15
  definePhase,
15
- reviewCloseOutSchema,
16
+ storedReviewCloseOutSchema,
16
17
  toJSONValue,
17
18
  } from '@boxxy/model';
18
19
  import { match } from 'ts-pattern';
@@ -59,19 +60,25 @@ export const build = definePhase({
59
60
  }),
60
61
  });
61
62
 
62
- // The findings come from the latest review, and only when it asked for changes: a build after
63
- // a passed review, sent back by the owner, gets none.
64
63
  function buildBuildInput(ctx: PhaseInputContext): BuildInput {
65
- const review = findLatestCloseOut(ctx.history, 'review', reviewCloseOutSchema);
66
-
67
64
  return {
68
65
  acceptance: buildCriterionTexts(ctx.item),
69
66
  design: findLatestDesign(ctx.history),
70
- findings: review?.verdict.kind === 'changes-requested' ? review.verdict.findings : [],
67
+ sentBack: findReviewSentBack(ctx),
71
68
  pullRequest: findPullRequestLink(ctx.pullRequests),
72
69
  };
73
70
  }
74
71
 
72
+ // Only a latest review that asked for changes sends the build its findings: a build after a
73
+ // passed review, sent back by the owner, gets none.
74
+ function findReviewSentBack(ctx: PhaseInputContext): SentBack | null {
75
+ const reading = findLatestCloseOut(ctx.history, 'review', storedReviewCloseOutSchema)?.reading;
76
+
77
+ return reading?.status === 'blocked' && reading.why === 'open-findings'
78
+ ? { from: 'review', head: reading.head, open: reading.open, answerHint: reading.answerHint }
79
+ : null;
80
+ }
81
+
75
82
  function collectBuildProblems(closeOut: BuildCloseOut, ctx: CloseOutContext): CloseOutProblem[] {
76
83
  const acceptance = new Set(ctx.item.acceptance.map((criterion) => criterion.id));
77
84
  const own = new Set(ctx.evidence.map((row) => row.factId));
@@ -0,0 +1,89 @@
1
+ import type {
2
+ GateReading,
3
+ PhaseInputContext,
4
+ ReviewCloseOut,
5
+ ReviewGate,
6
+ ReviewInput,
7
+ } from '@boxxy/model';
8
+ import {
9
+ brandSchemas,
10
+ collectSettled,
11
+ definePhase,
12
+ reviewCloseOutSchema,
13
+ toJSONValue,
14
+ } from '@boxxy/model';
15
+ import { match } from 'ts-pattern';
16
+ import { buildSchemaParser } from '../build-schema-parser.ts';
17
+ import { buildRoundCause } from '../review/build-round-cause.ts';
18
+ import { findPullRequestLink } from '../utils/find-pull-request-link.ts';
19
+
20
+ // The server passes the gate's pure `buildCommand`, so the module never imports an adapter. The
21
+ // session runs the command and sends the tool's raw result; the server builds the stored
22
+ // close-out from it, and this parser reads that.
23
+ export function createReviewPhase(deps: Pick<ReviewGate, 'buildCommand'>) {
24
+ return definePhase({
25
+ name: brandSchemas.phaseName.parse('review'),
26
+ version: 2,
27
+ worker: brandSchemas.workerKindName.parse('atc.session'),
28
+ outcomes: ['passed', 'changes-requested', 'failed'],
29
+ needsAcceptance: true,
30
+ input: (ctx) => toJSONValue(buildReviewInput(ctx, deps)),
31
+ closeOut: buildSchemaParser({
32
+ schema: reviewCloseOutSchema,
33
+ toBody: (raw): ReviewCloseOut => raw,
34
+ check: () => [],
35
+ }),
36
+ settle: (input) => {
37
+ const outcome = pickReviewOutcome(input.closeOut.reading);
38
+
39
+ return {
40
+ state: outcome === 'failed' ? 'failed' : 'succeeded',
41
+ failure: outcome === 'failed' ? 'review-failed' : null,
42
+ outcome,
43
+ effects: [],
44
+ frictionDropped: 0,
45
+ };
46
+ },
47
+ });
48
+ }
49
+
50
+ // The round counts the reviews since the item's latest retry that asked for changes.
51
+ function buildReviewInput(
52
+ ctx: PhaseInputContext,
53
+ deps: Pick<ReviewGate, 'buildCommand'>,
54
+ ): ReviewInput {
55
+ const since = ctx.retries.at(-1)?.at ?? Number.NEGATIVE_INFINITY;
56
+ const settled = collectSettled(ctx.history).filter((attempt) => attempt.endedAt > since);
57
+
58
+ const round =
59
+ 1 +
60
+ settled.filter(
61
+ (attempt) => attempt.phase === 'review' && attempt.outcome === 'changes-requested',
62
+ ).length;
63
+
64
+ const cause = buildRoundCause(settled);
65
+ const link = findPullRequestLink(ctx.pullRequests);
66
+
67
+ if (link === null) {
68
+ return { pullRequest: null, round, cause, command: null };
69
+ }
70
+
71
+ const pullRequest = { repo: link.repo, number: link.number };
72
+
73
+ return {
74
+ pullRequest,
75
+ round,
76
+ cause,
77
+ command: deps.buildCommand({ attempt: ctx.attempt, pullRequest, round, cause }),
78
+ };
79
+ }
80
+
81
+ // A failed call, an unreadable result or a reading that covers no head is `failed`: boxxy cannot
82
+ // tell whether the head is clear.
83
+ function pickReviewOutcome(reading: GateReading): 'passed' | 'changes-requested' | 'failed' {
84
+ return match(reading)
85
+ .with({ status: 'passed' }, () => 'passed' as const)
86
+ .with({ status: 'blocked', why: 'open-findings' }, () => 'changes-requested' as const)
87
+ .with({ status: 'blocked' }, { status: 'not-covered' }, () => 'failed' as const)
88
+ .exhaustive();
89
+ }
@@ -0,0 +1,67 @@
1
+ import type { PhaseInputContext, VerifyCloseOut, VerifyInput } from '@boxxy/model';
2
+ import {
3
+ brandSchemas,
4
+ definePhase,
5
+ storedReviewCloseOutSchema,
6
+ toJSONValue,
7
+ verifyCloseOutSchema,
8
+ verifyInputSchema,
9
+ } from '@boxxy/model';
10
+ import { match } from 'ts-pattern';
11
+ import type { z } from 'zod';
12
+ import { buildSchemaParser } from '../build-schema-parser.ts';
13
+ import { findLatestCloseOut } from '../utils/find-latest-close-out.ts';
14
+ import { findPullRequestLink } from '../utils/find-pull-request-link.ts';
15
+
16
+ // the parser copies `session` from the attempt's input, so the agent never sends it
17
+ const verifyBodySchema = verifyCloseOutSchema.omit({ session: true, frictionDropped: true });
18
+
19
+ export const verify = definePhase({
20
+ name: brandSchemas.phaseName.parse('verify'),
21
+ version: 1,
22
+ worker: brandSchemas.workerKindName.parse('atc.session'),
23
+ outcomes: ['verified', 'failed', 'blocked', 'abandoned'],
24
+ needsAcceptance: true,
25
+ input: (ctx) => toJSONValue(buildVerifyInput(ctx)),
26
+ closeOut: buildSchemaParser({
27
+ schema: verifyBodySchema,
28
+ toBody: (raw: z.output<typeof verifyBodySchema>, frictionDropped, ctx): VerifyCloseOut => ({
29
+ ...raw,
30
+ session: verifyInputSchema.parse(ctx.input).session,
31
+ frictionDropped,
32
+ }),
33
+ check: () => [],
34
+ }),
35
+
36
+ // A failed verify graded the work, so it succeeds, and a `session: builder` grader keeps its
37
+ // session for the rework. A blocked or abandoned verify graded nothing, and fails.
38
+ settle: (input) => ({
39
+ ...match(input.closeOut.result)
40
+ .with({ kind: 'verified' }, { kind: 'failed' }, () => ({
41
+ state: 'succeeded' as const,
42
+ failure: null,
43
+ }))
44
+ .with({ kind: 'blocked' }, { kind: 'abandoned' }, (ended) => ({
45
+ state: 'failed' as const,
46
+ failure: ended.kind,
47
+ }))
48
+ .exhaustive(),
49
+ outcome: input.closeOut.result.kind,
50
+ effects: input.closeOut.friction.map((friction) => ({
51
+ kind: 'record-proposal' as const,
52
+ friction,
53
+ })),
54
+ frictionDropped: input.closeOut.frictionDropped,
55
+ }),
56
+ });
57
+
58
+ // A recipe with no `session` field starts a fresh grader.
59
+ function buildVerifyInput(ctx: PhaseInputContext): VerifyInput {
60
+ const reading = findLatestCloseOut(ctx.history, 'review', storedReviewCloseOutSchema)?.reading;
61
+
62
+ return {
63
+ pullRequest: findPullRequestLink(ctx.pullRequests),
64
+ head: reading?.status === 'passed' ? reading.head : null,
65
+ session: ctx.session ?? 'fresh',
66
+ };
67
+ }
@@ -0,0 +1,20 @@
1
+ import type { RoundCause, Settled } from '@boxxy/model';
2
+
3
+ // Why a review round runs, from the item's settled attempts since its latest retry, oldest
4
+ // first. `rework`: the latest settled attempt that is not a build is a review that asked for
5
+ // changes. `recheck`: the item came back from another phase, or the last review failed.
6
+ export function buildRoundCause(history: readonly Settled[]): RoundCause {
7
+ const reviews = history.filter(
8
+ (settled) => settled.phase === 'review' && settled.outcome !== 'lost',
9
+ );
10
+
11
+ if (reviews.length === 0) {
12
+ return 'first';
13
+ }
14
+
15
+ const latestOther = history.findLast((settled) => settled.phase !== 'build');
16
+
17
+ return latestOther?.phase === 'review' && latestOther.outcome === 'changes-requested'
18
+ ? 'rework'
19
+ : 'recheck';
20
+ }
@@ -0,0 +1,13 @@
1
+ import type { ChangedFile } from '@boxxy/model';
2
+
3
+ // The changed lines in the files that had no open finding at the earlier review.
4
+ export function countUnrelatedLines(
5
+ changed: readonly ChangedFile[],
6
+ findingPaths: readonly string[],
7
+ ): number {
8
+ const withFindings = new Set(findingPaths);
9
+
10
+ return changed
11
+ .filter((file) => !withFindings.has(file.path))
12
+ .reduce((sum, file) => sum + file.changedLines, 0);
13
+ }
package/src/types.ts CHANGED
@@ -1,13 +1,4 @@
1
- import type { z } from 'zod';
2
- import type { noslopReportSchema } from './noslop/noslop-report-schema.ts';
3
-
4
1
  export interface CloseOutLimits {
5
2
  // a close-out as stored, in any phase
6
3
  readonly closeOutBytes: 120_000;
7
-
8
- // the most of noslop's result that a review close-out keeps whole
9
- readonly noslopReportBytes: 32_768;
10
4
  }
11
-
12
- // What `noslop review --json --report json` printed, as the review session sends it
13
- export type NoslopReport = z.output<typeof noslopReportSchema>;
@@ -1,3 +0,0 @@
1
- import type { NoslopSummary } from '@boxxy/model';
2
- import type { NoslopReport } from '../types.ts';
3
- export declare function buildNoslopSummary(report: NoslopReport): NoslopSummary;
@@ -1,9 +0,0 @@
1
- import { z } from 'zod';
2
- export declare const noslopReportSchema: z.ZodObject<{
3
- version: z.ZodNumber;
4
- reviewID: z.ZodString;
5
- head: z.core.$ZodBranded<z.ZodString, "CommitSha", "out">;
6
- findings: z.ZodArray<z.ZodUnknown>;
7
- failedCalls: z.ZodArray<z.ZodUnknown>;
8
- fixChecks: z.ZodExactOptional<z.ZodArray<z.ZodUnknown>>;
9
- }, z.core.$loose>;
@@ -1,2 +0,0 @@
1
- import type { NoslopReport } from '../types.ts';
2
- export declare function buildMockNoslopReport(overrides?: Partial<NoslopReport>): NoslopReport;
@@ -1,25 +0,0 @@
1
- import type { NoslopSummary } from '@boxxy/model';
2
- import { toJSONValue } from '@boxxy/model';
3
- import { closeOutLimits } from '../close-out-limits.ts';
4
- import { measureJsonBytes } from '../measure-json-bytes.ts';
5
- import type { NoslopReport } from '../types.ts';
6
-
7
- // Keeps the counts of noslop's result, and the result whole only when it fits the limit: a cut
8
- // part of the JSON would be no report at all.
9
- export function buildNoslopSummary(report: NoslopReport): NoslopSummary {
10
- const whole = toJSONValue(report);
11
- const reportBytes = measureJsonBytes(whole);
12
-
13
- return {
14
- version: report.version,
15
- reviewID: report.reviewID,
16
- head: report.head,
17
- counts: {
18
- findings: report.findings.length,
19
- failedCalls: report.failedCalls.length,
20
- fixChecks: report.fixChecks?.length ?? 0,
21
- },
22
- reportBytes,
23
- report: reportBytes <= closeOutLimits.noslopReportBytes ? whole : null,
24
- };
25
- }
@@ -1,13 +0,0 @@
1
- import { brandSchemas } from '@boxxy/model';
2
- import { z } from 'zod';
3
-
4
- // The fields of `noslop review --json --report json` that the summary reads. Every other field passes
5
- // through unread, and a list's entries are not checked, so a newer noslop still parses.
6
- export const noslopReportSchema = z.looseObject({
7
- version: z.number().int().positive(),
8
- reviewID: z.string().min(1),
9
- head: brandSchemas.commitSha,
10
- findings: z.array(z.unknown()),
11
- failedCalls: z.array(z.unknown()),
12
- fixChecks: z.array(z.unknown()).exactOptional(),
13
- });
@@ -1,50 +0,0 @@
1
- import type { PhaseInputContext, ReviewCloseOut, ReviewInput } from '@boxxy/model';
2
- import { brandSchemas, definePhase, reviewCloseOutSchema, toJSONValue } from '@boxxy/model';
3
- import type { z } from 'zod';
4
- import { buildSchemaParser } from '../build-schema-parser.ts';
5
- import { buildNoslopSummary } from '../noslop/build-noslop-summary.ts';
6
- import { noslopReportSchema } from '../noslop/noslop-report-schema.ts';
7
- import { buildCriterionTexts } from '../utils/build-criterion-texts.ts';
8
- import { findLatestDesign } from '../utils/find-latest-design.ts';
9
- import { findPullRequestLink } from '../utils/find-pull-request-link.ts';
10
-
11
- // The session sends noslop's whole result; the stored close-out holds its summary instead.
12
- const reviewBodySchema = reviewCloseOutSchema
13
- .omit({ frictionDropped: true })
14
- .extend({ noslop: noslopReportSchema });
15
-
16
- export const review = definePhase({
17
- name: brandSchemas.phaseName.parse('review'),
18
- version: 1,
19
- worker: brandSchemas.workerKindName.parse('atc.session'),
20
- outcomes: ['passed', 'changes-requested'],
21
- needsAcceptance: true,
22
- input: (ctx) => toJSONValue(buildReviewInput(ctx)),
23
- closeOut: buildSchemaParser({
24
- schema: reviewBodySchema,
25
- toBody: (raw: z.output<typeof reviewBodySchema>, frictionDropped): ReviewCloseOut => ({
26
- ...raw,
27
- noslop: buildNoslopSummary(raw.noslop),
28
- frictionDropped,
29
- }),
30
- check: (closeOut) =>
31
- closeOut.verdict.kind === 'changes-requested' && closeOut.verdict.findings.length === 0
32
- ? [{ kind: 'phase-rule', message: 'changes-requested needs at least one finding' }]
33
- : [],
34
- }),
35
- settle: (input) => ({
36
- state: 'succeeded',
37
- failure: null,
38
- outcome: input.closeOut.verdict.kind,
39
- effects: input.closeOut.friction.map((friction) => ({ kind: 'record-proposal', friction })),
40
- frictionDropped: input.closeOut.frictionDropped,
41
- }),
42
- });
43
-
44
- function buildReviewInput(ctx: PhaseInputContext): ReviewInput {
45
- return {
46
- acceptance: buildCriterionTexts(ctx.item),
47
- design: findLatestDesign(ctx.history),
48
- pullRequest: findPullRequestLink(ctx.pullRequests),
49
- };
50
- }
@@ -1,30 +0,0 @@
1
- import { brandSchemas } from '@boxxy/model';
2
- import { faker } from '@faker-js/faker';
3
- import type { NoslopReport } from '../types.ts';
4
-
5
- // A review result as noslop's schema version 7 prints it, with no findings.
6
- export function buildMockNoslopReport(overrides: Partial<NoslopReport> = {}): NoslopReport {
7
- return {
8
- version: 7,
9
- reviewID: faker.string.uuid(),
10
- repo: `${faker.word.noun()}/${faker.word.noun()}`,
11
- pullRequest: null,
12
- base: faker.git.commitSha(),
13
- head: brandSchemas.commitSha.parse(faker.git.commitSha()),
14
- title: faker.lorem.sentence(),
15
- depth: 'standard',
16
- special: null,
17
- riskClasses: [],
18
- lenses: [],
19
- calls: [],
20
- findings: [],
21
- dropped: [],
22
- failedCalls: [],
23
- notReviewed: [],
24
- notReviewedByLens: [],
25
- configHash: faker.string.hexadecimal({ length: 64, casing: 'lower', prefix: '' }),
26
- startedAt: '2026-11-02T10:00:00.000Z',
27
- endedAt: '2026-11-02T10:04:00.000Z',
28
- ...overrides,
29
- };
30
- }