forgedock 1.29.1 → 1.31.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/bin/engine/outcome-codes.mjs +5 -2
- package/bin/engine/phases.mjs +20 -3
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +453 -66
- package/bin/engine.mjs +50 -38
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +356 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +278 -22
- package/bin/lean/conformance.test.mjs +251 -5
- package/bin/lean/context.mjs +133 -55
- package/bin/lean/context.test.mjs +175 -9
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +55 -19
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +799 -127
- package/bin/lean/lean.test.mjs +1749 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +341 -99
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +180 -0
- package/bin/lean/precheck.test.mjs +205 -0
- package/bin/lean/prefilter.mjs +180 -9
- package/bin/lean/prefilter.test.mjs +175 -3
- package/bin/lean/preflight.mjs +238 -0
- package/bin/lean/preflight.test.mjs +108 -0
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +134 -12
- package/bin/lean/recovery.test.mjs +75 -0
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +385 -0
- package/bin/lean/sequence.test.mjs +373 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +77 -10
- package/bin/lean/verify.test.mjs +74 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +6 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +88 -6
- package/docs/OUTCOME-CODES.md +7 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
- package/scripts/trusted-comments.test.sh +1 -1
package/scripts/symbol-index.mjs
CHANGED
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
// run prints only {"unsupported":"ts"|"js","reason":"no-parser"} and exits 0.
|
|
9
9
|
// .py targets are handed to symbol-index.py. Never emits source text or literal values.
|
|
10
10
|
//
|
|
11
|
-
// Usage: node scripts/symbol-index.mjs <file | dir>...
|
|
11
|
+
// Usage: node scripts/symbol-index.mjs [--shape] <file | dir>...
|
|
12
12
|
// Output: {"symbols":[{name,kind,start,end,lines,public[,file]}], "edges":[{from,to}],
|
|
13
13
|
// "imports":[{module,names,line[,file]}], "cycles":[[module,...],...]}
|
|
14
14
|
// kind: function|async_function|class|method|const|interface|type|enum. public = exported (methods: exported class
|
|
15
15
|
// and not private/#private). One file: bare names; otherwise `file` on entries and "<module>:<symbol>" edge ends,
|
|
16
16
|
// module = path relative to its root without extension (index files name their directory). cycles = strongly
|
|
17
17
|
// connected components of the relative-import graph between indexed modules (`import type` ignored).
|
|
18
|
+
// --shape (forge#4449, bin/lean/apishape.mjs): functions, methods/accessors/constructors and consts initialised with
|
|
19
|
+
// an arrow or function expression also carry async, generator (`function*`) and sig: the normalized parameter list
|
|
20
|
+
// (`...` rest, name, `?` optional, `: <type text>`, `=` for a default but never its value) plus " -> <return type>".
|
|
21
|
+
// Kinds are unchanged; without --shape the output is unchanged. --shape is forwarded to symbol-index.py.
|
|
18
22
|
// Exit: 0 ok or unsupported | 2 usage or missing path.
|
|
19
23
|
|
|
20
24
|
import { spawnSync } from "node:child_process";
|
|
@@ -94,7 +98,18 @@ function tarjan(graph) {
|
|
|
94
98
|
return comps;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
function
|
|
101
|
+
function fnShape(ts, fn, src) {
|
|
102
|
+
const K = ts.SyntaxKind;
|
|
103
|
+
const params = fn.parameters.map((p) => `${p.dotDotDotToken ? "..." : ""}${p.name.getText(src)}${p.questionToken ? "?" : ""}`
|
|
104
|
+
+ `${p.type ? `: ${p.type.getText(src)}` : ""}${p.initializer ? "=" : ""}`);
|
|
105
|
+
return {
|
|
106
|
+
async: (ts.getModifiers?.(fn) ?? fn.modifiers ?? []).some((m) => m.kind === K.AsyncKeyword),
|
|
107
|
+
generator: !!fn.asteriskToken,
|
|
108
|
+
sig: `(${params.join(", ")})${fn.type ? ` -> ${fn.type.getText(src)}` : ""}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function index(ts, entries, multi, shape = false) {
|
|
98
113
|
const K = ts.SyntaxKind;
|
|
99
114
|
const byFile = new Map(entries.map((e) => [path.resolve(e.file), e]));
|
|
100
115
|
const symbols = [], imports = [], edges = new Set(), graph = new Map(entries.map((e) => [e.module, new Set()]));
|
|
@@ -114,7 +129,9 @@ function index(ts, entries, multi) {
|
|
|
114
129
|
tables.set(e.module, table);
|
|
115
130
|
const add = (name, kind, node, pub, owner = null) => {
|
|
116
131
|
const start = line(src, node.getStart(src)), end = line(src, node.getEnd());
|
|
117
|
-
|
|
132
|
+
const fn = !shape ? null : ts.isFunctionLike(node) ? node
|
|
133
|
+
: ts.isVariableDeclaration(node) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) ? node.initializer : null;
|
|
134
|
+
symbols.push({ name, kind, start, end, lines: end - start + 1, public: !!pub, ...(multi ? { file: e.file } : {}), ...(fn ? fnShape(ts, fn, src) : {}) });
|
|
118
135
|
table.set(name, { node, owner });
|
|
119
136
|
};
|
|
120
137
|
const exportNames = new Set();
|
|
@@ -218,9 +235,11 @@ function index(ts, entries, multi) {
|
|
|
218
235
|
};
|
|
219
236
|
}
|
|
220
237
|
|
|
221
|
-
function main(
|
|
238
|
+
function main(args) {
|
|
239
|
+
const shape = args.includes("--shape");
|
|
240
|
+
const argv = args.filter((a) => a !== "--shape");
|
|
222
241
|
if (!argv.length || argv[0] === "-h" || argv[0] === "--help") {
|
|
223
|
-
process.stderr.write("usage: symbol-index.mjs <file | dir>...\n");
|
|
242
|
+
process.stderr.write("usage: symbol-index.mjs [--shape] <file | dir>...\n");
|
|
224
243
|
return 2;
|
|
225
244
|
}
|
|
226
245
|
for (const p of argv) if (!existsSync(p)) { process.stderr.write(`symbol-index: no such path: ${p}\n`); return 2; }
|
|
@@ -229,7 +248,7 @@ function main(argv) {
|
|
|
229
248
|
if (exts.includes(".py")) {
|
|
230
249
|
if (exts.some((x) => x !== ".py")) return emit({ unsupported: "mixed", reason: "mixed-languages" }), 0;
|
|
231
250
|
const py = path.join(path.dirname(fileURLToPath(import.meta.url)), "symbol-index.py");
|
|
232
|
-
const r = spawnSync("python3", [py, ...argv], { stdio: "inherit" });
|
|
251
|
+
const r = spawnSync("python3", [py, ...(shape ? ["--shape"] : []), ...argv], { stdio: "inherit" });
|
|
233
252
|
if (r.error) return emit({ unsupported: "python", reason: "no-python3" }), 0;
|
|
234
253
|
return r.status ?? 2;
|
|
235
254
|
}
|
|
@@ -242,7 +261,7 @@ function main(argv) {
|
|
|
242
261
|
if (!ts) return emit({ unsupported: lang, reason: "no-parser" }), 0;
|
|
243
262
|
const multi = !(argv.length === 1 && files.length === 1);
|
|
244
263
|
let out;
|
|
245
|
-
try { out = index(ts, entries, multi); } catch (err) {
|
|
264
|
+
try { out = index(ts, entries, multi, shape); } catch (err) {
|
|
246
265
|
if (err && err.reason) return emit({ unsupported: err.lang, reason: err.reason }), 0;
|
|
247
266
|
throw err;
|
|
248
267
|
}
|
package/scripts/symbol-index.py
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
# symbol-index.py: deterministic Python symbol index + module import-cycle detector for refactor:split (forge#4208).
|
|
6
6
|
# stdlib only (ast). No LLM, no network. Never emits source text, docstrings or literal values.
|
|
7
7
|
#
|
|
8
|
-
# Usage: python3 scripts/symbol-index.py <file.py | dir>...
|
|
8
|
+
# Usage: python3 scripts/symbol-index.py [--shape] <file.py | dir>...
|
|
9
9
|
# Output (stdout, one JSON object):
|
|
10
10
|
# {"symbols":[{name,kind,start,end,lines,public[,file]}], "edges":[{from,to}],
|
|
11
11
|
# "imports":[{module,names,line[,file]}], "cycles":[[module,...],...]}
|
|
12
12
|
# kind: function|async_function|class|method|const. Methods (and nested classes) are named Class.method.
|
|
13
13
|
# start includes decorators; lines = end - start + 1. public = no leading underscore (method dunders count as
|
|
14
14
|
# public), and listed in a static __all__ when the module defines one.
|
|
15
|
+
# --shape (forge#4449, bin/lean/apishape.mjs): function/async_function/method entries also carry async (bool),
|
|
16
|
+
# generator (yield in the def's own scope) and sig: the normalized parameter list (kinds, names, annotation text,
|
|
17
|
+
# "=" for a default but never its value) plus " -> <return annotation>". Without --shape the output is unchanged.
|
|
15
18
|
# One .py file: names are bare. Otherwise (a directory or several files) each entry carries `file` and edge ends
|
|
16
19
|
# are "<module>:<symbol>"; module names are dotted paths relative to each root (a root that has __init__.py is
|
|
17
20
|
# itself the top package, so root .../pkg yields pkg.a). edges = references from one indexed symbol to another.
|
|
@@ -108,9 +111,47 @@ def module_imports(body):
|
|
|
108
111
|
return out
|
|
109
112
|
|
|
110
113
|
|
|
114
|
+
def is_generator(fn):
|
|
115
|
+
"""True when yield / yield from occurs in fn's own scope (not in nested defs, lambdas or classes)."""
|
|
116
|
+
work = list(ast.iter_child_nodes(fn))
|
|
117
|
+
while work:
|
|
118
|
+
n = work.pop()
|
|
119
|
+
if isinstance(n, (ast.Yield, ast.YieldFrom)):
|
|
120
|
+
return True
|
|
121
|
+
if not isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)):
|
|
122
|
+
work.extend(ast.iter_child_nodes(n))
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def signature(fn):
|
|
127
|
+
"""Normalized parameter list of a def: names, kinds, annotation text, '=' for a default (never its value)."""
|
|
128
|
+
a = fn.args
|
|
129
|
+
|
|
130
|
+
def one(arg, default=False, prefix=""):
|
|
131
|
+
out = prefix + arg.arg
|
|
132
|
+
if arg.annotation is not None:
|
|
133
|
+
out += ": " + ast.unparse(arg.annotation)
|
|
134
|
+
return out + ("=" if default else "")
|
|
135
|
+
|
|
136
|
+
positional = a.posonlyargs + a.args
|
|
137
|
+
first_default = len(positional) - len(a.defaults)
|
|
138
|
+
parts = [one(x, i >= first_default) for i, x in enumerate(positional)]
|
|
139
|
+
if a.posonlyargs:
|
|
140
|
+
parts.insert(len(a.posonlyargs), "/")
|
|
141
|
+
if a.vararg is not None:
|
|
142
|
+
parts.append(one(a.vararg, prefix="*"))
|
|
143
|
+
elif a.kwonlyargs:
|
|
144
|
+
parts.append("*")
|
|
145
|
+
parts += [one(x, d is not None) for x, d in zip(a.kwonlyargs, a.kw_defaults)]
|
|
146
|
+
if a.kwarg is not None:
|
|
147
|
+
parts.append(one(a.kwarg, prefix="**"))
|
|
148
|
+
sig = "(" + ", ".join(parts) + ")"
|
|
149
|
+
return sig + (" -> " + ast.unparse(fn.returns) if fn.returns is not None else "")
|
|
150
|
+
|
|
151
|
+
|
|
111
152
|
class Module:
|
|
112
|
-
def __init__(self, path, name, is_pkg):
|
|
113
|
-
self.path, self.name, self.is_pkg = path, name, is_pkg
|
|
153
|
+
def __init__(self, path, name, is_pkg, shape=False):
|
|
154
|
+
self.path, self.name, self.is_pkg, self.shape = path, name, is_pkg, shape
|
|
114
155
|
self.symbols = [] # dicts without file
|
|
115
156
|
self.nodes = {} # symbol name -> (ast node, owning class name or None)
|
|
116
157
|
self.imports = [] # (ast import node)
|
|
@@ -131,8 +172,11 @@ class Module:
|
|
|
131
172
|
|
|
132
173
|
def add(name, kind, node, public, owner=None):
|
|
133
174
|
start, end = def_start(node), node.end_lineno or node.lineno
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
sym = {"name": name, "kind": kind, "start": start, "end": end, "lines": end - start + 1, "public": public}
|
|
176
|
+
if self.shape and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
177
|
+
sym.update({"async": isinstance(node, ast.AsyncFunctionDef), "generator": is_generator(node),
|
|
178
|
+
"sig": signature(node)})
|
|
179
|
+
self.symbols.append(sym)
|
|
136
180
|
self.nodes[name] = (node, owner)
|
|
137
181
|
|
|
138
182
|
def add_class(node, prefix, public):
|
|
@@ -241,12 +285,12 @@ def tarjan(graph):
|
|
|
241
285
|
return comps
|
|
242
286
|
|
|
243
287
|
|
|
244
|
-
def build(paths):
|
|
288
|
+
def build(paths, shape=False):
|
|
245
289
|
entries = walk_roots(paths)
|
|
246
290
|
multi = not (len(entries) == 1 and os.path.isfile(paths[0]) and len(paths) == 1)
|
|
247
291
|
modules, by_name = [], {}
|
|
248
292
|
for path, name, is_pkg in entries:
|
|
249
|
-
m = Module(path, name, is_pkg)
|
|
293
|
+
m = Module(path, name, is_pkg, shape)
|
|
250
294
|
m.parse()
|
|
251
295
|
modules.append(m)
|
|
252
296
|
by_name.setdefault(name, m)
|
|
@@ -335,15 +379,17 @@ def build(paths):
|
|
|
335
379
|
|
|
336
380
|
|
|
337
381
|
def main(argv):
|
|
382
|
+
shape = "--shape" in argv
|
|
383
|
+
argv = [a for a in argv if a != "--shape"]
|
|
338
384
|
if not argv or argv[0] in ("-h", "--help"):
|
|
339
|
-
sys.stderr.write("usage: symbol-index.py <file.py | dir>...\n")
|
|
385
|
+
sys.stderr.write("usage: symbol-index.py [--shape] <file.py | dir>...\n")
|
|
340
386
|
return 2
|
|
341
387
|
for p in argv:
|
|
342
388
|
if not os.path.exists(p):
|
|
343
389
|
sys.stderr.write("symbol-index: no such path: %s\n" % p)
|
|
344
390
|
return 2
|
|
345
391
|
try:
|
|
346
|
-
out = build(argv)
|
|
392
|
+
out = build(argv, shape)
|
|
347
393
|
except Unsupported as u:
|
|
348
394
|
out = {"unsupported": u.lang, "reason": u.reason}
|
|
349
395
|
sys.stdout.write(json.dumps(out, sort_keys=True, separators=(",", ":")) + "\n")
|
package/scripts/test-map.sh
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
# substitution, `${{ }}`, `$GITHUB_*`, redirect or `<(...)`/`>(...)`, a non-check make target, or a fixer form
|
|
28
28
|
# (`--fix`, `--write`, `-w`, `black`/`ruff format`/`gofmt`/`prettier` without their check flag) drops the step (forge#4316). Globs from the tool (python/go/shell/js, under the cwd), else the
|
|
29
29
|
# workflow's `paths:` filter, else `DIR/**` for a cwd; a workflow entry never gets a bare `**`.
|
|
30
|
+
# forge#4318: also dropped: any quote, fixer/snapshot flag families (`--fix-only`, `--add-noqa`, `--write=*`, `--w`,
|
|
31
|
+
# `--update*`, `--updateSnapshot`, `--snapshot-update`, `-u` on jest/vitest/package managers), write targets (`go -o`,
|
|
32
|
+
# `-exec`, `-toolexec`, `tsc --outDir/--outFile`, `pytest --junitxml/--basetemp/--html`, any absolute or `..` path) and
|
|
33
|
+
# tox/nox `dist`/`pypi`/`twine` envs in any spelling; `-w` is a workspace on npm/pnpm. Also mapped: `uv run python -m X`,
|
|
34
|
+
# `python3.N -m X`, `npx --yes X`, `pnpm --filter X`, `npm -w PKG`, `make -C DIR` (DIR becomes the cwd), `cargo +TC <sub>`,
|
|
35
|
+
# `cargo fmt --check`, gradle `--tests X` / maven `-pl X`, `./x.test.sh`, `bash -x x.test.sh`. `clean` goals stay unmapped.
|
|
30
36
|
# Exit: 0 ok | 2 usage or bad repo path. bash 3.2 portable.
|
|
31
37
|
set -uo pipefail
|
|
32
38
|
|
|
@@ -207,6 +213,8 @@ check_cmd() {
|
|
|
207
213
|
esac
|
|
208
214
|
c=${c%" && popd"}
|
|
209
215
|
case "$c" in *';'*|*'|'*|*'&'*) return 1 ;; esac
|
|
216
|
+
# forge#4318: a quoted word hides a flag from the word checks below ("--fix"); a real check never needs quotes
|
|
217
|
+
case "$c" in *\"*|*\'*) return 1 ;; esac
|
|
210
218
|
CK_CMD=$c
|
|
211
219
|
set -f
|
|
212
220
|
# shellcheck disable=SC2086 # split into words on purpose; set -f stops glob expansion
|
|
@@ -214,11 +222,24 @@ check_cmd() {
|
|
|
214
222
|
set +f
|
|
215
223
|
while [ $# -gt 0 ]; do case "$1" in [A-Za-z_]*=*) shift ;; *) break ;; esac; done
|
|
216
224
|
case "${1:-} ${2:-}" in
|
|
217
|
-
"uv run"|"poetry run"
|
|
218
|
-
"
|
|
225
|
+
"uv run"|"poetry run") shift 2; case "${1:-} ${2:-}" in "python -m"|"python3 -m") shift 2 ;; esac ;; # forge#4318: uv run python -m X
|
|
226
|
+
"pnpm exec"|"yarn exec"|"python -m"|"python3 -m"|"bundle exec") shift 2 ;;
|
|
227
|
+
"python3."[0-9]" -m"|"python3."[0-9][0-9]" -m") shift 2 ;; # forge#4318: a versioned interpreter
|
|
228
|
+
"npx "*|"bunx "*) shift; case "${1:-}" in --yes|-y) shift ;; esac ;;
|
|
219
229
|
esac
|
|
220
|
-
|
|
221
|
-
|
|
230
|
+
tool=${1:-}
|
|
231
|
+
# forge#4316/#4318: a fixer, snapshot updater or write-target flag rewrites the tree or writes outside it; matched as a
|
|
232
|
+
# flag family (`--fix-only`, `--write=true`, `--w`), and `-w` only off a package manager (there it names a workspace)
|
|
233
|
+
for w in "$@"; do
|
|
234
|
+
case "$w" in
|
|
235
|
+
--fix|--fix[=-]*|--add-noqa*|--unsafe-fixes*|--write|--write[=-]*|--update|--update[=-]*|--updateSnapshot*|--snapshot-update*) return 1 ;;
|
|
236
|
+
--outDir*|--outdir*|--out-dir*|--outFile*|--declarationDir*|--tsBuildInfoFile*|--junitxml*|--junit-xml*|--basetemp*|--html*|-exec|-exec=*|-toolexec*) return 1 ;;
|
|
237
|
+
/*|*=/*|..|../*|*/..|*/../*|*=..|*=../*) return 1 ;; # an absolute or parent path is outside the checkout
|
|
238
|
+
-w|-w=*|--w|--w=*) case "$tool" in npm|pnpm|yarn|bun) ;; *) return 1 ;; esac ;;
|
|
239
|
+
-u) case "$tool" in jest|vitest|npm|pnpm|yarn|bun) return 1 ;; esac ;;
|
|
240
|
+
-o|-o=*) [ "$tool" = go ] && return 1 ;;
|
|
241
|
+
esac
|
|
242
|
+
done
|
|
222
243
|
case "${1:-} ${2:-}" in
|
|
223
244
|
"black "*|"ruff format") has_word --check "$@" || return 1 ;;
|
|
224
245
|
"gofmt "*) has_word -l "$@" || has_word -d "$@" || return 1 ;;
|
|
@@ -227,21 +248,26 @@ check_cmd() {
|
|
|
227
248
|
case "${1:-}" in
|
|
228
249
|
npm|pnpm|yarn|bun)
|
|
229
250
|
pm=$1; shift
|
|
230
|
-
case "$pm ${1:-}" in # forge#4315: pnpm -r test, npm --prefix DIR test
|
|
231
|
-
"pnpm -r"|"pnpm --recursive") shift ;;
|
|
232
|
-
"npm --prefix") [ $# -ge 2 ] && safe_dir "$2" || return 1; shift 2 ;;
|
|
251
|
+
case "$pm ${1:-}" in # forge#4315: pnpm -r test, npm --prefix DIR test; forge#4318: npm -w PKG, pnpm -w, pnpm --filter X
|
|
252
|
+
"pnpm -r"|"pnpm --recursive"|"pnpm -w"|"pnpm --workspace-root") shift ;;
|
|
253
|
+
"npm --prefix"|"npm -w"|"npm --workspace") [ $# -ge 2 ] && safe_dir "$2" || return 1; shift 2 ;;
|
|
254
|
+
"pnpm --filter"|"pnpm -F") [ $# -ge 2 ] || return 1; case "$2" in *[!A-Za-z0-9@._/-]*) return 1 ;; esac; shift 2 ;;
|
|
233
255
|
esac
|
|
234
256
|
[ "${1:-}" = test ] && return 0
|
|
235
257
|
[ "${1:-}" = run ] && pkg_check_script "${2:-}" && return 0
|
|
236
258
|
case "$pm" in yarn|pnpm) pkg_check_script "${1:-}" && return 0 ;; esac
|
|
237
259
|
return 1 ;;
|
|
238
|
-
cargo)
|
|
260
|
+
cargo)
|
|
261
|
+
a=${2:-}; [ "${a#+}" != "$a" ] && a=${3:-} # forge#4318: cargo +TOOLCHAIN <sub>
|
|
262
|
+
case "$a" in test|clippy|check) return 0 ;; fmt) has_word --check "$@"; return ;; esac; return 1 ;;
|
|
239
263
|
./gradlew|gradlew|gradle|./mvnw|mvnw|mvn)
|
|
240
|
-
# every non-flag word is a check task/goal (`:mod:test` qualified for gradle); publish/deploy/install never pass
|
|
264
|
+
# every non-flag word is a check task/goal (`:mod:test` qualified for gradle); publish/deploy/install/clean never pass;
|
|
265
|
+
# forge#4318: the value of --tests / -pl / --projects / -P / -D is not a goal
|
|
241
266
|
case "$1" in *mvn*) goals="test|verify|validate" ;; *) goals="test|check|lint" ;; esac
|
|
242
|
-
shift; nt=0
|
|
267
|
+
shift; nt=0; skipv=""
|
|
243
268
|
for w in "$@"; do
|
|
244
|
-
|
|
269
|
+
if [ -n "$skipv" ]; then skipv=""; continue; fi
|
|
270
|
+
case "$w" in --tests|-pl|--projects|-P|-D) skipv=1; continue ;; -*) continue ;; esac
|
|
245
271
|
t=${w##*:}
|
|
246
272
|
case "|$goals|" in *"|$t|"*) [ -n "$t" ] && nt=$((nt+1)) ;; *) return 1 ;; esac
|
|
247
273
|
done
|
|
@@ -249,19 +275,39 @@ check_cmd() {
|
|
|
249
275
|
rspec|unittest) return 0 ;;
|
|
250
276
|
python|python3) [ "${2:-}" = manage.py ] && [ "${3:-}" = test ]; return ;;
|
|
251
277
|
tox|nox)
|
|
278
|
+
# forge#4318: `--env=X`, `-eX` and comma lists are split, case-folded, and packaging envs (dist, pypi) are publish too
|
|
252
279
|
shift; prev=""
|
|
253
280
|
for w in "$@"; do
|
|
254
|
-
|
|
255
|
-
case "$
|
|
281
|
+
v=""
|
|
282
|
+
case "$prev" in -e|-s|--env|--session|--sessions) v=$w ;; esac
|
|
283
|
+
case "$w" in --env=*|--session=*|--sessions=*) v=${w#*=} ;; --*) ;; -e?*|-s?*) v=${w#-?} ;; esac
|
|
284
|
+
if [ -n "$v" ]; then
|
|
285
|
+
v=$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')
|
|
286
|
+
case ",$v," in *publish*|*release*|*deploy*|*upload*|*pypi*|*twine*|*,dist,*) return 1 ;; esac
|
|
287
|
+
fi
|
|
256
288
|
prev=$w
|
|
257
289
|
done
|
|
258
290
|
return 0 ;;
|
|
259
291
|
deno) [ "${2:-}" = test ]; return ;;
|
|
260
|
-
bash|sh)
|
|
292
|
+
bash|sh)
|
|
293
|
+
# forge#4318: shell option words (-x, -eu) before the script are allowed
|
|
294
|
+
shift; while [ $# -gt 0 ]; do case "$1" in -[a-z]*) shift ;; *) break ;; esac; done
|
|
295
|
+
case "${1:-}" in *.test.sh) safe_dir "$1"; return ;; esac; return 1 ;;
|
|
296
|
+
./*.test.sh) safe_dir "${1#./}"; return ;;
|
|
261
297
|
make)
|
|
262
298
|
# forge#4316: every target must be a check (`make test deploy` is dropped); VAR=value and flags are not targets
|
|
263
|
-
|
|
264
|
-
|
|
299
|
+
# forge#4318: `make -C DIR` runs in DIR: it becomes the entry cwd, not a target
|
|
300
|
+
shift; nt=0; prev=""; mdir=""
|
|
301
|
+
for w in "$@"; do
|
|
302
|
+
if [ "$prev" = -C ]; then mdir=$w; prev=$w; continue; fi
|
|
303
|
+
prev=$w
|
|
304
|
+
case "$w" in -*|*=*) continue ;; esac; make_check_target "$w" || return 1; nt=$((nt+1))
|
|
305
|
+
done
|
|
306
|
+
if [ -n "$mdir" ]; then
|
|
307
|
+
safe_dir "$mdir" || return 1
|
|
308
|
+
CK_CMD=${CK_CMD/ -C $mdir/}; mdir=${mdir%/}
|
|
309
|
+
if [ "$mdir" != . ]; then if [ "$CK_CWD" = . ]; then CK_CWD=$mdir; else CK_CWD="$CK_CWD/$mdir"; fi; fi
|
|
310
|
+
fi
|
|
265
311
|
[ "$nt" -gt 0 ]; return ;;
|
|
266
312
|
go) case "${2:-}" in test|vet|build) return 0 ;; esac; return 1 ;;
|
|
267
313
|
node) [ "${2:-}" = --test ]; return ;;
|
|
@@ -280,7 +326,7 @@ tool_globs() {
|
|
|
280
326
|
*\ deno\ *|*\ bun\ *|*\ bunx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
281
327
|
*[\ /]ruff\ *|*[\ /]flake8\ *|*[\ /]pylint\ *|*[\ /]mypy\ *|*[\ /]black\ *|*[\ /]pytest\ *) printf '**/*.py'; return ;;
|
|
282
328
|
*[\ /]golangci-lint\ *|*[\ /]gofmt\ *|*[\ /]staticcheck\ *|*\ go\ *) printf '**/*.go'; return ;;
|
|
283
|
-
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *) printf '**/*.sh'; return ;;
|
|
329
|
+
*[\ /]shellcheck\ *|*\ bash\ *.sh\ *|*\ sh\ *.sh\ *|*.test.sh\ *) printf '**/*.sh'; return ;;
|
|
284
330
|
*[\ /]eslint\ *|*[\ /]tsc\ *|*[\ /]prettier\ *|*[\ /]vitest\ *|*[\ /]jest\ *|*\ node\ --test*|*\ npm\ *|*\ pnpm\ *|*\ yarn\ *|*\ npx\ *) ext_globs . "$JS_EXT"; return ;;
|
|
285
331
|
esac
|
|
286
332
|
printf ''
|
package/scripts/test-map.test.sh
CHANGED
|
@@ -345,6 +345,61 @@ eslint .
|
|
|
345
345
|
WF
|
|
346
346
|
eq "unsafe forms dropped, check forms kept" "$(cmds)" "black --check .|eslint .|gofmt -l .|make test V=1|make test lint|prettier --check .|ruff format --check ."
|
|
347
347
|
|
|
348
|
+
# ---- 5g. forge#4318: flag-family fixers, snapshot updaters, write targets, quotes and tox publish envs are dropped ----
|
|
349
|
+
wf_case bypass <<'WF'
|
|
350
|
+
cargo clippy --fix
|
|
351
|
+
tox --env=publish
|
|
352
|
+
tox --session=release
|
|
353
|
+
tox -ePUBLISH
|
|
354
|
+
tox -e dist,pypi
|
|
355
|
+
nox -s Release
|
|
356
|
+
eslint "--fix" .
|
|
357
|
+
gofmt -l --w .
|
|
358
|
+
gofmt -w=true .
|
|
359
|
+
prettier --write=true .
|
|
360
|
+
prettier -cw .
|
|
361
|
+
ruff --fix-only .
|
|
362
|
+
ruff check --add-noqa .
|
|
363
|
+
jest -u
|
|
364
|
+
jest --updateSnapshot
|
|
365
|
+
vitest -u
|
|
366
|
+
vitest --update
|
|
367
|
+
pytest --snapshot-update
|
|
368
|
+
npm test -- -u
|
|
369
|
+
go build -o /tmp/x ./...
|
|
370
|
+
go test -o out.test ./...
|
|
371
|
+
go test -exec foo ./...
|
|
372
|
+
tsc --outDir /tmp/out
|
|
373
|
+
tsc --outFile out.js
|
|
374
|
+
pytest --junitxml=/abs/x.xml
|
|
375
|
+
pytest --basetemp=../t
|
|
376
|
+
mvn clean test
|
|
377
|
+
./gradlew clean test
|
|
378
|
+
tox -e py311,lint
|
|
379
|
+
pytest -q
|
|
380
|
+
WF
|
|
381
|
+
eq "bypass forms dropped" "$(cmds)" "pytest -q|tox -e py311,lint"
|
|
382
|
+
|
|
383
|
+
# ---- 5h. forge#4318: workspace flags, versioned/wrapped runners, -C, toolchains and test scripts are mapped ----
|
|
384
|
+
wf_case forms <<'WF'
|
|
385
|
+
npm -w pkg test
|
|
386
|
+
pnpm -w test
|
|
387
|
+
uv run python -m pytest
|
|
388
|
+
python3.12 -m pytest
|
|
389
|
+
npx --yes vitest
|
|
390
|
+
pnpm --filter web test
|
|
391
|
+
make -C sub test
|
|
392
|
+
mvn -pl mod test
|
|
393
|
+
./gradlew test --tests Foo
|
|
394
|
+
cargo +nightly test
|
|
395
|
+
cargo fmt --check
|
|
396
|
+
./x.test.sh
|
|
397
|
+
bash -x x.test.sh
|
|
398
|
+
WF
|
|
399
|
+
eq "real forms mapped" "$(cmds)" "./gradlew test --tests Foo|./x.test.sh|bash -x x.test.sh|cargo +nightly test|cargo fmt --check|make test|mvn -pl mod test|npm -w pkg test|npx --yes vitest|pnpm --filter web test|pnpm -w test|python3.12 -m pytest|uv run python -m pytest"
|
|
400
|
+
eq "make -C dir is the cwd" "$(printf '%s' "$OUTS" | jq -r '.entries[] | select(.command == "make test") | "\(.cwd) \(.globs | join(","))"')" "sub sub/**"
|
|
401
|
+
eq "test script globs" "$(globs_of "./x.test.sh")" "**/*.sh"
|
|
402
|
+
|
|
348
403
|
# ---- 6. usage ----
|
|
349
404
|
bash "$S" --repo-path "$TMP/missing" >/dev/null 2>&1; eq "bad repo path" "$?" 2
|
|
350
405
|
bash "$S" --nope >/dev/null 2>&1; eq "unknown option" "$?" 2
|
|
@@ -67,7 +67,7 @@ eq "trusted synthesis accepted" "$(printf '[%s]' "$(c NONE Bot b "$FAKE_SYN")" |
|
|
|
67
67
|
eq "synthesis marker mid-body rejected" "$(printf '[%s]' "$(c NONE Bot b "intro${NL}${FAKE_SYN}")" | bash "$TC" count "$SYNTH_RE")" 0
|
|
68
68
|
eq "synthesis for a different head rejected" "$(printf '[%s]' "$(c NONE Bot b "<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->${NL}Reviewed-SHA: ${OTHER}")" | bash "$TC" count "$SYNTH_RE")" 0
|
|
69
69
|
|
|
70
|
-
# Staging review
|
|
70
|
+
# Staging review (scripts/review-staging-post.sh delivery, formerly the commands/review-pr-staging.md fences): same predicate, domains may carry digits/hyphens (bug-hunter-api)
|
|
71
71
|
STG_RE="^<!-- FORGE:REVIEW-AGENT:[a-z0-9-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
|
|
72
72
|
STG_DOM_RE="^<!-- FORGE:REVIEW-AGENT:bug-hunter-api -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
|
|
73
73
|
STG_TRUE="<!-- FORGE:REVIEW-AGENT:bug-hunter-api -->${NL}Reviewed-SHA: ${SHA}${NL}<!-- FINDING:real-2 -->"
|