cans-spec 0.3.0 → 0.5.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 +15 -6
- package/bin/cans.js +87 -14
- package/bin/ts-loader.mjs +9 -2
- package/package.json +1 -1
- package/src/cli.ts +6 -1
- package/src/commands/budget.ts +87 -16
- package/src/commands/check.ts +313 -58
- package/src/commands/import.ts +216 -6
- package/src/core/fs.ts +31 -11
- package/src/core/outline.ts +17 -1
- package/src/core/output.ts +202 -57
- package/src/core/overflow.ts +4 -0
- package/src/core/redundancy.ts +5 -0
- package/src/core/refs.ts +197 -25
- package/src/core/report.ts +575 -0
- package/src/core/structure.ts +10 -0
- package/src/core/style.ts +2 -0
- package/src/core/token-budget.ts +34 -3
- package/src/types.ts +17 -0
package/src/core/output.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
CommandResult, CheckResult,
|
|
2
|
+
CommandResult, CheckResult, InitResult, NewResult, DoneResult, StatusResult,
|
|
3
3
|
BudgetReadResult, BudgetWriteResult, ImportResult, ExportResult, VersionResult,
|
|
4
4
|
} from '../types.ts';
|
|
5
|
+
import {
|
|
6
|
+
buildReport, topGroups, checkReportJson,
|
|
7
|
+
type IssueGroup, type SectionReport,
|
|
8
|
+
} from './report.ts';
|
|
5
9
|
|
|
6
10
|
/** Single emission point. Commands never console.log or process.exit directly.
|
|
7
|
-
* `refsOnly` (check only
|
|
8
|
-
*
|
|
9
|
-
|
|
11
|
+
* `refsOnly` (check only): human output is scoped to the REFS section (+ Rules).
|
|
12
|
+
* `show` (check only, issue #41): sections rendered UNFOLDED by --show.
|
|
13
|
+
* --json: check emits the lossless structured wire shape (sections → arrays
|
|
14
|
+
* of {file, line, rule, detail}, issue #41); other commands the raw result. */
|
|
15
|
+
export function emit(result: CommandResult, json: boolean, refsOnly?: boolean, show?: Set<string>): void {
|
|
10
16
|
if (json) {
|
|
11
|
-
|
|
17
|
+
const body = result.command === 'check' ? checkReportJson(result as unknown as Parameters<typeof checkReportJson>[0]) : result;
|
|
18
|
+
console.log(JSON.stringify(body, null, 2));
|
|
12
19
|
return;
|
|
13
20
|
}
|
|
14
|
-
printHuman(result, refsOnly);
|
|
21
|
+
printHuman(result, refsOnly, show);
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export function printHuman(result: CommandResult, refsOnly?: boolean): void {
|
|
24
|
+
export function printHuman(result: CommandResult, refsOnly?: boolean, show?: Set<string>): void {
|
|
20
25
|
switch (result.command) {
|
|
21
26
|
case 'check':
|
|
22
|
-
printCheckHuman(result as CheckResult, refsOnly);
|
|
27
|
+
printCheckHuman(result as CheckResult, refsOnly, show);
|
|
23
28
|
break;
|
|
24
29
|
case 'help':
|
|
25
30
|
printHelp();
|
|
@@ -206,7 +211,23 @@ export function printHuman(result: CommandResult, refsOnly?: boolean): void {
|
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
213
|
|
|
209
|
-
|
|
214
|
+
// ── issue #41: aggregated check report ──
|
|
215
|
+
// One line per pattern (`61× <min children (2/3)`), grouped by root cause,
|
|
216
|
+
// top-N + fold with --show expansion, timing on the summary line, and a
|
|
217
|
+
// ≤500-token default budget for issue-scale projects.
|
|
218
|
+
|
|
219
|
+
const DISPLAY_NAMES: Record<string, string> = {
|
|
220
|
+
structure: 'STRUCTURE', style: 'STYLE', refs: 'REFS', redundancy: 'REDUNDANCY',
|
|
221
|
+
overflow: 'OVERFLOW', parse: 'PARSE', content: 'CONTENT', io: 'IO', other: 'OTHER',
|
|
222
|
+
};
|
|
223
|
+
const SECTION_PRINT_ORDER = ['structure', 'style', 'refs', 'redundancy', 'overflow', 'parse', 'content', 'io', 'other'];
|
|
224
|
+
const FOLD_TOP_GROUPS = 5;
|
|
225
|
+
const FOLD_KEYWORD_ITEMS = 5;
|
|
226
|
+
const FOLD_OVERLAP_ITEMS = 3;
|
|
227
|
+
const FOLD_TARGETS = 8;
|
|
228
|
+
const WRAP_COLS = 72;
|
|
229
|
+
|
|
230
|
+
function printCheckHuman(r: CheckResult, refsOnly?: boolean, show?: Set<string>): void {
|
|
210
231
|
// §37: check-level failures (unknown flag, no cans workspace, invalid
|
|
211
232
|
// _rules.yaml, unmatched file filter) carry their diagnosis in `error` —
|
|
212
233
|
// print it standalone, never inside a report-shaped body.
|
|
@@ -216,63 +237,186 @@ function printCheckHuman(r: CheckResult, refsOnly?: boolean): void {
|
|
|
216
237
|
return;
|
|
217
238
|
}
|
|
218
239
|
|
|
219
|
-
const
|
|
220
|
-
for (const i of r.issues) {
|
|
221
|
-
const list = byCategory.get(i.category) ?? [];
|
|
222
|
-
list.push(i);
|
|
223
|
-
byCategory.set(i.category, list);
|
|
224
|
-
}
|
|
225
|
-
if (!refsOnly) {
|
|
226
|
-
console.log('Structure');
|
|
227
|
-
console.log(` ${r.files} files, ${r.nodes} nodes, max depth ${r.maxDepth}`);
|
|
228
|
-
printIssues(byCategory.get('structure'));
|
|
229
|
-
|
|
230
|
-
console.log('Style');
|
|
231
|
-
printIssues(byCategory.get('style'));
|
|
232
|
-
}
|
|
240
|
+
const expanded = (name: string): boolean => show !== undefined && (show.has('all') || show.has(name));
|
|
233
241
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.log(
|
|
237
|
-
printIssues(byCategory.get('refs'));
|
|
242
|
+
// issue #41: summary line first — severity mark, workspace shape, elapsed ms.
|
|
243
|
+
const mark = r.errorCount > 0 ? '✗' : r.warningCount > 0 ? '⚠' : '✓';
|
|
244
|
+
console.log(`${mark} ${r.files} files · ${r.nodes} nodes · depth ${r.maxDepth} · ${r.elapsedMs ?? 0}ms`);
|
|
238
245
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
const report = buildReport(r.issues);
|
|
247
|
+
for (const name of SECTION_PRINT_ORDER) {
|
|
248
|
+
if (refsOnly && name !== 'refs') continue;
|
|
249
|
+
let section = report.sections[name];
|
|
250
|
+
// issue #11: a --fix run that rewrote back-pointer files always surfaces
|
|
251
|
+
// the REFS block — even when the refs engine has no findings of its own
|
|
252
|
+
// (the clean/healthy case), the run's writes stay visible there.
|
|
253
|
+
if (name === 'refs' && section === undefined && r.backPointersUpdatedFiles.length > 0) {
|
|
254
|
+
section = { name: 'refs', errorCount: 0, warningCount: 0, groups: [] };
|
|
245
255
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
256
|
+
// Compact contract: STRUCTURE/STYLE/REDUNDANCY (and the small sections)
|
|
257
|
+
// print only when they carry findings; REFS and OVERFLOW always print
|
|
258
|
+
// (they carry the ✓ healthy state, as in the issue's expected output).
|
|
259
|
+
if (section === undefined) {
|
|
260
|
+
if (name === 'overflow' && !refsOnly) {
|
|
261
|
+
console.log('');
|
|
262
|
+
console.log('OVERFLOW ✓');
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
252
265
|
}
|
|
266
|
+
console.log('');
|
|
267
|
+
if (name === 'refs') printRefsSection(r, section, expanded('refs'));
|
|
268
|
+
else printSection(section, expanded(name));
|
|
253
269
|
}
|
|
254
270
|
|
|
255
|
-
// §22: the fixed report order ends Structure → Style → References →
|
|
256
|
-
// Redundancy → Overflow → Rules → Summary (QA-02 F17).
|
|
257
271
|
if (r.rulesSummary !== undefined) {
|
|
258
|
-
console.log('
|
|
259
|
-
console.log(` ✓ ${r.rulesSummary}`);
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(`RULES ✓ ${compactRules(r.rulesSummary)}`);
|
|
260
274
|
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function printSection(section: SectionReport, expanded: boolean): void {
|
|
278
|
+
const mark = section.errorCount > 0 ? '✗' : '⚠';
|
|
279
|
+
console.log(`${DISPLAY_NAMES[section.name] ?? section.name.toUpperCase()} ${mark} ${section.errorCount + section.warningCount}`);
|
|
280
|
+
const { shown, folded } = expanded ? { shown: section.groups, folded: 0 } : topGroups(section, FOLD_TOP_GROUPS);
|
|
281
|
+
for (const g of shown) printGroup(g, expanded);
|
|
282
|
+
if (folded > 0) console.log(` … ${folded} more → cans check --show ${section.name}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function printRefsSection(r: CheckResult, section: SectionReport, expanded: boolean): void {
|
|
286
|
+
// Header: per-root-cause counts (issue #41 example: `REFS ✗ 96 broken · ⚠ 8 stale · ⚠ 1 orphan`).
|
|
287
|
+
// Level-aware: named buckets subtract from the section totals, the residue
|
|
288
|
+
// prints as `✗ N` / `⚠ N` — no finding is ever hidden or double-counted.
|
|
289
|
+
const byRule = new Map<string, { err: number; warn: number }>();
|
|
290
|
+
for (const g of section.groups) {
|
|
291
|
+
const cur = byRule.get(g.rule) ?? { err: 0, warn: 0 };
|
|
292
|
+
if (g.level === 'error') cur.err += g.count;
|
|
293
|
+
else cur.warn += g.count;
|
|
294
|
+
byRule.set(g.rule, cur);
|
|
295
|
+
}
|
|
296
|
+
const named = ['refs.broken.file', 'refs.backpointer.stale', 'refs.orphan', 'refs.deep_hop'];
|
|
297
|
+
const namedErr = named.reduce((a, rl) => a + (byRule.get(rl)?.err ?? 0), 0);
|
|
298
|
+
const namedWarn = named.reduce((a, rl) => a + (byRule.get(rl)?.warn ?? 0), 0);
|
|
299
|
+
const parts: string[] = [];
|
|
300
|
+
const broken = byRule.get('refs.broken.file');
|
|
301
|
+
if (broken !== undefined && broken.err + broken.warn > 0) parts.push(`✗ ${broken.err + broken.warn} broken`);
|
|
302
|
+
const stale = byRule.get('refs.backpointer.stale');
|
|
303
|
+
if (stale !== undefined && stale.err + stale.warn > 0) parts.push(`⚠ ${stale.err + stale.warn} stale`);
|
|
304
|
+
const orphan = byRule.get('refs.orphan');
|
|
305
|
+
if (orphan !== undefined && orphan.err + orphan.warn > 0) parts.push(`⚠ ${orphan.err + orphan.warn} orphan`);
|
|
306
|
+
const hops = byRule.get('refs.deep_hop');
|
|
307
|
+
if (hops !== undefined && hops.err > 0) parts.push(`✗ ${hops.err} deep-hop`);
|
|
308
|
+
else if (hops !== undefined && hops.warn > 0) parts.push(`⚠ ${hops.warn} deep-hop`);
|
|
309
|
+
const errOther = section.errorCount - namedErr;
|
|
310
|
+
const warnOther = section.warningCount - namedWarn;
|
|
311
|
+
if (errOther > 0) parts.push(`✗ ${errOther}`);
|
|
312
|
+
if (warnOther > 0) parts.push(`⚠ ${warnOther}`);
|
|
313
|
+
if (parts.length > 0) {
|
|
314
|
+
console.log(`REFS ${parts.join(' · ')}`);
|
|
315
|
+
} else {
|
|
316
|
+
const bp = r.backPointers.total > 0 ? ` · ${r.backPointers.current}/${r.backPointers.total} back-ptrs` : '';
|
|
317
|
+
console.log(`REFS ✓ ${r.refs.total} refs${bp}`);
|
|
318
|
+
}
|
|
319
|
+
// Issue #11: name the files --fix actually rewrote (099e858 wording) —
|
|
320
|
+
// never printed as an empty list (plain checks show nothing here).
|
|
321
|
+
if (r.backPointersUpdatedFiles.length > 0) {
|
|
322
|
+
console.log(` --fix updated ref-by in: ${r.backPointersUpdatedFiles.join(', ')}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// issue #41 design rule 2: all missing-file refs coalesce into ONE block
|
|
326
|
+
// (91 broken refs → 4 targets with per-target counts, one fix hint).
|
|
327
|
+
const brokenGroups = section.groups.filter(g => g.rule === 'refs.broken.file');
|
|
328
|
+
const rest = section.groups.filter(g => g.rule !== 'refs.broken.file');
|
|
329
|
+
if (brokenGroups.length > 0) printBrokenFileBlock(brokenGroups);
|
|
330
|
+
const { shown, folded } = expanded ? { shown: rest, folded: 0 } : topGroups({ ...section, groups: rest }, FOLD_TOP_GROUPS);
|
|
331
|
+
for (const g of shown) printGroup(g, expanded);
|
|
332
|
+
if (folded > 0) console.log(` … ${folded} more → cans check --show refs`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function printBrokenFileBlock(groups: IssueGroup[]): void {
|
|
336
|
+
const total = groups.reduce((a, g) => a + g.count, 0);
|
|
337
|
+
console.log(` ${String(total).padStart(2)}× missing file`);
|
|
338
|
+
const targets = groups
|
|
339
|
+
.filter(g => g.key !== undefined)
|
|
340
|
+
.map(g => ({ label: g.key!, count: g.count }))
|
|
341
|
+
.sort((a, b) => b.count - a.count || (a.label < b.label ? -1 : 1));
|
|
342
|
+
const shown = targets.slice(0, FOLD_TARGETS);
|
|
343
|
+
for (const line of wrapText(shown.map(t => `${t.label} (${t.count})`).join(' · '), WRAP_COLS, 6)) console.log(line);
|
|
344
|
+
if (targets.length > shown.length) console.log(` ↳ ${targets.length - shown.length} more targets → cans check --show refs`);
|
|
345
|
+
const hint = groups.map(g => g.suggestion).find(s => s !== undefined);
|
|
346
|
+
if (hint !== undefined) console.log(` ↳ ${hint}`);
|
|
347
|
+
}
|
|
261
348
|
|
|
262
|
-
|
|
263
|
-
console.log(
|
|
264
|
-
|
|
349
|
+
function printGroup(g: IssueGroup, expanded: boolean): void {
|
|
350
|
+
console.log(` ${String(g.count).padStart(2)}× ${g.pattern}`);
|
|
351
|
+
switch (g.rule) {
|
|
352
|
+
case 'redundancy.keyword': {
|
|
353
|
+
const items = g.items ?? [];
|
|
354
|
+
const shownItems = expanded ? items : items.slice(0, FOLD_KEYWORD_ITEMS);
|
|
355
|
+
// issue #41: `artifacts:105 db:74` — keyword:nodeCount, metric-ranked.
|
|
356
|
+
for (const line of wrapText(shownItems.map(it => `${it.label}:${it.metric ?? it.count}`).join(' '), WRAP_COLS, 6)) console.log(line);
|
|
357
|
+
const hidden = items.slice(FOLD_KEYWORD_ITEMS).reduce((a, it) => a + it.count, 0);
|
|
358
|
+
if (!expanded && hidden > 0) console.log(` ↳ ${hidden} more → cans check --show redundancy`);
|
|
359
|
+
else if (g.suggestion !== undefined) console.log(` ↳ ${g.suggestion}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
case 'redundancy.overlap.exact':
|
|
363
|
+
case 'redundancy.overlap.fuzzy': {
|
|
364
|
+
const items = g.items ?? [];
|
|
365
|
+
const shownItems = expanded ? items : items.slice(0, FOLD_OVERLAP_ITEMS);
|
|
366
|
+
if (shownItems.length > 0) {
|
|
367
|
+
for (const line of wrapText(`worst: ${shownItems.map(it => it.label).join(' · ')}`, WRAP_COLS, 6)) console.log(line);
|
|
368
|
+
}
|
|
369
|
+
const hidden = items.length - shownItems.length;
|
|
370
|
+
if (!expanded && hidden > 0) console.log(` ↳ ${hidden} more pairs → cans check --show redundancy`);
|
|
371
|
+
else if (g.suggestion !== undefined) console.log(` ↳ ${g.suggestion}`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
case 'refs.backpointer.stale': {
|
|
375
|
+
// `budget:2 ← agent, effect, interface` — target lines + referrers.
|
|
376
|
+
const referrers = (g.items ?? []).map(it => it.label).join(', ');
|
|
377
|
+
const line = referrers !== '' ? `${g.locations.join(' ')} ← ${referrers}` : g.locations.join(' ');
|
|
378
|
+
for (const l of wrapText(line, WRAP_COLS, 6)) console.log(l);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
case 'refs.broken.anchor': {
|
|
382
|
+
const details = (expanded ? (g.items ?? []).map(it => it.label) : (g.detail !== undefined ? [g.detail] : (g.items ?? []).slice(0, FOLD_OVERLAP_ITEMS).map(it => it.label)));
|
|
383
|
+
for (const d of details) console.log(` ${d}`);
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
default: {
|
|
387
|
+
for (const line of wrapText(g.locations.join(' '), WRAP_COLS, 6)) console.log(line);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (g.suggestion !== undefined) console.log(` ↳ ${g.suggestion}`);
|
|
265
391
|
}
|
|
266
392
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
393
|
+
/** Greedy two-space-separator wrap (location lists), fixed indent. */
|
|
394
|
+
function wrapText(text: string, cols: number, indent: number): string[] {
|
|
395
|
+
const pad = ' '.repeat(indent);
|
|
396
|
+
const budget = Math.max(cols - indent, 20);
|
|
397
|
+
if (text.length <= budget) return text === '' ? [] : [pad + text];
|
|
398
|
+
const pieces = text.split(' ');
|
|
399
|
+
const lines: string[] = [];
|
|
400
|
+
let cur = '';
|
|
401
|
+
for (const p of pieces) {
|
|
402
|
+
const candidate = cur === '' ? p : `${cur} ${p}`;
|
|
403
|
+
if (cur === '' || candidate.length <= budget) cur = candidate;
|
|
404
|
+
else {
|
|
405
|
+
lines.push(pad + cur);
|
|
406
|
+
cur = p;
|
|
407
|
+
}
|
|
275
408
|
}
|
|
409
|
+
if (cur !== '') lines.push(pad + cur);
|
|
410
|
+
return lines;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** `node_length: 3–120 | siblings: 3–12 | depth: 5–7` → `len 3–120 · sib 3–12 · depth 5–7`. */
|
|
414
|
+
function compactRules(s: string): string {
|
|
415
|
+
return s
|
|
416
|
+
.replace('node_length: ', 'len ')
|
|
417
|
+
.replaceAll('siblings: ', 'sib ')
|
|
418
|
+
.replace('depth: ', 'depth ')
|
|
419
|
+
.replaceAll(' | ', ' · ');
|
|
276
420
|
}
|
|
277
421
|
|
|
278
422
|
function printHelp(): void {
|
|
@@ -282,7 +426,8 @@ Usage: cans <command> [args]
|
|
|
282
426
|
|
|
283
427
|
Commands:
|
|
284
428
|
init [--flat|--folders] [--bare] [--force] [--tool <name>]
|
|
285
|
-
check [--fix] [--strict] [--refs-only] [--no-redundancy] [file] [--json]
|
|
429
|
+
check [--fix] [--strict] [--refs-only] [--no-redundancy] [--show <section>] [file] [--json]
|
|
430
|
+
--fix + [file] rewrites ref-by comments in matching files only
|
|
286
431
|
new adr <title>
|
|
287
432
|
new task <name>
|
|
288
433
|
done <name> [--allow-incomplete] [--skip-check] [--json]
|
package/src/core/overflow.ts
CHANGED
|
@@ -24,6 +24,7 @@ export function checkOverflow(
|
|
|
24
24
|
level: 'error',
|
|
25
25
|
category: 'overflow',
|
|
26
26
|
message: 'code fence detected — extract to file and reference via see:',
|
|
27
|
+
rule: 'overflow.code_fence', // issue #41: machine-readable rule key
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
if (node.hasTable && forceSet.has('table')) {
|
|
@@ -33,6 +34,7 @@ export function checkOverflow(
|
|
|
33
34
|
level: 'error',
|
|
34
35
|
category: 'overflow',
|
|
35
36
|
message: 'table detected — extract to file and reference via see:',
|
|
37
|
+
rule: 'overflow.table', // issue #41
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
if (rules.max_node_chars !== null && node.text.length > rules.max_node_chars) {
|
|
@@ -42,6 +44,7 @@ export function checkOverflow(
|
|
|
42
44
|
level: 'error',
|
|
43
45
|
category: 'overflow',
|
|
44
46
|
message: `node exceeds max chars (${node.text.length} > ${rules.max_node_chars})`,
|
|
47
|
+
rule: 'overflow.node_chars', // issue #41
|
|
45
48
|
});
|
|
46
49
|
}
|
|
47
50
|
walk(node.children);
|
|
@@ -67,6 +70,7 @@ export function checkNoChaining(targets: Map<string, OutlineNode[]>): Issue[] {
|
|
|
67
70
|
category: 'overflow',
|
|
68
71
|
message: `no chaining: overflow target ${file} must not contain its own see: refs (found see ${ref.file})`,
|
|
69
72
|
suggestion: `remove the see: ref inside ${file} — overflow targets are leaf content, reference them from a spec file instead`,
|
|
73
|
+
rule: 'refs.chaining', // issue #41: §16 chaining is a refs-category rule
|
|
70
74
|
});
|
|
71
75
|
}
|
|
72
76
|
}
|
package/src/core/redundancy.ts
CHANGED
|
@@ -93,6 +93,7 @@ export function wordFrequency(
|
|
|
93
93
|
file: loc.file, line: loc.line, level: 'warning', category: 'redundancy',
|
|
94
94
|
message: `"${word}" × ${n} nodes (threshold: ${threshold})`,
|
|
95
95
|
suggestion: `pick one canonical home for "${word}" and see: it from the others`,
|
|
96
|
+
rule: 'redundancy.keyword', // issue #41: machine-readable rule key
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
return issues;
|
|
@@ -126,6 +127,8 @@ export function phraseOverlap(
|
|
|
126
127
|
file: a.node.file, line: a.node.line, level: 'warning', category: 'redundancy',
|
|
127
128
|
message: `${pct}% overlap: ${a.node.file}:${a.node.line} ↔ ${b.node.file}:${b.node.line}`,
|
|
128
129
|
suggestion: 'merge the duplicated bullets or see: the canonical one',
|
|
130
|
+
// issue #41: 100% overlap is an exact duplicate; below that is fuzzy.
|
|
131
|
+
rule: pct >= 100 ? 'redundancy.overlap.exact' : 'redundancy.overlap.fuzzy',
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
}
|
|
@@ -208,6 +211,7 @@ export function fuzzyDistance(
|
|
|
208
211
|
file: a.file, line: a.line, level: 'warning', category: 'redundancy',
|
|
209
212
|
message: `possible typo: "${a.text}" (${a.file}:${a.line}) ↔ "${b.text}" (${b.file}:${b.line}) — Levenshtein ${d}`,
|
|
210
213
|
suggestion: 'unify the spelling or map the variant as a synonym',
|
|
214
|
+
rule: 'redundancy.typo', // issue #41: word-form layer of the redundancy scheme
|
|
211
215
|
});
|
|
212
216
|
}
|
|
213
217
|
}
|
|
@@ -281,6 +285,7 @@ export function crossFileCanonicality(
|
|
|
281
285
|
file: entry.first.file, line: entry.first.line, level: 'warning', category: 'redundancy',
|
|
282
286
|
message: `"${concept}" at depth 0-1 in ${files.length}+ files without see: (${files.join(', ')})`,
|
|
283
287
|
suggestion: `keep "${concept}" in one canonical file and see: it from the others`,
|
|
288
|
+
rule: 'redundancy.duplicate_home', // issue #41
|
|
284
289
|
});
|
|
285
290
|
}
|
|
286
291
|
return issues;
|