agentic-workflow-manager 9.19.0 → 9.20.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.
Files changed (59) hide show
  1. package/dist/src/commands/change/index.js +492 -0
  2. package/dist/src/commands/changes/index.js +84 -0
  3. package/dist/src/commands/sensors/index.js +9 -0
  4. package/dist/src/commands/sensors/run.js +1 -0
  5. package/dist/src/core/changes/command.js +38 -0
  6. package/dist/src/core/changes/derive.js +120 -0
  7. package/dist/src/core/changes/fs-safe.js +47 -0
  8. package/dist/src/core/changes/git-env.js +68 -0
  9. package/dist/src/core/changes/git.js +216 -0
  10. package/dist/src/core/changes/list-parse.js +67 -0
  11. package/dist/src/core/changes/list.js +408 -0
  12. package/dist/src/core/changes/parse.js +72 -0
  13. package/dist/src/core/changes/range.js +53 -0
  14. package/dist/src/core/changes/receipt.js +274 -0
  15. package/dist/src/core/changes/record-io.js +149 -0
  16. package/dist/src/core/changes/render-list.js +109 -0
  17. package/dist/src/core/changes/render.js +178 -0
  18. package/dist/src/core/changes/text-safety.js +116 -0
  19. package/dist/src/core/changes/trailers.js +76 -0
  20. package/dist/src/core/changes/types.js +26 -0
  21. package/dist/src/core/changes/validate-shape.js +219 -0
  22. package/dist/src/core/changes/worktree.js +53 -0
  23. package/dist/src/index.js +4 -0
  24. package/dist/src/release/orchestrator.js +3 -1
  25. package/dist/tests/commands/change/new.test.js +1126 -0
  26. package/dist/tests/commands/change/validate.test.js +1145 -0
  27. package/dist/tests/commands/changes/list.test.js +423 -0
  28. package/dist/tests/commands/sensors/index.test.js +1 -1
  29. package/dist/tests/core/changes/derive.test.js +455 -0
  30. package/dist/tests/core/changes/fs-safe.test.js +82 -0
  31. package/dist/tests/core/changes/git-env.test.js +143 -0
  32. package/dist/tests/core/changes/git.test.js +442 -0
  33. package/dist/tests/core/changes/helpers.js +269 -0
  34. package/dist/tests/core/changes/list-fixtures.js +122 -0
  35. package/dist/tests/core/changes/list-long-history.test.js +52 -0
  36. package/dist/tests/core/changes/list-paging.test.js +511 -0
  37. package/dist/tests/core/changes/list-parse.test.js +120 -0
  38. package/dist/tests/core/changes/list-tree.test.js +44 -0
  39. package/dist/tests/core/changes/list.test.js +821 -0
  40. package/dist/tests/core/changes/parse.test.js +142 -0
  41. package/dist/tests/core/changes/predicates.test.js +83 -0
  42. package/dist/tests/core/changes/range.test.js +92 -0
  43. package/dist/tests/core/changes/receipt.test.js +947 -0
  44. package/dist/tests/core/changes/record-io.test.js +307 -0
  45. package/dist/tests/core/changes/record.test.js +549 -0
  46. package/dist/tests/core/changes/render-list.test.js +212 -0
  47. package/dist/tests/core/changes/render.test.js +202 -0
  48. package/dist/tests/core/changes/text-safety.test.js +226 -0
  49. package/dist/tests/core/changes/trailers.test.js +181 -0
  50. package/dist/tests/core/changes/worktree.test.js +259 -0
  51. package/dist/tests/release/orchestrator-trailer.test.js +72 -0
  52. package/dist/tests/release/orchestrator.test.js +1 -0
  53. package/dist/tests/structural/change-commands-are-registered.test.js +29 -0
  54. package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +77 -0
  55. package/package.json +1 -1
  56. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  57. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  58. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  59. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.nonRepoDir = exports.bad = exports.posixOnly = void 0;
40
+ exports.emptyGitConfig = emptyGitConfig;
41
+ exports.makeRepo = makeRepo;
42
+ exports.gitVersion = gitVersion;
43
+ exports.gitAtLeast = gitAtLeast;
44
+ exports.cloneRepo = cloneRepo;
45
+ exports.expectLinearScaling = expectLinearScaling;
46
+ exports.spyOnGit = spyOnGit;
47
+ exports.withEnv = withEnv;
48
+ exports.importCommits = importCommits;
49
+ exports.branchOff = branchOff;
50
+ exports.commitForgedSigned = commitForgedSigned;
51
+ exports.showSignaturesInLog = showSignaturesInLog;
52
+ exports.makeHiddenSubmodule = makeHiddenSubmodule;
53
+ const fs_1 = __importDefault(require("fs"));
54
+ const os_1 = __importDefault(require("os"));
55
+ const path_1 = __importDefault(require("path"));
56
+ const child_process_1 = __importStar(require("child_process"));
57
+ // Git's identity and config isolation travel in an explicit environment object for the helper's own git calls.
58
+ // HOME, AWM_HOME and the git config variables are also overridden on `process.env` (and restored), so that code under
59
+ // test (which hands process.env to git) can never reach the real `~/.awm` or the developer's / system git config.
60
+ const ENV_KEYS = ['HOME', 'AWM_HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM'];
61
+ /**
62
+ * An empty, real global git config inside `home`. `os.devNull` is not usable for GIT_CONFIG_GLOBAL: on Windows it is `\\.\nul`, which
63
+ * git cannot open (`fatal: unable to access '\\.\nul': Invalid argument`). A real empty file isolates the developer's config everywhere.
64
+ */
65
+ function emptyGitConfig(home) {
66
+ const file = path_1.default.join(home, 'gitconfig');
67
+ fs_1.default.writeFileSync(file, '');
68
+ return file;
69
+ }
70
+ /** The git runner of a repository: identity and config isolation in an explicit environment, optional explicit dates. */
71
+ function gitApi(root, home) {
72
+ const env = {
73
+ ...process.env, HOME: home, GIT_CONFIG_GLOBAL: path_1.default.join(home, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1',
74
+ GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t',
75
+ };
76
+ const run = (extra, args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd: root, encoding: 'utf8', env: { ...env, ...extra } }).trim();
77
+ const dates = (date) => {
78
+ const stamp = `${Math.floor(Date.parse(date) / 1000)} +0000`;
79
+ return { GIT_AUTHOR_DATE: stamp, GIT_COMMITTER_DATE: stamp };
80
+ };
81
+ const git = (...args) => run({}, args);
82
+ const gitInput = (input, ...args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd: root, encoding: 'utf8', env, input }).trim();
83
+ const gitAt = (date, ...args) => run(dates(date), args);
84
+ const commit = (files, message, date) => {
85
+ for (const [rel, content] of Object.entries(files)) {
86
+ const abs = path_1.default.join(root, rel);
87
+ fs_1.default.mkdirSync(path_1.default.dirname(abs), { recursive: true });
88
+ fs_1.default.writeFileSync(abs, content);
89
+ }
90
+ git('add', '-A');
91
+ run(date === undefined ? {} : dates(date), ['commit', '-q', '-m', message]);
92
+ return git('rev-parse', 'HEAD');
93
+ };
94
+ return { git, gitInput, gitAt, commit };
95
+ }
96
+ /** Isolated repo: HOME/AWM_HOME point at a tmpdir; never touches the real ~/.awm. */
97
+ function makeRepo() {
98
+ const saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
99
+ const home = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-')));
100
+ Object.assign(process.env, { HOME: home, AWM_HOME: path_1.default.join(home, '.awm'), GIT_CONFIG_GLOBAL: emptyGitConfig(home), GIT_CONFIG_NOSYSTEM: '1' });
101
+ const root = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-chg-')));
102
+ const { git, gitInput, gitAt, commit } = gitApi(root, home);
103
+ const cleanup = () => {
104
+ for (const [k, v] of Object.entries(saved)) {
105
+ if (v === undefined)
106
+ delete process.env[k];
107
+ else
108
+ process.env[k] = v;
109
+ }
110
+ fs_1.default.rmSync(root, { recursive: true, force: true });
111
+ fs_1.default.rmSync(home, { recursive: true, force: true });
112
+ };
113
+ try {
114
+ git('init', '-q', '-b', 'main');
115
+ commit({ '.gitignore': '.awm/\n', 'README.md': 'x' }, 'chore: init');
116
+ }
117
+ catch (error) {
118
+ cleanup();
119
+ throw error;
120
+ }
121
+ return { root, home, git, gitInput, gitAt, commit, cleanup };
122
+ }
123
+ /** The version of the installed git as [major, minor], or null when it cannot be read. */
124
+ function gitVersion() {
125
+ try {
126
+ const m = /^git version (\d+)\.(\d+)/.exec((0, child_process_1.execFileSync)('git', ['--version'], { encoding: 'utf8' }));
127
+ return m ? [Number(m[1]), Number(m[2])] : null;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ /** True when the installed git is `major.minor` or newer (false when its version cannot be read). */
134
+ function gitAtLeast(major, minor) {
135
+ const v = gitVersion();
136
+ return v !== null && (v[0] > major || (v[0] === major && v[1] >= minor));
137
+ }
138
+ /** `test` where symlinks and POSIX modes exist; reported as skipped (not as passed) on Windows. */
139
+ exports.posixOnly = process.platform === 'win32' ? test.skip : test;
140
+ /**
141
+ * A copy of `template` in its own directory (same throwaway home, same git isolation): a few milliseconds instead of the
142
+ * several git processes `makeRepo` spawns. For suites that need a fresh repository per test. `cleanup` removes only the copy;
143
+ * the template's `cleanup` removes the shared home and restores the environment.
144
+ */
145
+ function cloneRepo(template) {
146
+ const root = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-chg-')));
147
+ fs_1.default.cpSync(template.root, root, { recursive: true });
148
+ const { git, gitInput, gitAt, commit } = gitApi(root, template.home);
149
+ return { root, home: template.home, git, gitInput, gitAt, commit, cleanup: () => fs_1.default.rmSync(root, { recursive: true, force: true }) };
150
+ }
151
+ /**
152
+ * For the linear-time tests of PURE code (no git child): `run(n)` is called with a small and a 10x larger input and the best of a
153
+ * few runs of each is compared. Linear code takes about 10x as long; a quadratic scan about 100x. The bound is 40x the small time
154
+ * (floored at 1 ms so a clock tick is not a ratio), which separates the two with a margin on both sides and does not depend on
155
+ * how fast or how loaded the runner is. `base` must be large enough for a quadratic scan to cost milliseconds at that size.
156
+ */
157
+ function expectLinearScaling(run, base) {
158
+ const best = (n) => {
159
+ let min = Infinity;
160
+ for (let i = 0; i < 3; i++) {
161
+ const started = performance.now();
162
+ run(n);
163
+ min = Math.min(min, performance.now() - started);
164
+ }
165
+ return min;
166
+ };
167
+ run(Math.max(1, Math.floor(base / 10))); // warm-up: JIT and caches
168
+ const small = Math.max(best(base), 1);
169
+ const large = best(base * 10);
170
+ expect({ small, large, bounded: large <= 40 * small }).toMatchObject({ bounded: true });
171
+ }
172
+ /** Casts a deliberately wrong value to whatever the function under test declares. */
173
+ const bad = (v) => v;
174
+ exports.bad = bad;
175
+ /**
176
+ * Spies on every git child the code under test spawns (`child_process.execFileSync`): records each argv in `calls` and then runs
177
+ * the real git, unless `intercept` throws (to simulate a failing or timed-out git). Undo with `restore()` (or `jest.restoreAllMocks`).
178
+ */
179
+ function spyOnGit(intercept) {
180
+ const calls = [];
181
+ const real = child_process_1.default.execFileSync;
182
+ const spy = jest.spyOn(child_process_1.default, 'execFileSync').mockImplementation(((cmd, args, opts) => {
183
+ if (cmd === 'git')
184
+ calls.push(args);
185
+ intercept?.(args);
186
+ return real(cmd, args, opts);
187
+ }));
188
+ return { calls, restore: () => spy.mockRestore() };
189
+ }
190
+ /** Sets variables on process.env (the environment runGit hands to git) for the duration of `fn`. */
191
+ function withEnv(vars, fn) {
192
+ const saved = Object.fromEntries(Object.keys(vars).map((k) => [k, process.env[k]]));
193
+ Object.assign(process.env, vars);
194
+ try {
195
+ return fn();
196
+ }
197
+ finally {
198
+ for (const [k, v] of Object.entries(saved)) {
199
+ if (v === undefined)
200
+ delete process.env[k];
201
+ else
202
+ process.env[k] = v;
203
+ }
204
+ }
205
+ }
206
+ /**
207
+ * Adds many commits on top of the current branch with ONE `git fast-import` process (instead of one add + commit per commit), then
208
+ * resets the working tree to the new tip. Explicit identity and committer dates; the history is what `repo.commit` would build.
209
+ */
210
+ function importCommits(repo, commits) {
211
+ const ref = `refs/heads/${repo.git('symbolic-ref', '--short', 'HEAD')}`;
212
+ const parts = [];
213
+ commits.forEach((c, i) => {
214
+ const when = `${Math.floor(Date.parse(c.date) / 1000)} +0000`;
215
+ const bytes = (text) => `data ${Buffer.byteLength(text)}\n${text}\n`;
216
+ parts.push(`commit ${ref}\nauthor t <t@t> ${when}\ncommitter t <t@t> ${when}\n${bytes(c.message)}`);
217
+ if (i === 0)
218
+ parts.push(`from ${ref}^0\n`);
219
+ for (const [rel, content] of Object.entries(c.files))
220
+ parts.push(`M 100644 inline ${rel}\n${bytes(content)}`);
221
+ parts.push('\n');
222
+ });
223
+ repo.gitInput(parts.join(''), 'fast-import', '--quiet');
224
+ repo.git('reset', '-q', '--hard');
225
+ }
226
+ /** Checks out `feat/x` and returns the commit it forked from (main). */
227
+ function branchOff(repo) {
228
+ repo.git('checkout', '-q', '-b', 'feat/x');
229
+ return repo.git('rev-parse', 'main');
230
+ }
231
+ /** A directory that is not a git repository. */
232
+ const nonRepoDir = (repo) => fs_1.default.mkdtempSync(path_1.default.join(repo.home, 'plain-'));
233
+ exports.nonRepoDir = nonRepoDir;
234
+ /** A commit carrying a forged `gpgsig` header, written straight into the object store (no gpg needed). */
235
+ function commitForgedSigned(repo, message, file = 'signed.ts') {
236
+ fs_1.default.writeFileSync(path_1.default.join(repo.root, file), '1');
237
+ repo.git('add', '-A');
238
+ const body = [`tree ${repo.git('write-tree')}`, `parent ${repo.git('rev-parse', 'HEAD')}`, 'author t <t@t> 1700000000 +0000', 'committer t <t@t> 1700000000 +0000',
239
+ 'gpgsig -----BEGIN PGP SIGNATURE-----', ' ', ' forged', ' -----END PGP SIGNATURE-----', '', message, ''].join('\n');
240
+ const bodyFile = path_1.default.join(repo.home, 'forged-commit.txt');
241
+ fs_1.default.writeFileSync(bodyFile, body);
242
+ const sha = repo.git('hash-object', '-t', 'commit', '-w', bodyFile);
243
+ repo.git('reset', '-q', '--hard', sha);
244
+ return sha;
245
+ }
246
+ /** Repo-local config (the helper only isolates the global one): git prints verification text before each signed commit. */
247
+ function showSignaturesInLog(repo) {
248
+ repo.git('config', 'log.showSignature', 'true');
249
+ repo.git('config', 'gpg.program', process.execPath); // any program that writes to stderr; the output is what matters, not a real gpg
250
+ }
251
+ /**
252
+ * Offline fixture: a nested repository at `relPath` registered as a gitlink of the repository at `root`, hidden by
253
+ * `.gitmodules` `ignore = all` and the repo-local `diff.ignoreSubmodules=all`, then committed. Returns helpers to run git
254
+ * inside it and to commit there (which moves the checkout away from the recorded gitlink).
255
+ */
256
+ function makeHiddenSubmodule(root, run, relPath = 'vendor/sm') {
257
+ const dir = path_1.default.join(root, ...relPath.split('/'));
258
+ fs_1.default.mkdirSync(dir, { recursive: true });
259
+ const inSm = (...args) => run(dir, ...args);
260
+ const commitInSm = (name) => { fs_1.default.writeFileSync(path_1.default.join(dir, name), name); inSm('add', '-A'); inSm('commit', '-q', '-m', `feat: ${name}`); return inSm('rev-parse', 'HEAD'); };
261
+ inSm('init', '-q', '-b', 'main');
262
+ const first = commitInSm('one');
263
+ fs_1.default.writeFileSync(path_1.default.join(root, '.gitmodules'), `[submodule "${relPath}"]\n\tpath = ${relPath}\n\turl = ./nowhere\n\tignore = all\n`);
264
+ run(root, 'config', 'diff.ignoreSubmodules', 'all');
265
+ run(root, 'add', '.gitmodules');
266
+ run(root, 'update-index', '--add', '--cacheinfo', `160000,${first},${relPath}`);
267
+ run(root, 'commit', '-q', '-m', 'chore: add submodule');
268
+ return { dir, inSm, commitInSm };
269
+ }
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.byClass = exports.subjects = exports.listIn = exports.FIRST = exports.UNSAFE = exports.recordPath = exports.SINCE_48H = exports.now = exports.NOW = exports.SHA = void 0;
7
+ exports.record = record;
8
+ exports.commitLiterally = commitLiterally;
9
+ exports.ok = ok;
10
+ exports.adoptIn = adoptIn;
11
+ exports.historyIn = historyIn;
12
+ exports.snapshotDir = snapshotDir;
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const list_1 = require("../../../src/core/changes/list");
17
+ // Shared by the tests of `awm changes list`: a valid record text and the clock every list test runs at.
18
+ exports.SHA = 'a'.repeat(40);
19
+ /** The instant every list test pretends to run at. */
20
+ exports.NOW = new Date('2026-10-17T12:00:00.000Z');
21
+ const now = () => exports.NOW;
22
+ exports.now = now;
23
+ /** The start of the `48h` window at NOW. */
24
+ exports.SINCE_48H = new Date('2026-10-15T12:00:00.000Z');
25
+ /** A valid Change Record whose id is `id` (it must equal the file name without `.md`). */
26
+ function record(id, o = {}) {
27
+ const manual = o.manual ?? [['s1', 'e1'], ['s2', 'e2']];
28
+ const manualYaml = Array.isArray(manual)
29
+ ? manual.map(([step, expected]) => ` - step: ${step}\n expected: ${expected}`).join('\n')
30
+ : ` none: ${manual.none}`;
31
+ return `---
32
+ schema: awm.change-record/v1
33
+ id: ${id}
34
+ title: "${o.title ?? `T ${id}`}"
35
+ summary_short: "${o.summary ?? 'Primer cambio.'}"
36
+ type: ${o.type ?? 'feat'}
37
+ scope: null
38
+ semver: minor
39
+ breaking: false
40
+ modules: [cli]
41
+ verification:
42
+ commit: ${exports.SHA}
43
+ overall: ${o.overall ?? 'pass'}
44
+ sensors: { lint: pass }
45
+ receipt_digest: sha256:${'b'.repeat(64)}
46
+ manual_validation:
47
+ ${manualYaml}
48
+ risks: []
49
+ rollback: revert
50
+ ${o.extra ?? ''}---
51
+ ## Qué se hizo
52
+ a
53
+ ## Qué se probó
54
+ b
55
+ ## Qué falta validar
56
+ c
57
+ ## Riesgos y rollback
58
+ d
59
+ `;
60
+ }
61
+ const recordPath = (id) => `docs/changes/${id}.md`;
62
+ exports.recordPath = recordPath;
63
+ let oddCommits = 0;
64
+ /**
65
+ * A commit on top of HEAD whose committer line is `committer t <t@t> <committerDate>` verbatim (git itself would never write an
66
+ * empty, negative or garbage date), written straight into the object store and checked out. Returns its id.
67
+ */
68
+ function commitLiterally(repo, committerDate, message = 'fix: odd date') {
69
+ const body = [`tree ${repo.git('write-tree')}`, `parent ${repo.git('rev-parse', 'HEAD')}`, 'author t <t@t> 1700000000 +0000', `committer t <t@t> ${committerDate}`, '', message, ''].join('\n');
70
+ const file = path_1.default.join(repo.home, `odd-commit-${oddCommits++}.txt`);
71
+ fs_1.default.writeFileSync(file, body);
72
+ const sha = repo.git('hash-object', '-t', 'commit', '-w', '--literally', file);
73
+ repo.git('reset', '-q', '--hard', sha);
74
+ return sha;
75
+ }
76
+ exports.UNSAFE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
77
+ exports.FIRST = '2026-10-16-first';
78
+ const listIn = (repo, opts = {}) => (0, list_1.listChanges)(repo.root, { since: exports.SINCE_48H, ...opts });
79
+ exports.listIn = listIn;
80
+ function ok(outcome) {
81
+ if (!outcome.ok)
82
+ throw new Error(`expected ok, got ${outcome.failure.code}: ${outcome.failure.message}`);
83
+ return outcome.result;
84
+ }
85
+ const subjects = (r) => r.changes.map((c) => c.subject);
86
+ exports.subjects = subjects;
87
+ const byClass = (r) => Object.fromEntries(r.changes.map((c) => [c.subject, c.classification]));
88
+ exports.byClass = byClass;
89
+ /** The commit that opens the adoption point, before the 48h window. */
90
+ function adoptIn(repo, id = '2026-10-10-adopt') {
91
+ return repo.commit({ [(0, exports.recordPath)(id)]: record(id, { summary: 'Adopcion.' }) }, 'feat: adopt', '2026-10-10T00:00:00Z');
92
+ }
93
+ /** Pre-adoption, recorded, missing, exempt and invalid-record commits, tagged v1.0.0. */
94
+ function historyIn(repo) {
95
+ repo.commit({ 'old.ts': '1' }, 'feat: before adoption', '2026-10-16T09:00:00Z');
96
+ repo.commit({ [(0, exports.recordPath)(exports.FIRST)]: record(exports.FIRST), 'a.ts': '1' }, 'feat: first (#10)', '2026-10-16T20:00:00Z');
97
+ repo.commit({ 'b.ts': '1' }, 'fix: no record', '2026-10-16T21:00:00Z');
98
+ repo.commit({ 'c.ts': '1' }, 'chore(release): v1.0.0 [skip ci]\n\nChange-Record: none', '2026-10-16T22:00:00Z');
99
+ repo.commit({ [(0, exports.recordPath)('2026-10-17-bad')]: '---\nschema: nope\n---\n' }, 'feat: bad record', '2026-10-17T01:00:00Z');
100
+ repo.gitAt('2026-10-17T02:00:00Z', 'tag', '-a', 'v1.0.0', '-m', 'v1.0.0');
101
+ }
102
+ /**
103
+ * A digest of every file and directory under `root`, `.git` and its object database included: names and contents, never times.
104
+ * Two equal digests mean that nothing was written, fetched or refreshed in between.
105
+ */
106
+ function snapshotDir(root) {
107
+ const hash = crypto_1.default.createHash('sha1');
108
+ const walk = (dir, rel) => {
109
+ for (const name of fs_1.default.readdirSync(dir).sort()) {
110
+ const abs = path_1.default.join(dir, name);
111
+ const here = `${rel}/${name}`;
112
+ if (fs_1.default.lstatSync(abs).isDirectory()) {
113
+ hash.update(`d ${here}\n`);
114
+ walk(abs, here);
115
+ }
116
+ else
117
+ hash.update(`f ${here} ${crypto_1.default.createHash('sha1').update(fs_1.default.readFileSync(abs)).digest('hex')}\n`);
118
+ }
119
+ };
120
+ walk(root, '');
121
+ return hash.digest('hex');
122
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const helpers_1 = require("./helpers");
4
+ const list_fixtures_1 = require("./list-fixtures");
5
+ const list_1 = require("../../../src/core/changes/list");
6
+ // One 201-commit repository, built once and only ever read: no test writes into it and no test needs a repository of its own.
7
+ let big;
8
+ beforeAll(() => {
9
+ big = (0, helpers_1.makeRepo)();
10
+ big.commit({ [(0, list_fixtures_1.recordPath)('2026-10-10-adopt')]: (0, list_fixtures_1.record)('2026-10-10-adopt') }, 'feat: adopt', '2026-10-10T00:00:00Z');
11
+ // 201 empty commits with the same committer date, written by ONE git fast-import process instead of 201 `git commit`s.
12
+ (0, helpers_1.importCommits)(big, Array.from({ length: 201 }, (_, i) => ({ message: `fix: n${i + 1}`, files: {}, date: '2026-10-16T10:00:00Z' })));
13
+ }, 180_000);
14
+ afterAll(() => big.cleanup());
15
+ afterEach(() => jest.restoreAllMocks());
16
+ const listBig = (opts = {}) => (0, list_1.listChanges)(big.root, { since: list_fixtures_1.SINCE_48H, ...opts });
17
+ const result = (outcome) => {
18
+ if (!outcome.ok)
19
+ throw new Error(outcome.failure.message);
20
+ return outcome.result;
21
+ };
22
+ /** How many git children a run spawns. */
23
+ const spawns = (opts) => {
24
+ const spy = (0, helpers_1.spyOnGit)();
25
+ result(listBig(opts));
26
+ spy.restore();
27
+ return spy.calls.length;
28
+ };
29
+ describe('listChanges: a long history (R5.7)', () => {
30
+ test('beyond 200 entries only the newest 200 are listed and the rest is counted', () => {
31
+ const r = result(listBig());
32
+ expect(r.changes).toHaveLength(200);
33
+ expect(r.changes[0].subject).toBe('fix: n201');
34
+ expect(r.changes[199].subject).toBe('fix: n2');
35
+ expect(r).toMatchObject({ truncated: true, omitted: 1 });
36
+ }, 180_000);
37
+ test('the entries beyond a small limit are only counted', () => {
38
+ expect(result(listBig({ limit: 3 }))).toMatchObject({ truncated: true, omitted: 198 });
39
+ });
40
+ test('the whole history fits one page, so the scan is one git call', () => {
41
+ const spy = (0, helpers_1.spyOnGit)();
42
+ result(listBig({ limit: 3 }));
43
+ spy.restore();
44
+ expect(spy.calls.filter((a) => a.includes('--format=%H%x1f%ct%x1f%P'))).toHaveLength(1);
45
+ });
46
+ test('git calls per entry are constant: an entry costs the same however many there are', () => {
47
+ expect((spawns({ limit: 25 }) - spawns({ limit: 5 })) / 20).toBeLessThanOrEqual(4);
48
+ });
49
+ test('entries beyond the limit cost no per-entry call', () => {
50
+ expect(spawns({ limit: 5 })).toBeLessThan(spawns({ limit: 25 }) / 2);
51
+ });
52
+ });