logwork-cli 1.1.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/README.md +53 -25
- package/RELEASE.md +4 -2
- package/bin/logwork.mjs +2 -2
- package/commands/commits.mjs +128 -13
- package/commands/history.mjs +0 -4
- package/commands/mcp.mjs +266 -0
- package/commands/quick.mjs +38 -23
- package/commands/reminder.mjs +41 -2
- package/commands/setup.mjs +197 -42
- package/commands/tui.mjs +1 -0
- package/docs/advanced.md +18 -7
- package/docs/mcp-setup.md +120 -26
- package/docs/operations-guide.md +50 -2
- package/docs/recipes.md +40 -0
- package/docs/security.md +23 -2
- package/examples/mcp/antigravity.mcp_config.json +2 -2
- package/examples/mcp/codex.config.toml +2 -2
- package/examples/mcp/copilot.mcp.json +2 -2
- package/lib/api-auth-forms.mjs +393 -0
- package/lib/api-auth-http.mjs +184 -0
- package/lib/api-auth-support.mjs +101 -0
- package/lib/api-auth-tokens.mjs +129 -0
- package/lib/api-auth.mjs +39 -813
- package/lib/api.mjs +38 -1422
- package/lib/apply-ledger.mjs +1 -1
- package/lib/batch-workflow.mjs +187 -2
- package/lib/commit-workflow.mjs +345 -23
- package/lib/config.mjs +7 -0
- package/lib/diagnostics.mjs +116 -4
- package/lib/jira-api.mjs +25 -3
- package/lib/jira-workflow.mjs +36 -6
- package/lib/launcher.mjs +303 -0
- package/lib/logwork-config.mjs +64 -13
- package/lib/manual-apply-state.mjs +15 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +142 -1282
- package/lib/manual-ink-draft-actions.mjs +227 -0
- package/lib/manual-ink-interaction.mjs +318 -0
- package/lib/manual-ink-session.mjs +41 -0
- package/lib/manual-ink-task-editor.mjs +338 -0
- package/lib/manual-ink-ui.mjs +38 -0
- package/lib/manual-ink-wizard.mjs +520 -0
- package/lib/manual-mcp.mjs +78 -20
- package/lib/manual-repl.mjs +171 -22
- package/lib/mcp-install.mjs +930 -0
- package/lib/mcp-server.mjs +51 -20
- package/lib/node-runtime.mjs +155 -0
- package/lib/paths.mjs +7 -0
- package/lib/project-mapping-workflow.mjs +32 -27
- package/lib/query-workflow.mjs +19 -1
- package/lib/reconciliation-workflow.mjs +1 -1
- package/lib/reminder-config.mjs +38 -2
- package/lib/reminder-scheduler.mjs +85 -18
- package/lib/reminder-service.mjs +42 -3
- package/lib/reminder-workflow.mjs +118 -22
- package/lib/repo-project-suggestions.mjs +285 -0
- package/lib/ro-api-diagnostics.mjs +109 -0
- package/lib/ro-api-entries.mjs +359 -0
- package/lib/ro-api-fields.mjs +290 -0
- package/lib/ro-api-http.mjs +68 -0
- package/lib/ro-api-projects.mjs +137 -0
- package/lib/ro-api-tasks.mjs +50 -0
- package/lib/ro-api-timesheet.mjs +413 -0
- package/lib/ro-delete-workflow.mjs +2 -1
- package/lib/ro-entry-reader.mjs +11 -6
- package/lib/ro-resubmit-workflow.mjs +2 -1
- package/lib/terminal-auth-prompts.mjs +7 -4
- package/lib/ui.mjs +121 -56
- package/package.json +11 -4
- package/skills/logwork/SKILL.md +10 -4
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// Ranks the user's RO projects for a git repo that has no project mapping yet,
|
|
2
|
+
// so the user (or an agent) can confirm the likely project instead of typing a
|
|
3
|
+
// projectMemberId. Pure: every input is passed in.
|
|
4
|
+
//
|
|
5
|
+
// Evidence, strongest first:
|
|
6
|
+
// 1. ticket history: a ticket key from this repo's commits appears in one of
|
|
7
|
+
// the user's own RO entries (description or task) of the last 30 days;
|
|
8
|
+
// 2. name similarity: the repo folder name or git remote matches the project
|
|
9
|
+
// name, its acronym in parentheses, or the initials of its words;
|
|
10
|
+
// 3. single booked project: the repo's commit days had exactly one booked
|
|
11
|
+
// project.
|
|
12
|
+
import { extractTickets } from './commit-workflow.mjs';
|
|
13
|
+
import { projectIdentityKey, sameProjectIdentity } from './project-identity.mjs';
|
|
14
|
+
|
|
15
|
+
export const MAX_REPO_SUGGESTIONS = 5;
|
|
16
|
+
|
|
17
|
+
const SCORE_TICKET = 60;
|
|
18
|
+
const SCORE_TICKET_EXTRA = 5;
|
|
19
|
+
const SCORE_TICKET_MAX = 80;
|
|
20
|
+
const SCORE_ACRONYM = 30;
|
|
21
|
+
const SCORE_INITIALS = 25;
|
|
22
|
+
const SCORE_TOKEN = 15;
|
|
23
|
+
const SCORE_TOKEN_EXTRA = 5;
|
|
24
|
+
const SCORE_TOKEN_MAX = 28;
|
|
25
|
+
const SCORE_SINGLE_BOOKED = 15;
|
|
26
|
+
const MAX_TICKET_REASONS = 3;
|
|
27
|
+
|
|
28
|
+
// Words that say nothing about which project a repo belongs to.
|
|
29
|
+
export const NAME_STOP_WORDS = new Set([
|
|
30
|
+
'app', 'apps', 'web', 'website', 'api', 'apis', 'fe', 'be', 'frontend', 'front', 'backend', 'back', 'end',
|
|
31
|
+
'service', 'services', 'svc', 'mobile', 'the', 'a', 'an', 'of', 'and', 'for', 'project', 'projects', 'prj',
|
|
32
|
+
'sit', 'uat', 'prod', 'dev', 'staging', 'stg', 'team', 'repo', 'git', 'src', 'main', 'client', 'server',
|
|
33
|
+
'android', 'ios', 'ui', 'v1', 'v2', 'v3', 'new', 'old', 'core', 'common',
|
|
34
|
+
// Remote URL noise.
|
|
35
|
+
'github', 'gitlab', 'bitbucket', 'com', 'org', 'net', 'io', 'www', 'http', 'https', 'ssh', 'scm', 'azure',
|
|
36
|
+
'visualstudio', 'vs', 'tfs'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Lowercase tokens of a name: split on -_./ (and any other non-alphanumeric
|
|
40
|
+
// character) and on camelCase / PascalCase boundaries.
|
|
41
|
+
export function tokenizeName(value = '') {
|
|
42
|
+
return String(value)
|
|
43
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
44
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
45
|
+
.split(/[^A-Za-z0-9]+/)
|
|
46
|
+
.map((token) => token.toLowerCase())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function meaningfulTokens(value = '') {
|
|
51
|
+
return tokenizeName(value).filter((token) => token.length >= 2 && !NAME_STOP_WORDS.has(token) && !/^\d+$/.test(token));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// What a repo name can match in a project name: its meaningful tokens, the
|
|
55
|
+
// acronyms in parentheses ("GKE Fair Chem System (FCS)" -> fcs), and the
|
|
56
|
+
// initials of its words, with and without stop words ("gfcs").
|
|
57
|
+
export function projectNameKeys(projectName = '') {
|
|
58
|
+
const name = String(projectName);
|
|
59
|
+
const acronyms = [...name.matchAll(/\(([^)]+)\)/g)]
|
|
60
|
+
.flatMap((match) => tokenizeName(match[1]))
|
|
61
|
+
.filter((token) => token.length >= 2);
|
|
62
|
+
const words = tokenizeName(name.replace(/\([^)]*\)/g, ' ')).filter((token) => !/^\d+$/.test(token));
|
|
63
|
+
const initials = new Set();
|
|
64
|
+
for (const list of [words, words.filter((word) => !NAME_STOP_WORDS.has(word))]) {
|
|
65
|
+
if (list.length >= 2) {
|
|
66
|
+
initials.add(list.map((word) => word[0]).join(''));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
tokens: [...new Set(meaningfulTokens(name))],
|
|
71
|
+
acronyms: [...new Set(acronyms)],
|
|
72
|
+
initials: [...initials].filter((value) => value.length >= 2)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The path part of a git remote ("git@host:org/repo.git",
|
|
77
|
+
// "https://host/org/repo.git", "ssh://git@host:22/org/repo") without the host
|
|
78
|
+
// and the .git suffix.
|
|
79
|
+
export function remoteRepoPath(remoteUrl = '') {
|
|
80
|
+
const text = String(remoteUrl || '').trim();
|
|
81
|
+
if (!text) {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
let path = text;
|
|
85
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\/[^/]+\/?(.*)$/i.exec(text);
|
|
86
|
+
const scp = /^[^@/\s]+@[^:/\s]+:(.*)$/.exec(text);
|
|
87
|
+
if (scheme) {
|
|
88
|
+
path = scheme[1];
|
|
89
|
+
} else if (scp) {
|
|
90
|
+
path = scp[1];
|
|
91
|
+
}
|
|
92
|
+
return path.replace(/\.git\/?$/i, '').replace(/^\/+|\/+$/g, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// RO entries from queryLogwork(includeEntries: true) as history items.
|
|
96
|
+
export function historyFromQueryResult(queryResult) {
|
|
97
|
+
return (Array.isArray(queryResult?.entries) ? queryResult.entries : [])
|
|
98
|
+
.map((entry) => ({
|
|
99
|
+
date: entry.date || '',
|
|
100
|
+
projectName: entry.projectName || '',
|
|
101
|
+
...(hasValue(entry.projectMemberId) ? { projectMemberId: entry.projectMemberId } : {}),
|
|
102
|
+
...(hasValue(entry.projectId) ? { projectId: entry.projectId } : {}),
|
|
103
|
+
text: [entry.taskName, entry.description].filter(Boolean).join(' ')
|
|
104
|
+
}))
|
|
105
|
+
.filter((entry) => entry.text && (entry.projectName || hasValue(entry.projectMemberId) || hasValue(entry.projectId)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Booked projects (bookedHours > 0) per date from a queryLogwork result.
|
|
109
|
+
export function bookedProjectsByDateFromQuery(queryResult) {
|
|
110
|
+
const byDate = {};
|
|
111
|
+
for (const day of Array.isArray(queryResult?.days) ? queryResult.days : []) {
|
|
112
|
+
byDate[day.date] = (day.projects || []).filter((project) => Number(project.bookedHours || 0) > 0);
|
|
113
|
+
}
|
|
114
|
+
return byDate;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Every candidate with its score and reasons, highest score first; ties keep
|
|
118
|
+
// the candidates' order. A candidate without evidence scores 0.
|
|
119
|
+
export function rankRepoProjects({
|
|
120
|
+
repoPath = '',
|
|
121
|
+
repoGroups = [],
|
|
122
|
+
candidates = [],
|
|
123
|
+
history = [],
|
|
124
|
+
remoteUrl = '',
|
|
125
|
+
bookedProjectsByDate = {}
|
|
126
|
+
} = {}) {
|
|
127
|
+
const projects = uniqueProjects(candidates);
|
|
128
|
+
const tickets = [...new Set(repoGroups.flatMap((group) => group.tickets || []))];
|
|
129
|
+
const repoName = String(repoPath).split(/[\\/]/).filter(Boolean).pop() || '';
|
|
130
|
+
const nameSources = [
|
|
131
|
+
{ label: 'repo name', tokens: meaningfulTokens(repoName) },
|
|
132
|
+
{ label: 'git remote', tokens: meaningfulTokens(remoteRepoPath(remoteUrl)) }
|
|
133
|
+
].filter((source) => source.tokens.length);
|
|
134
|
+
const singleBooked = singleBookedProject(repoGroups, bookedProjectsByDate);
|
|
135
|
+
|
|
136
|
+
const ranked = projects.map((project, index) => {
|
|
137
|
+
const reasons = [];
|
|
138
|
+
let score = 0;
|
|
139
|
+
|
|
140
|
+
const ticketHits = ticketEvidence(project, tickets, history);
|
|
141
|
+
if (ticketHits.length) {
|
|
142
|
+
score += Math.min(SCORE_TICKET_MAX, SCORE_TICKET + SCORE_TICKET_EXTRA * (ticketHits.length - 1));
|
|
143
|
+
reasons.push(...ticketHits.slice(0, MAX_TICKET_REASONS)
|
|
144
|
+
.map((hit) => `ticket ${hit.ticket} was logged to this project on ${hit.date}`));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const name = nameEvidence(project, nameSources);
|
|
148
|
+
if (name) {
|
|
149
|
+
score += name.score;
|
|
150
|
+
reasons.push(name.reason);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (singleBooked && sameProjectIdentity(singleBooked.project, project)) {
|
|
154
|
+
score += SCORE_SINGLE_BOOKED;
|
|
155
|
+
reasons.push(`the only project booked on this repo's commit days (${formatDates(singleBooked.dates)})`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { ...projectRef(project), score, reasons, index };
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return ranked
|
|
162
|
+
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
163
|
+
.map(({ index: _index, ...item }) => item);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The top suggestions (score > 0), at most MAX_REPO_SUGGESTIONS.
|
|
167
|
+
export function suggestRepoProjects(input = {}) {
|
|
168
|
+
return rankRepoProjects(input)
|
|
169
|
+
.filter((item) => item.score > 0)
|
|
170
|
+
.slice(0, MAX_REPO_SUGGESTIONS);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Picker order: suggestions first (top one first), then every other candidate
|
|
174
|
+
// in its original order. Each item: { project, suggestion | null }.
|
|
175
|
+
export function orderProjectsForPicker(candidates = [], suggestions = []) {
|
|
176
|
+
const items = suggestions.map((suggestion) => ({ project: projectRef(suggestion), suggestion }));
|
|
177
|
+
for (const project of uniqueProjects(candidates)) {
|
|
178
|
+
if (!items.some((item) => sameProjectIdentity(item.project, project))) {
|
|
179
|
+
items.push({ project: projectRef(project), suggestion: null });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return items;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatSuggestionReason(suggestion) {
|
|
186
|
+
return (suggestion?.reasons || []).join('; ');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ticketEvidence(project, tickets, history) {
|
|
190
|
+
if (!tickets.length || !history.length) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const hits = [];
|
|
194
|
+
for (const ticket of tickets) {
|
|
195
|
+
const dates = history
|
|
196
|
+
.filter((entry) => sameProjectIdentity(entry, project) && extractTickets(entry.text).includes(ticket))
|
|
197
|
+
.map((entry) => entry.date)
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.sort();
|
|
200
|
+
if (dates.length) {
|
|
201
|
+
hits.push({ ticket, date: dates.at(-1) });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return hits.sort((left, right) => right.date.localeCompare(left.date));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function nameEvidence(project, sources) {
|
|
208
|
+
if (!sources.length) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const keys = projectNameKeys(project.projectName);
|
|
212
|
+
let best = null;
|
|
213
|
+
const consider = (candidate) => {
|
|
214
|
+
if (!best || candidate.score > best.score) {
|
|
215
|
+
best = candidate;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
for (const source of sources) {
|
|
219
|
+
const acronym = keys.acronyms.find((value) => source.tokens.includes(value));
|
|
220
|
+
if (acronym) {
|
|
221
|
+
consider({ score: SCORE_ACRONYM, reason: `${source.label} matches ${acronym.toUpperCase()}` });
|
|
222
|
+
}
|
|
223
|
+
const initials = keys.initials.find((value) => source.tokens.includes(value));
|
|
224
|
+
if (initials) {
|
|
225
|
+
consider({ score: SCORE_INITIALS, reason: `${source.label} matches the initials ${initials.toUpperCase()} of the project name` });
|
|
226
|
+
}
|
|
227
|
+
const shared = [...new Set(source.tokens.filter((token) => keys.tokens.includes(token)))];
|
|
228
|
+
if (shared.length) {
|
|
229
|
+
consider({
|
|
230
|
+
score: Math.min(SCORE_TOKEN_MAX, SCORE_TOKEN + SCORE_TOKEN_EXTRA * (shared.length - 1)),
|
|
231
|
+
reason: `${source.label} shares ${shared.map((token) => `"${token}"`).join(', ')} with the project name`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return best;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function singleBookedProject(repoGroups, bookedProjectsByDate) {
|
|
239
|
+
const dates = [...new Set(repoGroups.map((group) => group.date).filter(Boolean))].sort();
|
|
240
|
+
if (!dates.length) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const projects = [];
|
|
244
|
+
for (const date of dates) {
|
|
245
|
+
for (const project of bookedProjectsByDate?.[date] || []) {
|
|
246
|
+
if (!projects.some((known) => sameProjectIdentity(known, project))) {
|
|
247
|
+
projects.push(project);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return projects.length === 1 ? { project: projects[0], dates } : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function uniqueProjects(projects) {
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
const results = [];
|
|
257
|
+
for (const project of Array.isArray(projects) ? projects : []) {
|
|
258
|
+
if (!project || !(project.projectName || hasValue(project.projectMemberId) || hasValue(project.projectId))) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const key = projectIdentityKey(project);
|
|
262
|
+
if (seen.has(key) || results.some((known) => sameProjectIdentity(known, project))) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
seen.add(key);
|
|
266
|
+
results.push(project);
|
|
267
|
+
}
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function projectRef(project = {}) {
|
|
272
|
+
return {
|
|
273
|
+
projectName: project.projectName || '',
|
|
274
|
+
...(hasValue(project.projectMemberId) ? { projectMemberId: project.projectMemberId } : {}),
|
|
275
|
+
...(hasValue(project.projectId) ? { projectId: project.projectId } : {})
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function formatDates(dates) {
|
|
280
|
+
return dates.length > 3 ? `${dates.slice(0, 3).join(', ')} +${dates.length - 3} more` : dates.join(', ');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function hasValue(value) {
|
|
284
|
+
return value !== undefined && value !== null && String(value).trim() !== '';
|
|
285
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Normalization diagnostics attached (non-enumerable) to normalized RO record lists.
|
|
2
|
+
|
|
3
|
+
const NORMALIZATION_DIAGNOSTICS_KEY = 'normalization';
|
|
4
|
+
|
|
5
|
+
const MAX_DIAGNOSTIC_ITEMS = 25;
|
|
6
|
+
|
|
7
|
+
export function getNormalizationDiagnostics(value) {
|
|
8
|
+
return value?.[NORMALIZATION_DIAGNOSTICS_KEY] || emptyNormalizationDiagnostics();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createNormalizationDiagnostics(sourceShape = 'unknown') {
|
|
12
|
+
return {
|
|
13
|
+
status: 'ok',
|
|
14
|
+
sourceShape,
|
|
15
|
+
rowsRead: 0,
|
|
16
|
+
rowsAccepted: 0,
|
|
17
|
+
droppedRowCount: 0,
|
|
18
|
+
warningCount: 0,
|
|
19
|
+
warnings: [],
|
|
20
|
+
droppedRows: []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function emptyNormalizationDiagnostics() {
|
|
25
|
+
return createNormalizationDiagnostics('unknown');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function attachNormalizationDiagnostics(value, diagnostics) {
|
|
29
|
+
Object.defineProperty(value, NORMALIZATION_DIAGNOSTICS_KEY, {
|
|
30
|
+
value: finalizeNormalizationDiagnostics(diagnostics),
|
|
31
|
+
enumerable: false,
|
|
32
|
+
configurable: true
|
|
33
|
+
});
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function finalizeNormalizationDiagnostics(diagnostics) {
|
|
38
|
+
const warningCount = diagnostics.warnings.length;
|
|
39
|
+
const droppedRowCount = diagnostics.droppedRows.length;
|
|
40
|
+
return {
|
|
41
|
+
...diagnostics,
|
|
42
|
+
status: warningCount || droppedRowCount ? 'warning' : 'ok',
|
|
43
|
+
warningCount,
|
|
44
|
+
droppedRowCount
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function combineNormalizationDiagnostics(...items) {
|
|
49
|
+
const diagnostics = createNormalizationDiagnostics('combined');
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const current = item?.warnings || item?.droppedRows
|
|
52
|
+
? item
|
|
53
|
+
: getNormalizationDiagnostics(item);
|
|
54
|
+
if (!current) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
diagnostics.rowsRead += Number(current.rowsRead || 0);
|
|
58
|
+
diagnostics.rowsAccepted += Number(current.rowsAccepted || 0);
|
|
59
|
+
for (const warning of current.warnings || []) {
|
|
60
|
+
pushDiagnostic(diagnostics.warnings, warning);
|
|
61
|
+
}
|
|
62
|
+
for (const dropped of current.droppedRows || []) {
|
|
63
|
+
pushDiagnostic(diagnostics.droppedRows, dropped);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return finalizeNormalizationDiagnostics(diagnostics);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function addWarning(diagnostics, reason, details = {}) {
|
|
70
|
+
if (!diagnostics) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
pushDiagnostic(diagnostics.warnings, compactDiagnostic({
|
|
74
|
+
reason,
|
|
75
|
+
...details
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function addDroppedRow(diagnostics, reason, details = {}) {
|
|
80
|
+
if (!diagnostics) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
pushDiagnostic(diagnostics.droppedRows, compactDiagnostic({
|
|
84
|
+
reason,
|
|
85
|
+
...details
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pushDiagnostic(items, item) {
|
|
90
|
+
if (items.length < MAX_DIAGNOSTIC_ITEMS) {
|
|
91
|
+
items.push(item);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compactDiagnostic(details) {
|
|
96
|
+
return Object.fromEntries(Object.entries(details).filter(([, value]) => (
|
|
97
|
+
value !== undefined &&
|
|
98
|
+
value !== null &&
|
|
99
|
+
value !== ''
|
|
100
|
+
)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function diagnosticProjectFields({ projectMemberId, projectId, projectName } = {}) {
|
|
104
|
+
return compactDiagnostic({
|
|
105
|
+
projectMemberId,
|
|
106
|
+
projectId,
|
|
107
|
+
projectName
|
|
108
|
+
});
|
|
109
|
+
}
|