@magnusekdahl/parallix 1.2.1 → 1.3.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/index.js +190 -185
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1331 -1468
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +4 -4
- package/px.js +226 -208
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { detectRebaseState, getCurrentBranch, getUncommittedCount, getLastThreeCommits, run } from '../core/git.js';
|
|
2
|
+
import { findTaskFile, getTaskStatus } from '../tools/backlog.js';
|
|
3
|
+
import { findMissionDir, findCheckpoints, getFirstLine, inferSlug, missionBranchPrefix, missionBranchName, getPrimaryWorktree } from '../core/mission-utils.js';
|
|
4
|
+
import { WORKFLOW_AGENT_NAMES, eligibleAgentsForStep, readAgentConfigOrExit, workflowLauncherStatus } from '../agents/agents.js';
|
|
5
|
+
import { getPrStatus } from '../tools/forgejo.js';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as fmt from '../core/fmt.js';
|
|
8
|
+
|
|
9
|
+
/** @param {string} porcelain */
|
|
10
|
+
function parseWorktreeList(porcelain: string) {
|
|
11
|
+
const entries = [];
|
|
12
|
+
/** @type{{path: string, branch: string | null} | null} */
|
|
13
|
+
let current: { path: string; branch: string | null } | null = null;
|
|
14
|
+
|
|
15
|
+
for (const line of porcelain.split('\n')) {
|
|
16
|
+
if (!line.trim()) {
|
|
17
|
+
if (current) {
|
|
18
|
+
entries.push(current);
|
|
19
|
+
current = null;
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (line.startsWith('worktree ')) {
|
|
25
|
+
if (current) {entries.push(current);}
|
|
26
|
+
current = { path: line.slice('worktree '.length).trim(), branch: null };
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (line.startsWith('branch ') && current) {
|
|
31
|
+
current.branch = line.slice('branch '.length).trim() as string;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (current) {entries.push(current);}
|
|
36
|
+
return entries;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function findStaleMissionWorktrees({
|
|
40
|
+
gitRun = run,
|
|
41
|
+
findTaskFileFn = findTaskFile,
|
|
42
|
+
getTaskStatusFn = getTaskStatus,
|
|
43
|
+
primaryWorktree = null
|
|
44
|
+
} = {}) {
|
|
45
|
+
const result = gitRun('git', ['worktree', 'list', '--porcelain']);
|
|
46
|
+
if (result.status !== 0) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @type {string | null} */
|
|
51
|
+
let resolvedPrimary: string | null = primaryWorktree;
|
|
52
|
+
if (!resolvedPrimary) {
|
|
53
|
+
try {
|
|
54
|
+
resolvedPrimary = getPrimaryWorktree();
|
|
55
|
+
} catch (_) {
|
|
56
|
+
resolvedPrimary = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parseWorktreeList(result.stdout)
|
|
61
|
+
.filter(entry => entry.path !== resolvedPrimary)
|
|
62
|
+
.map(entry => {
|
|
63
|
+
const branchRef = entry.branch || '';
|
|
64
|
+
const prefix = missionBranchPrefix(resolvedPrimary || process.cwd());
|
|
65
|
+
const normalizedRefPrefix = `refs/heads/${prefix}`;
|
|
66
|
+
const match = branchRef.startsWith(normalizedRefPrefix)
|
|
67
|
+
? [branchRef, branchRef.slice(normalizedRefPrefix.length)]
|
|
68
|
+
: null;
|
|
69
|
+
if (!match) {return null;}
|
|
70
|
+
|
|
71
|
+
const slug = match[1];
|
|
72
|
+
const taskFile = findTaskFileFn(slug);
|
|
73
|
+
const taskStatus = taskFile ? getTaskStatusFn(taskFile) : null;
|
|
74
|
+
if (taskStatus !== 'done' && taskFile) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
slug,
|
|
80
|
+
path: entry.path,
|
|
81
|
+
branch: branchRef,
|
|
82
|
+
taskStatus: taskStatus || 'missing',
|
|
83
|
+
cleanupCommand: taskStatus === 'done'
|
|
84
|
+
? `scripts/cleanup-mission-worktree.sh ${slug}`
|
|
85
|
+
: `git worktree remove ${entry.path} && git branch -D ${missionBranchName(slug, resolvedPrimary || process.cwd())}`
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @param {string} ref */
|
|
92
|
+
function formatWorktreeBranch(ref: string) {
|
|
93
|
+
if (!ref) {return '(detached HEAD)';}
|
|
94
|
+
return ref.replace(/^refs\/heads\//, '');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @param {Function} log @param {string} label @param {{detached?: boolean, unmergedFiles: string[]}} rebaseState */
|
|
98
|
+
function logRebaseDiagnostics(log: Function, label: string, rebaseState: {detached?: boolean, unmergedFiles: string[]}) {
|
|
99
|
+
const detachedText = rebaseState.detached ? 'detached HEAD, ' : '';
|
|
100
|
+
log(`${label}: ${detachedText}${rebaseState.unmergedFiles.length} unmerged file(s)`);
|
|
101
|
+
rebaseState.unmergedFiles.forEach(file => {
|
|
102
|
+
log(` - ${file}`);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {string[]} args @param {{exit?: Function, log?: Function, inferSlugFn?: Function, getCurrentBranchFn?: Function, findTaskFileFn?: Function, getTaskStatusFn?: Function, findMissionDirFn?: Function, findCheckpointsFn?: Function, getFirstLineFn?: Function, getPrStatusFn?: Function, findStaleMissionWorktreesFn?: Function, readAgentConfigOrExitFn?: Function, eligibleAgentsForStepFn?: Function, allWorkflowAgentNamesFn?: Function, workflowLauncherStatusFn?: Function, getLastThreeCommitsFn?: Function, getUncommittedCountFn?: Function, detectRebaseStateFn?: Function}} opts */
|
|
107
|
+
function status(args: string[], opts: {exit?: Function, log?: Function, inferSlugFn?: Function, getCurrentBranchFn?: Function, findTaskFileFn?: Function, getTaskStatusFn?: Function, findMissionDirFn?: Function, findCheckpointsFn?: Function, getFirstLineFn?: Function, getPrStatusFn?: Function, findStaleMissionWorktreesFn?: Function, readAgentConfigOrExitFn?: Function, eligibleAgentsForStepFn?: Function, allWorkflowAgentNamesFn?: Function, workflowLauncherStatusFn?: Function, getLastThreeCommitsFn?: Function, getUncommittedCountFn?: Function, detectRebaseStateFn?: Function}) {
|
|
108
|
+
const exit = opts.exit || process.exit;
|
|
109
|
+
const log = opts.log || fmt.log.plain;
|
|
110
|
+
const inferSlugFn = opts.inferSlugFn || inferSlug;
|
|
111
|
+
const getCurrentBranchFn = opts.getCurrentBranchFn || getCurrentBranch;
|
|
112
|
+
const findTaskFileFn = opts.findTaskFileFn || findTaskFile;
|
|
113
|
+
const getTaskStatusFn = opts.getTaskStatusFn || getTaskStatus;
|
|
114
|
+
const findMissionDirFn = opts.findMissionDirFn || findMissionDir;
|
|
115
|
+
const findCheckpointsFn = opts.findCheckpointsFn || findCheckpoints;
|
|
116
|
+
const getFirstLineFn = opts.getFirstLineFn || getFirstLine;
|
|
117
|
+
const getPrStatusFn = opts.getPrStatusFn || getPrStatus;
|
|
118
|
+
const findStaleMissionWorktreesFn = opts.findStaleMissionWorktreesFn || findStaleMissionWorktrees;
|
|
119
|
+
const readAgentConfigOrExitFn = opts.readAgentConfigOrExitFn || readAgentConfigOrExit;
|
|
120
|
+
const eligibleAgentsForStepFn = opts.eligibleAgentsForStepFn || eligibleAgentsForStep;
|
|
121
|
+
const allWorkflowAgentNamesFn = opts.allWorkflowAgentNamesFn || (() => WORKFLOW_AGENT_NAMES);
|
|
122
|
+
const workflowLauncherStatusFn = opts.workflowLauncherStatusFn || workflowLauncherStatus;
|
|
123
|
+
const getLastThreeCommitsFn = opts.getLastThreeCommitsFn || getLastThreeCommits;
|
|
124
|
+
const getUncommittedCountFn = opts.getUncommittedCountFn || getUncommittedCount;
|
|
125
|
+
const detectRebaseStateFn = opts.detectRebaseStateFn || detectRebaseState;
|
|
126
|
+
|
|
127
|
+
const explicitSlug = args[0];
|
|
128
|
+
const slug = inferSlugFn(explicitSlug);
|
|
129
|
+
|
|
130
|
+
log(fmt.bold('--- Mission Status ---'));
|
|
131
|
+
log(`Branch: ${fmt.branch(getCurrentBranchFn())}`);
|
|
132
|
+
log(`Worktree: ${fmt.path(process.cwd())}`);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const rebaseState = detectRebaseStateFn(process.cwd());
|
|
136
|
+
if (rebaseState.inProgress && rebaseState.detached) {
|
|
137
|
+
logRebaseDiagnostics(log, 'Detached HEAD: rebase in progress', rebaseState);
|
|
138
|
+
}
|
|
139
|
+
} catch (_) {
|
|
140
|
+
// Ignore transient worktree/git-state failures in status output.
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (slug) {
|
|
144
|
+
const taskFile = findTaskFileFn(slug);
|
|
145
|
+
const taskStatus = getTaskStatusFn(taskFile);
|
|
146
|
+
log(`Backlog status: ${taskStatus || 'unknown'}`);
|
|
147
|
+
|
|
148
|
+
const missionDir = findMissionDirFn(slug);
|
|
149
|
+
if (missionDir) {
|
|
150
|
+
const checkpoints = findCheckpointsFn(missionDir);
|
|
151
|
+
if (checkpoints.length > 0) {
|
|
152
|
+
const lastCP = checkpoints[checkpoints.length - 1];
|
|
153
|
+
const firstLine = getFirstLineFn(lastCP);
|
|
154
|
+
log(`Last checkpoint: ${path.basename(lastCP)} - ${firstLine}`);
|
|
155
|
+
} else {
|
|
156
|
+
log('Last checkpoint: none');
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
log('Last checkpoint: unknown');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Forgejo PR state
|
|
163
|
+
const pr = getPrStatusFn(missionBranchName(slug));
|
|
164
|
+
if (pr.exists) {
|
|
165
|
+
log(`Forgejo PR: #${pr.number} (${pr.state})`);
|
|
166
|
+
} else if (pr.raw) {
|
|
167
|
+
log(`Forgejo PR: unavailable (${pr.raw})`);
|
|
168
|
+
} else {
|
|
169
|
+
log('Forgejo PR: none');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!explicitSlug) {
|
|
174
|
+
const staleWorktrees = findStaleMissionWorktreesFn();
|
|
175
|
+
staleWorktrees.forEach((entry: { path: string; branch: string | null; taskStatus: string | null; cleanupCommand?: string }) => {
|
|
176
|
+
log(`Stale worktree: ${fmt.path(entry.path)} (task: ${entry.taskStatus})`);
|
|
177
|
+
try {
|
|
178
|
+
const rebaseState = detectRebaseStateFn(entry.path);
|
|
179
|
+
if (rebaseState.inProgress) {
|
|
180
|
+
logRebaseDiagnostics(log, `Rebase in progress on ${formatWorktreeBranch(entry.branch || '')}`, rebaseState);
|
|
181
|
+
}
|
|
182
|
+
} catch (_) {
|
|
183
|
+
// Ignore stale worktrees that disappear during inspection.
|
|
184
|
+
}
|
|
185
|
+
log(` Cleanup: ${fmt.command(entry.cleanupCommand || '')}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Agent eligibility matrix
|
|
190
|
+
const config = readAgentConfigOrExitFn();
|
|
191
|
+
const draftEligible = eligibleAgentsForStepFn('draft', { config });
|
|
192
|
+
const activeEligible = eligibleAgentsForStepFn('active', { config });
|
|
193
|
+
const baseAgents = allWorkflowAgentNamesFn();
|
|
194
|
+
const allAgents = [...new Set([...baseAgents, ...draftEligible, ...activeEligible])].sort();
|
|
195
|
+
const envOverride = process.env.WORKFLOW_AGENT;
|
|
196
|
+
log('Agent launcher matrix:');
|
|
197
|
+
for (const agent of allAgents) {
|
|
198
|
+
const launcher = workflowLauncherStatusFn(agent);
|
|
199
|
+
const support = launcher.supported ? 'supported' : 'blocked';
|
|
200
|
+
const draftMark = draftEligible.includes(agent) ? 'draft' : '-';
|
|
201
|
+
const activeMark = activeEligible.includes(agent) ? 'active' : '-';
|
|
202
|
+
log(` ${fmt.agent(agent)}: ${support} | eligible: ${draftMark},${activeMark}`);
|
|
203
|
+
}
|
|
204
|
+
if (envOverride) {
|
|
205
|
+
log(` (WORKFLOW_AGENT override: ${fmt.agent(envOverride)})`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const lastThree = getLastThreeCommitsFn();
|
|
209
|
+
log('Last 3 commits:');
|
|
210
|
+
lastThree.forEach((c: string) => log(` - ${c}`));
|
|
211
|
+
|
|
212
|
+
log(`Uncommitted files: ${getUncommittedCountFn()}`);
|
|
213
|
+
log(fmt.bold('----------------------'));
|
|
214
|
+
exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
(status as any).parseWorktreeList = parseWorktreeList;
|
|
218
|
+
(status as any).findStaleMissionWorktrees = findStaleMissionWorktrees;
|
|
219
|
+
export = status;
|
package/lib/commands/verify.js
CHANGED
package/lib/core/nels.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* Net Engineering Lines (NEL) computation.
|
|
3
4
|
*
|
|
@@ -14,32 +15,33 @@
|
|
|
14
15
|
*
|
|
15
16
|
* @module lib/core/nels
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.BUCKET_MEDIUM_MAX = exports.BUCKET_SMALL_MAX = exports.EXCLUSION_PATTERNS = void 0;
|
|
20
|
+
exports.classifyBucket = classifyBucket;
|
|
21
|
+
exports.isExcluded = isExcluded;
|
|
22
|
+
exports.computeNEL = computeNEL;
|
|
23
|
+
exports.computeNELRecord = computeNELRecord;
|
|
24
|
+
const node_child_process_1 = require("node:child_process");
|
|
20
25
|
// ---------- exclusion globs (ADR 0047) ----------
|
|
21
26
|
// Each entry is a minimatch-compatible pattern. Patterns are checked in order;
|
|
22
27
|
// the first match wins.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'*.lock', // additional lockfile safety net
|
|
28
|
+
exports.EXCLUSION_PATTERNS = [
|
|
29
|
+
'missions/**', // workflow/process bookkeeping
|
|
30
|
+
'backlog/**', // workflow/process bookkeeping
|
|
31
|
+
'review-*', // workflow/process bookkeeping
|
|
32
|
+
'CP-*', // workflow/process bookkeeping
|
|
33
|
+
'**/*.md', // documentation
|
|
34
|
+
'docs/**', // documentation
|
|
35
|
+
'package-lock.json', // generated/vendored
|
|
36
|
+
'coverage/**', // generated/vendored
|
|
37
|
+
'*lock*', // lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, Gemfile.lock, etc.)
|
|
38
|
+
'*.lock', // additional lockfile safety net
|
|
35
39
|
];
|
|
36
|
-
|
|
37
40
|
/**
|
|
38
41
|
* Bucket constants derived from ADR 0047 empirical terciles.
|
|
39
42
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
exports.BUCKET_SMALL_MAX = 80;
|
|
44
|
+
exports.BUCKET_MEDIUM_MAX = 235;
|
|
43
45
|
/**
|
|
44
46
|
* Classify NEL count into a bucket label.
|
|
45
47
|
*
|
|
@@ -47,15 +49,14 @@ const BUCKET_MEDIUM_MAX = 235;
|
|
|
47
49
|
* @returns {{ label: 'Small' | 'Medium' | 'Large', min: number, max: number }}
|
|
48
50
|
*/
|
|
49
51
|
function classifyBucket(nel) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
if (nel <= exports.BUCKET_SMALL_MAX) {
|
|
53
|
+
return { label: 'Small', min: 0, max: exports.BUCKET_SMALL_MAX };
|
|
54
|
+
}
|
|
55
|
+
if (nel <= exports.BUCKET_MEDIUM_MAX) {
|
|
56
|
+
return { label: 'Medium', min: exports.BUCKET_SMALL_MAX + 1, max: exports.BUCKET_MEDIUM_MAX };
|
|
57
|
+
}
|
|
58
|
+
return { label: 'Large', min: exports.BUCKET_MEDIUM_MAX + 1, max: Infinity };
|
|
57
59
|
}
|
|
58
|
-
|
|
59
60
|
/**
|
|
60
61
|
* Check whether a file path matches any exclusion pattern.
|
|
61
62
|
*
|
|
@@ -63,17 +64,15 @@ function classifyBucket(nel) {
|
|
|
63
64
|
* @returns {boolean}
|
|
64
65
|
*/
|
|
65
66
|
function isExcluded(filePath) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
// Normalize to forward slashes
|
|
68
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
69
|
+
for (const pattern of exports.EXCLUSION_PATTERNS) {
|
|
70
|
+
if (patternMatches(normalized, pattern)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
return false;
|
|
74
|
+
return false;
|
|
75
75
|
}
|
|
76
|
-
|
|
77
76
|
/**
|
|
78
77
|
* Simple glob matcher supporting *, **, and literal segments.
|
|
79
78
|
* Does not require a full minimatch dependency — keeps this module light.
|
|
@@ -83,45 +82,47 @@ function isExcluded(filePath) {
|
|
|
83
82
|
* @returns {boolean}
|
|
84
83
|
*/
|
|
85
84
|
function patternMatches(str, pattern) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
// Convert minimatch-style glob to regex
|
|
86
|
+
// Escape regex special chars except * and ?
|
|
87
|
+
let regexStr = '';
|
|
88
|
+
let i = 0;
|
|
89
|
+
const pLen = pattern.length;
|
|
90
|
+
while (i < pLen) {
|
|
91
|
+
const ch = pattern[i];
|
|
92
|
+
if (ch === '*') {
|
|
93
|
+
if (i + 1 < pLen && pattern[i + 1] === '*') {
|
|
94
|
+
// ** matches everything including /
|
|
95
|
+
if (i + 2 < pLen && pattern[i + 2] === '/') {
|
|
96
|
+
regexStr += '(?:.*/)?';
|
|
97
|
+
i += 3;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
regexStr += '.*';
|
|
102
|
+
i += 2;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
regexStr += '[^/]*';
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (ch === '?') {
|
|
113
|
+
regexStr += '[^/]';
|
|
114
|
+
}
|
|
115
|
+
else if ('.+^${}()|[]\\'.includes(ch)) {
|
|
116
|
+
regexStr += '\\' + ch;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
regexStr += ch;
|
|
105
120
|
}
|
|
106
|
-
|
|
107
|
-
regexStr += '[^/]*';
|
|
108
|
-
i += 1;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
} else if (ch === '?') {
|
|
112
|
-
regexStr += '[^/]';
|
|
113
|
-
} else if ('.+^${}()|[]\\'.includes(ch)) {
|
|
114
|
-
regexStr += '\\' + ch;
|
|
115
|
-
} else {
|
|
116
|
-
regexStr += ch;
|
|
121
|
+
i++;
|
|
117
122
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const re = new RegExp('^' + regexStr + '$');
|
|
122
|
-
return re.test(str);
|
|
123
|
+
const re = new RegExp('^' + regexStr + '$');
|
|
124
|
+
return re.test(str);
|
|
123
125
|
}
|
|
124
|
-
|
|
125
126
|
/**
|
|
126
127
|
* Compute Net Engineering Lines (NEL) from a git diff range.
|
|
127
128
|
*
|
|
@@ -133,49 +134,43 @@ function patternMatches(str, pattern) {
|
|
|
133
134
|
* @returns {number} Total NEL count (insertions + deletions, whitespace-ignored)
|
|
134
135
|
*/
|
|
135
136
|
function computeNEL(range, options = {}) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
total += addsNum + delsNum;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return total;
|
|
137
|
+
const cwd = options.cwd || process.cwd();
|
|
138
|
+
const result = (0, node_child_process_1.spawnSync)('git', ['diff', '--numstat', '-w', range], {
|
|
139
|
+
cwd,
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
maxBuffer: 50 * 1024 * 1024, // 50 MB
|
|
142
|
+
});
|
|
143
|
+
if (result.status !== 0) {
|
|
144
|
+
// Git error, empty diff, or uncomputable — return 0
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
const stdout = result.stdout || '';
|
|
148
|
+
const lines = stdout.split('\n').filter(line => line.trim().length > 0);
|
|
149
|
+
let total = 0;
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
// numstat format: <insertions>\t<deletions>\t<filename>
|
|
152
|
+
// Binary files show: -\t-\t<filename>
|
|
153
|
+
const parts = line.split('\t');
|
|
154
|
+
if (parts.length < 3) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const adds = parts[0];
|
|
158
|
+
const dels = parts[1];
|
|
159
|
+
const filePath = parts.slice(2).join('\t');
|
|
160
|
+
// Skip binary files
|
|
161
|
+
if (adds === '-' || dels === '-') {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// Apply exclusion globs
|
|
165
|
+
if (isExcluded(filePath)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const addsNum = parseInt(adds, 10) || 0;
|
|
169
|
+
const delsNum = parseInt(dels, 10) || 0;
|
|
170
|
+
total += addsNum + delsNum;
|
|
171
|
+
}
|
|
172
|
+
return total;
|
|
177
173
|
}
|
|
178
|
-
|
|
179
174
|
/**
|
|
180
175
|
* Compute NEL and return the full NEL record including bucket classification.
|
|
181
176
|
*
|
|
@@ -183,17 +178,7 @@ function computeNEL(range, options = {}) {
|
|
|
183
178
|
* @param {{ cwd?: string }} [options]
|
|
184
179
|
* @returns {{ nel: number, bucket: { label: string, min: number, max: number } }}
|
|
185
180
|
*/
|
|
186
|
-
function computeNELRecord(range, options) {
|
|
187
|
-
|
|
188
|
-
|
|
181
|
+
function computeNELRecord(range, options = {}) {
|
|
182
|
+
const nel = computeNEL(range, options);
|
|
183
|
+
return { nel, bucket: classifyBucket(nel) };
|
|
189
184
|
}
|
|
190
|
-
|
|
191
|
-
module.exports = {
|
|
192
|
-
computeNEL,
|
|
193
|
-
computeNELRecord,
|
|
194
|
-
classifyBucket,
|
|
195
|
-
isExcluded,
|
|
196
|
-
EXCLUSION_PATTERNS,
|
|
197
|
-
BUCKET_SMALL_MAX,
|
|
198
|
-
BUCKET_MEDIUM_MAX,
|
|
199
|
-
};
|