forgedock 1.26.0 → 1.28.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/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +38 -2
- package/bin/engine/telemetry-seam.mjs +41 -6
- package/bin/engine/telemetry-seam.test.mjs +35 -0
- package/bin/engine-cli.mjs +616 -54
- package/bin/engine.mjs +13 -5
- package/bin/forgedock.mjs +169 -39
- package/bin/gc.mjs +580 -0
- package/bin/init-detect.mjs +68 -3
- package/bin/init-enrich.mjs +3 -0
- package/bin/init-repo.mjs +213 -0
- package/bin/journey.mjs +278 -43
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +24 -5
- package/bin/lean/batch.test.mjs +54 -4
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +235 -37
- package/bin/lean/conformance.test.mjs +143 -5
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +178 -16
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +877 -151
- package/bin/lean/lean.test.mjs +1798 -28
- package/bin/lean/ledger.mjs +92 -0
- package/bin/lean/ledger.test.mjs +100 -0
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +321 -19
- package/bin/lean/prompts.mjs +125 -17
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +25 -6
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +124 -0
- package/bin/lean/verify.test.mjs +118 -0
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/adopt.md +3 -0
- package/commands/audit.md +5 -8
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +125 -6
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/review-pr.md +2 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +227 -3
- package/docs/OUTCOME-CODES.md +11 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +321 -0
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +125 -0
- package/scripts/check-devdocs-freshness.test.sh +130 -0
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +16 -5
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +14 -2
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/northstar-docs.test.sh +68 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/review-prepare.sh +11 -9
- package/scripts/review-prepare.test.sh +14 -0
- package/scripts/structure-delta.sh +155 -0
- package/scripts/structure-delta.test.sh +136 -0
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# gen_big.py: deterministic fixture for scripts/symbol-move.test.sh (forge#4210). stdlib only, no randomness.
|
|
6
|
+
# Usage: python3 gen_big.py <out_root>
|
|
7
|
+
# Writes <out_root>/pkg/__init__.py, pkg/big.py (~3k lines) and tests/__init__.py, tests/test_big.py.
|
|
8
|
+
# big.py groups (the test's plan): consts (LIMIT_*, _SEPARATOR), helpers (_scale_*, compute_*, join_parts),
|
|
9
|
+
# aio (fetch_*), models (Item_*); VERSION, describe() and DEFAULT_TOTAL stay behind and use moved names.
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
N_FUNCS = 90
|
|
15
|
+
N_ASYNC = 30
|
|
16
|
+
N_CLASSES = 30
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def big():
|
|
20
|
+
out = ['"""Fixture module for the symbol-move codemod test."""', "",
|
|
21
|
+
"from __future__ import annotations", "",
|
|
22
|
+
"import functools", "import os", "", "", "# ---- constants ----", ""]
|
|
23
|
+
for i in range(N_FUNCS):
|
|
24
|
+
out.append("LIMIT_%02d = %d # upper bound for compute_%02d" % (i, 10 + i, i))
|
|
25
|
+
out += ["", '_SEPARATOR = os.sep + "|"', "", ""]
|
|
26
|
+
out += ["# ---- helpers ----", ""]
|
|
27
|
+
for i in range(N_FUNCS):
|
|
28
|
+
out += [
|
|
29
|
+
"",
|
|
30
|
+
"def _scale_%02d(value: int) -> int:" % i,
|
|
31
|
+
" # private helper: scales by the group factor",
|
|
32
|
+
" factor = %d" % (i % 7 + 1),
|
|
33
|
+
" if value < 0:",
|
|
34
|
+
" return -value * factor",
|
|
35
|
+
" return value * factor",
|
|
36
|
+
"",
|
|
37
|
+
"",
|
|
38
|
+
"def compute_%02d(value: int) -> int:" % i,
|
|
39
|
+
' """Scale value and clamp it to LIMIT_%02d."""' % i,
|
|
40
|
+
" scaled = _scale_%02d(value)" % i,
|
|
41
|
+
" if scaled > LIMIT_%02d:" % i,
|
|
42
|
+
" return LIMIT_%02d" % i,
|
|
43
|
+
" total = 0",
|
|
44
|
+
" for step in range(scaled):",
|
|
45
|
+
" total += step % 3",
|
|
46
|
+
" return scaled + total - total",
|
|
47
|
+
"",
|
|
48
|
+
]
|
|
49
|
+
out += [
|
|
50
|
+
"",
|
|
51
|
+
"def join_parts(*parts: str) -> str:",
|
|
52
|
+
" # joins with the module separator",
|
|
53
|
+
" return _SEPARATOR.join(str(p) for p in parts)",
|
|
54
|
+
"",
|
|
55
|
+
"",
|
|
56
|
+
"# ---- async ----",
|
|
57
|
+
"",
|
|
58
|
+
]
|
|
59
|
+
for i in range(N_ASYNC):
|
|
60
|
+
out += [
|
|
61
|
+
"",
|
|
62
|
+
"async def fetch_%02d(value: int) -> dict:" % i,
|
|
63
|
+
' """Pretend to fetch: returns the computed value in a record."""',
|
|
64
|
+
" result = compute_%02d(value)" % i,
|
|
65
|
+
' record = {"id": %d, "value": result}' % i,
|
|
66
|
+
" if result % 2:",
|
|
67
|
+
' record["odd"] = True',
|
|
68
|
+
" return record",
|
|
69
|
+
"",
|
|
70
|
+
]
|
|
71
|
+
out += ["", "# ---- models ----", ""]
|
|
72
|
+
for i in range(N_CLASSES):
|
|
73
|
+
out += [
|
|
74
|
+
"",
|
|
75
|
+
"@functools.total_ordering",
|
|
76
|
+
"class Item_%02d:" % i,
|
|
77
|
+
' """A comparable item backed by compute_%02d."""' % i,
|
|
78
|
+
"",
|
|
79
|
+
' kind = "item-%02d"' % i,
|
|
80
|
+
"",
|
|
81
|
+
" def __init__(self, raw: int) -> None:",
|
|
82
|
+
" self.raw = raw",
|
|
83
|
+
"",
|
|
84
|
+
" @property",
|
|
85
|
+
" def value(self) -> int:",
|
|
86
|
+
" return compute_%02d(self.raw)" % i,
|
|
87
|
+
"",
|
|
88
|
+
" def label(self) -> str:",
|
|
89
|
+
' return join_parts(self.kind, self.value, LIMIT_%02d)' % i,
|
|
90
|
+
"",
|
|
91
|
+
" def __eq__(self, other: object) -> bool:",
|
|
92
|
+
" if not isinstance(other, Item_%02d):" % i,
|
|
93
|
+
" return NotImplemented",
|
|
94
|
+
" return self.value == other.value",
|
|
95
|
+
"",
|
|
96
|
+
" def __lt__(self, other: Item_%02d) -> bool:" % i,
|
|
97
|
+
" return self.value < other.value",
|
|
98
|
+
"",
|
|
99
|
+
" def __hash__(self) -> int:",
|
|
100
|
+
" return hash((self.kind, self.value))",
|
|
101
|
+
"",
|
|
102
|
+
]
|
|
103
|
+
out += [
|
|
104
|
+
"",
|
|
105
|
+
"# ---- kept in place ----",
|
|
106
|
+
"",
|
|
107
|
+
'VERSION = "1.0"',
|
|
108
|
+
"",
|
|
109
|
+
"",
|
|
110
|
+
"def describe() -> str:",
|
|
111
|
+
" return join_parts(VERSION, compute_00(1), Item_01(2).label())",
|
|
112
|
+
"",
|
|
113
|
+
"",
|
|
114
|
+
"DEFAULT_TOTAL = LIMIT_03 * 2",
|
|
115
|
+
]
|
|
116
|
+
return "\n".join(out) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
TESTS = '''import asyncio
|
|
120
|
+
import os
|
|
121
|
+
import unittest
|
|
122
|
+
|
|
123
|
+
from pkg.big import (
|
|
124
|
+
DEFAULT_TOTAL,
|
|
125
|
+
LIMIT_05,
|
|
126
|
+
VERSION,
|
|
127
|
+
Item_03,
|
|
128
|
+
_scale_02,
|
|
129
|
+
_SEPARATOR,
|
|
130
|
+
compute_04,
|
|
131
|
+
compute_89,
|
|
132
|
+
describe,
|
|
133
|
+
fetch_08,
|
|
134
|
+
join_parts,
|
|
135
|
+
)
|
|
136
|
+
import pkg.big as big
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class BigTest(unittest.TestCase):
|
|
140
|
+
def test_constant(self):
|
|
141
|
+
self.assertEqual(LIMIT_05, 15)
|
|
142
|
+
|
|
143
|
+
def test_private_helper(self):
|
|
144
|
+
self.assertEqual(_scale_02(-2), 6)
|
|
145
|
+
|
|
146
|
+
def test_compute_clamps(self):
|
|
147
|
+
self.assertEqual(compute_04(100), 14)
|
|
148
|
+
|
|
149
|
+
def test_compute_last(self):
|
|
150
|
+
self.assertEqual(compute_89(1), 6)
|
|
151
|
+
|
|
152
|
+
def test_join(self):
|
|
153
|
+
self.assertEqual(join_parts("a", 1), "a" + os.sep + "|1")
|
|
154
|
+
self.assertEqual(_SEPARATOR, os.sep + "|")
|
|
155
|
+
|
|
156
|
+
def test_async(self):
|
|
157
|
+
self.assertEqual(asyncio.run(fetch_08(1)), {"id": 8, "value": 2})
|
|
158
|
+
|
|
159
|
+
def test_item_order(self):
|
|
160
|
+
self.assertLess(Item_03(1), Item_03(2))
|
|
161
|
+
self.assertEqual(Item_03(1), Item_03(1))
|
|
162
|
+
self.assertGreaterEqual(Item_03(3), Item_03(2))
|
|
163
|
+
|
|
164
|
+
def test_item_label(self):
|
|
165
|
+
self.assertEqual(Item_03(1).label(), join_parts("item-03", 4, 13))
|
|
166
|
+
|
|
167
|
+
def test_kept_names(self):
|
|
168
|
+
self.assertEqual(VERSION, "1.0")
|
|
169
|
+
self.assertEqual(DEFAULT_TOTAL, 26)
|
|
170
|
+
self.assertTrue(describe().startswith("1.0"))
|
|
171
|
+
|
|
172
|
+
def test_module_attributes(self):
|
|
173
|
+
self.assertIs(big.os, os)
|
|
174
|
+
self.assertEqual(big.Item_29(1).value, 2)
|
|
175
|
+
|
|
176
|
+
@unittest.skip("documents a skipped case for the count check")
|
|
177
|
+
def test_skipped(self):
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
unittest.main()
|
|
183
|
+
'''
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv):
|
|
187
|
+
if len(argv) != 1:
|
|
188
|
+
sys.stderr.write("usage: gen_big.py <out_root>\n")
|
|
189
|
+
return 2
|
|
190
|
+
root = argv[0]
|
|
191
|
+
for d in ("pkg", "tests"):
|
|
192
|
+
os.makedirs(os.path.join(root, d), exist_ok=True)
|
|
193
|
+
files = {"pkg/__init__.py": "", "pkg/big.py": big(), "tests/__init__.py": "", "tests/test_big.py": TESTS}
|
|
194
|
+
for rel, text in files.items():
|
|
195
|
+
with open(os.path.join(root, rel), "w", encoding="utf-8", newline="\n") as fh:
|
|
196
|
+
fh.write(text)
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
//
|
|
5
|
+
// gen_big.mjs <repo dir>: writes the TypeScript refactor:split fixture used by scripts/symbol-move-ts.test.sh
|
|
6
|
+
// (forge#4212): src/big.ts (~700 lines: exported consts, non-exported helpers each used only by its exported
|
|
7
|
+
// function, interfaces, classes, async functions, kept code that uses moved names, a relative import), src/util.ts
|
|
8
|
+
// and a node:test suite in test/big.test.ts (one skipped test). Specifiers carry the .ts extension so Node runs
|
|
9
|
+
// the sources with type stripping.
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
const root = process.argv[2];
|
|
14
|
+
if (!root) { process.stderr.write("usage: gen_big.mjs <dir>\n"); process.exit(2); }
|
|
15
|
+
const N = 20;
|
|
16
|
+
const pad = (i) => String(i).padStart(2, "0");
|
|
17
|
+
const out = [];
|
|
18
|
+
out.push("// SPDX-License-Identifier: MIT", "// big.ts: a hotspot module for the split fixture.", "",
|
|
19
|
+
'import { clamp, type Unit } from "./util.ts";', "");
|
|
20
|
+
for (let i = 0; i < N; i++) out.push(`export const LIMIT_${pad(i)} = ${100 + i};`);
|
|
21
|
+
out.push("");
|
|
22
|
+
for (let i = 0; i < N; i++) {
|
|
23
|
+
out.push(`// scale step ${i}`, `function _scale_${pad(i)}(x: number): number {`, ` const factor = ${i + 1};`, " return x * factor;", "}", "");
|
|
24
|
+
out.push(`/** compute ${i}: scaled and clamped. */`, `export function compute_${pad(i)}(x: number): number {`, ` const scaled = _scale_${pad(i)}(x);`,
|
|
25
|
+
` return clamp(scaled, 0, LIMIT_${pad(i)});`, "}", "");
|
|
26
|
+
}
|
|
27
|
+
for (let i = 0; i < N; i++) {
|
|
28
|
+
out.push(`export interface Item_${pad(i)} {`, " id: number;", " unit: Unit;", "}", "");
|
|
29
|
+
out.push(`export class Store_${pad(i)} {`, ` items: Item_${pad(i)}[] = [];`, ` add(item: Item_${pad(i)}): number {`, " this.items.push(item);",
|
|
30
|
+
` return compute_${pad(i)}(item.id);`, " }", "", " size(): number {", " return this.items.length;", " }", "}", "");
|
|
31
|
+
}
|
|
32
|
+
for (let i = 0; i < N; i++) {
|
|
33
|
+
out.push(`export async function fetch_${pad(i)}(x: number): Promise<number> {`, " await Promise.resolve();", ` return compute_${pad(i)}(x) + 1;`, "}", "");
|
|
34
|
+
}
|
|
35
|
+
out.push("// kept code that uses moved names", "export function summary(): number {", " return compute_00(1) + LIMIT_01 + new Store_02().size();", "}", "",
|
|
36
|
+
"function _shared(x: number): number {", " return x + 1;", "}", "", "export function kept_total(x: number): number {", " return _shared(x) * 2;", "}", "",
|
|
37
|
+
"export function ping(n: number): number {", " return n <= 0 ? 0 : pong(n - 1);", "}", "", "export function pong(n: number): number {", " return n <= 0 ? 1 : ping(n - 1);", "}");
|
|
38
|
+
|
|
39
|
+
const test = [
|
|
40
|
+
'import { test } from "node:test";', 'import assert from "node:assert/strict";',
|
|
41
|
+
'import { compute_03, LIMIT_05, Store_04, fetch_06, summary, kept_total, ping } from "../src/big.ts";', "",
|
|
42
|
+
'test("compute clamps", () => { assert.equal(compute_03(1000), 103); assert.equal(compute_03(2), 8); });',
|
|
43
|
+
'test("limits", () => { assert.equal(LIMIT_05, 105); });',
|
|
44
|
+
'test("store", () => { const s = new Store_04(); assert.equal(s.add({ id: 2, unit: "m" }), 10); assert.equal(s.size(), 1); });',
|
|
45
|
+
'test("fetch", async () => { assert.equal(await fetch_06(1), 8); });',
|
|
46
|
+
'test("kept", () => { assert.equal(summary(), 1 + 101 + 0); assert.equal(kept_total(1), 4); assert.equal(ping(3), 1); });',
|
|
47
|
+
'test("later", { skip: "not yet" }, () => {});', "",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
mkdirSync(path.join(root, "src"), { recursive: true });
|
|
51
|
+
mkdirSync(path.join(root, "test"), { recursive: true });
|
|
52
|
+
writeFileSync(path.join(root, "src/big.ts"), out.join("\n") + "\n");
|
|
53
|
+
writeFileSync(path.join(root, "src/util.ts"),
|
|
54
|
+
'export type Unit = "m" | "s";\n\nexport function clamp(x: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, x));\n}\n');
|
|
55
|
+
writeFileSync(path.join(root, "test/big.test.ts"), test.join("\n"));
|
|
56
|
+
writeFileSync(path.join(root, "package.json"), '{ "name": "split-fixture", "private": true, "type": "module" }\n');
|
|
@@ -341,7 +341,7 @@ hr_check_stub review-pr "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT derive-finding-mi
|
|
|
341
341
|
hr_check_stub review-pr "$HR_CLASSIFY" PRIORITY_SCRIPT severity-to-priority.sh
|
|
342
342
|
hr_check_stub review-prepare "$HR_PREPARE" CODE_INDEX_SCRIPT code-index.sh
|
|
343
343
|
hr_check_stub review-pr "$HR_CLASSIFY" DEDUP_SCRIPT issue-dedup.sh
|
|
344
|
-
|
|
344
|
+
hr_check_stub issue "$ROOT/commands/issue.md" ISSUE_CREATE_SCRIPT issue-create.sh
|
|
345
345
|
stub_block "$HR_CLASSIFY" DERIVE_MILESTONE_SCRIPT > "$T/hr_dm.sh"
|
|
346
346
|
stub_block "$HR_CLASSIFY" PRIORITY_SCRIPT > "$T/hr_pr.sh"
|
|
347
347
|
# no bare cwd-relative helper call in either review spec
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# hotspots.sh — deterministic size and churn facts for named files (forge#4195). No LLM, no network: git + wc + jq,
|
|
6
|
+
# yq (optional) only to read forge.yaml. Run from the repo root.
|
|
7
|
+
#
|
|
8
|
+
# Usage: hotspots.sh [--base <ref>] [--] <path>...
|
|
9
|
+
# Config: forge.yaml maintenance.hotspot {enabled, file_lines, growth_lines, exclude_globs}; defaults true/2000/150/[]
|
|
10
|
+
# (also when forge.yaml or yq is missing).
|
|
11
|
+
# Output (stdout, one JSON object):
|
|
12
|
+
# {"enabled","file_lines","growth_lines","files":[{path,lines,commits_90d,hotspot,over,excluded,base_lines,growth}]}
|
|
13
|
+
# lines = `wc -l` of the working-tree file (0 when missing); commits_90d = `git rev-list --count --since=90.days HEAD`;
|
|
14
|
+
# hotspot = lines × commits_90d; over = lines > file_lines; excluded = linguist-generated / linguist-vendored set by
|
|
15
|
+
# .gitattributes, or a match of an exclude_glob (shell pattern, `*` crosses `/`). Without --base no blob is read and
|
|
16
|
+
# base_lines/growth are null; with --base they are the line count at git merge-base(<ref>, HEAD) (0 when absent there)
|
|
17
|
+
# and lines - base_lines. enabled:false prints files:[] and does no per-file work.
|
|
18
|
+
# Exit: 0 ok | 2 usage or bad ref. bash 3.2 portable.
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
die() { echo "hotspots: $*" >&2; exit 2; }
|
|
22
|
+
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
23
|
+
BASE=""
|
|
24
|
+
while [ $# -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--base) [ $# -ge 2 ] && [ -n "$2" ] || die "usage: hotspots.sh [--base <ref>] [--] <path>..."; BASE="$2"; shift 2 ;;
|
|
27
|
+
--) shift; break ;;
|
|
28
|
+
-*) die "unknown option: $1" ;;
|
|
29
|
+
*) break ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
# ---- config: maintenance.hotspot as JSON, {} when unreadable ----
|
|
34
|
+
CFG='{}'
|
|
35
|
+
if [ -f forge.yaml ] && command -v yq >/dev/null 2>&1; then
|
|
36
|
+
c=$(yq -o=json '.maintenance.hotspot // {}' forge.yaml 2>/dev/null) || c=""
|
|
37
|
+
printf '%s' "$c" | jq -e 'type == "object"' >/dev/null 2>&1 && CFG="$c"
|
|
38
|
+
fi
|
|
39
|
+
CFG=$(printf '%s' "$CFG" | jq -c '
|
|
40
|
+
def pos($d): if type == "number" and . > 0 then floor else $d end;
|
|
41
|
+
{ enabled: (.enabled != false), file_lines: (.file_lines | pos(2000)), growth_lines: (.growth_lines | pos(150)),
|
|
42
|
+
exclude_globs: ([(.exclude_globs // [])[]? | strings]) }')
|
|
43
|
+
ENABLED=$(printf '%s' "$CFG" | jq -r '.enabled')
|
|
44
|
+
FILE_LINES=$(printf '%s' "$CFG" | jq -r '.file_lines')
|
|
45
|
+
GROWTH_LINES=$(printf '%s' "$CFG" | jq -r '.growth_lines')
|
|
46
|
+
head_json() { printf '%s' "$CFG" | jq -c --argjson files "$1" '{enabled, file_lines, growth_lines, files: $files}'; }
|
|
47
|
+
[ "$ENABLED" = true ] || { head_json '[]'; exit 0; }
|
|
48
|
+
|
|
49
|
+
MB=""
|
|
50
|
+
if [ -n "$BASE" ]; then
|
|
51
|
+
git rev-parse --verify -q "$BASE^{commit}" >/dev/null || die "bad base ref: $BASE"
|
|
52
|
+
MB=$(git merge-base "$BASE" HEAD) || die "no merge base for $BASE and HEAD"
|
|
53
|
+
fi
|
|
54
|
+
HAS_HEAD=0; git rev-parse --verify -q "HEAD^{commit}" >/dev/null && HAS_HEAD=1
|
|
55
|
+
GLOBS=$(printf '%s' "$CFG" | jq -r '.exclude_globs[]')
|
|
56
|
+
|
|
57
|
+
T=$(mktemp -d "${TMPDIR:-/tmp}/hotspots.XXXXXX") || die "mktemp failed"
|
|
58
|
+
trap 'rm -rf "$T"' EXIT
|
|
59
|
+
: > "$T/rows"
|
|
60
|
+
# one history walk per file, 8 at a time: wall time is the slowest batch, not the sum
|
|
61
|
+
i=0
|
|
62
|
+
for p in "$@"; do
|
|
63
|
+
i=$((i+1))
|
|
64
|
+
[ "$HAS_HEAD" = 1 ] && [ -n "${p#./}" ] && { git rev-list --count --since=90.days HEAD -- "${p#./}" > "$T/c.$i" 2>/dev/null & }
|
|
65
|
+
[ $((i % 8)) -eq 0 ] && wait
|
|
66
|
+
done
|
|
67
|
+
wait
|
|
68
|
+
i=0
|
|
69
|
+
for p in "$@"; do
|
|
70
|
+
i=$((i+1))
|
|
71
|
+
p=${p#./}
|
|
72
|
+
[ -n "$p" ] || continue
|
|
73
|
+
lines=0; [ -f "$p" ] && lines=$(wc -l < "$p" | tr -d ' ')
|
|
74
|
+
commits=$(cat "$T/c.$i" 2>/dev/null); case "$commits" in ''|*[!0-9]*) commits=0 ;; esac
|
|
75
|
+
excluded=false
|
|
76
|
+
if git check-attr linguist-generated linguist-vendored -- "$p" 2>/dev/null | grep -qE ': (set|true)$'; then excluded=true; fi
|
|
77
|
+
if [ "$excluded" = false ] && [ -n "$GLOBS" ]; then
|
|
78
|
+
while IFS= read -r g; do
|
|
79
|
+
[ -n "$g" ] || continue
|
|
80
|
+
# shellcheck disable=SC2254
|
|
81
|
+
case "$p" in $g) excluded=true; break ;; esac
|
|
82
|
+
done <<EOF
|
|
83
|
+
$GLOBS
|
|
84
|
+
EOF
|
|
85
|
+
fi
|
|
86
|
+
base_lines=null; growth=null
|
|
87
|
+
if [ -n "$MB" ]; then
|
|
88
|
+
base_lines=0
|
|
89
|
+
git cat-file -e "$MB:$p" 2>/dev/null && base_lines=$(git show "$MB:$p" | wc -l | tr -d ' ')
|
|
90
|
+
growth=$((lines - base_lines))
|
|
91
|
+
fi
|
|
92
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$p" "$lines" "$commits" "$excluded" "$base_lines" "$growth" >> "$T/rows"
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
FILES=$(jq -R -s -c --argjson fl "$FILE_LINES" '
|
|
96
|
+
[split("\n")[] | select(length > 0) | split("\t") |
|
|
97
|
+
{ path: .[0], lines: (.[1] | tonumber), commits_90d: (.[2] | tonumber) } as $r |
|
|
98
|
+
$r + { hotspot: ($r.lines * $r.commits_90d), over: ($r.lines > $fl), excluded: (.[3] == "true"),
|
|
99
|
+
base_lines: (.[4] | fromjson), growth: (.[5] | fromjson) }]' < "$T/rows")
|
|
100
|
+
head_json "$FILES"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hotspots.test.sh — cases for scripts/hotspots.sh (forge#4195).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
S="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/hotspots.sh"
|
|
7
|
+
PASS=0; FAILN=0
|
|
8
|
+
TMP="$(mktemp -d "${TMPDIR:-/tmp}/hotspots-test.XXXXXX")"; trap 'rm -rf "$TMP"' EXIT
|
|
9
|
+
ok() { PASS=$((PASS+1)); }
|
|
10
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
11
|
+
eq() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got: $2 want: $3)"; fi }
|
|
12
|
+
q() { printf '%s' "$OUT" | jq -c "$1"; }
|
|
13
|
+
HAVE_YQ=1; yq -o=json '.' - <<<'a: 1' 2>/dev/null | grep -q '"a"' || HAVE_YQ=0
|
|
14
|
+
|
|
15
|
+
new_repo() {
|
|
16
|
+
rm -rf "$TMP/w"; mkdir -p "$TMP/w"; cd "$TMP/w" || exit 1
|
|
17
|
+
git init -q -b main; git config user.email t@example.com; git config user.name t; git config commit.gpgsign false
|
|
18
|
+
}
|
|
19
|
+
commit() { git add -A; git commit -qm "$1"; }
|
|
20
|
+
lines() { i=0; : > "$1"; while [ $i -lt "$2" ]; do echo "l$i" >> "$1"; i=$((i+1)); done; }
|
|
21
|
+
|
|
22
|
+
# ---- 1. exact lines, commits_90d, hotspot, over; old commits outside the window are not counted ----
|
|
23
|
+
new_repo
|
|
24
|
+
lines big.py 2100; GIT_COMMITTER_DATE="2000-01-01T00:00:00Z" GIT_AUTHOR_DATE="2000-01-01T00:00:00Z" commit old
|
|
25
|
+
echo x >> big.py; commit c1
|
|
26
|
+
echo y >> big.py; lines small.py 10; commit c2
|
|
27
|
+
OUT="$(bash "$S" big.py ./small.py missing/gone.py)"; rc=$?
|
|
28
|
+
eq "rc" "$rc" 0
|
|
29
|
+
eq "defaults" "$(q '[.enabled,.file_lines,.growth_lines]')" '[true,2000,150]'
|
|
30
|
+
eq "big facts" "$(q '.files[0]')" '{"path":"big.py","lines":2102,"commits_90d":2,"hotspot":4204,"over":true,"excluded":false,"base_lines":null,"growth":null}'
|
|
31
|
+
eq "small facts (./ stripped)" "$(q '.files[1] | [.path,.lines,.commits_90d,.over]')" '["small.py",10,1,false]'
|
|
32
|
+
eq "missing file" "$(q '.files[2] | [.path,.lines,.commits_90d,.over]')" '["missing/gone.py",0,0,false]'
|
|
33
|
+
OUT="$(bash "$S")"; eq "no paths" "$(q '.files')" '[]'
|
|
34
|
+
|
|
35
|
+
# ---- 2. --base growth: merge-base side, new file counts from 0 ----
|
|
36
|
+
git checkout -q -b feat
|
|
37
|
+
lines extra 200; cat extra >> big.py; lines fresh.py 30; commit grow
|
|
38
|
+
OUT="$(bash "$S" --base main -- big.py fresh.py)"
|
|
39
|
+
eq "growth" "$(q '.files[0] | [.base_lines,.growth,.lines]')" '[2102,200,2302]'
|
|
40
|
+
eq "new file growth" "$(q '.files[1] | [.base_lines,.growth]')" '[0,30]'
|
|
41
|
+
bash "$S" --base no-such-ref big.py >/dev/null 2>&1; eq "bad ref exit" "$?" 2
|
|
42
|
+
bash "$S" --base >/dev/null 2>&1; eq "usage exit" "$?" 2
|
|
43
|
+
bash "$S" --bogus x >/dev/null 2>&1; eq "unknown option exit" "$?" 2
|
|
44
|
+
|
|
45
|
+
# ---- 3. exclusions: linguist-generated, linguist-vendored, forge.yaml globs ----
|
|
46
|
+
mkdir -p gen vendor/lib dist
|
|
47
|
+
lines gen/api.py 2500; lines vendor/lib/x.js 2500; lines dist/bundle.js 2500
|
|
48
|
+
printf 'gen/** linguist-generated\nvendor/** linguist-vendored=true\n' > .gitattributes
|
|
49
|
+
commit gen
|
|
50
|
+
OUT="$(bash "$S" gen/api.py vendor/lib/x.js dist/bundle.js big.py)"
|
|
51
|
+
eq "attr exclusions" "$(q '[.files[] | .excluded]')" '[true,true,false,false]'
|
|
52
|
+
if [ "$HAVE_YQ" = 1 ]; then
|
|
53
|
+
printf 'maintenance:\n hotspot:\n file_lines: 100\n growth_lines: 5\n exclude_globs: ["dist/*", "*.lock"]\n' > forge.yaml
|
|
54
|
+
OUT="$(bash "$S" dist/bundle.js small.py big.py)"
|
|
55
|
+
eq "config thresholds" "$(q '[.file_lines,.growth_lines]')" '[100,5]'
|
|
56
|
+
eq "glob exclusion" "$(q '[.files[] | .excluded]')" '[true,false,false]'
|
|
57
|
+
eq "over uses configured file_lines" "$(q '[.files[] | .over]')" '[true,false,true]'
|
|
58
|
+
printf 'maintenance:\n hotspot:\n enabled: false\n' > forge.yaml
|
|
59
|
+
OUT="$(bash "$S" big.py)"
|
|
60
|
+
eq "opt-out" "$(q '[.enabled,.files]')" '[false,[]]'
|
|
61
|
+
printf 'maintenance:\n hotspot:\n file_lines: nope\n' > forge.yaml
|
|
62
|
+
OUT="$(bash "$S" big.py)"; eq "bad value falls back" "$(q '.file_lines')" 2000
|
|
63
|
+
rm -f forge.yaml
|
|
64
|
+
else
|
|
65
|
+
echo "SKIP: forge.yaml cases need mikefarah yq v4"
|
|
66
|
+
fi
|
|
67
|
+
# yq missing: defaults, still exit 0
|
|
68
|
+
mkdir -p "$TMP/noyq"; printf '#!/bin/sh\nexit 1\n' > "$TMP/noyq/yq"; chmod +x "$TMP/noyq/yq"
|
|
69
|
+
printf 'maintenance:\n hotspot:\n file_lines: 100\n' > forge.yaml
|
|
70
|
+
OUT="$(PATH="$TMP/noyq:$PATH" bash "$S" big.py)"; eq "yq unusable: defaults" "$(q '.file_lines')" 2000
|
|
71
|
+
rm -f forge.yaml
|
|
72
|
+
|
|
73
|
+
# ---- 4. 30k-commit repo: exact counts under 1 s, no blob reads, no network ----
|
|
74
|
+
rm -rf "$TMP/big"; git init -q -b main "$TMP/big"; cd "$TMP/big" || exit 1
|
|
75
|
+
now=$(date +%s)
|
|
76
|
+
awk -v now="$now" 'BEGIN { for (i = 1; i <= 30000; i++) {
|
|
77
|
+
ts = (i > 27000) ? now - (30000 - i) * 60 : now - 200 * 86400 - (27000 - i) * 600
|
|
78
|
+
print "commit refs/heads/main"; print "mark :" i; print "committer t <t@e> " ts " +0000"; print "data 1"; print "x"
|
|
79
|
+
if (i > 1) print "from :" (i - 1)
|
|
80
|
+
f = (i % 10 == 0) ? "hot.py" : "other" (i % 50) ".txt"; c = "line " i
|
|
81
|
+
print "M 644 inline " f; print "data " length(c) + 1; print c; print "" } }' | git fast-import --quiet
|
|
82
|
+
git checkout -q main
|
|
83
|
+
mkdir -p "$TMP/bin"
|
|
84
|
+
for t in gh curl wget npm npx pnpm yarn nc ssh; do printf '#!/bin/sh\necho "%s" >> "%s/net.log"\nexit 1\n' "$t" "$TMP" > "$TMP/bin/$t"; chmod +x "$TMP/bin/$t"; done
|
|
85
|
+
best=""
|
|
86
|
+
for _ in 1 2 3; do
|
|
87
|
+
start=$(date +%s%N 2>/dev/null)
|
|
88
|
+
OUT="$(PATH="$TMP/bin:$PATH" GIT_TRACE2_EVENT="$TMP/trace.json" bash "$S" hot.py other1.txt other2.txt other3.txt)"
|
|
89
|
+
end=$(date +%s%N 2>/dev/null)
|
|
90
|
+
case "$start$end" in *N*|"") best=0; break ;; esac
|
|
91
|
+
ms=$(( (end - start) / 1000000 )); if [ -z "$best" ] || [ "$ms" -lt "$best" ]; then best=$ms; fi
|
|
92
|
+
done
|
|
93
|
+
eq "30k exact commits" "$(q '[.files[] | .commits_90d]')" '[300,60,60,60]'
|
|
94
|
+
eq "30k exact lines" "$(q '[.files[] | .lines]')" '[1,1,1,1]'
|
|
95
|
+
if [ "$best" -lt 1000 ]; then ok; else bad "30k-commit run took ${best} ms (limit 1000)"; fi
|
|
96
|
+
grep -q '"rev-list"' "$TMP/trace.json" && ok || bad "git trace not captured"
|
|
97
|
+
if grep -qE '"(cat-file|show)"' "$TMP/trace.json"; then bad "blob read without --base"; else ok; fi
|
|
98
|
+
if [ -e "$TMP/net.log" ]; then bad "network tool invoked: $(tr '\n' ' ' < "$TMP/net.log")"; else ok; fi
|
|
99
|
+
|
|
100
|
+
echo "hotspots.test.sh: $PASS passed, $FAILN failed"
|
|
101
|
+
[ "$FAILN" -eq 0 ]
|