@jwilger/pi-development-system 0.45.2 → 0.46.1

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.45.2",
3
+ "version": "0.46.1",
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"
@@ -47,7 +47,7 @@ const direct = (matrix: ModelMatrix, tier: Tier): string[] =>
47
47
  * frontier but lives in strong). Undefined when it is in none of them.
48
48
  */
49
49
  export function tierOf(matrix: ModelMatrix, model: string): Tier | undefined {
50
- return [...TIERS].reverse().find((tier) => matches(direct(matrix, tier), model));
50
+ return TIERS.findLast((tier) => matches(direct(matrix, tier), model));
51
51
  }
52
52
 
53
53
  /** The best tier that lists the model at all: what a slot resolving to it is asking for. */
@@ -1,7 +1,7 @@
1
1
  /** Removes credentials from text before it is written to committed logs or session entries (R11). */
2
2
  export function redactSecrets(text: string): string {
3
3
  return text
4
- .replace(/(\w+:\/\/)[^\s/@]+@/g, "$1[redacted]@")
4
+ .replace(/(?<!\w)(\w+:\/\/)[^\s/@]+@/g, "$1[redacted]@")
5
5
  .replace(/\b(?:ghp|gho|ghu|ghs|github_pat)_[A-Za-z0-9_]{16,}/g, "[redacted]")
6
6
  .replace(/\b(\w*(?:TOKEN|KEY|SECRET|PASSWORD)\w*)=("[^"]*"|'[^']*'|\S+)/gi, "$1=[redacted]")
7
7
  .replace(/\b(Authorization:\s*)\w+\s+[^\s'"]+/gi, "$1[redacted]")
@@ -144,8 +144,11 @@ export function adoptSeverity(
144
144
  };
145
145
  }
146
146
 
147
- const where = (f: Finding): string =>
148
- f.path === undefined ? "" : ` \`${f.path}${f.line === undefined ? "" : `:${f.line}`}\``;
147
+ const where = (f: Finding): string => {
148
+ if (f.path === undefined) return "";
149
+ const line = f.line === undefined ? "" : `:${f.line}`;
150
+ return ` \`${f.path}${line}\``;
151
+ };
149
152
 
150
153
  /**
151
154
  * Nits are never stored. Each is reported back so the coordinator fixes it now or drops it
@@ -10,11 +10,7 @@ export type ReviewPacket = {
10
10
  readonly verdict: "no-blocking" | "blocking";
11
11
  };
12
12
 
13
- const SEP = "[—–-]";
14
- const HEADER = new RegExp(
15
- `^##\\s+Review\\s+${SEP}\\s+(.+?)\\s+${SEP}\\s+round\\s+(\\S+)\\s+${SEP}\\s+lenses:\\s*(.*)$`,
16
- "m",
17
- );
13
+ const HEADER = /^##\s+Review\s+[—–-]\s+(.+?)\s+[—–-]\s+round\s+(\S+)\s+[—–-]\s+lenses:\s*(.*)$/m;
18
14
  // Reviewers add text after the location (`fn`, a second location); the first backticked one is the finding's.
19
15
  const FINDING = /^-\s*\[([^\]]+)\]\s+(\S+)\s+(?:`([^`]+)`[^—–]*?\s+)?[—–-]\s+(.+)$/;
20
16
  // An indented line under a finding is its detail (trigger, fix), unless it is itself a finding.
@@ -24,8 +20,11 @@ const isDetail = (line: string): boolean =>
24
20
  // `until` ends a section; by default any heading does. Findings ends only at a known section so a stray
25
21
  // heading inside it (### Nits) makes its lines errors instead of silently cutting the findings off.
26
22
  const section = (text: string, name: string, until = /^###?\s/m): string | undefined => {
27
- const match = new RegExp(`^###\\s+${name}\\s*$`, "im").exec(text);
28
- if (match === null) return undefined;
23
+ const wanted = name.toLowerCase();
24
+ const match = [...text.matchAll(/^###\s+(.+?)\s*$/gim)].find(
25
+ (m) => m[1]?.toLowerCase() === wanted,
26
+ );
27
+ if (match === undefined) return undefined;
29
28
  const rest = text.slice(match.index + match[0].length);
30
29
  const next = until.exec(rest);
31
30
  return next === null ? rest : rest.slice(0, next.index);
@@ -94,8 +93,7 @@ export function parseReviewPacket(text: string): ReviewPacket | ParseError {
94
93
  }
95
94
  const lines = findingsText
96
95
  .split("\n")
97
- .filter((l) => !isDetail(l))
98
- .map((l) => l.trim())
96
+ .flatMap((l) => (isDetail(l) ? [] : [l.trim()]))
99
97
  .filter((l) => l !== "" && !/^(?:-\s*)?[([]?(?:none|no findings?)[)\]]?\.?$/i.test(l));
100
98
  for (const [i, line] of lines.entries()) {
101
99
  const finding = parseFinding(line, lenses, i + 1);
@@ -2,7 +2,7 @@ import { basename, isAssignment, segments, splitLines, WRAPPERS } from "./git-in
2
2
  import { redactSecrets } from "./redact.ts";
3
3
 
4
4
  const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun"]);
5
- const DIRECT_RUNNERS = new Set(["vitest", "jest", "pytest", "mocha", "ava", "tap"]);
5
+ const DIRECT_RUNNERS = new Set(["vitest", "jest", "pytest", "mocha", "ava"]);
6
6
  const EXEC_PREFIX = new Set(["npx", "bunx", "pnpx"]);
7
7
  const SUMMARY_LINES = 3;
8
8
  const SUMMARY_MAX = 300;
@@ -59,6 +59,7 @@ export type ResultLike = {
59
59
  };
60
60
 
61
61
  /** A pipe or sequence makes the shell report the last command's status, not the runner's. */
62
+ const NO_TEST_SCRIPT = /Missing script: "?test/;
62
63
  const MASKS_STATUS = /[|;\n]/;
63
64
  const FAILURE_MARKERS =
64
65
  /^(?:# |ℹ )fail [1-9]|test result: FAILED|\b[1-9]\d* (?:failed|failing)\b|^FAILED\b|^FAIL\b|\bnot ok\b/m;
@@ -68,6 +69,8 @@ const isRecord = (v: unknown): v is Record<string, unknown> =>
68
69
 
69
70
  /** Exit code of a bash tool result: structured `exit_code`, else pi's status line, else error/not. */
70
71
  export function exitCodeOf(result: ResultLike): number {
72
+ // `npm test` in a package without a test script exits 1 but ran no tests, so it is not a RED.
73
+ if (NO_TEST_SCRIPT.test(result.text)) return 0;
71
74
  const reported = reportedExit(result);
72
75
  if (reported !== 0) return reported;
73
76
  // `npm test | tail` exits 0 whatever the tests did, even in the structured status; trust the output.
@@ -174,7 +174,7 @@ const stagedThenEdited = (
174
174
  // re-staged from the work tree, which was reviewed. Message text is one argument and never equals a path.
175
175
  const named = upto
176
176
  .filter((i) => (i.sub === "commit" || i.sub === "add") && !isDryRun(i) && inThisDir(i, cwd))
177
- .flatMap((i) => i.args.filter((a) => !a.startsWith("-")).map(cleanPath));
177
+ .flatMap((i) => i.args.flatMap((a) => (a.startsWith("-") ? [] : [cleanPath(a)])));
178
178
  const covered = (name: string) => named.some((n) => name === n || name.startsWith(`${n}/`));
179
179
  return names.staged.filter(
180
180
  (name) => names.unstaged.includes(name) && !covered(name) && !DECISION_LOG.test(name),
@@ -31,7 +31,6 @@ export type DevsysConfig = {
31
31
  readonly profiles: { readonly override: readonly string[] };
32
32
  readonly models: ModelMatrix;
33
33
  readonly routing: Readonly<Record<string, Route>>;
34
- readonly jev: { readonly timeoutMs: number; readonly confidenceFloor: number };
35
34
  readonly verifier: { readonly maxPerSession: number };
36
35
  readonly cadence: { readonly pushMinutes: number };
37
36
  readonly eventModel: { readonly provider: string };
@@ -170,12 +169,20 @@ function route(raw: unknown, key: string): Result<Route, ConfigError> {
170
169
  : ok({ slot, thinkingLevel });
171
170
  }
172
171
 
172
+ const ROUTING_KEY = /^(?:trivial|routine|complex|expert|\*)\/(?:low|medium|high|\*)$/;
173
+
173
174
  function routing(root: Table): Result<Readonly<Record<string, Route>>, ConfigError> {
174
175
  const raw = root.routing;
175
176
  if (raw === undefined) return ok(DEFAULT_ROUTING);
176
177
  if (!isTable(raw)) return fail("routing must be a table", "routing");
177
178
  const result: Record<string, Route> = {};
178
179
  for (const [pattern, value] of Object.entries(raw)) {
180
+ if (!ROUTING_KEY.test(pattern)) {
181
+ return fail(
182
+ `routing.${pattern}: key must be <difficulty>/<risk>, each a level or *`,
183
+ `routing.${pattern}`,
184
+ );
185
+ }
179
186
  const parsed = route(value, `routing.${pattern}`);
180
187
  if (!parsed.ok) return parsed;
181
188
  result[pattern] = parsed.value;
@@ -201,7 +208,6 @@ const TOP_LEVEL = [
201
208
  "profiles",
202
209
  "models",
203
210
  "routing",
204
- "jev",
205
211
  "verifier",
206
212
  "cadence",
207
213
  "event_model",
@@ -237,27 +243,20 @@ function deliveryAndReview(
237
243
 
238
244
  function limits(
239
245
  root: Table,
240
- ): Result<Pick<DevsysConfig, "jev" | "verifier" | "cadence" | "eventModel">, ConfigError> {
241
- const j = section(root, "jev", ["timeout_ms", "confidence_floor"]);
246
+ ): Result<Pick<DevsysConfig, "verifier" | "cadence" | "eventModel">, ConfigError> {
242
247
  const v = section(root, "verifier", ["max_per_session"]);
243
248
  const c = section(root, "cadence", ["push_minutes"]);
244
249
  const e = section(root, "event_model", ["provider"]);
245
- if (!j.ok) return j;
246
250
  if (!v.ok) return v;
247
251
  if (!c.ok) return c;
248
252
  if (!e.ok) return e;
249
- const timeout = number(j.value, "jev", "timeout_ms", 4000, POSITIVE);
250
- const floor = number(j.value, "jev", "confidence_floor", 0.6, { min: 0, max: 1, integer: false });
251
253
  const max = number(v.value, "verifier", "max_per_session", 6, POSITIVE);
252
254
  const push = number(c.value, "cadence", "push_minutes", 60, POSITIVE);
253
255
  const provider = text(e.value, "event_model", "provider", "builtin");
254
- if (!timeout.ok) return timeout;
255
- if (!floor.ok) return floor;
256
256
  if (!max.ok) return max;
257
257
  if (!push.ok) return push;
258
258
  if (!provider.ok) return provider;
259
259
  return ok({
260
- jev: { timeoutMs: timeout.value, confidenceFloor: floor.value },
261
260
  verifier: { maxPerSession: max.value },
262
261
  cadence: { pushMinutes: push.value },
263
262
  eventModel: { provider: provider.value },