@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
|
@@ -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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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")
|
|
84
|
-
|
|
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);
|
package/src/tracker/github.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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: (.*)
|
|
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;
|