@jwilger/pi-development-system 0.3.5 → 0.3.7
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 +1 -1
- package/src/core/git-intent.ts +2 -2
- package/src/core/shell-parse.ts +21 -0
- package/src/core/test-weakening.ts +2 -2
- package/src/gates/test-guard.ts +38 -31
package/package.json
CHANGED
package/src/core/git-intent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseShell } from "./shell-parse.ts";
|
|
2
2
|
|
|
3
3
|
export type GitIntent =
|
|
4
4
|
| "history-rewrite"
|
|
@@ -58,7 +58,7 @@ const isRedirect = (op: string): boolean => /^[<>]/.test(op) && !op.endsWith("("
|
|
|
58
58
|
/** Tokens of one line grouped into simple commands (operators separate segments). */
|
|
59
59
|
function segments(line: string): string[][] {
|
|
60
60
|
const result: string[][] = [[]];
|
|
61
|
-
for (const token of
|
|
61
|
+
for (const token of parseShell(line, (name) => `$${name}`)) {
|
|
62
62
|
if (typeof token === "string") result[result.length - 1]?.push(token);
|
|
63
63
|
else if ("pattern" in token) result[result.length - 1]?.push(token.pattern);
|
|
64
64
|
else if ("op" in token && isRedirect(token.op)) continue;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ParseEntry, parse } from "shell-quote";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* shell-quote throws on text that is valid shell or valid file content (a template literal inside a
|
|
5
|
+
* heredoc), and a guard must never crash the tool call. Retry with the `${` openers neutralised, then
|
|
6
|
+
* fall back to whitespace-split words so command classification still sees every token.
|
|
7
|
+
*/
|
|
8
|
+
export function parseShell(line: string, env?: (name: string) => string): ParseEntry[] {
|
|
9
|
+
const attempt = (text: string): ParseEntry[] | undefined => {
|
|
10
|
+
try {
|
|
11
|
+
return env === undefined ? parse(text) : parse(text, env);
|
|
12
|
+
} catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return (
|
|
17
|
+
attempt(line) ??
|
|
18
|
+
attempt(line.replaceAll("${", "$")) ??
|
|
19
|
+
line.split(/\s+/).filter((word) => word !== "")
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { parseShell } from "./shell-parse.ts";
|
|
3
3
|
|
|
4
4
|
export type WeakeningSignals = { addsSkip: boolean; emptied: boolean; commentedOut: boolean };
|
|
5
5
|
|
|
@@ -151,7 +151,7 @@ function splitLines(command: string): string {
|
|
|
151
151
|
|
|
152
152
|
/** Boundary decode of shell-quote's output into domain tokens; globs count as words. */
|
|
153
153
|
function tokenize(command: string): Token[] {
|
|
154
|
-
return
|
|
154
|
+
return parseShell(splitLines(command)).flatMap((entry): Token[] => {
|
|
155
155
|
if (typeof entry === "string") return [{ kind: "word", value: entry }];
|
|
156
156
|
if ("pattern" in entry) return [{ kind: "word", value: entry.pattern }];
|
|
157
157
|
if ("op" in entry) return [{ kind: "op", op: entry.op }];
|
package/src/gates/test-guard.ts
CHANGED
|
@@ -70,6 +70,35 @@ const RISKY_ADDITION = /\/\*|<!--|=begin\b|"""|'''|\breturn\b|\bthrow\b|\bexit\b
|
|
|
70
70
|
|
|
71
71
|
const GLOB = /[*?[\]{]/;
|
|
72
72
|
|
|
73
|
+
const NO_SIGNALS: WeakeningSignals = { addsSkip: false, emptied: false, commentedOut: false };
|
|
74
|
+
|
|
75
|
+
/** Adding lines never weakens a test unless an added line itself disables something. */
|
|
76
|
+
const isHarmlessAddition = (before: string, after: string, signals: WeakeningSignals): boolean =>
|
|
77
|
+
!signals.addsSkip &&
|
|
78
|
+
!signals.commentedOut &&
|
|
79
|
+
isPureAddition(before, after) &&
|
|
80
|
+
!addedLines(before, after).some((line) => RISKY_ADDITION.test(line));
|
|
81
|
+
|
|
82
|
+
/** Jev's reading of a change, or `undefined` when it sees nothing to act on. */
|
|
83
|
+
function jevVerdict(
|
|
84
|
+
judged: { weakens: number; motive: string },
|
|
85
|
+
deterministicSignal: boolean,
|
|
86
|
+
): Verdict | undefined {
|
|
87
|
+
const weakening = judged.weakens >= WEAKENS_THRESHOLD;
|
|
88
|
+
if ((weakening || deterministicSignal) && judged.motive === "gate-gaming") {
|
|
89
|
+
return {
|
|
90
|
+
kind: "hard-stop",
|
|
91
|
+
why: "Jev reads this change as weakening a check to get a green result",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return weakening
|
|
95
|
+
? {
|
|
96
|
+
kind: "require-departure",
|
|
97
|
+
why: `Jev reads this change as weakening the tests (motive: ${judged.motive})`,
|
|
98
|
+
}
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
/**
|
|
74
103
|
* Soft gate `tests.weaken`: deleting, skipping, emptying or loosening tests needs a recorded
|
|
75
104
|
* departure; a change Jev reads as gaming a failing gate escalates to a hard stop.
|
|
@@ -90,10 +119,13 @@ export function registerTestGuard(deps: TestGuardDeps): void {
|
|
|
90
119
|
|
|
91
120
|
const consumeOpenDeparture = (): void => {
|
|
92
121
|
const { openDepartures, activeSlice } = deps.state.get();
|
|
93
|
-
const
|
|
122
|
+
const applicable = openDepartures.filter(
|
|
94
123
|
(d) => d.gate === gate && (d.scope.kind !== "slice" || d.scope.slice === activeSlice),
|
|
95
124
|
);
|
|
96
|
-
|
|
125
|
+
// A broader departure covers the call without being spent; only a lone once-scoped one is used up.
|
|
126
|
+
if (applicable.some((d) => d.scope.kind !== "once")) return;
|
|
127
|
+
const used = applicable[0];
|
|
128
|
+
if (used === undefined) return;
|
|
97
129
|
deps.state.update((s) => ({
|
|
98
130
|
...s,
|
|
99
131
|
openDepartures: s.openDepartures.filter((d) => d.id !== used.id),
|
|
@@ -103,41 +135,16 @@ export function registerTestGuard(deps: TestGuardDeps): void {
|
|
|
103
135
|
const judge = async (change: Change, ctx: ExtensionContext): Promise<Verdict> => {
|
|
104
136
|
const deleted = change.after === undefined && change.rewritten === undefined;
|
|
105
137
|
const after = change.after ?? change.rewritten ?? "";
|
|
106
|
-
const signals = deleted
|
|
107
|
-
|
|
108
|
-
: weakeningSignals(change.before, after);
|
|
109
|
-
// Adding lines never weakens a test unless the added line itself is a skip marker.
|
|
110
|
-
if (
|
|
111
|
-
!deleted &&
|
|
112
|
-
!signals.addsSkip &&
|
|
113
|
-
!signals.commentedOut &&
|
|
114
|
-
isPureAddition(change.before, after) &&
|
|
115
|
-
!addedLines(change.before, after).some((line) => RISKY_ADDITION.test(line))
|
|
116
|
-
) {
|
|
117
|
-
return { kind: "allow" };
|
|
118
|
-
}
|
|
138
|
+
const signals = deleted ? NO_SIGNALS : weakeningSignals(change.before, after);
|
|
139
|
+
if (!deleted && isHarmlessAddition(change.before, after, signals)) return { kind: "allow" };
|
|
119
140
|
const deterministic = describeSignals(deleted, signals);
|
|
120
141
|
const judged = await judgeTestChange(deps.jev(ctx), {
|
|
121
142
|
path: change.path,
|
|
122
143
|
before: change.before,
|
|
123
144
|
after: after === "" ? undefined : redactSecrets(after),
|
|
124
145
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const weakening = weakens >= WEAKENS_THRESHOLD || deterministic !== undefined;
|
|
128
|
-
if (weakening && motive === "gate-gaming") {
|
|
129
|
-
return {
|
|
130
|
-
kind: "hard-stop",
|
|
131
|
-
why: "Jev reads this change as weakening a check to get a green result",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (weakens >= WEAKENS_THRESHOLD) {
|
|
135
|
-
return {
|
|
136
|
-
kind: "require-departure",
|
|
137
|
-
why: `Jev reads this change as weakening the tests (motive: ${motive})`,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
}
|
|
146
|
+
const fromJev = judged.ok ? jevVerdict(judged.value, deterministic !== undefined) : undefined;
|
|
147
|
+
if (fromJev !== undefined) return fromJev;
|
|
141
148
|
return deterministic !== undefined
|
|
142
149
|
? { kind: "require-departure", why: `this change ${deterministic}` }
|
|
143
150
|
: { kind: "allow" };
|