@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
package/src/core/model-advice.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/src/core/redact.ts
CHANGED
|
@@ -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]")
|
package/src/core/review-flow.ts
CHANGED
|
@@ -144,8 +144,11 @@ export function adoptSeverity(
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
const where = (f: Finding): string =>
|
|
148
|
-
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
-
.
|
|
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);
|
package/src/core/test-runner.ts
CHANGED
|
@@ -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"
|
|
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.
|
|
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),
|
package/src/state/config.ts
CHANGED
|
@@ -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, "
|
|
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 },
|