@kgdiff/cli 0.4.0 → 0.6.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 +21 -4
- package/dist/args.d.ts +8 -1
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +37 -0
- package/dist/args.js.map +1 -1
- package/dist/diff-command.d.ts.map +1 -1
- package/dist/diff-command.js +8 -1
- package/dist/diff-command.js.map +1 -1
- package/dist/errors.d.ts +8 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -4
- package/dist/errors.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +9 -0
- package/dist/help.js.map +1 -1
- package/dist/html.js +2 -2
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +5 -0
- package/dist/io.d.ts.map +1 -1
- package/dist/locale.d.ts +34 -1
- package/dist/locale.d.ts.map +1 -1
- package/dist/locale.js +65 -0
- package/dist/locale.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -2
- package/dist/main.js.map +1 -1
- package/dist/markdown.d.ts +12 -2
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +94 -11
- package/dist/markdown.js.map +1 -1
- package/dist/model.d.ts +42 -2
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +82 -0
- package/dist/model.js.map +1 -1
- package/dist/node-io.d.ts.map +1 -1
- package/dist/node-io.js +1 -0
- package/dist/node-io.js.map +1 -1
- package/dist/report.d.ts +3 -2
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +5 -4
- package/dist/report.js.map +1 -1
- package/dist/summary.d.ts +19 -4
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +105 -19
- package/dist/summary.js.map +1 -1
- package/package.json +6 -6
- package/src/args.ts +58 -2
- package/src/diff-command.ts +8 -1
- package/src/errors.ts +30 -4
- package/src/help.ts +9 -0
- package/src/html.ts +2 -2
- package/src/index.ts +2 -0
- package/src/io.ts +5 -0
- package/src/locale.ts +123 -1
- package/src/main.ts +5 -2
- package/src/markdown.ts +128 -12
- package/src/model.ts +115 -2
- package/src/node-io.ts +1 -0
- package/src/report.ts +13 -5
- package/src/summary.ts +145 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kgdiff/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "git diff for knowledge graphs. Compare two graph files from the terminal, CI, or as a shareable HTML report.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kgdiff",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"registry": "https://registry.npmjs.org/"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@kgdiff/core": "0.
|
|
49
|
-
"@kgdiff/formats": "0.
|
|
50
|
-
"@kgdiff/layout": "0.
|
|
51
|
-
"@kgdiff/scene": "0.
|
|
52
|
-
"@kgdiff/viewer": "0.
|
|
48
|
+
"@kgdiff/core": "0.6.0",
|
|
49
|
+
"@kgdiff/formats": "0.6.0",
|
|
50
|
+
"@kgdiff/layout": "0.6.0",
|
|
51
|
+
"@kgdiff/scene": "0.6.0",
|
|
52
|
+
"@kgdiff/viewer": "0.6.0"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsc -p tsconfig.build.json",
|
package/src/args.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import type { DiffOptions, EdgeIdentity, Locale, MatchOptions } from '@kgdiff/core';
|
|
14
14
|
import type { FormatId } from '@kgdiff/formats';
|
|
15
|
-
import type { ThemePreference, ViewerMode, ViewerScope } from '@kgdiff/viewer';
|
|
15
|
+
import type { ThemePreference, ViewerMode, ViewerPanels, ViewerScope } from '@kgdiff/viewer';
|
|
16
|
+
// The viewer's own panels syntax (no DOM): `--panels` reads exactly what `panels` does.
|
|
17
|
+
import { PANEL_REGIONS, PANEL_STATES, parsePanels } from '@kgdiff/viewer/panels';
|
|
16
18
|
import { CliError } from './errors.js';
|
|
17
19
|
import { matchLocale } from './locale.js';
|
|
18
20
|
import type { ColorMode } from './style.js';
|
|
@@ -38,7 +40,6 @@ const THEMES: readonly ThemePreference[] = ['auto', 'light', 'dark'];
|
|
|
38
40
|
const MODES: readonly (ViewerMode | 'changes')[] = ['overlay', 'swipe', 'replay', 'changes'];
|
|
39
41
|
const SCOPES: readonly ViewerScope[] = ['all', 'changes'];
|
|
40
42
|
const COLOR_MODES: readonly ColorMode[] = ['auto', 'always', 'never'];
|
|
41
|
-
|
|
42
43
|
/** Default `--limit`. */
|
|
43
44
|
export const DEFAULT_LIMIT = 50;
|
|
44
45
|
|
|
@@ -70,6 +71,8 @@ export interface DiffCommandOptions extends CommonOptions {
|
|
|
70
71
|
readonly theme?: ThemePreference;
|
|
71
72
|
readonly mode?: ViewerMode;
|
|
72
73
|
readonly scope?: ViewerScope;
|
|
74
|
+
/** `--panels`: the report's initial panel states (`view.panels`). */
|
|
75
|
+
readonly panels?: ViewerPanels;
|
|
73
76
|
readonly reproducible: boolean;
|
|
74
77
|
/** `--allow-empty`: an empty input is an empty graph instead of an error. */
|
|
75
78
|
readonly allowEmpty: boolean;
|
|
@@ -78,6 +81,11 @@ export interface DiffCommandOptions extends CommonOptions {
|
|
|
78
81
|
readonly exitCode: boolean;
|
|
79
82
|
/** Max hunks in the summary; 0 = all. */
|
|
80
83
|
readonly limit: number;
|
|
84
|
+
/**
|
|
85
|
+
* `--watch-attr`: attributes whose changes are flagged ⚑ for review first, replacing the
|
|
86
|
+
* default definitions. Absent when not given.
|
|
87
|
+
*/
|
|
88
|
+
readonly watchAttrs?: readonly string[];
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
export interface ApplyCommandOptions extends CommonOptions {
|
|
@@ -143,11 +151,13 @@ export const DIFF_OPTIONS: Readonly<Record<string, OptionSpec>> = {
|
|
|
143
151
|
theme: { type: 'string' },
|
|
144
152
|
mode: { type: 'string' },
|
|
145
153
|
scope: { type: 'string' },
|
|
154
|
+
panels: { type: 'string', multiple: true },
|
|
146
155
|
reproducible: { type: 'boolean' },
|
|
147
156
|
'no-compute': { type: 'boolean' },
|
|
148
157
|
'allow-empty': { type: 'boolean' },
|
|
149
158
|
'exit-code': { type: 'boolean' },
|
|
150
159
|
limit: { type: 'string' },
|
|
160
|
+
'watch-attr': { type: 'string', multiple: true },
|
|
151
161
|
};
|
|
152
162
|
|
|
153
163
|
/** Options of `kgdiff apply <graph> <delta>`. */
|
|
@@ -217,6 +227,42 @@ function usageError(message: string, hint = HELP_HINT): CliError {
|
|
|
217
227
|
return new CliError('usage', message, hint);
|
|
218
228
|
}
|
|
219
229
|
|
|
230
|
+
/**
|
|
231
|
+
* `--panels "detail:collapsed minimap:hidden"` (pairs separated by spaces or commas, the option
|
|
232
|
+
* repeatable), read by the viewer's own parser; a region named twice takes the last state. The
|
|
233
|
+
* first pair it rejects becomes a usage error that says what is wrong with it.
|
|
234
|
+
*/
|
|
235
|
+
function panelsOption(values: readonly string[]): ViewerPanels | undefined {
|
|
236
|
+
const { panels, invalid } = parsePanels(values.join(' '));
|
|
237
|
+
const [token] = invalid;
|
|
238
|
+
if (token !== undefined) throw panelsError(token);
|
|
239
|
+
return Object.keys(panels).length > 0 ? panels : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Why `parsePanels` rejected `token`: not a pair, an unknown region, or an unknown state. */
|
|
243
|
+
function panelsError(token: string): CliError {
|
|
244
|
+
const regions = `Regions: ${PANEL_REGIONS.join(', ')}; states: ${PANEL_STATES.join(', ')}.`;
|
|
245
|
+
const [region = '', state, ...rest] = token.split(':');
|
|
246
|
+
if (state === undefined || rest.length > 0) {
|
|
247
|
+
return usageError(
|
|
248
|
+
`Invalid value "${token}" for --panels.`,
|
|
249
|
+
`Expected region:state pairs, e.g. --panels "detail:collapsed minimap:hidden". ${regions}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (!(PANEL_REGIONS as readonly string[]).includes(region)) {
|
|
253
|
+
const close = suggest(region, PANEL_REGIONS);
|
|
254
|
+
return usageError(
|
|
255
|
+
`Unknown panel region "${region}" in --panels.`,
|
|
256
|
+
`${close ? `Did you mean "${close}"? ` : ''}${regions}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const close = suggest(state, PANEL_STATES);
|
|
260
|
+
return usageError(
|
|
261
|
+
`Invalid state "${state}" for the ${region} panel in --panels.`,
|
|
262
|
+
`${close ? `Did you mean "${close}"? ` : ''}Expected one of: ${PANEL_STATES.join(', ')}.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
220
266
|
function choice<T extends string>(name: string, value: string, choices: readonly T[]): T {
|
|
221
267
|
if ((choices as readonly string[]).includes(value)) return value as T;
|
|
222
268
|
const close = suggest(value, choices);
|
|
@@ -492,6 +538,7 @@ function parseDiff(argv: readonly string[]): Command {
|
|
|
492
538
|
: mode === 'changes'
|
|
493
539
|
? 'changes'
|
|
494
540
|
: undefined;
|
|
541
|
+
const panels = panelsOption(v.strings('panels'));
|
|
495
542
|
const limit = v.string('limit');
|
|
496
543
|
const languages = parseLanguages(v);
|
|
497
544
|
const title = v.string('title');
|
|
@@ -499,6 +546,13 @@ function parseDiff(argv: readonly string[]): Command {
|
|
|
499
546
|
const delta = v.target('delta');
|
|
500
547
|
const markdown = v.target('markdown');
|
|
501
548
|
const html = v.string('html');
|
|
549
|
+
const watchAttrs = v.strings('watch-attr');
|
|
550
|
+
if (watchAttrs.some((key) => key.trim() === '')) {
|
|
551
|
+
throw usageError(
|
|
552
|
+
'--watch-attr needs an attribute key.',
|
|
553
|
+
'e.g. --watch-attr rdfs:comment. Attribute keys are listed in the JSON output (--json).',
|
|
554
|
+
);
|
|
555
|
+
}
|
|
502
556
|
const options: DiffCommandOptions = {
|
|
503
557
|
...common,
|
|
504
558
|
before,
|
|
@@ -519,6 +573,7 @@ function parseDiff(argv: readonly string[]): Command {
|
|
|
519
573
|
...(theme !== undefined ? { theme: choice('theme', theme, THEMES) } : {}),
|
|
520
574
|
...(mode !== undefined ? { mode: mode === 'changes' ? 'overlay' : mode } : {}),
|
|
521
575
|
...(scope !== undefined ? { scope } : {}),
|
|
576
|
+
...(panels !== undefined ? { panels } : {}),
|
|
522
577
|
reproducible: v.flag('reproducible'),
|
|
523
578
|
compute: !v.flag('no-compute'),
|
|
524
579
|
allowEmpty: v.flag('allow-empty'),
|
|
@@ -527,6 +582,7 @@ function parseDiff(argv: readonly string[]): Command {
|
|
|
527
582
|
limit === undefined
|
|
528
583
|
? DEFAULT_LIMIT
|
|
529
584
|
: parseNumber('limit', limit, 0, Number.MAX_SAFE_INTEGER, true),
|
|
585
|
+
...(watchAttrs.length > 0 ? { watchAttrs } : {}),
|
|
530
586
|
};
|
|
531
587
|
checkOutputs(options);
|
|
532
588
|
return { kind: 'diff', options };
|
package/src/diff-command.ts
CHANGED
|
@@ -235,7 +235,12 @@ export async function runDiff(o: DiffCommandOptions, io: CliIO): Promise<number>
|
|
|
235
235
|
}
|
|
236
236
|
if (o.markdown !== undefined) {
|
|
237
237
|
const { renderMarkdown } = await import('./markdown.js');
|
|
238
|
-
const md = renderMarkdown(result, {
|
|
238
|
+
const md = renderMarkdown(result, {
|
|
239
|
+
locale: ctx.locale,
|
|
240
|
+
title,
|
|
241
|
+
generator,
|
|
242
|
+
...(o.watchAttrs ? { watch: o.watchAttrs } : {}),
|
|
243
|
+
});
|
|
239
244
|
if (o.markdown !== STDIO)
|
|
240
245
|
files.push({ path: o.markdown, content: md, label: 'Markdown summary' });
|
|
241
246
|
else stdoutText = md;
|
|
@@ -255,6 +260,7 @@ export async function runDiff(o: DiffCommandOptions, io: CliIO): Promise<number>
|
|
|
255
260
|
...(o.scope ? { scope: o.scope } : {}),
|
|
256
261
|
...(o.theme ? { theme: o.theme } : {}),
|
|
257
262
|
locale: ctx.locale,
|
|
263
|
+
...(o.panels ? { panels: o.panels } : {}),
|
|
258
264
|
},
|
|
259
265
|
},
|
|
260
266
|
{ main: bundle, compute: () => loadCompute(o, io, ctx) },
|
|
@@ -284,6 +290,7 @@ export async function runDiff(o: DiffCommandOptions, io: CliIO): Promise<number>
|
|
|
284
290
|
locale: ctx.locale,
|
|
285
291
|
width: outputWidth(io),
|
|
286
292
|
limit: o.limit,
|
|
293
|
+
...(o.watchAttrs ? { watch: o.watchAttrs } : {}),
|
|
287
294
|
};
|
|
288
295
|
if (o.json === STDIO) stdoutText = json(diffToJSON(result), o.pretty);
|
|
289
296
|
else if (o.delta === STDIO) stdoutText = json(result.delta, o.pretty);
|
package/src/errors.ts
CHANGED
|
@@ -78,8 +78,27 @@ const KGDIFF_HINTS: Readonly<Record<string, string>> = {
|
|
|
78
78
|
'dangling-edge': 'An edge refers to a node the graph does not define.',
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
/** The bug report form an unexpected error links to. */
|
|
82
|
+
const BUG_REPORT_FORM = 'https://github.com/iiwish/kgdiff/issues/new';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Link to the bug report form, prefilled through the form's field ids (`version`,
|
|
86
|
+
* `environment`). Only public metadata goes into it: error messages and input paths can contain
|
|
87
|
+
* private data, and the person reporting decides what to paste into the form.
|
|
88
|
+
*/
|
|
89
|
+
export function bugReportUrl(version: string, runtime?: string): string {
|
|
90
|
+
const url = new URL(BUG_REPORT_FORM);
|
|
91
|
+
url.searchParams.set('template', 'bug_report.yml');
|
|
92
|
+
url.searchParams.set('version', `kgdiff ${version}`);
|
|
93
|
+
if (runtime) url.searchParams.set('environment', runtime);
|
|
94
|
+
return url.href;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
/** User-facing message and hint for any thrown value. */
|
|
82
|
-
export function describeError(
|
|
98
|
+
export function describeError(
|
|
99
|
+
error: unknown,
|
|
100
|
+
bugReportUrl?: string,
|
|
101
|
+
): { message: string; hint?: string } {
|
|
83
102
|
if (error instanceof CliError) {
|
|
84
103
|
return error.hint ? { message: error.message, hint: error.hint } : { message: error.message };
|
|
85
104
|
}
|
|
@@ -91,7 +110,9 @@ export function describeError(error: unknown): { message: string; hint?: string
|
|
|
91
110
|
if (code) return { message: systemReason(error) };
|
|
92
111
|
return {
|
|
93
112
|
message: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
|
-
hint:
|
|
113
|
+
hint: bugReportUrl
|
|
114
|
+
? `This is a bug in kgdiff. Run again with DEBUG=kgdiff and report it: ${bugReportUrl}`
|
|
115
|
+
: 'This is a bug in kgdiff. Run again with DEBUG=kgdiff and report the output.',
|
|
95
116
|
};
|
|
96
117
|
}
|
|
97
118
|
|
|
@@ -103,10 +124,15 @@ export function debugEnabled(debug: string | undefined): boolean {
|
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
/** The text written to stderr for an error (ends with a newline). */
|
|
106
|
-
export function formatError(
|
|
127
|
+
export function formatError(
|
|
128
|
+
error: unknown,
|
|
129
|
+
paint: Painter,
|
|
130
|
+
debug: boolean,
|
|
131
|
+
bugReportUrl?: string,
|
|
132
|
+
): string {
|
|
107
133
|
// Messages quote user input (paths, parser snippets, JSON.parse excerpts): never let it
|
|
108
134
|
// carry escape sequences or bidi controls to the terminal.
|
|
109
|
-
const { message, hint } = describeError(error);
|
|
135
|
+
const { message, hint } = describeError(error, bugReportUrl);
|
|
110
136
|
const lines = [`${paint.bold(paint.tone('removed', 'error:'))} ${sanitize(message)}`];
|
|
111
137
|
if (hint) lines.push(`${paint.tone('accent', 'hint:')} ${sanitize(hint)}`);
|
|
112
138
|
if (debug) {
|
package/src/help.ts
CHANGED
|
@@ -110,6 +110,10 @@ const MAIN: readonly Section[] = [
|
|
|
110
110
|
text: 'Show the whole graph or only the changes (in any mode): all, changes',
|
|
111
111
|
default: 'changes for large graphs, else all',
|
|
112
112
|
},
|
|
113
|
+
{
|
|
114
|
+
flags: '--panels <spec>',
|
|
115
|
+
text: 'Initial panel states, e.g. "detail:collapsed minimap:hidden" (repeatable)',
|
|
116
|
+
},
|
|
113
117
|
{
|
|
114
118
|
flags: '--reproducible',
|
|
115
119
|
text: 'Omit the timestamp: byte-identical reports for identical input',
|
|
@@ -138,6 +142,11 @@ const MAIN: readonly Section[] = [
|
|
|
138
142
|
default: 'auto',
|
|
139
143
|
},
|
|
140
144
|
{ flags: '--limit <n>', text: 'Hunks listed in the summary; 0 lists all', default: '50' },
|
|
145
|
+
{
|
|
146
|
+
flags: '--watch-attr <key>',
|
|
147
|
+
text: 'Flag changes to this attribute ⚑ for review first (repeatable); replaces the defaults',
|
|
148
|
+
default: 'definitions',
|
|
149
|
+
},
|
|
141
150
|
{ flags: '-h, --help', text: 'Show this help' },
|
|
142
151
|
{ flags: '-v, --version', text: 'Show the version' },
|
|
143
152
|
],
|
package/src/html.ts
CHANGED
|
@@ -107,8 +107,8 @@ export function renderReportHtml(options: ReportHtmlOptions): string {
|
|
|
107
107
|
`id="${VIEWER_ID}"`,
|
|
108
108
|
// The report page is the viewer: its keyboard shortcuts apply to the whole page.
|
|
109
109
|
'global-shortcuts',
|
|
110
|
-
`
|
|
111
|
-
|
|
110
|
+
// Theme, language and panels travel in the payload's `view`, not as attributes: what the
|
|
111
|
+
// reader chose in an earlier report (remembered by the viewer) wins over them.
|
|
112
112
|
...(view.mode ? [`mode="${escapeHtml(view.mode)}"`] : []),
|
|
113
113
|
...(view.scope ? [`scope="${escapeHtml(view.scope)}"`] : []),
|
|
114
114
|
].join(' ');
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
} from './markdown.js';
|
|
41
41
|
export {
|
|
42
42
|
type ChangeFamily,
|
|
43
|
+
DEFAULT_WATCH_ATTRS,
|
|
43
44
|
hunkFamily,
|
|
44
45
|
hunkGlyph,
|
|
45
46
|
KIND_FAMILY,
|
|
@@ -47,6 +48,7 @@ export {
|
|
|
47
48
|
nameStatus,
|
|
48
49
|
type Overview,
|
|
49
50
|
overview,
|
|
51
|
+
watchedChanges,
|
|
50
52
|
} from './model.js';
|
|
51
53
|
export { nodeIO, openCommand } from './node-io.js';
|
|
52
54
|
export { buildReportHtml, buildReportPayload, createGzip, type ReportInfo } from './report.js';
|
package/src/io.ts
CHANGED
|
@@ -52,6 +52,11 @@ export interface CliIO {
|
|
|
52
52
|
readonly stdinIsTTY: boolean;
|
|
53
53
|
/** Terminal width of stdout, when known. */
|
|
54
54
|
readonly columns: number | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The platform and runtime, for bug reports: `darwin arm64, Node.js v24.11.0`. Optional:
|
|
57
|
+
* without it the bug report link of an unexpected error names only the CLI version.
|
|
58
|
+
*/
|
|
59
|
+
readonly runtime?: string;
|
|
55
60
|
/** Clock for `generatedAt` in HTML reports. */
|
|
56
61
|
now(): Date;
|
|
57
62
|
/** Create a fresh private directory (`mkdtemp`) for reports opened with `--open` and no `-o`. */
|
package/src/locale.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* searched for.
|
|
5
5
|
*/
|
|
6
6
|
import type { Locale, ReplayPhase } from '@kgdiff/core';
|
|
7
|
-
import type
|
|
7
|
+
import { type ChangeFamily, isDefinitionKey, type PossibleMatchSide } from './model.js';
|
|
8
8
|
import type { Env } from './style.js';
|
|
9
9
|
|
|
10
10
|
export const LOCALES: readonly Locale[] = ['en', 'zh-CN'];
|
|
@@ -67,13 +67,71 @@ export interface Messages {
|
|
|
67
67
|
readonly byType: string;
|
|
68
68
|
readonly untyped: string;
|
|
69
69
|
readonly moreTypes: (n: number) => string;
|
|
70
|
+
/** Watched attributes (`--watch-attr`): changes flagged ⚑ for review first. */
|
|
71
|
+
readonly watch: {
|
|
72
|
+
/** How the default definition keys (`def`, `IAO:0000115`, …) are named. */
|
|
73
|
+
readonly definition: string;
|
|
74
|
+
/** After ⚑ under the diffstat bar: `Review first: definition changed in 10 hunks`. */
|
|
75
|
+
readonly line: (labels: readonly string[], hunks: number) => string;
|
|
76
|
+
/** After ⚑ in a hunk's summary: `definition`. */
|
|
77
|
+
readonly marker: (labels: readonly string[]) => string;
|
|
78
|
+
};
|
|
79
|
+
/** Pairings kgdiff considered but did not apply, listed after the hunks. */
|
|
80
|
+
readonly possible: {
|
|
81
|
+
readonly heading: string;
|
|
82
|
+
/** `“Saudi Aramco” → “Aramco”`; each side's id is added when both have the same name. */
|
|
83
|
+
readonly title: (before: PossibleMatchSide, after: PossibleMatchSide) => string;
|
|
84
|
+
readonly belowThreshold: (threshold: string) => string;
|
|
85
|
+
readonly ambiguous: string;
|
|
86
|
+
readonly evidence: (label: string, neighbors: string) => string;
|
|
87
|
+
/** Terminal: what "not applied" means and how to pair one. */
|
|
88
|
+
readonly hint: string;
|
|
89
|
+
readonly more: (n: number) => string;
|
|
90
|
+
};
|
|
70
91
|
readonly markdown: {
|
|
71
92
|
readonly legend: string;
|
|
72
93
|
readonly more: (n: number) => string;
|
|
94
|
+
/** Heading of the block of hunks that change a watched attribute (after ⚑). */
|
|
95
|
+
readonly reviewFirst: string;
|
|
96
|
+
readonly reviewFirstHint: (labels: readonly string[]) => string;
|
|
97
|
+
readonly reviewFirstMore: (n: number) => string;
|
|
98
|
+
/** Like `possible.hint`, as Markdown. */
|
|
99
|
+
readonly possibleHint: string;
|
|
100
|
+
readonly possibleMore: (n: number) => string;
|
|
73
101
|
readonly generatedBy: (generator: string) => string;
|
|
74
102
|
};
|
|
75
103
|
}
|
|
76
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Display names of watched attribute keys, in order: the default definition keys are all
|
|
107
|
+
* “definition” (once), other keys are shown as they are.
|
|
108
|
+
*/
|
|
109
|
+
export function watchLabels(keys: readonly string[], m: Messages): string[] {
|
|
110
|
+
const labels: string[] = [];
|
|
111
|
+
for (const key of keys) {
|
|
112
|
+
const label = isDefinitionKey(key) ? m.watch.definition : key;
|
|
113
|
+
if (!labels.includes(label)) labels.push(label);
|
|
114
|
+
}
|
|
115
|
+
return labels;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Both sides of a possible match, quoted and joined by `arrow`. When they share a name (two
|
|
120
|
+
* “Apple”s), `withId` adds each side's id, as hunk titles do for equally named nodes.
|
|
121
|
+
*/
|
|
122
|
+
function possibleTitle(
|
|
123
|
+
before: PossibleMatchSide,
|
|
124
|
+
after: PossibleMatchSide,
|
|
125
|
+
quote: (name: string) => string,
|
|
126
|
+
withId: (quoted: string, id: string) => string,
|
|
127
|
+
arrow: string,
|
|
128
|
+
): string {
|
|
129
|
+
const same = before.name === after.name;
|
|
130
|
+
const side = (s: PossibleMatchSide): string =>
|
|
131
|
+
same ? withId(quote(s.name), s.id) : quote(s.name);
|
|
132
|
+
return `${side(before)}${arrow}${side(after)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
77
135
|
/** A count before and after the change. */
|
|
78
136
|
export type Count = readonly [before: number, after: number];
|
|
79
137
|
|
|
@@ -123,12 +181,47 @@ const en: Messages = {
|
|
|
123
181
|
byType: 'By type',
|
|
124
182
|
untyped: '(untyped)',
|
|
125
183
|
moreTypes: (n) => `… and ${plural(n, 'more type', 'more types')}`,
|
|
184
|
+
watch: {
|
|
185
|
+
definition: 'definition',
|
|
186
|
+
line: (labels, hunks) =>
|
|
187
|
+
`Review first: ${labels.join(', ')} changed in ${plural(hunks, 'hunk', 'hunks')}`,
|
|
188
|
+
marker: (labels) => labels.join(', '),
|
|
189
|
+
},
|
|
190
|
+
possible: {
|
|
191
|
+
heading: 'Possible matches',
|
|
192
|
+
title: (before, after) =>
|
|
193
|
+
possibleTitle(
|
|
194
|
+
before,
|
|
195
|
+
after,
|
|
196
|
+
(name) => `“${name}”`,
|
|
197
|
+
(quoted, id) => `${quoted} (${id})`,
|
|
198
|
+
' → ',
|
|
199
|
+
),
|
|
200
|
+
belowThreshold: (threshold) => `below the ${threshold} threshold`,
|
|
201
|
+
ambiguous: 'too close to another candidate',
|
|
202
|
+
evidence: (label, neighbors) => `name ${label} · neighbours ${neighbors}`,
|
|
203
|
+
hint:
|
|
204
|
+
'Not paired, so listed as removed or added. To pair one, add a synonym or a ' +
|
|
205
|
+
'replacement link, or lower --threshold.',
|
|
206
|
+
more: (n) => `… and ${formatInt(n)} more · --json lists all`,
|
|
207
|
+
},
|
|
126
208
|
markdown: {
|
|
127
209
|
legend:
|
|
128
210
|
'🟩 added · 🟥 removed · 🟨 content (renames, attributes, types, synonyms) · ' +
|
|
129
211
|
'🟦 structure (moves, relations) · 🟪 identity (merges, splits)',
|
|
130
212
|
more: (n) =>
|
|
131
213
|
`…and ${plural(n, 'more hunk', 'more hunks')}. Open the HTML report for the full list.`,
|
|
214
|
+
reviewFirst: 'Review first',
|
|
215
|
+
reviewFirstHint: (labels) =>
|
|
216
|
+
`These hunks change a watched attribute (${labels.join(', ')}). Such an edit can change ` +
|
|
217
|
+
'what existing data means even when the diff looks small.',
|
|
218
|
+
reviewFirstMore: (n) => `…and ${formatInt(n)} more.`,
|
|
219
|
+
possibleHint:
|
|
220
|
+
'kgdiff did not pair these nodes, so they are listed above as removed or added. If a ' +
|
|
221
|
+
'pair is the same thing, add a synonym or a replacement link, or lower `--threshold`.',
|
|
222
|
+
possibleMore: (n) =>
|
|
223
|
+
`…and ${plural(n, 'more possible match', 'more possible matches')}. The HTML report ` +
|
|
224
|
+
"shows them in each node's details.",
|
|
132
225
|
generatedBy: (generator) => `Generated by ${generator}`,
|
|
133
226
|
},
|
|
134
227
|
};
|
|
@@ -162,10 +255,39 @@ const zh: Messages = {
|
|
|
162
255
|
byType: '按类型',
|
|
163
256
|
untyped: '(无类型)',
|
|
164
257
|
moreTypes: (n) => `… 另有 ${formatInt(n)} 个类型`,
|
|
258
|
+
watch: {
|
|
259
|
+
definition: '定义',
|
|
260
|
+
line: (labels, hunks) => `优先审查:${formatInt(hunks)} 组变更修改了${labels.join('、')}`,
|
|
261
|
+
marker: (labels) => labels.join('、'),
|
|
262
|
+
},
|
|
263
|
+
possible: {
|
|
264
|
+
heading: '可能的匹配',
|
|
265
|
+
title: (before, after) =>
|
|
266
|
+
possibleTitle(
|
|
267
|
+
before,
|
|
268
|
+
after,
|
|
269
|
+
(name) => `「${name}」`,
|
|
270
|
+
(quoted, id) => `${quoted}(${id})`,
|
|
271
|
+
'→',
|
|
272
|
+
),
|
|
273
|
+
belowThreshold: (threshold) => `低于阈值 ${threshold}`,
|
|
274
|
+
ambiguous: '与其他候选过于接近',
|
|
275
|
+
evidence: (label, neighbors) => `名称 ${label} · 邻居 ${neighbors}`,
|
|
276
|
+
hint: '这些节点未配对,因此按删除或新增列出。如是同一节点,可补充别名或替换链接,或调低 --threshold。',
|
|
277
|
+
more: (n) => `… 另有 ${formatInt(n)} 对 · --json 列出全部`,
|
|
278
|
+
},
|
|
165
279
|
markdown: {
|
|
166
280
|
legend:
|
|
167
281
|
'🟩 新增 · 🟥 删除 · 🟨 内容(改名、属性、类型、别名) · 🟦 结构(移动、关系) · 🟪 身份(合并、拆分、换 ID)',
|
|
168
282
|
more: (n) => `……另有 ${formatInt(n)} 组变更未列出,完整列表请查看 HTML 报告。`,
|
|
283
|
+
reviewFirst: '优先审查',
|
|
284
|
+
reviewFirstHint: (labels) =>
|
|
285
|
+
`这些变更修改了受关注的属性(${labels.join('、')})。这类修改在差异里看起来很小,却可能改变已有数据的含义。`,
|
|
286
|
+
reviewFirstMore: (n) => `……另有 ${formatInt(n)} 组。`,
|
|
287
|
+
possibleHint:
|
|
288
|
+
'kgdiff 没有配对这些节点,因此它们在上方按删除或新增列出。如果某一对其实是同一节点,可补充别名或替换链接,或调低 `--threshold`。',
|
|
289
|
+
possibleMore: (n) =>
|
|
290
|
+
`……另有 ${formatInt(n)} 对可能的匹配未列出,HTML 报告的节点详情中可以看到全部。`,
|
|
169
291
|
generatedBy: (generator) => `由 ${generator} 生成`,
|
|
170
292
|
},
|
|
171
293
|
};
|
package/src/main.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* load the diff engine.
|
|
5
5
|
*/
|
|
6
6
|
import { parseCommandLine, peekColorMode } from './args.js';
|
|
7
|
-
import { debugEnabled, formatError } from './errors.js';
|
|
7
|
+
import { bugReportUrl, debugEnabled, formatError } from './errors.js';
|
|
8
8
|
import { renderHelp } from './help.js';
|
|
9
9
|
import type { CliIO } from './io.js';
|
|
10
10
|
import { createPainter, isLightBackground, resolveColorLevel } from './style.js';
|
|
@@ -50,7 +50,10 @@ export async function main(argv: readonly string[], io: CliIO): Promise<number>
|
|
|
50
50
|
resolveColorLevel(peekColorMode(argv), io.env, io.stderrIsTTY),
|
|
51
51
|
light,
|
|
52
52
|
);
|
|
53
|
-
io
|
|
53
|
+
// The platform comes from `io` like every other side effect, so `main` stays runnable
|
|
54
|
+
// in-process and its output deterministic in tests.
|
|
55
|
+
const report = bugReportUrl(cliVersion(), io.runtime);
|
|
56
|
+
io.stderr.write(formatError(error, paint, debugEnabled(io.env.DEBUG), report));
|
|
54
57
|
return 2;
|
|
55
58
|
}
|
|
56
59
|
}
|