@boxxy/closeout 0.2.2 → 0.2.4

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,8 +30,8 @@ The package runs on [Bun](https://bun.sh) 1.3 or newer.
29
30
 
30
31
  ## Features
31
32
 
32
- - `triage`, `design` and `build`, and `phaseKinds`, which lists them for the rules loader. Each
33
- parser returns the stored close-out, or every problem with it in one pass.
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.
34
35
  - `createReviewPhase`, which builds the review phase kind on the review gate's `buildCommand`. The
35
36
  server adds it to `phaseKinds`.
36
37
  - `countUnrelatedLines`, the changed lines of a later review in files that had no open finding.
@@ -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
@@ -6,5 +6,6 @@ export { build } from './phases/build.ts';
6
6
  export { design } from './phases/design.ts';
7
7
  export { createReviewPhase } from './phases/create-review-phase.ts';
8
8
  export { triage } from './phases/triage.ts';
9
+ export { verify } from './phases/verify.ts';
9
10
  export { countUnrelatedLines } from './review/count-unrelated-lines.ts';
10
11
  export type { CloseOutLimits } from './types.ts';
@@ -0,0 +1 @@
1
+ export declare const verify: import("@boxxy/model").PhaseKind<unknown, import("@boxxy/content-hash").JsonValue, readonly string[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxxy/closeout",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",
@@ -30,14 +30,14 @@
30
30
  "typecheck": "tsrs --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "@boxxy/model": "0.2.2",
33
+ "@boxxy/model": "0.2.4",
34
34
  "tiny-invariant": "1.3.3",
35
35
  "ts-pattern": "5.9.2",
36
36
  "zod": "4.6.7"
37
37
  },
38
38
  "devDependencies": {
39
- "@boxxy/planning": "0.2.2",
40
- "@boxxy/rules": "0.2.2",
39
+ "@boxxy/planning": "0.2.4",
40
+ "@boxxy/rules": "0.2.4",
41
41
  "@faker-js/faker": "10.6.0"
42
42
  },
43
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) {
package/src/index.ts CHANGED
@@ -6,5 +6,6 @@ export { build } from './phases/build.ts';
6
6
  export { design } from './phases/design.ts';
7
7
  export { createReviewPhase } from './phases/create-review-phase.ts';
8
8
  export { triage } from './phases/triage.ts';
9
+ export { verify } from './phases/verify.ts';
9
10
  export { countUnrelatedLines } from './review/count-unrelated-lines.ts';
10
11
  export type { CloseOutLimits } from './types.ts';
@@ -2,7 +2,8 @@ import type { PhaseKind } from '@boxxy/model';
2
2
  import { build } from './phases/build.ts';
3
3
  import { design } from './phases/design.ts';
4
4
  import { triage } from './phases/triage.ts';
5
+ import { verify } from './phases/verify.ts';
5
6
 
6
7
  // The phase kinds that need no port, in pipeline order. The server adds the review phase kind,
7
8
  // which it builds on its review gate.
8
- export const phaseKinds: readonly PhaseKind[] = [triage, design, build];
9
+ export const phaseKinds: readonly PhaseKind[] = [triage, design, build, verify];
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ AttemptRow,
2
3
  BuildCloseOut,
3
4
  BuildInput,
4
5
  CloseOutContext,
@@ -15,6 +16,8 @@ import {
15
16
  definePhase,
16
17
  storedReviewCloseOutSchema,
17
18
  toJSONValue,
19
+ verifyCloseOutSchema,
20
+ verifyInputSchema,
18
21
  } from '@boxxy/model';
19
22
  import { match } from 'ts-pattern';
20
23
  import type { z } from 'zod';
@@ -64,21 +67,43 @@ function buildBuildInput(ctx: PhaseInputContext): BuildInput {
64
67
  return {
65
68
  acceptance: buildCriterionTexts(ctx.item),
66
69
  design: findLatestDesign(ctx.history),
67
- sentBack: findReviewSentBack(ctx),
70
+ sentBack: findSentBack(ctx.history),
68
71
  pullRequest: findPullRequestLink(ctx.pullRequests),
69
72
  };
70
73
  }
71
74
 
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;
75
+ // The latest grading of the work decides the send-back: a review that asked for changes sends
76
+ // its findings, and a failed verify sends its features. A build after a passed review or a
77
+ // verified verify, sent back by the owner, gets none.
78
+ function findSentBack(history: readonly AttemptRow[]): SentBack | null {
79
+ const graded = history.findLast(
80
+ (row) => (row.phase === 'review' || row.phase === 'verify') && row.state === 'succeeded',
81
+ );
82
+
83
+ if (graded?.phase === 'verify') {
84
+ return findVerifySentBack(graded);
85
+ }
86
+
87
+ const reading = findLatestCloseOut(history, 'review', storedReviewCloseOutSchema)?.reading;
76
88
 
77
89
  return reading?.status === 'blocked' && reading.why === 'open-findings'
78
90
  ? { from: 'review', head: reading.head, open: reading.open, answerHint: reading.answerHint }
79
91
  : null;
80
92
  }
81
93
 
94
+ function findVerifySentBack(verify: AttemptRow): SentBack | null {
95
+ const closeOut = verifyCloseOutSchema.safeParse(verify.closeOut);
96
+ const input = verifyInputSchema.safeParse(verify.input);
97
+
98
+ return closeOut.success && closeOut.data.result.kind === 'failed'
99
+ ? {
100
+ from: 'verify',
101
+ head: input.success ? input.data.head : null,
102
+ features: closeOut.data.result.features,
103
+ }
104
+ : null;
105
+ }
106
+
82
107
  function collectBuildProblems(closeOut: BuildCloseOut, ctx: CloseOutContext): CloseOutProblem[] {
83
108
  const acceptance = new Set(ctx.item.acceptance.map((criterion) => criterion.id));
84
109
  const own = new Set(ctx.evidence.map((row) => row.factId));
@@ -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
+ }