@jwilger/pi-development-system 0.52.0 → 0.53.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -0,0 +1,51 @@
1
+ import { type ParseError, parseError, type Sizing } from "./types.ts";
2
+
3
+ export const ARTIFACTS = [
4
+ "task-record",
5
+ "review",
6
+ "adr-if-needed",
7
+ "brief-lite",
8
+ "brief",
9
+ "decision-register",
10
+ "journeys",
11
+ "event-model",
12
+ "architecture",
13
+ "lens-review-optional",
14
+ "lens-review",
15
+ ] as const;
16
+
17
+ export type ArtifactId = (typeof ARTIFACTS)[number];
18
+
19
+ const SIZINGS: readonly Sizing[] = ["fix", "change", "capability", "product"];
20
+
21
+ /** The set the work needs by default; D4: proportional, and skipping any of it is a recorded departure. */
22
+ const BY_SIZING: Readonly<Record<Sizing, readonly ArtifactId[]>> = {
23
+ fix: ["task-record"],
24
+ change: ["task-record", "review", "adr-if-needed"],
25
+ capability: [
26
+ "task-record",
27
+ "review",
28
+ "adr-if-needed",
29
+ "brief-lite",
30
+ "journeys",
31
+ "event-model",
32
+ "lens-review-optional",
33
+ ],
34
+ product: [
35
+ "task-record",
36
+ "review",
37
+ "adr-if-needed",
38
+ "brief",
39
+ "decision-register",
40
+ "journeys",
41
+ "event-model",
42
+ "architecture",
43
+ "lens-review",
44
+ ],
45
+ };
46
+
47
+ export const recommendedArtifacts = (sizing: Sizing): readonly ArtifactId[] => BY_SIZING[sizing];
48
+
49
+ export const parseSizing = (value: string): Sizing | ParseError =>
50
+ SIZINGS.find((s) => s === value) ??
51
+ parseError(`unknown sizing "${value}": expected fix, change, capability or product`);
@@ -0,0 +1,111 @@
1
+ import type { ClassifierBoolQuestion, ClassifierChoiceQuestion } from "@earendil-works/pi-ai";
2
+ import { redactSecrets } from "../../core/redact.ts";
3
+ import { err, ok, type Result } from "../../core/result.ts";
4
+ import type { ArtifactId } from "../../core/sizing.ts";
5
+ import type { Sizing } from "../../core/types.ts";
6
+ import type { Jev, JevError } from "../client.ts";
7
+ import { medianLabel } from "./route.ts";
8
+
9
+ const LEVELS: readonly Sizing[] = ["fix", "change", "capability", "product"];
10
+
11
+ export const SIZING_QUESTION: ClassifierChoiceQuestion = {
12
+ type: "choice",
13
+ instructions:
14
+ "How big is the work in `request`, given `repoSummary`? Judge what must be understood and decided, not how much typing it takes. A bug with a known cause is a fix; a behaviour change inside one existing feature is a change; a new user-facing capability spanning several parts is a capability; a new product or a rethink of one is a product.",
15
+ criteria: {
16
+ fix: "A defect or small correction with a known cause, one place, no design decision",
17
+ change:
18
+ "A modification to existing behaviour inside one feature or module, with a clear shape and a test",
19
+ capability:
20
+ "A new user-visible capability that touches several parts, needs a design, or has more than one user journey",
21
+ product:
22
+ "A new product, a new bounded context, or a rethink of who the users are and what outcome they need",
23
+ },
24
+ };
25
+
26
+ const need = (instructions: string): ClassifierBoolQuestion => ({
27
+ type: "bool",
28
+ instructions,
29
+ criteria: {
30
+ true: "Skipping it would likely cost rework or a wrong build",
31
+ false: "The work is clear enough without it",
32
+ },
33
+ });
34
+
35
+ /** One independent judgement per artifact the sizing table does not settle on its own. */
36
+ export const ARTIFACT_QUESTIONS = {
37
+ "adr-if-needed": need(
38
+ "Does `request` involve an architectural decision that is costly to reverse and that a future reader will ask 'why' about (a new dependency, data shape, boundary, protocol)?",
39
+ ),
40
+ brief: need(
41
+ "Is it unclear who the users are, what outcome they want, or why this work is worth doing, so a written product brief is needed before building?",
42
+ ),
43
+ journeys: need(
44
+ "Does `request` involve a user performing a sequence of actions to reach an outcome, such that listing those journeys would expose missing steps?",
45
+ ),
46
+ "event-model": need(
47
+ "Does `request` change how information flows through a system with commands, state and views, such that modelling that flow before coding would find gaps?",
48
+ ),
49
+ architecture: need(
50
+ "Does `request` need a statement of how the parts of the system fit together and where the boundaries are, because no such statement exists in `repoSummary`?",
51
+ ),
52
+ "lens-review": need(
53
+ "Do the product assumptions in `request` carry enough risk that independent product-lens critique (value, discovery, strategy) is worth running before building?",
54
+ ),
55
+ } as const satisfies Partial<Record<ArtifactId, ClassifierBoolQuestion>>;
56
+
57
+ export type SizingInput = { request: string; repoSummary: string };
58
+ export type SizingJudgement = {
59
+ sizing: Sizing;
60
+ confidence: number;
61
+ artifactNeed: Record<ArtifactId, number>;
62
+ };
63
+
64
+ const REQUEST_MAX = 4000;
65
+ const SUMMARY_MAX = 3000;
66
+ const clip = (text: string, max: number): string =>
67
+ redactSecrets(text.slice(0, max * 2)).slice(0, max);
68
+
69
+ export async function judgeSizing(
70
+ jev: Jev,
71
+ input: SizingInput,
72
+ ): Promise<Result<SizingJudgement, JevError>> {
73
+ const asked = await jev.ask(
74
+ {
75
+ request: clip(input.request, REQUEST_MAX),
76
+ repoSummary: clip(input.repoSummary, SUMMARY_MAX),
77
+ },
78
+ { sizing: SIZING_QUESTION, ...ARTIFACT_QUESTIONS },
79
+ );
80
+ if (!asked.ok) return asked;
81
+ const sizing = asked.value.sizing;
82
+ if (sizing?.type !== "choice") return err({ kind: "provider", message: "missing sizing answer" });
83
+ const label = medianLabel(LEVELS, sizing);
84
+ if (label === undefined) return err({ kind: "provider", message: "unknown sizing label" });
85
+ const probabilities: Partial<Record<string, number>> = {};
86
+ for (const key of Object.keys(ARTIFACT_QUESTIONS)) {
87
+ const answer = asked.value[key];
88
+ if (answer?.type !== "bool" || !Number.isFinite(answer.probability)) {
89
+ return err({ kind: "provider", message: `missing ${key} answer` });
90
+ }
91
+ probabilities[key] = answer.probability;
92
+ }
93
+ const p = (key: keyof typeof ARTIFACT_QUESTIONS): number => probabilities[key] ?? 0;
94
+ return ok({
95
+ sizing: label,
96
+ confidence: sizing.confidence,
97
+ artifactNeed: {
98
+ "task-record": 1,
99
+ review: 1,
100
+ "adr-if-needed": p("adr-if-needed"),
101
+ brief: p("brief"),
102
+ "brief-lite": p("brief"),
103
+ "decision-register": p("brief"),
104
+ journeys: p("journeys"),
105
+ "event-model": p("event-model"),
106
+ architecture: p("architecture"),
107
+ "lens-review": p("lens-review"),
108
+ "lens-review-optional": p("lens-review"),
109
+ },
110
+ });
111
+ }