@jwilger/pi-development-system 0.8.7 → 0.8.9

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.8.7",
3
+ "version": "0.8.9",
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"
@@ -6,13 +6,20 @@ export type CommitExtraction =
6
6
  | { readonly kind: "not-commit" }
7
7
  | { readonly kind: "message"; readonly message: string }
8
8
  | { readonly kind: "file"; readonly path: string }
9
- | { readonly kind: "unknown"; readonly trailers: readonly string[] };
9
+ | {
10
+ readonly kind: "unknown";
11
+ readonly trailers: readonly string[];
12
+ /** A `-m` value is a substitution or variable: the message exists but cannot be read. */
13
+ readonly opaqueMessage: boolean;
14
+ };
10
15
 
11
16
  type Options = {
12
17
  messages: string[];
13
18
  trailers: string[];
14
19
  file: string | undefined;
15
20
  opaque: boolean;
21
+ /** Heredoc markers found inside opaque `-m` values (`$(cat <<EOF …)`). */
22
+ markers: number[];
16
23
  };
17
24
 
18
25
  type Parsed = {
@@ -63,12 +70,21 @@ const OPAQUE_VALUE = /^\s*(?:\$\(|`)|^\$\{?\w+\}?$/;
63
70
  function record(options: Options, parsed: Parsed): void {
64
71
  if (parsed.kind === "file") options.file = parsed.value;
65
72
  else if (parsed.kind === "trailer") options.trailers.push(parsed.value ?? "");
66
- else if (parsed.value === undefined || OPAQUE_VALUE.test(parsed.value)) options.opaque = true;
67
- else options.messages.push(parsed.value);
73
+ else if (parsed.value === undefined || OPAQUE_VALUE.test(parsed.value)) {
74
+ options.opaque = true;
75
+ for (const hit of (parsed.value ?? "").matchAll(/#HD(\d+)/g))
76
+ options.markers.push(Number(hit[1]));
77
+ } else options.messages.push(parsed.value);
68
78
  }
69
79
 
70
80
  function readOptions(args: readonly string[]): Options {
71
- const options: Options = { messages: [], trailers: [], file: undefined, opaque: false };
81
+ const options: Options = {
82
+ messages: [],
83
+ trailers: [],
84
+ file: undefined,
85
+ opaque: false,
86
+ markers: [],
87
+ };
72
88
  for (let i = 0; i < args.length; ) {
73
89
  const parsed = parseArg(args, i);
74
90
  if (parsed !== undefined) record(options, parsed);
@@ -77,9 +93,10 @@ function readOptions(args: readonly string[]): Options {
77
93
  return options;
78
94
  }
79
95
 
80
- const unknown = (trailers: readonly string[] = []): CommitExtraction => ({
96
+ const unknown = (trailers: readonly string[] = [], opaqueMessage = false): CommitExtraction => ({
81
97
  kind: "unknown",
82
98
  trailers,
99
+ opaqueMessage,
83
100
  });
84
101
 
85
102
  const STDIN_PATHS = new Set(["-", "/dev/stdin", "/proc/self/fd/0"]);
@@ -94,7 +111,11 @@ const heredocFrom = (lines: readonly string[], line: number, bodies: readonly st
94
111
  return undefined;
95
112
  };
96
113
 
97
- function extractOne(args: readonly string[], heredoc: string | undefined): CommitExtraction {
114
+ function extractOne(
115
+ args: readonly string[],
116
+ heredoc: string | undefined,
117
+ bodies: readonly string[],
118
+ ): CommitExtraction {
98
119
  const options = readOptions(args);
99
120
  if (options.file !== undefined && STDIN_PATHS.has(options.file)) {
100
121
  return heredoc === undefined
@@ -104,8 +125,12 @@ function extractOne(args: readonly string[], heredoc: string | undefined): Commi
104
125
  if (options.file !== undefined) return { kind: "file", path: options.file };
105
126
  const parts = [...options.messages];
106
127
  if (options.opaque) {
107
- if (heredoc === undefined) return unknown(options.trailers);
108
- parts.push(heredoc);
128
+ // The value's own heredoc, not whichever heredoc opens first on a shared line.
129
+ const own = options.markers.flatMap((i) => bodies[i] ?? []);
130
+ const fallback = heredoc === undefined ? [] : [heredoc];
131
+ const bodiesOfValue = own.length > 0 ? own : fallback;
132
+ if (bodiesOfValue.length === 0) return unknown(options.trailers, true);
133
+ parts.push(...bodiesOfValue);
109
134
  }
110
135
  if (parts.length === 0) return unknown(options.trailers);
111
136
  const trailers = options.trailers.length === 0 ? "" : `\n\n${options.trailers.join("\n")}`;
@@ -118,7 +143,7 @@ export function extractCommits(command: string): CommitExtraction[] {
118
143
  const resolution = resolveGit(rest);
119
144
  const lines = splitLines(rest);
120
145
  const found = resolution.invocations.flatMap((g) =>
121
- g.sub === "commit" ? [extractOne(g.args, heredocFrom(lines, g.line, bodies))] : [],
146
+ g.sub === "commit" ? [extractOne(g.args, heredocFrom(lines, g.line, bodies), bodies)] : [],
122
147
  );
123
148
  const hidden = opaqueMentions(resolution, "commit");
124
149
  return hidden ? [...found, unknown()] : found;
@@ -170,6 +170,12 @@ export function registerCommitGuard(deps: CommitGuardDeps): void {
170
170
  why: "the message file cannot be read before the commit runs, so its rationale cannot be checked; use -m or a heredoc",
171
171
  });
172
172
  }
173
+ if (extractions.some((e) => e.kind === "unknown" && e.opaqueMessage)) {
174
+ all.push({
175
+ gate: RATIONALE,
176
+ why: "the message is built by a substitution or variable and cannot be checked; use a literal -m or a heredoc",
177
+ });
178
+ }
173
179
  for (const message of known) {
174
180
  for (const need of await needsOf(deps, ctx, message, command)) {
175
181
  if (!all.some((n) => n.gate === need.gate)) all.push(need);