@jwilger/pi-development-system 0.64.0 → 0.65.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.64.0",
3
+ "version": "0.65.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"
@@ -125,8 +125,10 @@ async function reviewNeeds(
125
125
  ctx: ExtensionContext,
126
126
  command: string,
127
127
  ): Promise<Need[]> {
128
- const { phase, activeSlice } = deps.state.get();
128
+ const { phase, activeSlice, sizing } = deps.state.get();
129
129
  if ((phase !== "implementing" && phase !== "reviewing") || activeSlice === undefined) return [];
130
+ // A fix is sized without a review artifact (I8.1); the gate must not demand rounds the size never asked for.
131
+ if (sizing === "fix") return [];
130
132
  const snap = await snapshotDiff(deps.exec, ctx.cwd, "HEAD");
131
133
  // An unreadable diff cannot be compared; the gate asks for a departure rather than guessing.
132
134
  const gap = reviewGap(
@@ -28,8 +28,8 @@ type Section = (typeof SECTIONS)[number];
28
28
 
29
29
  const HEADER = /^## (\S+) [—-] (.+)$/;
30
30
  const SECTION_LINE = /^\*\*([^:*]+)(?::\*\*|\*\*:)\s*(.*)$/;
31
- /** Top-level list items only; indented sub-items belong to the step above them. */
32
- const STEP = /^(?:\d+[.)]|[-*])\s+(.*)$/;
31
+ /** Top-level list items only (CommonMark allows up to three spaces of indent); deeper items belong to the step above. */
32
+ const STEP = /^ {0,3}(?:\d+[.)]|[-*+])\s+(.*)$/;
33
33
  const STEPS_MIN = 3;
34
34
  const STEPS_MAX = 7;
35
35
  /** Words that stand in for a command or an observable result. */
@@ -88,7 +88,19 @@ function splitSections(lines: readonly string[]): Sections {
88
88
  found.set(name, current);
89
89
  } else current?.push(line);
90
90
  }
91
- const text = new Map([...found].map(([name, body]) => [name, body.join("\n").trim()] as const));
91
+ // Leading indentation is kept so the first list item of a section is read like the others.
92
+ const text = new Map(
93
+ [...found].map(
94
+ ([name, body]) =>
95
+ [
96
+ name,
97
+ body
98
+ .join("\n")
99
+ .replace(/^\s*\n/, "")
100
+ .trimEnd(),
101
+ ] as const,
102
+ ),
103
+ );
92
104
  return { text, duplicates };
93
105
  }
94
106
 
@@ -131,9 +143,14 @@ const stripTicks = (text: string): string => {
131
143
  const stepsOf = (text: string): string[] => {
132
144
  const lines = text.split("\n");
133
145
  const fenced = fencedFlags(lines);
146
+ // The first item sets the list's indent; an item two or more columns deeper is nested under a step.
147
+ let base: number | undefined;
134
148
  return lines.flatMap((line, at) => {
135
149
  const item = fenced[at] === true ? undefined : STEP.exec(line)?.[1]?.trim();
136
- return item === undefined || item === "" ? [] : [item];
150
+ if (item === undefined || item === "") return [];
151
+ const indent = line.length - line.trimStart().length;
152
+ base ??= indent;
153
+ return indent >= base + 2 ? [] : [item];
137
154
  });
138
155
  };
139
156
 
@@ -148,6 +165,7 @@ function concrete(name: "Run" | "Expected", text: string): string | undefined {
148
165
  }
149
166
 
150
167
  const get = (sections: ReadonlyMap<Section, string>, s: Section): string => sections.get(s) ?? "";
168
+ const got = (sections: ReadonlyMap<Section, string>, s: Section): string => get(sections, s).trim();
151
169
 
152
170
  /** Every readiness problem in the sections, so the author fixes them in one pass. */
153
171
  function problemsOf(markdown: string, split: Sections): string[] {
@@ -156,7 +174,7 @@ function problemsOf(markdown: string, split: Sections): string[] {
156
174
  if (split.duplicates.length > 0) {
157
175
  problems.push(`duplicate section: ${[...new Set(split.duplicates)].join(", ")}`);
158
176
  }
159
- const missing = SECTIONS.filter((s) => get(sections, s) === "");
177
+ const missing = SECTIONS.filter((s) => got(sections, s) === "");
160
178
  if (missing.length > 0) problems.push(`missing section: ${missing.join(", ")}`);
161
179
  if (/\bTBD\b/.test(markdown)) problems.push("contains TBD; decide it or split the task");
162
180
  const steps = stepsOf(get(sections, "Steps"));
@@ -164,7 +182,7 @@ function problemsOf(markdown: string, split: Sections): string[] {
164
182
  problems.push(`Steps must be ${STEPS_MIN}-${STEPS_MAX} steps, found ${steps.length}`);
165
183
  }
166
184
  for (const name of ["Run", "Expected"] as const) {
167
- const why = missing.includes(name) ? undefined : concrete(name, get(sections, name));
185
+ const why = missing.includes(name) ? undefined : concrete(name, got(sections, name));
168
186
  if (why !== undefined) problems.push(why);
169
187
  }
170
188
  return problems;
@@ -202,9 +220,9 @@ export function parseTaskRecord(markdown: string, id?: string): TaskRecord | Par
202
220
  const found = locate(markdown.replace(/\r\n?/g, "\n"), id);
203
221
  if (isParseError(found)) return found;
204
222
  const split = splitSections(found.lines);
205
- const problems = problemsOf(found.lines.join("\n"), split);
223
+ const problems = problemsOf(`${found.id} ${found.title}\n${found.lines.join("\n")}`, split);
206
224
  if (problems.length > 0) return parseError(problems.join("; "));
207
- const text = (s: Section): string => get(split.text, s);
225
+ const text = (s: Section): string => got(split.text, s);
208
226
  return {
209
227
  id: found.id,
210
228
  title: found.title,
@@ -212,7 +230,7 @@ export function parseTaskRecord(markdown: string, id?: string): TaskRecord | Par
212
230
  files: filesOf(text("Files")),
213
231
  interfaces: text("Interfaces"),
214
232
  firstFailingTest: text("First failing test"),
215
- steps: stepsOf(text("Steps")),
233
+ steps: stepsOf(get(split.text, "Steps")),
216
234
  run: stripTicks(text("Run")),
217
235
  expected: text("Expected"),
218
236
  outOfScope: text("Out of scope"),