@jwilger/pi-development-system 0.59.0 → 0.60.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.59.0",
3
+ "version": "0.60.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"
@@ -38,6 +38,19 @@ const PLACEHOLDER =
38
38
 
39
39
  const isSection = (name: string): name is Section => SECTIONS.some((s) => s === name);
40
40
 
41
+ const FENCE = /^\s*(?:```|~~~)/;
42
+ const HEADING = /^#{1,6}\s/;
43
+
44
+ /** For each line, whether it sits inside (or on the edge of) a fenced code block. */
45
+ const fencedFlags = (lines: readonly string[]): boolean[] => {
46
+ let open = false;
47
+ return lines.map((line) => {
48
+ if (!FENCE.test(line)) return open;
49
+ open = !open;
50
+ return true;
51
+ });
52
+ };
53
+
41
54
  type Sections = { readonly text: Map<Section, string>; readonly duplicates: Section[] };
42
55
 
43
56
  /** Splits the body into section texts keyed by name; text may continue over several lines. */
@@ -45,8 +58,9 @@ function splitSections(lines: readonly string[]): Sections {
45
58
  const found = new Map<Section, string[]>();
46
59
  const duplicates: Section[] = [];
47
60
  let current: string[] | undefined;
48
- for (const line of lines) {
49
- const match = SECTION_LINE.exec(line);
61
+ const fenced = fencedFlags(lines);
62
+ for (const [at, line] of lines.entries()) {
63
+ const match = fenced[at] === true ? null : SECTION_LINE.exec(line);
50
64
  const name = match?.[1]?.trim();
51
65
  if (match !== null && name !== undefined && isSection(name)) {
52
66
  if (found.has(name)) duplicates.push(name);
@@ -60,12 +74,13 @@ function splitSections(lines: readonly string[]): Sections {
60
74
 
61
75
  type Located = { readonly id: string; readonly title: string; readonly lines: readonly string[] };
62
76
 
63
- /** Every `## <id> — <title>` block; any other `## ` heading ends the block before it. */
77
+ /** Every `## <id> — <title>` block; any other heading (outside code fences) ends the block before it. */
64
78
  function recordsIn(lines: readonly string[]): Located[] {
65
79
  const records: { id: string; title: string; lines: string[] }[] = [];
66
80
  let open: { id: string; title: string; lines: string[] } | undefined;
67
- for (const line of lines) {
68
- if (line.startsWith("## ")) {
81
+ const fenced = fencedFlags(lines);
82
+ for (const [at, line] of lines.entries()) {
83
+ if (fenced[at] !== true && HEADING.test(line)) {
69
84
  const header = HEADER.exec(line);
70
85
  open =
71
86
  header === null
@@ -77,13 +92,25 @@ function recordsIn(lines: readonly string[]): Located[] {
77
92
  return records;
78
93
  }
79
94
 
80
- const stripTicks = (text: string): string => text.replace(/^`+|`+$/g, "").trim();
95
+ /** A command or result without its backticks, or the body of a fenced block (its info string dropped). */
96
+ const stripTicks = (text: string): string => {
97
+ const lines = text.split("\n");
98
+ if (FENCE.test(lines[0] ?? "")) {
99
+ const body = lines.slice(1);
100
+ if (FENCE.test(body.at(-1) ?? "")) body.pop();
101
+ return body.join("\n").trim();
102
+ }
103
+ return text.replace(/^`+|`+$/g, "").trim();
104
+ };
81
105
 
82
- const stepsOf = (text: string): string[] =>
83
- text.split("\n").flatMap((line) => {
84
- const item = STEP.exec(line)?.[1]?.trim();
106
+ const stepsOf = (text: string): string[] => {
107
+ const lines = text.split("\n");
108
+ const fenced = fencedFlags(lines);
109
+ return lines.flatMap((line, at) => {
110
+ const item = fenced[at] === true ? undefined : STEP.exec(line)?.[1]?.trim();
85
111
  return item === undefined || item === "" ? [] : [item];
86
112
  });
113
+ };
87
114
 
88
115
  function concrete(name: "Run" | "Expected", text: string): string | undefined {
89
116
  const bare = stripTicks(text);
@@ -1,6 +1,13 @@
1
1
  import type { Exec } from "../core/exec.ts";
2
2
  import { ok } from "../core/result.ts";
3
3
  import { unwritable } from "./item-format.ts";
4
+
5
+ /** Our status marker is a label; a user label with that name would change the status as a side effect. */
6
+ const unusable = (item: { title: string; labels: readonly string[] }): string | undefined =>
7
+ item.labels.includes("in-progress")
8
+ ? '"in-progress" is reserved for the in-progress status; set status instead'
9
+ : unwritable(item);
10
+
4
11
  import {
5
12
  type ItemFilter,
6
13
  type NewWorkItem,
@@ -142,7 +149,7 @@ const listIssues = async (gh: Gh, filter: ItemFilter): Promise<TrackerResult<Wor
142
149
  };
143
150
 
144
151
  const createIssue = async (gh: Gh, input: NewWorkItem): Promise<TrackerResult<WorkItem>> => {
145
- const problem = unwritable({ title: input.title, labels: input.labels ?? [] });
152
+ const problem = unusable({ title: input.title, labels: input.labels ?? [] });
146
153
  if (problem !== undefined) return trackerError(problem);
147
154
  const labels = (input.labels ?? []).flatMap((l) => ["--label", l]);
148
155
  const out = await gh([
@@ -203,7 +210,7 @@ const updateIssue = async (
203
210
  id: string,
204
211
  patch: WorkItemPatch,
205
212
  ): Promise<TrackerResult<WorkItem>> => {
206
- const problem = unwritable({ title: patch.title ?? "x", labels: patch.labels ?? [] });
213
+ const problem = unusable({ title: patch.title ?? "x", labels: patch.labels ?? [] });
207
214
  if (problem !== undefined) return trackerError(problem);
208
215
  const raw = await viewRaw(gh, id);
209
216
  if (!raw.ok) return raw;
@@ -32,17 +32,13 @@ export const unwritable = (item: Pick<WorkItem, "title" | "labels">): string | u
32
32
  : `label ${JSON.stringify(bad)} is blank or holds a comma or line break`;
33
33
  };
34
34
 
35
+ /** Written to pass our own markdownlint: no trailing space, no repeated heading text, no stacked blank lines. */
35
36
  export const renderItem = (item: WorkItem): string => {
36
- const comments = item.comments.map((c) => `\n### Comment\n\n${quote(c)}\n`).join("");
37
- return [
38
- `# ${item.title}`,
39
- "",
40
- `Status: ${item.status}`,
41
- `Labels: ${item.labels.join(", ")}`,
42
- "",
43
- item.body,
44
- COMMENTS + comments,
45
- ].join("\n");
37
+ const labels = item.labels.length === 0 ? "" : ` ${item.labels.join(", ")}`;
38
+ const top = `# ${item.title}\n\nStatus: ${item.status}\nLabels:${labels}`;
39
+ const body = item.body === "" ? [] : [item.body];
40
+ const comments = item.comments.map((c, n) => `\n### Comment ${n + 1}\n\n${quote(c)}\n`);
41
+ return `${[top, ...body, "## Comments"].join("\n\n")}\n${comments.join("")}`;
46
42
  };
47
43
 
48
44
  const isStatus = (value: string): value is WorkStatus =>
@@ -50,7 +46,7 @@ const isStatus = (value: string): value is WorkStatus =>
50
46
 
51
47
  const commentsOf = (section: string): string[] =>
52
48
  section
53
- .split(/^### Comment\n\n/m)
49
+ .split(/^### Comment(?: \d+)?\n\n/m)
54
50
  .slice(1)
55
51
  .map((c) => unquote(c.replace(/\n+$/, "")));
56
52
 
@@ -59,7 +55,7 @@ export const parseItem = (id: string, raw: string): TrackerResult<WorkItem> => {
59
55
  const marker = text.lastIndexOf(COMMENTS);
60
56
  const main = marker < 0 ? text : text.slice(0, marker);
61
57
  const comments = marker < 0 ? [] : commentsOf(text.slice(marker + COMMENTS.length));
62
- const head = /^# (.+)\n\nStatus: (.*)\nLabels: (.*)\n\n?/.exec(main);
58
+ const head = /^# (.+)\n\nStatus: (.*)\nLabels:[ ]?(.*)(?:\n\n|\n$|$)/.exec(main);
63
59
  if (head === null)
64
60
  return trackerError(`work item ${id}: expected "# title", Status and Labels lines`);
65
61
  const [whole, title = "", status = "", labels = ""] = head;