ultra11y 2.28.0 → 2.30.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +447 -23
- package/scripts/vendor/wcag-2.2-fr.json +1 -1
- package/scripts/vendor/wcag-2.2-sc-universe.json +1 -1
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +447 -23
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +447 -23
- package/src/data/wcag-universe.json +1 -1
- package/src/types.ts +1 -1
- package/src/vendor/codeindex-engine.d.mts +49 -6
- package/src/vendor/codeindex-engine.mjs +432 -22
- package/src/vendor/engine.meta.json +5 -5
|
@@ -14,9 +14,9 @@ var ENGINE_VERSION, SCHEMA_VERSION, EXTRACTOR_VERSION;
|
|
|
14
14
|
var init_types = __esm({
|
|
15
15
|
"src/types.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
ENGINE_VERSION = "2.
|
|
17
|
+
ENGINE_VERSION = "2.27.1";
|
|
18
18
|
SCHEMA_VERSION = 5;
|
|
19
|
-
EXTRACTOR_VERSION =
|
|
19
|
+
EXTRACTOR_VERSION = 13;
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -922,7 +922,18 @@ var init_common = __esm({
|
|
|
922
922
|
".scss": "scss",
|
|
923
923
|
".vue": "vue",
|
|
924
924
|
".svelte": "svelte",
|
|
925
|
-
".astro": "astro"
|
|
925
|
+
".astro": "astro",
|
|
926
|
+
// Extended-tier AST languages (src/ast/loader.ts EXT_GRAMMAR). Without an
|
|
927
|
+
// entry here `extToLang` answered "other", which `classify` reads as non-code
|
|
928
|
+
// — so a real scan never called extractCode for them and they extracted
|
|
929
|
+
// nothing, while the quality harness (which calls extractCode directly)
|
|
930
|
+
// published a perfect score. The grammar still arrives via `grammars pull`;
|
|
931
|
+
// absent it these fall back to the regex tier like any other language.
|
|
932
|
+
".zig": "zig",
|
|
933
|
+
".hcl": "hcl",
|
|
934
|
+
".tf": "terraform",
|
|
935
|
+
".tfvars": "terraform",
|
|
936
|
+
".sol": "solidity"
|
|
926
937
|
};
|
|
927
938
|
REEXPORT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
928
939
|
MAX_REEXPORTS = 400;
|
|
@@ -1689,6 +1700,65 @@ var init_markdown = __esm({
|
|
|
1689
1700
|
}
|
|
1690
1701
|
});
|
|
1691
1702
|
|
|
1703
|
+
// src/extract/literals.ts
|
|
1704
|
+
function isInterestingString(value) {
|
|
1705
|
+
if (value.length < MIN_LITERAL_LEN || value.length > MAX_LITERAL_LEN) return false;
|
|
1706
|
+
return HAS_SUBSTANCE.test(value);
|
|
1707
|
+
}
|
|
1708
|
+
function isInterestingNumber(raw) {
|
|
1709
|
+
const v = raw.trim();
|
|
1710
|
+
if (!v || v.length > MAX_LITERAL_LEN) return false;
|
|
1711
|
+
if (TRIVIAL_NUMBERS.has(v)) return false;
|
|
1712
|
+
return Number.isFinite(Number(v));
|
|
1713
|
+
}
|
|
1714
|
+
function unquote(text) {
|
|
1715
|
+
let s = text;
|
|
1716
|
+
const raw = /^(?:[rRbBuUfF]{1,2}|@|\$)?(?:#*)?(['"`])/.exec(s);
|
|
1717
|
+
if (raw) {
|
|
1718
|
+
const q = raw[1];
|
|
1719
|
+
const start2 = s.indexOf(q);
|
|
1720
|
+
const end = s.lastIndexOf(q);
|
|
1721
|
+
if (end > start2) s = s.slice(start2 + 1, end);
|
|
1722
|
+
}
|
|
1723
|
+
return s;
|
|
1724
|
+
}
|
|
1725
|
+
var MAX_LITERAL_LEN, MIN_LITERAL_LEN, MAX_LITERALS, TRIVIAL_NUMBERS, HAS_SUBSTANCE, LiteralCollector;
|
|
1726
|
+
var init_literals = __esm({
|
|
1727
|
+
"src/extract/literals.ts"() {
|
|
1728
|
+
"use strict";
|
|
1729
|
+
init_sort();
|
|
1730
|
+
MAX_LITERAL_LEN = 80;
|
|
1731
|
+
MIN_LITERAL_LEN = 2;
|
|
1732
|
+
MAX_LITERALS = 256;
|
|
1733
|
+
TRIVIAL_NUMBERS = /* @__PURE__ */ new Set(["0", "1", "-1", "2", "-2"]);
|
|
1734
|
+
HAS_SUBSTANCE = /[\p{L}\p{N}]/u;
|
|
1735
|
+
LiteralCollector = class {
|
|
1736
|
+
seen = /* @__PURE__ */ new Set();
|
|
1737
|
+
out = [];
|
|
1738
|
+
get full() {
|
|
1739
|
+
return this.out.length >= MAX_LITERALS;
|
|
1740
|
+
}
|
|
1741
|
+
add(kind, value, line) {
|
|
1742
|
+
if (this.full) return;
|
|
1743
|
+
if (kind === "string" && !isInterestingString(value)) return;
|
|
1744
|
+
if (kind === "number" && !isInterestingNumber(value)) return;
|
|
1745
|
+
if (kind === "regex" && !value) return;
|
|
1746
|
+
const key = `${kind}\0${value}\0${line}`;
|
|
1747
|
+
if (this.seen.has(key)) return;
|
|
1748
|
+
this.seen.add(key);
|
|
1749
|
+
this.out.push({ value, line, kind });
|
|
1750
|
+
}
|
|
1751
|
+
addString(text, line) {
|
|
1752
|
+
this.add("string", unquote(text), line);
|
|
1753
|
+
}
|
|
1754
|
+
result() {
|
|
1755
|
+
if (!this.out.length) return void 0;
|
|
1756
|
+
return this.out.sort((a, b) => byStr(a.value, b.value) || a.line - b.line || byStr(a.kind, b.kind));
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1692
1762
|
// node_modules/.pnpm/web-tree-sitter@0.26.11/node_modules/web-tree-sitter/web-tree-sitter.js
|
|
1693
1763
|
function assertInternal(x) {
|
|
1694
1764
|
if (x !== INTERNAL) throw new Error("Illegal constructor");
|
|
@@ -7059,6 +7129,9 @@ var init_doc = __esm({
|
|
|
7059
7129
|
});
|
|
7060
7130
|
|
|
7061
7131
|
// src/ast/extract.ts
|
|
7132
|
+
function isPlainString(node) {
|
|
7133
|
+
return node.namedChildren.every((c2) => STRING_PART.test(c2.type));
|
|
7134
|
+
}
|
|
7062
7135
|
function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
7063
7136
|
const identsFound = /* @__PURE__ */ new Set();
|
|
7064
7137
|
const wantCalls = spec.calls !== void 0;
|
|
@@ -7080,6 +7153,7 @@ function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
|
7080
7153
|
termsFound.add(t);
|
|
7081
7154
|
}
|
|
7082
7155
|
};
|
|
7156
|
+
const literals = new LiteralCollector();
|
|
7083
7157
|
const wantNames = spec.imports?.import_statement !== void 0;
|
|
7084
7158
|
const namesFound = /* @__PURE__ */ new Set();
|
|
7085
7159
|
const wantRefs = wantImports && spec.imports !== void 0;
|
|
@@ -7101,9 +7175,15 @@ function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
|
7101
7175
|
}
|
|
7102
7176
|
if (COMMENT_NODE.test(type)) {
|
|
7103
7177
|
for (const line of node.text.split(/\r?\n/)) addTerms2(stripCommentMarkers(line));
|
|
7104
|
-
} else if (kids.length === 0 && STRING_NODE.test(type) && node.endIndex - node.startIndex <=
|
|
7178
|
+
} else if (kids.length === 0 && STRING_NODE.test(type) && node.endIndex - node.startIndex <= MAX_LITERAL_LEN2) {
|
|
7105
7179
|
addTerms2(node.text.replace(/^['"`]+|['"`]+$/g, ""));
|
|
7106
7180
|
}
|
|
7181
|
+
if (!literals.full) {
|
|
7182
|
+
const line = node.startPosition.row + 1;
|
|
7183
|
+
if (STRING_NODE.test(type) && isPlainString(node)) literals.addString(node.text, line);
|
|
7184
|
+
else if (kids.length === 0 && NUMBER_NODE.test(type)) literals.add("number", node.text.trim(), line);
|
|
7185
|
+
else if (REGEX_NODE.test(type)) literals.add("regex", node.text, line);
|
|
7186
|
+
}
|
|
7107
7187
|
if (wantCalls && !(spec.kindFrom?.[type] && spec.kindFrom[type](node)) && !spec.skipCall?.(node)) {
|
|
7108
7188
|
const how = spec.calls[type];
|
|
7109
7189
|
if (how === "function") {
|
|
@@ -7152,7 +7232,8 @@ function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
|
7152
7232
|
idents: [...identsFound].sort().slice(0, MAX_REF_IDENTS),
|
|
7153
7233
|
calls: calls.slice(0, maxCalls),
|
|
7154
7234
|
importedNames: [...namesFound].sort(byStr).slice(0, MAX_IMPORTED_NAMES),
|
|
7155
|
-
terms: [...termsFound].sort(byStr)
|
|
7235
|
+
terms: [...termsFound].sort(byStr),
|
|
7236
|
+
literals: literals.result() ?? []
|
|
7156
7237
|
};
|
|
7157
7238
|
}
|
|
7158
7239
|
function boundNames(pattern) {
|
|
@@ -7494,7 +7575,7 @@ function extractAst(rel2, ext, content, opts = {}) {
|
|
|
7494
7575
|
for (const s of symbols) if (!s.exported && exportedNames.has(s.name)) s.exported = true;
|
|
7495
7576
|
}
|
|
7496
7577
|
const wantImports = opts.imports !== false;
|
|
7497
|
-
const { refs, idents, calls, importedNames, terms } = collectAll(
|
|
7578
|
+
const { refs, idents, calls, importedNames, terms, literals } = collectAll(
|
|
7498
7579
|
root,
|
|
7499
7580
|
spec,
|
|
7500
7581
|
new Set(symbols.map((s) => s.name)),
|
|
@@ -7516,6 +7597,7 @@ function extractAst(rel2, ext, content, opts = {}) {
|
|
|
7516
7597
|
importedNames,
|
|
7517
7598
|
relations,
|
|
7518
7599
|
terms,
|
|
7600
|
+
literals,
|
|
7519
7601
|
...symbols.length >= maxSymbols ? { truncated: true } : {}
|
|
7520
7602
|
};
|
|
7521
7603
|
} catch {
|
|
@@ -7524,10 +7606,11 @@ function extractAst(rel2, ext, content, opts = {}) {
|
|
|
7524
7606
|
tree?.delete();
|
|
7525
7607
|
}
|
|
7526
7608
|
}
|
|
7527
|
-
var MAX_REF_IDENTS, MAX_CALLS, MAX_IMPORTED_NAMES, MAX_SYMBOLS, MAX_RELATIONS, MAX_TERMS,
|
|
7609
|
+
var MAX_REF_IDENTS, MAX_CALLS, MAX_IMPORTED_NAMES, MAX_SYMBOLS, MAX_RELATIONS, MAX_TERMS, MAX_LITERAL_LEN2, MAX_FUNC_DEPTH, NESTED_TYPE_KINDS, ANON_DEFAULT_FN, ANON_DEFAULT_CLASS, REF_IDENT_TYPE, REF_IDENT_TEXT, COMMENT_NODE, STRING_NODE, NUMBER_NODE, REGEX_NODE, STRING_PART;
|
|
7528
7610
|
var init_extract = __esm({
|
|
7529
7611
|
"src/ast/extract.ts"() {
|
|
7530
7612
|
"use strict";
|
|
7613
|
+
init_literals();
|
|
7531
7614
|
init_sort();
|
|
7532
7615
|
init_loader();
|
|
7533
7616
|
init_node();
|
|
@@ -7542,7 +7625,7 @@ var init_extract = __esm({
|
|
|
7542
7625
|
MAX_SYMBOLS = 2e3;
|
|
7543
7626
|
MAX_RELATIONS = 256;
|
|
7544
7627
|
MAX_TERMS = 512;
|
|
7545
|
-
|
|
7628
|
+
MAX_LITERAL_LEN2 = 80;
|
|
7546
7629
|
MAX_FUNC_DEPTH = 2;
|
|
7547
7630
|
NESTED_TYPE_KINDS = /* @__PURE__ */ new Set(["class", "struct", "enum", "interface", "trait", "type", "record", "union"]);
|
|
7548
7631
|
ANON_DEFAULT_FN = /* @__PURE__ */ new Set([
|
|
@@ -7558,6 +7641,9 @@ var init_extract = __esm({
|
|
|
7558
7641
|
REF_IDENT_TEXT = /^[A-Za-z_]\w{4,}$/;
|
|
7559
7642
|
COMMENT_NODE = /(^|_)comment$/;
|
|
7560
7643
|
STRING_NODE = /(^|_)string(_literal)?$/;
|
|
7644
|
+
NUMBER_NODE = /(^|_)(integer|float|number|decimal|numeric)(_literal)?$/;
|
|
7645
|
+
REGEX_NODE = /(^|_)(regex|regular_expression)(_pattern|_literal)?$/;
|
|
7646
|
+
STRING_PART = /(^|_)(fragment|content|escape_sequence|character)$/;
|
|
7561
7647
|
}
|
|
7562
7648
|
});
|
|
7563
7649
|
|
|
@@ -7807,17 +7893,55 @@ function collectTermsRegex(content) {
|
|
|
7807
7893
|
}
|
|
7808
7894
|
for (const m of line.matchAll(/(['"`])((?:\\.|(?!\1)[^\\])*)\1/g)) {
|
|
7809
7895
|
const body2 = m[2];
|
|
7810
|
-
if (body2.length && body2.length <=
|
|
7896
|
+
if (body2.length && body2.length <= MAX_LITERAL_LEN3) add(body2);
|
|
7811
7897
|
}
|
|
7812
7898
|
}
|
|
7813
7899
|
return [...found].sort();
|
|
7814
7900
|
}
|
|
7901
|
+
function collectLiteralsRegex(content) {
|
|
7902
|
+
const literals = new LiteralCollector();
|
|
7903
|
+
let inBlock = false;
|
|
7904
|
+
let lineNo = 0;
|
|
7905
|
+
for (const raw of content.split("\n")) {
|
|
7906
|
+
lineNo++;
|
|
7907
|
+
if (literals.full) break;
|
|
7908
|
+
let line = raw;
|
|
7909
|
+
if (inBlock) {
|
|
7910
|
+
const close = line.indexOf("*/");
|
|
7911
|
+
if (close === -1) continue;
|
|
7912
|
+
inBlock = false;
|
|
7913
|
+
line = line.slice(close + 2);
|
|
7914
|
+
}
|
|
7915
|
+
const open = line.indexOf("/*");
|
|
7916
|
+
if (open !== -1) {
|
|
7917
|
+
const close = line.indexOf("*/", open + 2);
|
|
7918
|
+
if (close === -1) {
|
|
7919
|
+
inBlock = true;
|
|
7920
|
+
line = line.slice(0, open);
|
|
7921
|
+
} else line = line.slice(0, open) + line.slice(close + 2);
|
|
7922
|
+
}
|
|
7923
|
+
const lineComment = /(^|\s)(\/\/|#|--)(.*)$/.exec(line);
|
|
7924
|
+
if (lineComment) line = line.slice(0, lineComment.index);
|
|
7925
|
+
for (const m of line.matchAll(/(['"`])((?:\\.|(?!\1)[^\\])*)\1/g)) {
|
|
7926
|
+
literals.add("string", m[2], lineNo);
|
|
7927
|
+
}
|
|
7928
|
+
for (const m of line.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, " ").matchAll(/(?<![\w.])-?\d[\d_]*(?:\.\d+)?(?![\w.])/g)) {
|
|
7929
|
+
literals.add("number", m[0].replace(/_/g, ""), lineNo);
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
return literals.result();
|
|
7933
|
+
}
|
|
7815
7934
|
function extractCode(rel2, ext, content, opts = {}) {
|
|
7816
7935
|
const ast = extractAst(rel2, ext, content, { maxCalls: opts.maxCallsPerFile, imports: false });
|
|
7817
7936
|
const raw = ast ? ast.symbols : extractSymbols(rel2, ext, content);
|
|
7818
7937
|
const symbols = raw.slice(0, MAX_FILE_SYMBOLS);
|
|
7819
7938
|
const known = new Set(symbols.map((s) => s.name));
|
|
7820
7939
|
const reexports = extractReexports(rel2, content, symbols).filter((s) => !known.has(s.name));
|
|
7940
|
+
const refs = extractImports(ext, content);
|
|
7941
|
+
const importSpecs = new Set(refs.map((r) => r.spec));
|
|
7942
|
+
const literals = (ast ? ast.literals.length ? ast.literals : void 0 : collectLiteralsRegex(content))?.filter(
|
|
7943
|
+
(l) => !(l.kind === "string" && importSpecs.has(l.value))
|
|
7944
|
+
);
|
|
7821
7945
|
return {
|
|
7822
7946
|
symbols: [...symbols, ...reexports],
|
|
7823
7947
|
// A re-export list that hit its own ceiling is truncated too — a barrel that
|
|
@@ -7825,7 +7949,7 @@ function extractCode(rel2, ext, content, opts = {}) {
|
|
|
7825
7949
|
// exists to prevent.
|
|
7826
7950
|
...ast?.truncated || raw.length > symbols.length || reexports.length >= MAX_REEXPORTS ? { truncated: true } : {},
|
|
7827
7951
|
summary: topDocComment(content),
|
|
7828
|
-
refs
|
|
7952
|
+
refs,
|
|
7829
7953
|
// pkg anchors namespace→source-root resolution: Java's `package`, C#'s
|
|
7830
7954
|
// `namespace` (block or file-scoped). Both feed the same resolver pattern.
|
|
7831
7955
|
pkg: ext === ".java" ? /^\s*package\s+([\w.]+)\s*;/m.exec(content)?.[1] : ext === ".cs" ? /^\s*(?:file-scoped\s+)?namespace\s+([\w.]+)/m.exec(content)?.[1] : void 0,
|
|
@@ -7840,13 +7964,15 @@ function extractCode(rel2, ext, content, opts = {}) {
|
|
|
7840
7964
|
// The AST tier reads comments and literals structurally; without a grammar
|
|
7841
7965
|
// the line scanner above still supplies a vocabulary, so search quality does
|
|
7842
7966
|
// not silently collapse for a language with no wasm.
|
|
7843
|
-
terms: ast ? ast.terms : collectTermsRegex(content)
|
|
7967
|
+
terms: ast ? ast.terms : collectTermsRegex(content),
|
|
7968
|
+
literals: literals?.length ? literals : void 0
|
|
7844
7969
|
};
|
|
7845
7970
|
}
|
|
7846
|
-
var MAX_FILE_SYMBOLS, JS_TS, PY, C_CPP, MAX_USE_EXPANSION, CALL_KEYWORDS, DEF_INTRODUCERS, MAX_TERMS2,
|
|
7971
|
+
var MAX_FILE_SYMBOLS, JS_TS, PY, C_CPP, MAX_USE_EXPANSION, CALL_KEYWORDS, DEF_INTRODUCERS, MAX_TERMS2, MAX_LITERAL_LEN3;
|
|
7847
7972
|
var init_code = __esm({
|
|
7848
7973
|
"src/extract/code.ts"() {
|
|
7849
7974
|
"use strict";
|
|
7975
|
+
init_literals();
|
|
7850
7976
|
init_registry();
|
|
7851
7977
|
init_extract();
|
|
7852
7978
|
init_common();
|
|
@@ -7895,7 +8021,51 @@ var init_code = __esm({
|
|
|
7895
8021
|
]);
|
|
7896
8022
|
DEF_INTRODUCERS = /(?:\bfunction|\bdef|\bfunc|\bfun|\bfn|\bclass|\bsub|\bmacro|\bproc)\s*[*]?\s*$/;
|
|
7897
8023
|
MAX_TERMS2 = 512;
|
|
7898
|
-
|
|
8024
|
+
MAX_LITERAL_LEN3 = 80;
|
|
8025
|
+
}
|
|
8026
|
+
});
|
|
8027
|
+
|
|
8028
|
+
// src/extract/config.ts
|
|
8029
|
+
function extractConfigLiterals(content) {
|
|
8030
|
+
const literals = new LiteralCollector();
|
|
8031
|
+
let lineNo = 0;
|
|
8032
|
+
for (const raw of content.split("\n")) {
|
|
8033
|
+
lineNo++;
|
|
8034
|
+
if (literals.full) break;
|
|
8035
|
+
const line = stripTrailingComment(raw);
|
|
8036
|
+
for (const m of line.matchAll(QUOTED)) literals.add("string", m[2], lineNo);
|
|
8037
|
+
const bare = UNQUOTED_SCALAR.exec(line);
|
|
8038
|
+
if (bare) {
|
|
8039
|
+
const v = bare[1].trim();
|
|
8040
|
+
if (v && !/^[[{|>&*-]/.test(v) && !HAS_QUOTE.test(v)) literals.add("string", v, lineNo);
|
|
8041
|
+
}
|
|
8042
|
+
for (const m of line.replace(QUOTED, " ").matchAll(BARE_NUMBER)) {
|
|
8043
|
+
literals.add("number", m[0].replace(/_/g, ""), lineNo);
|
|
8044
|
+
}
|
|
8045
|
+
}
|
|
8046
|
+
return literals.result();
|
|
8047
|
+
}
|
|
8048
|
+
function stripTrailingComment(line) {
|
|
8049
|
+
let inQuote;
|
|
8050
|
+
for (let i2 = 0; i2 < line.length; i2++) {
|
|
8051
|
+
const c2 = line[i2];
|
|
8052
|
+
if (inQuote) {
|
|
8053
|
+
if (c2 === "\\") i2++;
|
|
8054
|
+
else if (c2 === inQuote) inQuote = void 0;
|
|
8055
|
+
} else if (c2 === '"' || c2 === "'") inQuote = c2;
|
|
8056
|
+
else if (c2 === "#") return line.slice(0, i2);
|
|
8057
|
+
}
|
|
8058
|
+
return line;
|
|
8059
|
+
}
|
|
8060
|
+
var QUOTED, HAS_QUOTE, BARE_NUMBER, UNQUOTED_SCALAR;
|
|
8061
|
+
var init_config = __esm({
|
|
8062
|
+
"src/extract/config.ts"() {
|
|
8063
|
+
"use strict";
|
|
8064
|
+
init_literals();
|
|
8065
|
+
QUOTED = /(['"])((?:\\.|(?!\1)[^\\])*)\1/g;
|
|
8066
|
+
HAS_QUOTE = /(['"])((?:\\.|(?!\1)[^\\])*)\1/;
|
|
8067
|
+
BARE_NUMBER = /(?<![\w.$-])-?\d[\d_]*(?:\.\d+)?(?![\w.$-])/g;
|
|
8068
|
+
UNQUOTED_SCALAR = /^\s*[\w.$-]+\s*[:=]\s*([^#\n]+?)\s*$/;
|
|
7899
8069
|
}
|
|
7900
8070
|
});
|
|
7901
8071
|
|
|
@@ -8045,6 +8215,10 @@ function scanRepo(root, opts = {}) {
|
|
|
8045
8215
|
record.truncated = code.truncated;
|
|
8046
8216
|
record.relations = code.relations;
|
|
8047
8217
|
record.terms = code.terms;
|
|
8218
|
+
record.literals = code.literals;
|
|
8219
|
+
} else if (kind === "config") {
|
|
8220
|
+
record.title = basename(f.rel);
|
|
8221
|
+
record.literals = extractConfigLiterals(content);
|
|
8048
8222
|
} else {
|
|
8049
8223
|
record.title = basename(f.rel);
|
|
8050
8224
|
}
|
|
@@ -8084,6 +8258,7 @@ var init_scan = __esm({
|
|
|
8084
8258
|
init_sort();
|
|
8085
8259
|
init_markdown();
|
|
8086
8260
|
init_code();
|
|
8261
|
+
init_config();
|
|
8087
8262
|
}
|
|
8088
8263
|
});
|
|
8089
8264
|
|
|
@@ -11373,6 +11548,137 @@ var init_graph_json = __esm({
|
|
|
11373
11548
|
}
|
|
11374
11549
|
});
|
|
11375
11550
|
|
|
11551
|
+
// src/literals.ts
|
|
11552
|
+
function isDistinctive2(value, kind) {
|
|
11553
|
+
if (kind !== "string") return true;
|
|
11554
|
+
return value.length >= DISTINCTIVE_MIN_LEN || HAS_SEPARATOR.test(value);
|
|
11555
|
+
}
|
|
11556
|
+
function isFunctionValued(signature) {
|
|
11557
|
+
if (!signature) return false;
|
|
11558
|
+
for (let i2 = 0; i2 < signature.length; i2++) {
|
|
11559
|
+
if (signature[i2] !== "=") continue;
|
|
11560
|
+
if (signature[i2 + 1] === "=" || signature[i2 + 1] === ">") continue;
|
|
11561
|
+
if ("=!<>+-*/%&|^".includes(signature[i2 - 1] ?? "")) continue;
|
|
11562
|
+
return FUNCTION_RHS.test(signature.slice(i2 + 1));
|
|
11563
|
+
}
|
|
11564
|
+
return false;
|
|
11565
|
+
}
|
|
11566
|
+
function holderFor(symbols, line) {
|
|
11567
|
+
let best;
|
|
11568
|
+
for (const s of symbols) {
|
|
11569
|
+
if (!HOLDER_KINDS.has(s.kind)) continue;
|
|
11570
|
+
if (isFunctionValued(s.signature)) continue;
|
|
11571
|
+
const end = s.endLine ?? s.line;
|
|
11572
|
+
if (end - s.line > MAX_HOLDER_SPAN) continue;
|
|
11573
|
+
if (line < s.line || line > end) continue;
|
|
11574
|
+
if (!best || s.line > best.line) best = s;
|
|
11575
|
+
}
|
|
11576
|
+
return best;
|
|
11577
|
+
}
|
|
11578
|
+
function findLiteralDuplications(scan2, opts = {}) {
|
|
11579
|
+
const minFiles = opts.minFiles ?? DEFAULTS.minFiles;
|
|
11580
|
+
const minCount = opts.minCount ?? DEFAULTS.minCount;
|
|
11581
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11582
|
+
for (const f of scan2.files) {
|
|
11583
|
+
if (!f.literals?.length) continue;
|
|
11584
|
+
if (!opts.includeTests && isTestPath(f.rel)) continue;
|
|
11585
|
+
for (const lit of f.literals) {
|
|
11586
|
+
if (opts.kinds && !opts.kinds.has(lit.kind)) continue;
|
|
11587
|
+
if (!isDistinctive2(lit.value, lit.kind)) continue;
|
|
11588
|
+
const key = `${lit.kind}\0${lit.value}`;
|
|
11589
|
+
let g = groups.get(key);
|
|
11590
|
+
if (!g) groups.set(key, g = { value: lit.value, kind: lit.kind, sites: [] });
|
|
11591
|
+
const holder = holderFor(f.symbols, lit.line);
|
|
11592
|
+
g.sites.push(
|
|
11593
|
+
holder ? { file: f.rel, line: lit.line, holder: holder.name, holderExported: holder.exported } : { file: f.rel, line: lit.line }
|
|
11594
|
+
);
|
|
11595
|
+
}
|
|
11596
|
+
}
|
|
11597
|
+
const duplications = [];
|
|
11598
|
+
for (const g of groups.values()) {
|
|
11599
|
+
const files = new Set(g.sites.map((s) => s.file));
|
|
11600
|
+
if (files.size < minFiles || g.sites.length < minCount) continue;
|
|
11601
|
+
const holders = g.sites.filter((s) => s.holder);
|
|
11602
|
+
const literals = g.sites.filter((s) => !s.holder);
|
|
11603
|
+
const distinctHolderNames = new Set(holders.map((h) => h.holder));
|
|
11604
|
+
const distinctHolders = new Set(holders.map((h) => `${h.file}\0${h.holder}`));
|
|
11605
|
+
let tier = distinctHolders.size >= 2 ? "competing" : holders.length > 0 ? "bypassed" : "uncentralized";
|
|
11606
|
+
if (g.kind === "number") {
|
|
11607
|
+
if (distinctHolderNames.size !== 1 || literals.length === 0) continue;
|
|
11608
|
+
tier = "bypassed";
|
|
11609
|
+
}
|
|
11610
|
+
if (tier === "bypassed" && literals.length === 0) continue;
|
|
11611
|
+
duplications.push({
|
|
11612
|
+
value: g.value,
|
|
11613
|
+
kind: g.kind,
|
|
11614
|
+
tier,
|
|
11615
|
+
holders: holders.sort(siteOrder),
|
|
11616
|
+
literals: literals.sort(siteOrder),
|
|
11617
|
+
files: files.size,
|
|
11618
|
+
count: g.sites.length
|
|
11619
|
+
});
|
|
11620
|
+
}
|
|
11621
|
+
duplications.sort(
|
|
11622
|
+
(a, b) => tierRank(a.tier) - tierRank(b.tier) || b.files - a.files || b.count - a.count || byStr(a.value, b.value)
|
|
11623
|
+
);
|
|
11624
|
+
return { duplications, families: groupFamilies(duplications) };
|
|
11625
|
+
}
|
|
11626
|
+
function siteOrder(a, b) {
|
|
11627
|
+
return byStr(a.file, b.file) || a.line - b.line;
|
|
11628
|
+
}
|
|
11629
|
+
function tierRank(t) {
|
|
11630
|
+
return t === "competing" ? 0 : t === "bypassed" ? 1 : 2;
|
|
11631
|
+
}
|
|
11632
|
+
function groupFamilies(dups) {
|
|
11633
|
+
const byPrefix = /* @__PURE__ */ new Map();
|
|
11634
|
+
for (const d of dups) {
|
|
11635
|
+
if (d.kind !== "string" || !PATH_LIKE.test(d.value)) continue;
|
|
11636
|
+
const prefix = pathPrefix(d.value);
|
|
11637
|
+
if (!prefix) continue;
|
|
11638
|
+
const list = byPrefix.get(prefix);
|
|
11639
|
+
if (list) list.push(d);
|
|
11640
|
+
else byPrefix.set(prefix, [d]);
|
|
11641
|
+
}
|
|
11642
|
+
const families = [];
|
|
11643
|
+
for (const [prefix, members] of byPrefix) {
|
|
11644
|
+
if (members.length < FAMILY_MIN_MEMBERS) continue;
|
|
11645
|
+
const files = /* @__PURE__ */ new Set();
|
|
11646
|
+
let count = 0;
|
|
11647
|
+
for (const m of members) {
|
|
11648
|
+
for (const s of [...m.holders, ...m.literals]) files.add(s.file);
|
|
11649
|
+
count += m.count;
|
|
11650
|
+
}
|
|
11651
|
+
families.push({ prefix, members, files: files.size, count });
|
|
11652
|
+
}
|
|
11653
|
+
families.sort((a, b) => b.files - a.files || b.count - a.count || byStr(a.prefix, b.prefix));
|
|
11654
|
+
return families;
|
|
11655
|
+
}
|
|
11656
|
+
function pathPrefix(value) {
|
|
11657
|
+
const body2 = value.startsWith("/") ? value.slice(1) : value;
|
|
11658
|
+
const cut = body2.search(/[/:]/);
|
|
11659
|
+
if (cut <= 0) return void 0;
|
|
11660
|
+
const prefix = (value.startsWith("/") ? "/" : "") + body2.slice(0, cut);
|
|
11661
|
+
return prefix.length >= FAMILY_MIN_PREFIX ? prefix : void 0;
|
|
11662
|
+
}
|
|
11663
|
+
var LITERAL_DUPLICATION_CAP, DEFAULTS, HOLDER_KINDS, MAX_HOLDER_SPAN, DISTINCTIVE_MIN_LEN, HAS_SEPARATOR, PATH_LIKE, FAMILY_MIN_MEMBERS, FAMILY_MIN_PREFIX, FUNCTION_RHS;
|
|
11664
|
+
var init_literals2 = __esm({
|
|
11665
|
+
"src/literals.ts"() {
|
|
11666
|
+
"use strict";
|
|
11667
|
+
init_tests_map();
|
|
11668
|
+
init_sort();
|
|
11669
|
+
LITERAL_DUPLICATION_CAP = 24;
|
|
11670
|
+
DEFAULTS = { minFiles: 2, minCount: 3 };
|
|
11671
|
+
HOLDER_KINDS = /* @__PURE__ */ new Set(["const", "constant", "variable", "enum", "enumerator", "property", "field", "static"]);
|
|
11672
|
+
MAX_HOLDER_SPAN = 12;
|
|
11673
|
+
DISTINCTIVE_MIN_LEN = 6;
|
|
11674
|
+
HAS_SEPARATOR = /[/:._-]/;
|
|
11675
|
+
PATH_LIKE = /^[/@a-z0-9][\w./:@-]*$/i;
|
|
11676
|
+
FAMILY_MIN_MEMBERS = 2;
|
|
11677
|
+
FAMILY_MIN_PREFIX = 4;
|
|
11678
|
+
FUNCTION_RHS = /^\s*(?:async\b|function\b|\(|[A-Za-z_$][\w$]*\s*=>)/;
|
|
11679
|
+
}
|
|
11680
|
+
});
|
|
11681
|
+
|
|
11376
11682
|
// src/pipeline.ts
|
|
11377
11683
|
function buildIndexArtifacts(repo, opts = {}) {
|
|
11378
11684
|
return buildArtifactsFromScan(scanRepo(repo, opts), opts);
|
|
@@ -11397,6 +11703,8 @@ function buildArtifactsFromScan(scan2, opts = {}) {
|
|
|
11397
11703
|
}
|
|
11398
11704
|
const surprises = computeSurprises(graph);
|
|
11399
11705
|
if (surprises.length) graph.surprises = surprises;
|
|
11706
|
+
const { duplications } = findLiteralDuplications(scan2);
|
|
11707
|
+
if (duplications.length) graph.literalDuplications = duplications.slice(0, LITERAL_DUPLICATION_CAP);
|
|
11400
11708
|
const symbols = buildSymbolIndex(scan2, symbolRefsFor(scan2), opts.meta?.schemaVersion);
|
|
11401
11709
|
return { scan: scan2, graph, symbols };
|
|
11402
11710
|
}
|
|
@@ -11411,6 +11719,7 @@ var init_pipeline = __esm({
|
|
|
11411
11719
|
init_centrality();
|
|
11412
11720
|
init_tests_map();
|
|
11413
11721
|
init_surprise();
|
|
11722
|
+
init_literals2();
|
|
11414
11723
|
init_symbols_json();
|
|
11415
11724
|
}
|
|
11416
11725
|
});
|
|
@@ -11907,8 +12216,14 @@ function parseRules(input) {
|
|
|
11907
12216
|
throw new Error(`${at} (${r.name}): \`comment\` must be a string`);
|
|
11908
12217
|
if (r.builtin !== void 0) {
|
|
11909
12218
|
if (!BUILTINS.has(r.builtin))
|
|
11910
|
-
throw new Error(`${at} (${r.name}): \`builtin\` must be "cycles" or "
|
|
11911
|
-
return {
|
|
12219
|
+
throw new Error(`${at} (${r.name}): \`builtin\` must be "cycles", "orphans" or "literals"`);
|
|
12220
|
+
return {
|
|
12221
|
+
name: r.name,
|
|
12222
|
+
builtin: r.builtin,
|
|
12223
|
+
severity: r.severity,
|
|
12224
|
+
comment: r.comment,
|
|
12225
|
+
...r.tiers !== void 0 ? { tiers: r.tiers } : {}
|
|
12226
|
+
};
|
|
11912
12227
|
}
|
|
11913
12228
|
const glob = (field) => {
|
|
11914
12229
|
const v = r[field];
|
|
@@ -12017,6 +12332,17 @@ function checkRules(graph, rules) {
|
|
|
12017
12332
|
for (const c2 of findImportCycles(graph)) {
|
|
12018
12333
|
emit2(rule, { from: c2.start, to: c2.path.join(" -> "), kind: "cycle" });
|
|
12019
12334
|
}
|
|
12335
|
+
} else if (rule.builtin === "literals") {
|
|
12336
|
+
const wanted = new Set(rule.tiers?.length ? rule.tiers : GATED_TIERS);
|
|
12337
|
+
for (const d of graph.literalDuplications ?? []) {
|
|
12338
|
+
if (!wanted.has(d.tier)) continue;
|
|
12339
|
+
const origin = d.holders[0] ?? d.literals[0];
|
|
12340
|
+
emit2(rule, {
|
|
12341
|
+
from: `${origin.file}:${origin.line}`,
|
|
12342
|
+
to: `${d.tier} ${JSON.stringify(d.value)} (${d.count} sites, ${d.files} files)`,
|
|
12343
|
+
kind: "literal"
|
|
12344
|
+
});
|
|
12345
|
+
}
|
|
12020
12346
|
} else {
|
|
12021
12347
|
for (const f of graph.files) {
|
|
12022
12348
|
if (f.fileKind !== "code" || f.degIn !== 0 || f.degOut !== 0) continue;
|
|
@@ -12040,7 +12366,7 @@ function checkRules(graph, rules) {
|
|
|
12040
12366
|
out2.sort((a, b) => byStr(a.rule, b.rule) || byStr(a.from, b.from) || byStr(a.to, b.to) || byStr(a.kind, b.kind));
|
|
12041
12367
|
return out2;
|
|
12042
12368
|
}
|
|
12043
|
-
var EDGE_KINDS, SEVERITIES, BUILTINS, ENTRYPOINT_STEMS;
|
|
12369
|
+
var EDGE_KINDS, SEVERITIES, BUILTINS, GATED_TIERS, ENTRYPOINT_STEMS;
|
|
12044
12370
|
var init_rules = __esm({
|
|
12045
12371
|
"src/rules.ts"() {
|
|
12046
12372
|
"use strict";
|
|
@@ -12048,7 +12374,8 @@ var init_rules = __esm({
|
|
|
12048
12374
|
init_sort();
|
|
12049
12375
|
EDGE_KINDS = /* @__PURE__ */ new Set(["contains", "doc-link", "import", "call", "use", "mention"]);
|
|
12050
12376
|
SEVERITIES = /* @__PURE__ */ new Set(["error", "warn"]);
|
|
12051
|
-
BUILTINS = /* @__PURE__ */ new Set(["cycles", "orphans"]);
|
|
12377
|
+
BUILTINS = /* @__PURE__ */ new Set(["cycles", "orphans", "literals"]);
|
|
12378
|
+
GATED_TIERS = ["competing", "bypassed"];
|
|
12052
12379
|
ENTRYPOINT_STEMS = /* @__PURE__ */ new Set([
|
|
12053
12380
|
"index",
|
|
12054
12381
|
"main",
|
|
@@ -12595,6 +12922,22 @@ var init_tools = __esm({
|
|
|
12595
12922
|
required: ["repo"]
|
|
12596
12923
|
}
|
|
12597
12924
|
},
|
|
12925
|
+
{
|
|
12926
|
+
name: "duplicated_literals",
|
|
12927
|
+
description: "Values with no single source of truth: one literal written out across many files. Three labeled tiers \u2014 'competing' (two or more exported constants hold the same value), 'bypassed' (a constant holds it and other files rewrite it anyway), 'uncentralized' (nothing holds it). Path-like values are also grouped into namespace families, so a whole route space reports once instead of once per route. Covers config files (JSON/YAML/TOML) as well as code, which is where the dangerous cases live: a threshold declared in TypeScript and again in a rules JSON is checked by no compiler. Use it to answer 'what breaks if this value changes' and 'is there already a helper for this'.",
|
|
12928
|
+
inputSchema: {
|
|
12929
|
+
type: "object",
|
|
12930
|
+
properties: {
|
|
12931
|
+
...repoProp,
|
|
12932
|
+
...scopeProps,
|
|
12933
|
+
minFiles: { type: "number", description: "Distinct files a value must span (default 2)" },
|
|
12934
|
+
minCount: { type: "number", description: "Total occurrences required (default 3)" },
|
|
12935
|
+
includeTests: { type: "boolean", description: "Count test files too (default false)" },
|
|
12936
|
+
limit: { type: "number", description: "Cap duplications (default: all)" }
|
|
12937
|
+
},
|
|
12938
|
+
required: ["repo"]
|
|
12939
|
+
}
|
|
12940
|
+
},
|
|
12598
12941
|
{
|
|
12599
12942
|
name: "complexity",
|
|
12600
12943
|
description: "Cyclomatic-complexity estimates (branch-token counting over AST line spans), most-complex first. Pass `file` for one file's symbols, omit for the repo-wide top. Combine with hotspots: the `risk` field of this tool's sibling ranks complexity \xD7 churn.",
|
|
@@ -12804,6 +13147,14 @@ var init_tools = __esm({
|
|
|
12804
13147
|
properties: { ok: { type: "boolean" }, couplings: { type: "array", items: anyObj } },
|
|
12805
13148
|
required: ["ok", "couplings"]
|
|
12806
13149
|
},
|
|
13150
|
+
duplicated_literals: {
|
|
13151
|
+
type: "object",
|
|
13152
|
+
properties: {
|
|
13153
|
+
duplications: { type: "array", items: anyObj },
|
|
13154
|
+
families: { type: "array", items: anyObj }
|
|
13155
|
+
},
|
|
13156
|
+
required: ["duplications", "families"]
|
|
13157
|
+
},
|
|
12807
13158
|
embed_status: {
|
|
12808
13159
|
type: "object",
|
|
12809
13160
|
properties: {
|
|
@@ -12859,6 +13210,7 @@ var init_tools = __esm({
|
|
|
12859
13210
|
list_memories: { title: "List memories" },
|
|
12860
13211
|
delete_memory: { title: "Delete memory", write: true, destructive: true, idempotent: true },
|
|
12861
13212
|
dead_code: { title: "Dead-code candidates" },
|
|
13213
|
+
duplicated_literals: { title: "Values with no single source of truth" },
|
|
12862
13214
|
complexity: { title: "Complexity" },
|
|
12863
13215
|
mermaid: { title: "Mermaid module diagram" },
|
|
12864
13216
|
grep: { title: "Grep file contents" },
|
|
@@ -13145,6 +13497,26 @@ async function callTool(name2, args2, defaultRepo) {
|
|
|
13145
13497
|
if (limit === void 0 || all.length <= limit) return JSON.stringify(all, null, 2);
|
|
13146
13498
|
return JSON.stringify({ total: all.length, shown: limit, truncated: true, candidates: all.slice(0, limit) }, null, 2);
|
|
13147
13499
|
}
|
|
13500
|
+
if (name2 === "duplicated_literals") {
|
|
13501
|
+
const report = findLiteralDuplications(getScan(repo, scanOpts, walked), {
|
|
13502
|
+
minFiles: num(args2.minFiles),
|
|
13503
|
+
minCount: num(args2.minCount),
|
|
13504
|
+
includeTests: args2.includeTests === true
|
|
13505
|
+
});
|
|
13506
|
+
const limit = num(args2.limit);
|
|
13507
|
+
if (limit === void 0 || report.duplications.length <= limit) return JSON.stringify(report, null, 2);
|
|
13508
|
+
return JSON.stringify(
|
|
13509
|
+
{
|
|
13510
|
+
total: report.duplications.length,
|
|
13511
|
+
shown: limit,
|
|
13512
|
+
truncated: true,
|
|
13513
|
+
duplications: report.duplications.slice(0, limit),
|
|
13514
|
+
families: report.families
|
|
13515
|
+
},
|
|
13516
|
+
null,
|
|
13517
|
+
2
|
|
13518
|
+
);
|
|
13519
|
+
}
|
|
13148
13520
|
if (name2 === "complexity") {
|
|
13149
13521
|
const scan2 = getScan(repo, scanOpts, walked);
|
|
13150
13522
|
if (args2.risk === true) {
|
|
@@ -13382,6 +13754,7 @@ var init_mcp = __esm({
|
|
|
13382
13754
|
init_coupling();
|
|
13383
13755
|
init_repomap();
|
|
13384
13756
|
init_deadcode();
|
|
13757
|
+
init_literals2();
|
|
13385
13758
|
init_complexity();
|
|
13386
13759
|
init_viz();
|
|
13387
13760
|
init_query();
|
|
@@ -13713,7 +14086,16 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
|
|
|
13713
14086
|
".sh",
|
|
13714
14087
|
".bash",
|
|
13715
14088
|
".zig",
|
|
13716
|
-
".elm"
|
|
14089
|
+
".elm",
|
|
14090
|
+
".hcl",
|
|
14091
|
+
".tf",
|
|
14092
|
+
".tfvars",
|
|
14093
|
+
".sol",
|
|
14094
|
+
".hh",
|
|
14095
|
+
".sc",
|
|
14096
|
+
".pyi",
|
|
14097
|
+
".rake",
|
|
14098
|
+
".cxx"
|
|
13717
14099
|
]);
|
|
13718
14100
|
var STYLE_EXTS = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less", ".styl", ".pcss"]);
|
|
13719
14101
|
var DOC_EXTS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".adoc", ".txt"]);
|
|
@@ -14400,6 +14782,7 @@ init_rules();
|
|
|
14400
14782
|
init_coupling();
|
|
14401
14783
|
init_repomap();
|
|
14402
14784
|
init_deadcode();
|
|
14785
|
+
init_literals2();
|
|
14403
14786
|
init_complexity();
|
|
14404
14787
|
init_viz();
|
|
14405
14788
|
|
|
@@ -14813,6 +15196,7 @@ init_grep();
|
|
|
14813
15196
|
init_coupling();
|
|
14814
15197
|
init_repomap();
|
|
14815
15198
|
init_deadcode();
|
|
15199
|
+
init_literals2();
|
|
14816
15200
|
init_complexity();
|
|
14817
15201
|
init_viz();
|
|
14818
15202
|
init_bm25();
|
|
@@ -14865,12 +15249,21 @@ Commands:
|
|
|
14865
15249
|
asset into the shared cache (sha256-verified,
|
|
14866
15250
|
atomic). Override the source with
|
|
14867
15251
|
CODEINDEX_GRAMMARS_URL
|
|
14868
|
-
rules Architecture rules (forbidden edges, cycles, orphans)
|
|
14869
|
-
against the link-graph: --config <codeindex.rules.json>;
|
|
14870
|
-
on any error-severity violation (a CI gate)
|
|
15252
|
+
rules Architecture rules (forbidden edges, cycles, orphans, literals)
|
|
15253
|
+
validated against the link-graph: --config <codeindex.rules.json>;
|
|
15254
|
+
exits 1 on any error-severity violation (a CI gate)
|
|
14871
15255
|
repomap Token-budgeted map of the highest-PageRank files (--budget-tokens)
|
|
14872
15256
|
hotspots Churn \xD7 size ranking of the files where work concentrates (JSON)
|
|
14873
15257
|
coupling Change coupling: files that change together (JSON; --since <ref>)
|
|
15258
|
+
literals Values with no single source of truth: one literal written out
|
|
15259
|
+
across many files, in three labeled tiers \u2014 'competing' (two or
|
|
15260
|
+
more exported constants hold it), 'bypassed' (a constant holds
|
|
15261
|
+
it and other files rewrite it anyway), 'uncentralized' (nothing
|
|
15262
|
+
holds it). Groups path-like values into namespace families so a
|
|
15263
|
+
whole route space reports once, not forty times. Reads code AND
|
|
15264
|
+
config files (JSON/YAML/TOML), because the duplications that hurt
|
|
15265
|
+
are the ones crossing a language boundary no compiler checks.
|
|
15266
|
+
(--min-files, --min-count, --include-tests)
|
|
14874
15267
|
deadcode Dead-code candidates in two labeled tiers: 'unreferenced' (no
|
|
14875
15268
|
call site binds AND nothing references the name) and 'uncalled'
|
|
14876
15269
|
(referenced \u2014 re-export, type position \u2014 but never called)
|
|
@@ -14891,7 +15284,7 @@ Commands:
|
|
|
14891
15284
|
and exits 0, or exits 1 when it has no opinion (run the original).
|
|
14892
15285
|
Deliberately conservative \u2014 any shell metacharacter or unknown
|
|
14893
15286
|
flag refuses the rewrite
|
|
14894
|
-
mcp Run as an MCP server over stdio (
|
|
15287
|
+
mcp Run as an MCP server over stdio (30 tools: scan_summary, graph,
|
|
14895
15288
|
symbols, callers, workspaces, churn, symbols_overview,
|
|
14896
15289
|
find_symbol, find_references, repo_map, hotspots, coupling,
|
|
14897
15290
|
dead_code, complexity, mermaid, grep, search, embed_status,
|
|
@@ -14945,6 +15338,10 @@ Flags (accepted before OR after the subcommand: '--repo X scan' and
|
|
|
14945
15338
|
each site corroborated|unique-name
|
|
14946
15339
|
--ignore-case \`grep\`: case-insensitive matching
|
|
14947
15340
|
--max-hits <n> \`grep\`: cap returned hits (default 200)
|
|
15341
|
+
--min-files <n> \`literals\`: distinct files a value must span (default 2)
|
|
15342
|
+
--min-count <n> \`literals\`: total occurrences required (default 3)
|
|
15343
|
+
--include-tests \`literals\`: count test files too. Off by default \u2014 a test
|
|
15344
|
+
restating a value is usually asserting it deliberately
|
|
14948
15345
|
`;
|
|
14949
15346
|
function parseFlags(args2) {
|
|
14950
15347
|
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
@@ -14977,6 +15374,9 @@ function parseFlags(args2) {
|
|
|
14977
15374
|
else if (a === "--ignore-case") flags2.ignoreCase = true;
|
|
14978
15375
|
else if (a === "--max-hits") flags2.maxHits = num2();
|
|
14979
15376
|
else if (a === "--budget-tokens") flags2.budgetTokens = num2();
|
|
15377
|
+
else if (a === "--min-files") flags2.minFiles = num2();
|
|
15378
|
+
else if (a === "--min-count") flags2.minCount = num2();
|
|
15379
|
+
else if (a === "--include-tests") flags2.includeTests = true;
|
|
14980
15380
|
else if (a === "--no-ast") flags2.noAst = true;
|
|
14981
15381
|
else if (a === "--index") flags2.indexDir = next();
|
|
14982
15382
|
else if (a === "--no-index-cache") flags2.noIndexCache = true;
|
|
@@ -15070,6 +15470,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
15070
15470
|
"--max-calls",
|
|
15071
15471
|
"--max-hits",
|
|
15072
15472
|
"--budget-tokens",
|
|
15473
|
+
"--min-files",
|
|
15474
|
+
"--min-count",
|
|
15073
15475
|
"--since",
|
|
15074
15476
|
"--config",
|
|
15075
15477
|
"--limit",
|
|
@@ -15501,6 +15903,13 @@ async function runCli(rawArgv) {
|
|
|
15501
15903
|
emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
|
|
15502
15904
|
} else if (cmd === "deadcode") {
|
|
15503
15905
|
emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags2.out);
|
|
15906
|
+
} else if (cmd === "literals") {
|
|
15907
|
+
const report = findLiteralDuplications(readScan(), {
|
|
15908
|
+
minFiles: flags2.minFiles,
|
|
15909
|
+
minCount: flags2.minCount,
|
|
15910
|
+
includeTests: flags2.includeTests
|
|
15911
|
+
});
|
|
15912
|
+
emit(JSON.stringify(report, null, 2) + "\n", flags2.out);
|
|
15504
15913
|
} else if (cmd === "complexity") {
|
|
15505
15914
|
const scan2 = readScan();
|
|
15506
15915
|
emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
|
|
@@ -15625,6 +16034,7 @@ export {
|
|
|
15625
16034
|
fetchExpectedSha256,
|
|
15626
16035
|
fetchGrammarsTarball,
|
|
15627
16036
|
findDeadCode,
|
|
16037
|
+
findLiteralDuplications,
|
|
15628
16038
|
findReferences,
|
|
15629
16039
|
findSymbol,
|
|
15630
16040
|
foldText,
|