@magnusekdahl/parallix 1.1.0 → 1.2.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 (68) hide show
  1. package/config/integration-pipelines.json +9 -0
  2. package/config/workflow.config.schema.json +12 -0
  3. package/docs/adr/0032-mission-refinement-state-and-usage-budget-signals.md +10 -8
  4. package/docs/adr/0036-mission-sizing-and-dependency-wave-heuristics.md +14 -12
  5. package/docs/adr/0041-integration-pipeline-gates.md +1 -1
  6. package/docs/adr/0047-per-mission-change-size-budget.md +161 -0
  7. package/docs/adr/index.md +1 -0
  8. package/docs/use-cases.md +44 -0
  9. package/lib/agents/agents.js +149 -100
  10. package/lib/agents/claude-telemetry.js +14 -10
  11. package/lib/agents/claude.js +32 -11
  12. package/lib/agents/codex-telemetry.js +32 -19
  13. package/lib/agents/codex.js +36 -11
  14. package/lib/agents/limit-hit.js +42 -25
  15. package/lib/agents/mistral-telemetry.js +1 -1
  16. package/lib/agents/mistral.js +13 -3
  17. package/lib/agents/opencode-export.js +11 -5
  18. package/lib/agents/opencode-telemetry.js +75 -90
  19. package/lib/agents/opencode.js +86 -29
  20. package/lib/agents/stage-telemetry.js +3 -7
  21. package/lib/commands/active.js +139 -94
  22. package/lib/commands/checkpoint.js +3 -1
  23. package/lib/commands/config.js +5 -3
  24. package/lib/commands/coverage-gate.js +17 -10
  25. package/lib/commands/diff.js +11 -5
  26. package/lib/commands/draft.js +95 -55
  27. package/lib/commands/handoff.js +253 -65
  28. package/lib/commands/integrate.js +288 -188
  29. package/lib/commands/mission-start.js +39 -34
  30. package/lib/commands/rebase.js +32 -21
  31. package/lib/commands/repair-handoff.js +21 -18
  32. package/lib/commands/resolve-conflict.js +5 -2
  33. package/lib/commands/review.js +1 -0
  34. package/lib/commands/setup-review.js +1 -0
  35. package/lib/commands/stats-backfill.js +50 -41
  36. package/lib/commands/stats.js +653 -204
  37. package/lib/commands/status.js +33 -28
  38. package/lib/core/fmt.js +58 -15
  39. package/lib/core/git.js +7 -5
  40. package/lib/core/gitignore.js +6 -4
  41. package/lib/core/mission-utils.js +161 -88
  42. package/lib/core/nels.js +199 -0
  43. package/lib/core/persistent-data-migration.js +55 -23
  44. package/lib/core/product-config.js +44 -23
  45. package/lib/core/runtime-matrix.js +15 -2
  46. package/lib/core/spawn-tee.js +40 -13
  47. package/lib/core/state-map.js +32 -13
  48. package/lib/core/storage.js +19 -5
  49. package/lib/core/subagent-limit.js +28 -0
  50. package/lib/core/verification.js +29 -12
  51. package/lib/review/rebase.js +11 -3
  52. package/lib/review/review-adapter.js +93 -6
  53. package/lib/review/review-artifacts.js +89 -23
  54. package/lib/review/review-commands.js +139 -45
  55. package/lib/review/review-events.js +103 -52
  56. package/lib/review/review-loop.js +117 -87
  57. package/lib/review/review-polling.js +25 -5
  58. package/lib/review/review-prompts.js +36 -5
  59. package/lib/review/review-state.js +39 -15
  60. package/lib/tools/backlog.js +162 -71
  61. package/lib/tools/forgejo.js +290 -129
  62. package/lib/tools/gatekeeper.js +15 -0
  63. package/lib/tools/redgreen.js +220 -0
  64. package/lib/tools/sessions.js +12 -5
  65. package/lib/tools/setup-review.js +170 -70
  66. package/package.json +11 -3
  67. package/prompts/draft.md +6 -0
  68. package/templates/mission-scaffold.md +1 -1
@@ -3,12 +3,13 @@ const { findTaskFile, getTaskStatus } = require('../tools/backlog');
3
3
  const { findMissionDir, findCheckpoints, getFirstLine, inferSlug, missionBranchPrefix, missionBranchName } = require('../core/mission-utils');
4
4
  const { WORKFLOW_AGENT_NAMES, eligibleAgentsForStep, readAgentConfigOrExit, workflowLauncherStatus } = require('../agents/agents');
5
5
  const { getPrStatus } = require('../tools/forgejo');
6
- const fs = require('fs');
7
6
  const path = require('path');
8
7
  const fmt = require('../core/fmt');
9
8
 
9
+ /** @param {string} porcelain */
10
10
  function parseWorktreeList(porcelain) {
11
11
  const entries = [];
12
+ /** @type{{path: string, branch: string | null} | null} */
12
13
  let current = null;
13
14
 
14
15
  for (const line of porcelain.split('\n')) {
@@ -21,7 +22,7 @@ function parseWorktreeList(porcelain) {
21
22
  }
22
23
 
23
24
  if (line.startsWith('worktree ')) {
24
- if (current) entries.push(current);
25
+ if (current) {entries.push(current);}
25
26
  current = { path: line.slice('worktree '.length).trim(), branch: null };
26
27
  continue;
27
28
  }
@@ -31,7 +32,7 @@ function parseWorktreeList(porcelain) {
31
32
  }
32
33
  }
33
34
 
34
- if (current) entries.push(current);
35
+ if (current) {entries.push(current);}
35
36
  return entries;
36
37
  }
37
38
 
@@ -46,6 +47,7 @@ function findStaleMissionWorktrees({
46
47
  return [];
47
48
  }
48
49
 
50
+ /** @type {string | null} */
49
51
  let resolvedPrimary = primaryWorktree;
50
52
  if (!resolvedPrimary) {
51
53
  try {
@@ -65,7 +67,7 @@ function findStaleMissionWorktrees({
65
67
  const match = branchRef.startsWith(normalizedRefPrefix)
66
68
  ? [branchRef, branchRef.slice(normalizedRefPrefix.length)]
67
69
  : null;
68
- if (!match) return null;
70
+ if (!match) {return null;}
69
71
 
70
72
  const slug = match[1];
71
73
  const taskFile = findTaskFileFn(slug);
@@ -87,11 +89,13 @@ function findStaleMissionWorktrees({
87
89
  .filter(Boolean);
88
90
  }
89
91
 
92
+ /** @param {string} ref */
90
93
  function formatWorktreeBranch(ref) {
91
- if (!ref) return '(detached HEAD)';
94
+ if (!ref) {return '(detached HEAD)';}
92
95
  return ref.replace(/^refs\/heads\//, '');
93
96
  }
94
97
 
98
+ /** @param {Function} log @param {string} label @param {{detached?: boolean, unmergedFiles: string[]}} rebaseState */
95
99
  function logRebaseDiagnostics(log, label, rebaseState) {
96
100
  const detachedText = rebaseState.detached ? 'detached HEAD, ' : '';
97
101
  log(`${label}: ${detachedText}${rebaseState.unmergedFiles.length} unmerged file(s)`);
@@ -100,25 +104,26 @@ function logRebaseDiagnostics(log, label, rebaseState) {
100
104
  });
101
105
  }
102
106
 
103
- function status(args, options = {}) {
104
- const exit = options.exit || process.exit;
105
- const log = options.log || fmt.log.plain;
106
- const inferSlugFn = options.inferSlugFn || inferSlug;
107
- const getCurrentBranchFn = options.getCurrentBranchFn || getCurrentBranch;
108
- const findTaskFileFn = options.findTaskFileFn || findTaskFile;
109
- const getTaskStatusFn = options.getTaskStatusFn || getTaskStatus;
110
- const findMissionDirFn = options.findMissionDirFn || findMissionDir;
111
- const findCheckpointsFn = options.findCheckpointsFn || findCheckpoints;
112
- const getFirstLineFn = options.getFirstLineFn || getFirstLine;
113
- const getPrStatusFn = options.getPrStatusFn || getPrStatus;
114
- const findStaleMissionWorktreesFn = options.findStaleMissionWorktreesFn || findStaleMissionWorktrees;
115
- const readAgentConfigOrExitFn = options.readAgentConfigOrExitFn || readAgentConfigOrExit;
116
- const eligibleAgentsForStepFn = options.eligibleAgentsForStepFn || eligibleAgentsForStep;
117
- const allWorkflowAgentNamesFn = options.allWorkflowAgentNamesFn || (() => WORKFLOW_AGENT_NAMES);
118
- const workflowLauncherStatusFn = options.workflowLauncherStatusFn || workflowLauncherStatus;
119
- const getLastThreeCommitsFn = options.getLastThreeCommitsFn || getLastThreeCommits;
120
- const getUncommittedCountFn = options.getUncommittedCountFn || getUncommittedCount;
121
- const detectRebaseStateFn = options.detectRebaseStateFn || detectRebaseState;
107
+ /** @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 */
108
+ function status(args, opts = {}) {
109
+ const exit = opts.exit || process.exit;
110
+ const log = opts.log || fmt.log.plain;
111
+ const inferSlugFn = opts.inferSlugFn || inferSlug;
112
+ const getCurrentBranchFn = opts.getCurrentBranchFn || getCurrentBranch;
113
+ const findTaskFileFn = opts.findTaskFileFn || findTaskFile;
114
+ const getTaskStatusFn = opts.getTaskStatusFn || getTaskStatus;
115
+ const findMissionDirFn = opts.findMissionDirFn || findMissionDir;
116
+ const findCheckpointsFn = opts.findCheckpointsFn || findCheckpoints;
117
+ const getFirstLineFn = opts.getFirstLineFn || getFirstLine;
118
+ const getPrStatusFn = opts.getPrStatusFn || getPrStatus;
119
+ const findStaleMissionWorktreesFn = opts.findStaleMissionWorktreesFn || findStaleMissionWorktrees;
120
+ const readAgentConfigOrExitFn = opts.readAgentConfigOrExitFn || readAgentConfigOrExit;
121
+ const eligibleAgentsForStepFn = opts.eligibleAgentsForStepFn || eligibleAgentsForStep;
122
+ const allWorkflowAgentNamesFn = opts.allWorkflowAgentNamesFn || (() => WORKFLOW_AGENT_NAMES);
123
+ const workflowLauncherStatusFn = opts.workflowLauncherStatusFn || workflowLauncherStatus;
124
+ const getLastThreeCommitsFn = opts.getLastThreeCommitsFn || getLastThreeCommits;
125
+ const getUncommittedCountFn = opts.getUncommittedCountFn || getUncommittedCount;
126
+ const detectRebaseStateFn = opts.detectRebaseStateFn || detectRebaseState;
122
127
 
123
128
  const explicitSlug = args[0];
124
129
  const slug = inferSlugFn(explicitSlug);
@@ -168,17 +173,17 @@ function status(args, options = {}) {
168
173
 
169
174
  if (!explicitSlug) {
170
175
  const staleWorktrees = findStaleMissionWorktreesFn();
171
- staleWorktrees.forEach(entry => {
176
+ staleWorktrees.forEach(/** @param {{path: string, branch: string | null, taskStatus: string | null, cleanupCommand?: string}} entry */ (entry) => {
172
177
  log(`Stale worktree: ${fmt.path(entry.path)} (task: ${entry.taskStatus})`);
173
178
  try {
174
179
  const rebaseState = detectRebaseStateFn(entry.path);
175
180
  if (rebaseState.inProgress) {
176
- logRebaseDiagnostics(log, `Rebase in progress on ${formatWorktreeBranch(entry.branch)}`, rebaseState);
181
+ logRebaseDiagnostics(log, `Rebase in progress on ${formatWorktreeBranch(entry.branch || '')}`, rebaseState);
177
182
  }
178
183
  } catch (_) {
179
184
  // Ignore stale worktrees that disappear during inspection.
180
185
  }
181
- log(` Cleanup: ${fmt.command(entry.cleanupCommand)}`);
186
+ log(` Cleanup: ${fmt.command(entry.cleanupCommand || '')}`);
182
187
  });
183
188
  }
184
189
 
@@ -203,7 +208,7 @@ function status(args, options = {}) {
203
208
 
204
209
  const lastThree = getLastThreeCommitsFn();
205
210
  log('Last 3 commits:');
206
- lastThree.forEach(c => log(` - ${c}`));
211
+ lastThree.forEach(/** @param {string} c */ (c) => log(` - ${c}`));
207
212
 
208
213
  log(`Uncommitted files: ${getUncommittedCountFn()}`);
209
214
  log(fmt.bold('----------------------'));
package/lib/core/fmt.js CHANGED
@@ -34,18 +34,22 @@ const agentMap = {
34
34
  custom: 'yellow',
35
35
  };
36
36
 
37
+ /** @param {import('node:util').InspectColor} format @param {string} text */
37
38
  function colorize(format, text) {
38
39
  return styleText(format, String(text ?? ''));
39
40
  }
40
41
 
42
+ /** @param {string} text */
41
43
  function stripAnsi(text) {
42
44
  return String(text ?? '').replace(/\x1B\[[0-9;]*m/g, '');
43
45
  }
44
46
 
47
+ /** @param {string} text */
45
48
  function visibleWidth(text) {
46
49
  return stripAnsi(text).length;
47
50
  }
48
51
 
52
+ /** @param {string} text @param {number} width */
49
53
  function padVisibleEnd(text, width) {
50
54
  const stringValue = String(text ?? '');
51
55
  const padLength = Math.max(0, width - visibleWidth(stringValue));
@@ -55,15 +59,19 @@ function padVisibleEnd(text, width) {
55
59
  /**
56
60
  * Formats a status line prefix (e.g. "[PASS]").
57
61
  */
62
+ /** @param {string} type @param {string} text */
58
63
  function status(type, text) {
59
- const format = statusMap[type];
60
- if (!format) return `[${type}] ${text}`;
61
- return `${colorize(format, `[${type}]`)} ${text}`;
64
+ /** @type {Record<string,string>} */
65
+ const sm = statusMap;
66
+ const format = sm[type];
67
+ if (!format) {return `[${type}] ${text}`;}
68
+ return `${colorize(/** @type {import('node:util').InspectColor} */(format), `[${type}]`)} ${text}`;
62
69
  }
63
70
 
64
71
  /**
65
72
  * Formats an agent family name.
66
73
  */
74
+ /** @param {string} family @param {string} [text] */
67
75
  function agent(family, text = family) {
68
76
  let label = text;
69
77
  // Disambiguate the opencode-backed `custom` family only when the caller
@@ -73,15 +81,19 @@ function agent(family, text = family) {
73
81
  if (family === 'custom' && text !== 'custom') {
74
82
  label = 'custom (opencode)';
75
83
  }
76
- const format = agentMap[family];
77
- if (!format) return label;
78
- return colorize(format, label);
84
+ /** @type {Record<string,string>} */
85
+ const am = agentMap;
86
+ const format = am[family];
87
+ if (!format) {return label;}
88
+ return colorize(/** @type {import('node:util').InspectColor} */(format), label);
79
89
  }
80
90
 
91
+ /** @param {string} text */
81
92
  function bold(text) {
82
93
  return colorize('bold', text);
83
94
  }
84
95
 
96
+ /** @param {string} text */
85
97
  function dim(text) {
86
98
  return colorize('gray', text);
87
99
  }
@@ -89,6 +101,7 @@ function dim(text) {
89
101
  /**
90
102
  * Aligned key-value pair.
91
103
  */
104
+ /** @param {string} key @param {string} value @param {number} width */
92
105
  function kv(key, value, width = 30) {
93
106
  return `${bold(key.padEnd(width))} ${value}`;
94
107
  }
@@ -96,93 +109,123 @@ function kv(key, value, width = 30) {
96
109
  /**
97
110
  * Aligned table of columns.
98
111
  */
112
+ /** @param {string[][]} rows @param {{indent?: number, colPadding?: number}} [opts] */
99
113
  function table(rows, { indent = 2, colPadding = 2 } = {}) {
100
- if (rows.length === 0) return '';
114
+ if (rows.length === 0) {return '';}
115
+ /** @param {unknown} _ @param {number} i */
101
116
  const colWidths = rows[0].map((_, i) => Math.max(...rows.map(row => visibleWidth(row[i] ?? ''))));
102
117
  const padding = ' '.repeat(indent);
118
+ /** @param {string[]} row */
103
119
  return rows.map(row => padding + row.map((col, i) => padVisibleEnd(col ?? '', colWidths[i] + colPadding)).join('')).join('\n');
104
120
  }
105
121
 
106
122
  /**
107
123
  * Bulleted list.
108
124
  */
125
+ /** @param {string[]} items @param {{bullet?: string, indent?: number}} [opts] */
109
126
  function list(items, { bullet = '-', indent = 2 } = {}) {
110
127
  const padding = ' '.repeat(indent);
128
+ /** @param {string} item */
111
129
  return items.map(item => `${padding}${bullet} ${item}`).join('\n');
112
130
  }
113
131
 
114
132
  const semantic = {
115
- path: (text) => colorize('blue', text),
116
- slug: (text) => colorize('cyan', bold(text)),
117
- branch: (text) => colorize('magenta', text),
118
- sha: (text) => colorize('yellow', text),
119
- command: (text) => colorize('green', text),
133
+ path: /** @param {string} text */(text) => colorize('blue', text),
134
+ slug: /** @param {string} text */(text) => colorize('cyan', bold(text)),
135
+ branch: /** @param {string} text */(text) => colorize('magenta', text),
136
+ sha: /** @param {string} text */(text) => colorize('yellow', text),
137
+ command: /** @param {string} text */(text) => colorize('green', text),
120
138
  };
121
139
 
140
+ /** @type {{log: Function, error: Function}} */
122
141
  let currentLogger = {
142
+ /** @param {...any} args */
123
143
  log: (...args) => console.log(...args),
144
+ /** @param {...any} args */
124
145
  error: (...args) => console.error(...args),
125
146
  };
126
147
 
148
+ /** @typedef {{log?: Function, error?: Function}} LoggerObj */
149
+
150
+ /** @type{{info: Function, pass: Function, fail: Function, warn: Function, error: Function, debug: Function, plain: Function, plainError: Function}} */
127
151
  const log = {
152
+ /** @param {string} text */
128
153
  info: (text) => {
154
+ /** @param {string} line */
129
155
  return text.toString().split('\n').map(line => {
130
156
  const s = status('INFO', line);
131
157
  currentLogger.log(s);
132
158
  return s;
133
159
  }).join('\n');
134
160
  },
161
+ /** @param {string} text */
135
162
  pass: (text) => {
163
+ /** @param {string} line */
136
164
  return text.toString().split('\n').map(line => {
137
165
  const s = status('PASS', line);
138
166
  currentLogger.log(s);
139
167
  return s;
140
168
  }).join('\n');
141
169
  },
170
+ /** @param {string} text */
142
171
  fail: (text) => {
172
+ /** @param {string} line */
143
173
  return text.toString().split('\n').map(line => {
144
174
  const s = status('FAIL', line);
145
175
  currentLogger.error(s);
146
176
  return s;
147
177
  }).join('\n');
148
178
  },
179
+ /** @param {string} text */
149
180
  warn: (text) => {
181
+ /** @param {string} line */
150
182
  return text.toString().split('\n').map(line => {
151
183
  const s = status('WARN', line);
152
184
  currentLogger.log(s);
153
185
  return s;
154
186
  }).join('\n');
155
187
  },
188
+ /** @param {string} text */
156
189
  error: (text) => {
190
+ /** @param {string} line */
157
191
  return text.toString().split('\n').map(line => {
158
192
  const s = status('FAIL', line);
159
193
  currentLogger.error(s);
160
194
  return s;
161
195
  }).join('\n');
162
196
  },
197
+ /** @param {string} text */
163
198
  debug: (text) => {
164
- if (!process.env.DEBUG) return null;
199
+ if (!process.env.DEBUG) {return null;}
200
+ /** @param {string} line */
165
201
  return text.toString().split('\n').map(line => {
166
202
  const s = status('DEBUG', line);
167
203
  currentLogger.log(s);
168
204
  return s;
169
205
  }).join('\n');
170
206
  },
207
+ /** @param {string} text */
171
208
  plain: (text) => {
209
+ /** @param {string} line */
172
210
  text.toString().split('\n').forEach(line => currentLogger.log(line));
173
211
  return text;
174
212
  },
213
+ /** @param {string} text */
175
214
  plainError: (text) => {
215
+ /** @param {string} line */
176
216
  text.toString().split('\n').forEach(line => currentLogger.error(line));
177
217
  return text;
178
218
  },
179
219
  };
180
220
 
221
+ /** @param {LoggerObj|Function} newLogger */
181
222
  function setLogger(newLogger) {
182
223
  const old = currentLogger;
224
+ /** @type {LoggerObj} */
225
+ const logger = /** @type {LoggerObj} */(newLogger);
183
226
  currentLogger = {
184
- log: newLogger.log || newLogger,
185
- error: newLogger.error || newLogger.log || newLogger,
227
+ log: typeof logger.log === 'function' ? logger.log : (() => {}),
228
+ error: typeof logger.error === 'function' ? logger.error : (typeof logger.log === 'function' ? logger.log : (() => {})),
186
229
  };
187
230
  return old;
188
231
  }
package/lib/core/git.js CHANGED
@@ -1,26 +1,28 @@
1
1
  const childProcess = require('child_process');
2
2
 
3
+ /** @param {string[]} args @param {object} options */
3
4
  function git(args, options = {}) {
4
5
  const spawnOptions = {
5
6
  encoding: 'utf8',
6
7
  stdio: ['ignore', 'pipe', 'pipe'],
7
8
  ...options
8
9
  };
9
- const result = childProcess.spawnSync('git', args, spawnOptions);
10
- if (result.error && result.status == null) {
10
+ const result = childProcess.spawnSync('git', args, /** @type {import('child_process').SpawnSyncOptionsWithStringEncoding} */(spawnOptions));
11
+ if (result.error && result.status === null) {
11
12
  throw result.error;
12
13
  }
13
14
  return result;
14
15
  }
15
16
 
17
+ /** @param {string} command @param {string[]} args @param {object} options */
16
18
  function run(command, args, options = {}) {
17
19
  const spawnOptions = {
18
20
  encoding: 'utf8',
19
21
  stdio: ['ignore', 'pipe', 'pipe'],
20
22
  ...options
21
23
  };
22
- const result = childProcess.spawnSync(command, args, spawnOptions);
23
- if (result.error && result.status == null) {
24
+ const result = childProcess.spawnSync(command, args, /** @type {import('child_process').SpawnSyncOptionsWithStringEncoding} */(spawnOptions));
25
+ if (result.error && result.status === null) {
24
26
  throw result.error;
25
27
  }
26
28
  return result;
@@ -46,7 +48,7 @@ function isDirty(cwd = process.cwd()) {
46
48
 
47
49
  function getUncommittedCount(cwd = process.cwd()) {
48
50
  const result = git(['-C', cwd, 'status', '--porcelain']);
49
- if (!result.stdout.trim()) return 0;
51
+ if (!result.stdout.trim()) {return 0;}
50
52
  return result.stdout.trim().split('\n').length;
51
53
  }
52
54
 
@@ -77,16 +77,18 @@ function ensureWorkflowGitignore(rootDir, options = {}) {
77
77
  try {
78
78
  existingContent = readFileSyncFn(gitignorePath, 'utf8');
79
79
  } catch (err) {
80
- logFn(`[gitignore] Could not read ${gitignorePath}: ${err.message}`);
81
- return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${err.message}` };
80
+ logFn(`[gitignore] Could not read ${gitignorePath}: ${/** @type{Error} */(err).message}`);
81
+ return { ok: false, created: false, appended: 0, skipped: false, reason: `read-error: ${/** @type{Error} */(err).message}` };
82
82
  }
83
83
 
84
84
  // Parse existing entries
85
85
  const existingLines = existingContent.split(/\r?\n/);
86
86
  const existingEntries = new Set(
87
87
  existingLines
88
- .map(line => line.trim())
89
- .filter(line => line.length > 0 && !line.startsWith('#'))
88
+ /** @param {string} line */
89
+ .map(/** @param {string} line */(line) => line.trim())
90
+ /** @param {string} line */
91
+ .filter(/** @param {string} line */(line) => line.length > 0 && !line.startsWith('#'))
90
92
  );
91
93
 
92
94
  // Find missing entries