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
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative4, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "2.
|
|
9
|
+
var VERSION = "2.30.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -1197,7 +1197,7 @@ var wcag_universe_default = {
|
|
|
1197
1197
|
wcagVersion: "2.2",
|
|
1198
1198
|
source: "https://www.w3.org/TR/WCAG22/",
|
|
1199
1199
|
criteriaSource: "https://github.com/w3c/wcag",
|
|
1200
|
-
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-
|
|
1200
|
+
provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-06 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
|
|
1201
1201
|
criteria: [
|
|
1202
1202
|
{
|
|
1203
1203
|
id: "1.1.1",
|
|
@@ -18887,9 +18887,9 @@ var EXTRACTOR_VERSION;
|
|
|
18887
18887
|
var init_types = __esm({
|
|
18888
18888
|
"src/types.ts"() {
|
|
18889
18889
|
"use strict";
|
|
18890
|
-
ENGINE_VERSION = "2.
|
|
18890
|
+
ENGINE_VERSION = "2.27.1";
|
|
18891
18891
|
SCHEMA_VERSION2 = 5;
|
|
18892
|
-
EXTRACTOR_VERSION =
|
|
18892
|
+
EXTRACTOR_VERSION = 13;
|
|
18893
18893
|
}
|
|
18894
18894
|
});
|
|
18895
18895
|
function sh(cmd, args2, opts = {}) {
|
|
@@ -19786,7 +19786,18 @@ var init_common = __esm({
|
|
|
19786
19786
|
".scss": "scss",
|
|
19787
19787
|
".vue": "vue",
|
|
19788
19788
|
".svelte": "svelte",
|
|
19789
|
-
".astro": "astro"
|
|
19789
|
+
".astro": "astro",
|
|
19790
|
+
// Extended-tier AST languages (src/ast/loader.ts EXT_GRAMMAR). Without an
|
|
19791
|
+
// entry here `extToLang` answered "other", which `classify` reads as non-code
|
|
19792
|
+
// — so a real scan never called extractCode for them and they extracted
|
|
19793
|
+
// nothing, while the quality harness (which calls extractCode directly)
|
|
19794
|
+
// published a perfect score. The grammar still arrives via `grammars pull`;
|
|
19795
|
+
// absent it these fall back to the regex tier like any other language.
|
|
19796
|
+
".zig": "zig",
|
|
19797
|
+
".hcl": "hcl",
|
|
19798
|
+
".tf": "terraform",
|
|
19799
|
+
".tfvars": "terraform",
|
|
19800
|
+
".sol": "solidity"
|
|
19790
19801
|
};
|
|
19791
19802
|
REEXPORT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
19792
19803
|
MAX_REEXPORTS = 400;
|
|
@@ -20547,6 +20558,68 @@ var init_markdown = __esm({
|
|
|
20547
20558
|
"use strict";
|
|
20548
20559
|
}
|
|
20549
20560
|
});
|
|
20561
|
+
function isInterestingString(value) {
|
|
20562
|
+
if (value.length < MIN_LITERAL_LEN || value.length > MAX_LITERAL_LEN) return false;
|
|
20563
|
+
return HAS_SUBSTANCE.test(value);
|
|
20564
|
+
}
|
|
20565
|
+
function isInterestingNumber(raw) {
|
|
20566
|
+
const v = raw.trim();
|
|
20567
|
+
if (!v || v.length > MAX_LITERAL_LEN) return false;
|
|
20568
|
+
if (TRIVIAL_NUMBERS.has(v)) return false;
|
|
20569
|
+
return Number.isFinite(Number(v));
|
|
20570
|
+
}
|
|
20571
|
+
function unquote(text) {
|
|
20572
|
+
let s = text;
|
|
20573
|
+
const raw = /^(?:[rRbBuUfF]{1,2}|@|\$)?(?:#*)?(['"`])/.exec(s);
|
|
20574
|
+
if (raw) {
|
|
20575
|
+
const q = raw[1];
|
|
20576
|
+
const start2 = s.indexOf(q);
|
|
20577
|
+
const end = s.lastIndexOf(q);
|
|
20578
|
+
if (end > start2) s = s.slice(start2 + 1, end);
|
|
20579
|
+
}
|
|
20580
|
+
return s;
|
|
20581
|
+
}
|
|
20582
|
+
var MAX_LITERAL_LEN;
|
|
20583
|
+
var MIN_LITERAL_LEN;
|
|
20584
|
+
var MAX_LITERALS;
|
|
20585
|
+
var TRIVIAL_NUMBERS;
|
|
20586
|
+
var HAS_SUBSTANCE;
|
|
20587
|
+
var LiteralCollector;
|
|
20588
|
+
var init_literals = __esm({
|
|
20589
|
+
"src/extract/literals.ts"() {
|
|
20590
|
+
"use strict";
|
|
20591
|
+
init_sort();
|
|
20592
|
+
MAX_LITERAL_LEN = 80;
|
|
20593
|
+
MIN_LITERAL_LEN = 2;
|
|
20594
|
+
MAX_LITERALS = 256;
|
|
20595
|
+
TRIVIAL_NUMBERS = /* @__PURE__ */ new Set(["0", "1", "-1", "2", "-2"]);
|
|
20596
|
+
HAS_SUBSTANCE = /[\p{L}\p{N}]/u;
|
|
20597
|
+
LiteralCollector = class {
|
|
20598
|
+
seen = /* @__PURE__ */ new Set();
|
|
20599
|
+
out = [];
|
|
20600
|
+
get full() {
|
|
20601
|
+
return this.out.length >= MAX_LITERALS;
|
|
20602
|
+
}
|
|
20603
|
+
add(kind, value, line) {
|
|
20604
|
+
if (this.full) return;
|
|
20605
|
+
if (kind === "string" && !isInterestingString(value)) return;
|
|
20606
|
+
if (kind === "number" && !isInterestingNumber(value)) return;
|
|
20607
|
+
if (kind === "regex" && !value) return;
|
|
20608
|
+
const key = `${kind}\0${value}\0${line}`;
|
|
20609
|
+
if (this.seen.has(key)) return;
|
|
20610
|
+
this.seen.add(key);
|
|
20611
|
+
this.out.push({ value, line, kind });
|
|
20612
|
+
}
|
|
20613
|
+
addString(text, line) {
|
|
20614
|
+
this.add("string", unquote(text), line);
|
|
20615
|
+
}
|
|
20616
|
+
result() {
|
|
20617
|
+
if (!this.out.length) return void 0;
|
|
20618
|
+
return this.out.sort((a, b) => byStr(a.value, b.value) || a.line - b.line || byStr(a.kind, b.kind));
|
|
20619
|
+
}
|
|
20620
|
+
};
|
|
20621
|
+
}
|
|
20622
|
+
});
|
|
20550
20623
|
function assertInternal(x) {
|
|
20551
20624
|
if (x !== INTERNAL) throw new Error("Illegal constructor");
|
|
20552
20625
|
}
|
|
@@ -25961,6 +26034,9 @@ var init_doc = __esm({
|
|
|
25961
26034
|
DECORATION = /decorator|annotation|modifiers/;
|
|
25962
26035
|
}
|
|
25963
26036
|
});
|
|
26037
|
+
function isPlainString(node) {
|
|
26038
|
+
return node.namedChildren.every((c2) => STRING_PART.test(c2.type));
|
|
26039
|
+
}
|
|
25964
26040
|
function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
25965
26041
|
const identsFound = /* @__PURE__ */ new Set();
|
|
25966
26042
|
const wantCalls = spec.calls !== void 0;
|
|
@@ -25982,6 +26058,7 @@ function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
|
25982
26058
|
termsFound.add(t2);
|
|
25983
26059
|
}
|
|
25984
26060
|
};
|
|
26061
|
+
const literals = new LiteralCollector();
|
|
25985
26062
|
const wantNames = spec.imports?.import_statement !== void 0;
|
|
25986
26063
|
const namesFound = /* @__PURE__ */ new Set();
|
|
25987
26064
|
const wantRefs = wantImports && spec.imports !== void 0;
|
|
@@ -26003,9 +26080,15 @@ function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
|
26003
26080
|
}
|
|
26004
26081
|
if (COMMENT_NODE.test(type)) {
|
|
26005
26082
|
for (const line of node.text.split(/\r?\n/)) addTerms2(stripCommentMarkers(line));
|
|
26006
|
-
} else if (kids.length === 0 && STRING_NODE.test(type) && node.endIndex - node.startIndex <=
|
|
26083
|
+
} else if (kids.length === 0 && STRING_NODE.test(type) && node.endIndex - node.startIndex <= MAX_LITERAL_LEN2) {
|
|
26007
26084
|
addTerms2(node.text.replace(/^['"`]+|['"`]+$/g, ""));
|
|
26008
26085
|
}
|
|
26086
|
+
if (!literals.full) {
|
|
26087
|
+
const line = node.startPosition.row + 1;
|
|
26088
|
+
if (STRING_NODE.test(type) && isPlainString(node)) literals.addString(node.text, line);
|
|
26089
|
+
else if (kids.length === 0 && NUMBER_NODE.test(type)) literals.add("number", node.text.trim(), line);
|
|
26090
|
+
else if (REGEX_NODE.test(type)) literals.add("regex", node.text, line);
|
|
26091
|
+
}
|
|
26009
26092
|
if (wantCalls && !(spec.kindFrom?.[type] && spec.kindFrom[type](node)) && !spec.skipCall?.(node)) {
|
|
26010
26093
|
const how = spec.calls[type];
|
|
26011
26094
|
if (how === "function") {
|
|
@@ -26054,7 +26137,8 @@ function collectAll(root, spec, defNames, maxCalls, wantImports) {
|
|
|
26054
26137
|
idents: [...identsFound].sort().slice(0, MAX_REF_IDENTS),
|
|
26055
26138
|
calls: calls.slice(0, maxCalls),
|
|
26056
26139
|
importedNames: [...namesFound].sort(byStr).slice(0, MAX_IMPORTED_NAMES),
|
|
26057
|
-
terms: [...termsFound].sort(byStr)
|
|
26140
|
+
terms: [...termsFound].sort(byStr),
|
|
26141
|
+
literals: literals.result() ?? []
|
|
26058
26142
|
};
|
|
26059
26143
|
}
|
|
26060
26144
|
function boundNames(pattern) {
|
|
@@ -26396,7 +26480,7 @@ function extractAst(rel2, ext2, content, opts = {}) {
|
|
|
26396
26480
|
for (const s of symbols) if (!s.exported && exportedNames.has(s.name)) s.exported = true;
|
|
26397
26481
|
}
|
|
26398
26482
|
const wantImports = opts.imports !== false;
|
|
26399
|
-
const { refs, idents, calls, importedNames, terms } = collectAll(
|
|
26483
|
+
const { refs, idents, calls, importedNames, terms, literals } = collectAll(
|
|
26400
26484
|
root,
|
|
26401
26485
|
spec,
|
|
26402
26486
|
new Set(symbols.map((s) => s.name)),
|
|
@@ -26418,6 +26502,7 @@ function extractAst(rel2, ext2, content, opts = {}) {
|
|
|
26418
26502
|
importedNames,
|
|
26419
26503
|
relations,
|
|
26420
26504
|
terms,
|
|
26505
|
+
literals,
|
|
26421
26506
|
...symbols.length >= maxSymbols ? { truncated: true } : {}
|
|
26422
26507
|
};
|
|
26423
26508
|
} catch {
|
|
@@ -26432,7 +26517,7 @@ var MAX_IMPORTED_NAMES;
|
|
|
26432
26517
|
var MAX_SYMBOLS;
|
|
26433
26518
|
var MAX_RELATIONS;
|
|
26434
26519
|
var MAX_TERMS;
|
|
26435
|
-
var
|
|
26520
|
+
var MAX_LITERAL_LEN2;
|
|
26436
26521
|
var MAX_FUNC_DEPTH;
|
|
26437
26522
|
var NESTED_TYPE_KINDS;
|
|
26438
26523
|
var ANON_DEFAULT_FN;
|
|
@@ -26441,9 +26526,13 @@ var REF_IDENT_TYPE;
|
|
|
26441
26526
|
var REF_IDENT_TEXT;
|
|
26442
26527
|
var COMMENT_NODE;
|
|
26443
26528
|
var STRING_NODE;
|
|
26529
|
+
var NUMBER_NODE;
|
|
26530
|
+
var REGEX_NODE;
|
|
26531
|
+
var STRING_PART;
|
|
26444
26532
|
var init_extract = __esm({
|
|
26445
26533
|
"src/ast/extract.ts"() {
|
|
26446
26534
|
"use strict";
|
|
26535
|
+
init_literals();
|
|
26447
26536
|
init_sort();
|
|
26448
26537
|
init_loader();
|
|
26449
26538
|
init_node();
|
|
@@ -26458,7 +26547,7 @@ var init_extract = __esm({
|
|
|
26458
26547
|
MAX_SYMBOLS = 2e3;
|
|
26459
26548
|
MAX_RELATIONS = 256;
|
|
26460
26549
|
MAX_TERMS = 512;
|
|
26461
|
-
|
|
26550
|
+
MAX_LITERAL_LEN2 = 80;
|
|
26462
26551
|
MAX_FUNC_DEPTH = 2;
|
|
26463
26552
|
NESTED_TYPE_KINDS = /* @__PURE__ */ new Set(["class", "struct", "enum", "interface", "trait", "type", "record", "union"]);
|
|
26464
26553
|
ANON_DEFAULT_FN = /* @__PURE__ */ new Set([
|
|
@@ -26474,6 +26563,9 @@ var init_extract = __esm({
|
|
|
26474
26563
|
REF_IDENT_TEXT = /^[A-Za-z_]\w{4,}$/;
|
|
26475
26564
|
COMMENT_NODE = /(^|_)comment$/;
|
|
26476
26565
|
STRING_NODE = /(^|_)string(_literal)?$/;
|
|
26566
|
+
NUMBER_NODE = /(^|_)(integer|float|number|decimal|numeric)(_literal)?$/;
|
|
26567
|
+
REGEX_NODE = /(^|_)(regex|regular_expression)(_pattern|_literal)?$/;
|
|
26568
|
+
STRING_PART = /(^|_)(fragment|content|escape_sequence|character)$/;
|
|
26477
26569
|
}
|
|
26478
26570
|
});
|
|
26479
26571
|
function topDocComment(content) {
|
|
@@ -26721,17 +26813,55 @@ function collectTermsRegex(content) {
|
|
|
26721
26813
|
}
|
|
26722
26814
|
for (const m of line.matchAll(/(['"`])((?:\\.|(?!\1)[^\\])*)\1/g)) {
|
|
26723
26815
|
const body2 = m[2];
|
|
26724
|
-
if (body2.length && body2.length <=
|
|
26816
|
+
if (body2.length && body2.length <= MAX_LITERAL_LEN3) add2(body2);
|
|
26725
26817
|
}
|
|
26726
26818
|
}
|
|
26727
26819
|
return [...found].sort();
|
|
26728
26820
|
}
|
|
26821
|
+
function collectLiteralsRegex(content) {
|
|
26822
|
+
const literals = new LiteralCollector();
|
|
26823
|
+
let inBlock = false;
|
|
26824
|
+
let lineNo = 0;
|
|
26825
|
+
for (const raw of content.split("\n")) {
|
|
26826
|
+
lineNo++;
|
|
26827
|
+
if (literals.full) break;
|
|
26828
|
+
let line = raw;
|
|
26829
|
+
if (inBlock) {
|
|
26830
|
+
const close = line.indexOf("*/");
|
|
26831
|
+
if (close === -1) continue;
|
|
26832
|
+
inBlock = false;
|
|
26833
|
+
line = line.slice(close + 2);
|
|
26834
|
+
}
|
|
26835
|
+
const open = line.indexOf("/*");
|
|
26836
|
+
if (open !== -1) {
|
|
26837
|
+
const close = line.indexOf("*/", open + 2);
|
|
26838
|
+
if (close === -1) {
|
|
26839
|
+
inBlock = true;
|
|
26840
|
+
line = line.slice(0, open);
|
|
26841
|
+
} else line = line.slice(0, open) + line.slice(close + 2);
|
|
26842
|
+
}
|
|
26843
|
+
const lineComment = /(^|\s)(\/\/|#|--)(.*)$/.exec(line);
|
|
26844
|
+
if (lineComment) line = line.slice(0, lineComment.index);
|
|
26845
|
+
for (const m of line.matchAll(/(['"`])((?:\\.|(?!\1)[^\\])*)\1/g)) {
|
|
26846
|
+
literals.add("string", m[2], lineNo);
|
|
26847
|
+
}
|
|
26848
|
+
for (const m of line.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, " ").matchAll(/(?<![\w.])-?\d[\d_]*(?:\.\d+)?(?![\w.])/g)) {
|
|
26849
|
+
literals.add("number", m[0].replace(/_/g, ""), lineNo);
|
|
26850
|
+
}
|
|
26851
|
+
}
|
|
26852
|
+
return literals.result();
|
|
26853
|
+
}
|
|
26729
26854
|
function extractCode(rel2, ext2, content, opts = {}) {
|
|
26730
26855
|
const ast = extractAst(rel2, ext2, content, { maxCalls: opts.maxCallsPerFile, imports: false });
|
|
26731
26856
|
const raw = ast ? ast.symbols : extractSymbols(rel2, ext2, content);
|
|
26732
26857
|
const symbols = raw.slice(0, MAX_FILE_SYMBOLS);
|
|
26733
26858
|
const known = new Set(symbols.map((s) => s.name));
|
|
26734
26859
|
const reexports = extractReexports(rel2, content, symbols).filter((s) => !known.has(s.name));
|
|
26860
|
+
const refs = extractImports(ext2, content);
|
|
26861
|
+
const importSpecs = new Set(refs.map((r) => r.spec));
|
|
26862
|
+
const literals = (ast ? ast.literals.length ? ast.literals : void 0 : collectLiteralsRegex(content))?.filter(
|
|
26863
|
+
(l) => !(l.kind === "string" && importSpecs.has(l.value))
|
|
26864
|
+
);
|
|
26735
26865
|
return {
|
|
26736
26866
|
symbols: [...symbols, ...reexports],
|
|
26737
26867
|
// A re-export list that hit its own ceiling is truncated too — a barrel that
|
|
@@ -26739,7 +26869,7 @@ function extractCode(rel2, ext2, content, opts = {}) {
|
|
|
26739
26869
|
// exists to prevent.
|
|
26740
26870
|
...ast?.truncated || raw.length > symbols.length || reexports.length >= MAX_REEXPORTS ? { truncated: true } : {},
|
|
26741
26871
|
summary: topDocComment(content),
|
|
26742
|
-
refs
|
|
26872
|
+
refs,
|
|
26743
26873
|
// pkg anchors namespace→source-root resolution: Java's `package`, C#'s
|
|
26744
26874
|
// `namespace` (block or file-scoped). Both feed the same resolver pattern.
|
|
26745
26875
|
pkg: ext2 === ".java" ? /^\s*package\s+([\w.]+)\s*;/m.exec(content)?.[1] : ext2 === ".cs" ? /^\s*(?:file-scoped\s+)?namespace\s+([\w.]+)/m.exec(content)?.[1] : void 0,
|
|
@@ -26754,7 +26884,8 @@ function extractCode(rel2, ext2, content, opts = {}) {
|
|
|
26754
26884
|
// The AST tier reads comments and literals structurally; without a grammar
|
|
26755
26885
|
// the line scanner above still supplies a vocabulary, so search quality does
|
|
26756
26886
|
// not silently collapse for a language with no wasm.
|
|
26757
|
-
terms: ast ? ast.terms : collectTermsRegex(content)
|
|
26887
|
+
terms: ast ? ast.terms : collectTermsRegex(content),
|
|
26888
|
+
literals: literals?.length ? literals : void 0
|
|
26758
26889
|
};
|
|
26759
26890
|
}
|
|
26760
26891
|
var MAX_FILE_SYMBOLS;
|
|
@@ -26765,10 +26896,11 @@ var MAX_USE_EXPANSION;
|
|
|
26765
26896
|
var CALL_KEYWORDS;
|
|
26766
26897
|
var DEF_INTRODUCERS;
|
|
26767
26898
|
var MAX_TERMS2;
|
|
26768
|
-
var
|
|
26899
|
+
var MAX_LITERAL_LEN3;
|
|
26769
26900
|
var init_code = __esm({
|
|
26770
26901
|
"src/extract/code.ts"() {
|
|
26771
26902
|
"use strict";
|
|
26903
|
+
init_literals();
|
|
26772
26904
|
init_registry();
|
|
26773
26905
|
init_extract();
|
|
26774
26906
|
init_common();
|
|
@@ -26817,7 +26949,52 @@ var init_code = __esm({
|
|
|
26817
26949
|
]);
|
|
26818
26950
|
DEF_INTRODUCERS = /(?:\bfunction|\bdef|\bfunc|\bfun|\bfn|\bclass|\bsub|\bmacro|\bproc)\s*[*]?\s*$/;
|
|
26819
26951
|
MAX_TERMS2 = 512;
|
|
26820
|
-
|
|
26952
|
+
MAX_LITERAL_LEN3 = 80;
|
|
26953
|
+
}
|
|
26954
|
+
});
|
|
26955
|
+
function extractConfigLiterals(content) {
|
|
26956
|
+
const literals = new LiteralCollector();
|
|
26957
|
+
let lineNo = 0;
|
|
26958
|
+
for (const raw of content.split("\n")) {
|
|
26959
|
+
lineNo++;
|
|
26960
|
+
if (literals.full) break;
|
|
26961
|
+
const line = stripTrailingComment(raw);
|
|
26962
|
+
for (const m of line.matchAll(QUOTED)) literals.add("string", m[2], lineNo);
|
|
26963
|
+
const bare = UNQUOTED_SCALAR.exec(line);
|
|
26964
|
+
if (bare) {
|
|
26965
|
+
const v = bare[1].trim();
|
|
26966
|
+
if (v && !/^[[{|>&*-]/.test(v) && !HAS_QUOTE.test(v)) literals.add("string", v, lineNo);
|
|
26967
|
+
}
|
|
26968
|
+
for (const m of line.replace(QUOTED, " ").matchAll(BARE_NUMBER)) {
|
|
26969
|
+
literals.add("number", m[0].replace(/_/g, ""), lineNo);
|
|
26970
|
+
}
|
|
26971
|
+
}
|
|
26972
|
+
return literals.result();
|
|
26973
|
+
}
|
|
26974
|
+
function stripTrailingComment(line) {
|
|
26975
|
+
let inQuote;
|
|
26976
|
+
for (let i2 = 0; i2 < line.length; i2++) {
|
|
26977
|
+
const c2 = line[i2];
|
|
26978
|
+
if (inQuote) {
|
|
26979
|
+
if (c2 === "\\") i2++;
|
|
26980
|
+
else if (c2 === inQuote) inQuote = void 0;
|
|
26981
|
+
} else if (c2 === '"' || c2 === "'") inQuote = c2;
|
|
26982
|
+
else if (c2 === "#") return line.slice(0, i2);
|
|
26983
|
+
}
|
|
26984
|
+
return line;
|
|
26985
|
+
}
|
|
26986
|
+
var QUOTED;
|
|
26987
|
+
var HAS_QUOTE;
|
|
26988
|
+
var BARE_NUMBER;
|
|
26989
|
+
var UNQUOTED_SCALAR;
|
|
26990
|
+
var init_config = __esm({
|
|
26991
|
+
"src/extract/config.ts"() {
|
|
26992
|
+
"use strict";
|
|
26993
|
+
init_literals();
|
|
26994
|
+
QUOTED = /(['"])((?:\\.|(?!\1)[^\\])*)\1/g;
|
|
26995
|
+
HAS_QUOTE = /(['"])((?:\\.|(?!\1)[^\\])*)\1/;
|
|
26996
|
+
BARE_NUMBER = /(?<![\w.$-])-?\d[\d_]*(?:\.\d+)?(?![\w.$-])/g;
|
|
26997
|
+
UNQUOTED_SCALAR = /^\s*[\w.$-]+\s*[:=]\s*([^#\n]+?)\s*$/;
|
|
26821
26998
|
}
|
|
26822
26999
|
});
|
|
26823
27000
|
function countLines(s) {
|
|
@@ -26964,6 +27141,10 @@ function scanRepo(root, opts = {}) {
|
|
|
26964
27141
|
record.truncated = code2.truncated;
|
|
26965
27142
|
record.relations = code2.relations;
|
|
26966
27143
|
record.terms = code2.terms;
|
|
27144
|
+
record.literals = code2.literals;
|
|
27145
|
+
} else if (kind === "config") {
|
|
27146
|
+
record.title = basename(f.rel);
|
|
27147
|
+
record.literals = extractConfigLiterals(content);
|
|
26967
27148
|
} else {
|
|
26968
27149
|
record.title = basename(f.rel);
|
|
26969
27150
|
}
|
|
@@ -27003,6 +27184,7 @@ var init_scan = __esm({
|
|
|
27003
27184
|
init_sort();
|
|
27004
27185
|
init_markdown();
|
|
27005
27186
|
init_code();
|
|
27187
|
+
init_config();
|
|
27006
27188
|
}
|
|
27007
27189
|
});
|
|
27008
27190
|
function toCacheMap(scan2) {
|
|
@@ -30277,6 +30459,144 @@ var init_graph_json = __esm({
|
|
|
30277
30459
|
init_sort();
|
|
30278
30460
|
}
|
|
30279
30461
|
});
|
|
30462
|
+
function isDistinctive2(value, kind) {
|
|
30463
|
+
if (kind !== "string") return true;
|
|
30464
|
+
return value.length >= DISTINCTIVE_MIN_LEN || HAS_SEPARATOR.test(value);
|
|
30465
|
+
}
|
|
30466
|
+
function isFunctionValued(signature) {
|
|
30467
|
+
if (!signature) return false;
|
|
30468
|
+
for (let i2 = 0; i2 < signature.length; i2++) {
|
|
30469
|
+
if (signature[i2] !== "=") continue;
|
|
30470
|
+
if (signature[i2 + 1] === "=" || signature[i2 + 1] === ">") continue;
|
|
30471
|
+
if ("=!<>+-*/%&|^".includes(signature[i2 - 1] ?? "")) continue;
|
|
30472
|
+
return FUNCTION_RHS.test(signature.slice(i2 + 1));
|
|
30473
|
+
}
|
|
30474
|
+
return false;
|
|
30475
|
+
}
|
|
30476
|
+
function holderFor(symbols, line) {
|
|
30477
|
+
let best;
|
|
30478
|
+
for (const s of symbols) {
|
|
30479
|
+
if (!HOLDER_KINDS.has(s.kind)) continue;
|
|
30480
|
+
if (isFunctionValued(s.signature)) continue;
|
|
30481
|
+
const end = s.endLine ?? s.line;
|
|
30482
|
+
if (end - s.line > MAX_HOLDER_SPAN) continue;
|
|
30483
|
+
if (line < s.line || line > end) continue;
|
|
30484
|
+
if (!best || s.line > best.line) best = s;
|
|
30485
|
+
}
|
|
30486
|
+
return best;
|
|
30487
|
+
}
|
|
30488
|
+
function findLiteralDuplications(scan2, opts = {}) {
|
|
30489
|
+
const minFiles = opts.minFiles ?? DEFAULTS.minFiles;
|
|
30490
|
+
const minCount = opts.minCount ?? DEFAULTS.minCount;
|
|
30491
|
+
const groups = /* @__PURE__ */ new Map();
|
|
30492
|
+
for (const f of scan2.files) {
|
|
30493
|
+
if (!f.literals?.length) continue;
|
|
30494
|
+
if (!opts.includeTests && isTestPath(f.rel)) continue;
|
|
30495
|
+
for (const lit of f.literals) {
|
|
30496
|
+
if (opts.kinds && !opts.kinds.has(lit.kind)) continue;
|
|
30497
|
+
if (!isDistinctive2(lit.value, lit.kind)) continue;
|
|
30498
|
+
const key = `${lit.kind}\0${lit.value}`;
|
|
30499
|
+
let g = groups.get(key);
|
|
30500
|
+
if (!g) groups.set(key, g = { value: lit.value, kind: lit.kind, sites: [] });
|
|
30501
|
+
const holder = holderFor(f.symbols, lit.line);
|
|
30502
|
+
g.sites.push(
|
|
30503
|
+
holder ? { file: f.rel, line: lit.line, holder: holder.name, holderExported: holder.exported } : { file: f.rel, line: lit.line }
|
|
30504
|
+
);
|
|
30505
|
+
}
|
|
30506
|
+
}
|
|
30507
|
+
const duplications = [];
|
|
30508
|
+
for (const g of groups.values()) {
|
|
30509
|
+
const files = new Set(g.sites.map((s) => s.file));
|
|
30510
|
+
if (files.size < minFiles || g.sites.length < minCount) continue;
|
|
30511
|
+
const holders = g.sites.filter((s) => s.holder);
|
|
30512
|
+
const literals = g.sites.filter((s) => !s.holder);
|
|
30513
|
+
const distinctHolderNames = new Set(holders.map((h) => h.holder));
|
|
30514
|
+
const distinctHolders = new Set(holders.map((h) => `${h.file}\0${h.holder}`));
|
|
30515
|
+
let tier = distinctHolders.size >= 2 ? "competing" : holders.length > 0 ? "bypassed" : "uncentralized";
|
|
30516
|
+
if (g.kind === "number") {
|
|
30517
|
+
if (distinctHolderNames.size !== 1 || literals.length === 0) continue;
|
|
30518
|
+
tier = "bypassed";
|
|
30519
|
+
}
|
|
30520
|
+
if (tier === "bypassed" && literals.length === 0) continue;
|
|
30521
|
+
duplications.push({
|
|
30522
|
+
value: g.value,
|
|
30523
|
+
kind: g.kind,
|
|
30524
|
+
tier,
|
|
30525
|
+
holders: holders.sort(siteOrder),
|
|
30526
|
+
literals: literals.sort(siteOrder),
|
|
30527
|
+
files: files.size,
|
|
30528
|
+
count: g.sites.length
|
|
30529
|
+
});
|
|
30530
|
+
}
|
|
30531
|
+
duplications.sort(
|
|
30532
|
+
(a, b) => tierRank(a.tier) - tierRank(b.tier) || b.files - a.files || b.count - a.count || byStr(a.value, b.value)
|
|
30533
|
+
);
|
|
30534
|
+
return { duplications, families: groupFamilies(duplications) };
|
|
30535
|
+
}
|
|
30536
|
+
function siteOrder(a, b) {
|
|
30537
|
+
return byStr(a.file, b.file) || a.line - b.line;
|
|
30538
|
+
}
|
|
30539
|
+
function tierRank(t2) {
|
|
30540
|
+
return t2 === "competing" ? 0 : t2 === "bypassed" ? 1 : 2;
|
|
30541
|
+
}
|
|
30542
|
+
function groupFamilies(dups) {
|
|
30543
|
+
const byPrefix = /* @__PURE__ */ new Map();
|
|
30544
|
+
for (const d of dups) {
|
|
30545
|
+
if (d.kind !== "string" || !PATH_LIKE.test(d.value)) continue;
|
|
30546
|
+
const prefix2 = pathPrefix(d.value);
|
|
30547
|
+
if (!prefix2) continue;
|
|
30548
|
+
const list = byPrefix.get(prefix2);
|
|
30549
|
+
if (list) list.push(d);
|
|
30550
|
+
else byPrefix.set(prefix2, [d]);
|
|
30551
|
+
}
|
|
30552
|
+
const families = [];
|
|
30553
|
+
for (const [prefix2, members] of byPrefix) {
|
|
30554
|
+
if (members.length < FAMILY_MIN_MEMBERS) continue;
|
|
30555
|
+
const files = /* @__PURE__ */ new Set();
|
|
30556
|
+
let count = 0;
|
|
30557
|
+
for (const m of members) {
|
|
30558
|
+
for (const s of [...m.holders, ...m.literals]) files.add(s.file);
|
|
30559
|
+
count += m.count;
|
|
30560
|
+
}
|
|
30561
|
+
families.push({ prefix: prefix2, members, files: files.size, count });
|
|
30562
|
+
}
|
|
30563
|
+
families.sort((a, b) => b.files - a.files || b.count - a.count || byStr(a.prefix, b.prefix));
|
|
30564
|
+
return families;
|
|
30565
|
+
}
|
|
30566
|
+
function pathPrefix(value) {
|
|
30567
|
+
const body2 = value.startsWith("/") ? value.slice(1) : value;
|
|
30568
|
+
const cut = body2.search(/[/:]/);
|
|
30569
|
+
if (cut <= 0) return void 0;
|
|
30570
|
+
const prefix2 = (value.startsWith("/") ? "/" : "") + body2.slice(0, cut);
|
|
30571
|
+
return prefix2.length >= FAMILY_MIN_PREFIX ? prefix2 : void 0;
|
|
30572
|
+
}
|
|
30573
|
+
var LITERAL_DUPLICATION_CAP;
|
|
30574
|
+
var DEFAULTS;
|
|
30575
|
+
var HOLDER_KINDS;
|
|
30576
|
+
var MAX_HOLDER_SPAN;
|
|
30577
|
+
var DISTINCTIVE_MIN_LEN;
|
|
30578
|
+
var HAS_SEPARATOR;
|
|
30579
|
+
var PATH_LIKE;
|
|
30580
|
+
var FAMILY_MIN_MEMBERS;
|
|
30581
|
+
var FAMILY_MIN_PREFIX;
|
|
30582
|
+
var FUNCTION_RHS;
|
|
30583
|
+
var init_literals2 = __esm({
|
|
30584
|
+
"src/literals.ts"() {
|
|
30585
|
+
"use strict";
|
|
30586
|
+
init_tests_map();
|
|
30587
|
+
init_sort();
|
|
30588
|
+
LITERAL_DUPLICATION_CAP = 24;
|
|
30589
|
+
DEFAULTS = { minFiles: 2, minCount: 3 };
|
|
30590
|
+
HOLDER_KINDS = /* @__PURE__ */ new Set(["const", "constant", "variable", "enum", "enumerator", "property", "field", "static"]);
|
|
30591
|
+
MAX_HOLDER_SPAN = 12;
|
|
30592
|
+
DISTINCTIVE_MIN_LEN = 6;
|
|
30593
|
+
HAS_SEPARATOR = /[/:._-]/;
|
|
30594
|
+
PATH_LIKE = /^[/@a-z0-9][\w./:@-]*$/i;
|
|
30595
|
+
FAMILY_MIN_MEMBERS = 2;
|
|
30596
|
+
FAMILY_MIN_PREFIX = 4;
|
|
30597
|
+
FUNCTION_RHS = /^\s*(?:async\b|function\b|\(|[A-Za-z_$][\w$]*\s*=>)/;
|
|
30598
|
+
}
|
|
30599
|
+
});
|
|
30280
30600
|
function buildIndexArtifacts(repo, opts = {}) {
|
|
30281
30601
|
return buildArtifactsFromScan(scanRepo(repo, opts), opts);
|
|
30282
30602
|
}
|
|
@@ -30300,6 +30620,8 @@ function buildArtifactsFromScan(scan2, opts = {}) {
|
|
|
30300
30620
|
}
|
|
30301
30621
|
const surprises = computeSurprises(graph);
|
|
30302
30622
|
if (surprises.length) graph.surprises = surprises;
|
|
30623
|
+
const { duplications } = findLiteralDuplications(scan2);
|
|
30624
|
+
if (duplications.length) graph.literalDuplications = duplications.slice(0, LITERAL_DUPLICATION_CAP);
|
|
30303
30625
|
const symbols = buildSymbolIndex(scan2, symbolRefsFor(scan2), opts.meta?.schemaVersion);
|
|
30304
30626
|
return { scan: scan2, graph, symbols };
|
|
30305
30627
|
}
|
|
@@ -30314,6 +30636,7 @@ var init_pipeline = __esm({
|
|
|
30314
30636
|
init_centrality();
|
|
30315
30637
|
init_tests_map();
|
|
30316
30638
|
init_surprise();
|
|
30639
|
+
init_literals2();
|
|
30317
30640
|
init_symbols_json();
|
|
30318
30641
|
}
|
|
30319
30642
|
});
|
|
@@ -30797,8 +31120,14 @@ function parseRules(input) {
|
|
|
30797
31120
|
throw new Error(`${at} (${r.name}): \`comment\` must be a string`);
|
|
30798
31121
|
if (r.builtin !== void 0) {
|
|
30799
31122
|
if (!BUILTINS.has(r.builtin))
|
|
30800
|
-
throw new Error(`${at} (${r.name}): \`builtin\` must be "cycles" or "
|
|
30801
|
-
return {
|
|
31123
|
+
throw new Error(`${at} (${r.name}): \`builtin\` must be "cycles", "orphans" or "literals"`);
|
|
31124
|
+
return {
|
|
31125
|
+
name: r.name,
|
|
31126
|
+
builtin: r.builtin,
|
|
31127
|
+
severity: r.severity,
|
|
31128
|
+
comment: r.comment,
|
|
31129
|
+
...r.tiers !== void 0 ? { tiers: r.tiers } : {}
|
|
31130
|
+
};
|
|
30802
31131
|
}
|
|
30803
31132
|
const glob = (field) => {
|
|
30804
31133
|
const v = r[field];
|
|
@@ -30907,6 +31236,17 @@ function checkRules(graph, rules) {
|
|
|
30907
31236
|
for (const c2 of findImportCycles(graph)) {
|
|
30908
31237
|
emit2(rule, { from: c2.start, to: c2.path.join(" -> "), kind: "cycle" });
|
|
30909
31238
|
}
|
|
31239
|
+
} else if (rule.builtin === "literals") {
|
|
31240
|
+
const wanted = new Set(rule.tiers?.length ? rule.tiers : GATED_TIERS);
|
|
31241
|
+
for (const d of graph.literalDuplications ?? []) {
|
|
31242
|
+
if (!wanted.has(d.tier)) continue;
|
|
31243
|
+
const origin = d.holders[0] ?? d.literals[0];
|
|
31244
|
+
emit2(rule, {
|
|
31245
|
+
from: `${origin.file}:${origin.line}`,
|
|
31246
|
+
to: `${d.tier} ${JSON.stringify(d.value)} (${d.count} sites, ${d.files} files)`,
|
|
31247
|
+
kind: "literal"
|
|
31248
|
+
});
|
|
31249
|
+
}
|
|
30910
31250
|
} else {
|
|
30911
31251
|
for (const f of graph.files) {
|
|
30912
31252
|
if (f.fileKind !== "code" || f.degIn !== 0 || f.degOut !== 0) continue;
|
|
@@ -30933,6 +31273,7 @@ function checkRules(graph, rules) {
|
|
|
30933
31273
|
var EDGE_KINDS;
|
|
30934
31274
|
var SEVERITIES;
|
|
30935
31275
|
var BUILTINS;
|
|
31276
|
+
var GATED_TIERS;
|
|
30936
31277
|
var ENTRYPOINT_STEMS;
|
|
30937
31278
|
var init_rules = __esm({
|
|
30938
31279
|
"src/rules.ts"() {
|
|
@@ -30941,7 +31282,8 @@ var init_rules = __esm({
|
|
|
30941
31282
|
init_sort();
|
|
30942
31283
|
EDGE_KINDS = /* @__PURE__ */ new Set(["contains", "doc-link", "import", "call", "use", "mention"]);
|
|
30943
31284
|
SEVERITIES = /* @__PURE__ */ new Set(["error", "warn"]);
|
|
30944
|
-
BUILTINS = /* @__PURE__ */ new Set(["cycles", "orphans"]);
|
|
31285
|
+
BUILTINS = /* @__PURE__ */ new Set(["cycles", "orphans", "literals"]);
|
|
31286
|
+
GATED_TIERS = ["competing", "bypassed"];
|
|
30945
31287
|
ENTRYPOINT_STEMS = /* @__PURE__ */ new Set([
|
|
30946
31288
|
"index",
|
|
30947
31289
|
"main",
|
|
@@ -31490,6 +31832,22 @@ var init_tools = __esm({
|
|
|
31490
31832
|
required: ["repo"]
|
|
31491
31833
|
}
|
|
31492
31834
|
},
|
|
31835
|
+
{
|
|
31836
|
+
name: "duplicated_literals",
|
|
31837
|
+
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'.",
|
|
31838
|
+
inputSchema: {
|
|
31839
|
+
type: "object",
|
|
31840
|
+
properties: {
|
|
31841
|
+
...repoProp,
|
|
31842
|
+
...scopeProps,
|
|
31843
|
+
minFiles: { type: "number", description: "Distinct files a value must span (default 2)" },
|
|
31844
|
+
minCount: { type: "number", description: "Total occurrences required (default 3)" },
|
|
31845
|
+
includeTests: { type: "boolean", description: "Count test files too (default false)" },
|
|
31846
|
+
limit: { type: "number", description: "Cap duplications (default: all)" }
|
|
31847
|
+
},
|
|
31848
|
+
required: ["repo"]
|
|
31849
|
+
}
|
|
31850
|
+
},
|
|
31493
31851
|
{
|
|
31494
31852
|
name: "complexity",
|
|
31495
31853
|
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.",
|
|
@@ -31699,6 +32057,14 @@ var init_tools = __esm({
|
|
|
31699
32057
|
properties: { ok: { type: "boolean" }, couplings: { type: "array", items: anyObj } },
|
|
31700
32058
|
required: ["ok", "couplings"]
|
|
31701
32059
|
},
|
|
32060
|
+
duplicated_literals: {
|
|
32061
|
+
type: "object",
|
|
32062
|
+
properties: {
|
|
32063
|
+
duplications: { type: "array", items: anyObj },
|
|
32064
|
+
families: { type: "array", items: anyObj }
|
|
32065
|
+
},
|
|
32066
|
+
required: ["duplications", "families"]
|
|
32067
|
+
},
|
|
31702
32068
|
embed_status: {
|
|
31703
32069
|
type: "object",
|
|
31704
32070
|
properties: {
|
|
@@ -31754,6 +32120,7 @@ var init_tools = __esm({
|
|
|
31754
32120
|
list_memories: { title: "List memories" },
|
|
31755
32121
|
delete_memory: { title: "Delete memory", write: true, destructive: true, idempotent: true },
|
|
31756
32122
|
dead_code: { title: "Dead-code candidates" },
|
|
32123
|
+
duplicated_literals: { title: "Values with no single source of truth" },
|
|
31757
32124
|
complexity: { title: "Complexity" },
|
|
31758
32125
|
mermaid: { title: "Mermaid module diagram" },
|
|
31759
32126
|
grep: { title: "Grep file contents" },
|
|
@@ -32034,6 +32401,26 @@ async function callTool(name2, args2, defaultRepo) {
|
|
|
32034
32401
|
if (limit === void 0 || all.length <= limit) return JSON.stringify(all, null, 2);
|
|
32035
32402
|
return JSON.stringify({ total: all.length, shown: limit, truncated: true, candidates: all.slice(0, limit) }, null, 2);
|
|
32036
32403
|
}
|
|
32404
|
+
if (name2 === "duplicated_literals") {
|
|
32405
|
+
const report = findLiteralDuplications(getScan(repo, scanOpts, walked), {
|
|
32406
|
+
minFiles: num(args2.minFiles),
|
|
32407
|
+
minCount: num(args2.minCount),
|
|
32408
|
+
includeTests: args2.includeTests === true
|
|
32409
|
+
});
|
|
32410
|
+
const limit = num(args2.limit);
|
|
32411
|
+
if (limit === void 0 || report.duplications.length <= limit) return JSON.stringify(report, null, 2);
|
|
32412
|
+
return JSON.stringify(
|
|
32413
|
+
{
|
|
32414
|
+
total: report.duplications.length,
|
|
32415
|
+
shown: limit,
|
|
32416
|
+
truncated: true,
|
|
32417
|
+
duplications: report.duplications.slice(0, limit),
|
|
32418
|
+
families: report.families
|
|
32419
|
+
},
|
|
32420
|
+
null,
|
|
32421
|
+
2
|
|
32422
|
+
);
|
|
32423
|
+
}
|
|
32037
32424
|
if (name2 === "complexity") {
|
|
32038
32425
|
const scan2 = getScan(repo, scanOpts, walked);
|
|
32039
32426
|
if (args2.risk === true) {
|
|
@@ -32271,6 +32658,7 @@ var init_mcp = __esm({
|
|
|
32271
32658
|
init_coupling();
|
|
32272
32659
|
init_repomap();
|
|
32273
32660
|
init_deadcode();
|
|
32661
|
+
init_literals2();
|
|
32274
32662
|
init_complexity();
|
|
32275
32663
|
init_viz();
|
|
32276
32664
|
init_query();
|
|
@@ -32587,7 +32975,16 @@ var CODE_EXTS = /* @__PURE__ */ new Set([
|
|
|
32587
32975
|
".sh",
|
|
32588
32976
|
".bash",
|
|
32589
32977
|
".zig",
|
|
32590
|
-
".elm"
|
|
32978
|
+
".elm",
|
|
32979
|
+
".hcl",
|
|
32980
|
+
".tf",
|
|
32981
|
+
".tfvars",
|
|
32982
|
+
".sol",
|
|
32983
|
+
".hh",
|
|
32984
|
+
".sc",
|
|
32985
|
+
".pyi",
|
|
32986
|
+
".rake",
|
|
32987
|
+
".cxx"
|
|
32591
32988
|
]);
|
|
32592
32989
|
var STYLE_EXTS = /* @__PURE__ */ new Set([".css", ".scss", ".sass", ".less", ".styl", ".pcss"]);
|
|
32593
32990
|
var DOC_EXTS = /* @__PURE__ */ new Set([".md", ".mdx", ".rst", ".adoc", ".txt"]);
|
|
@@ -33251,6 +33648,7 @@ init_rules();
|
|
|
33251
33648
|
init_coupling();
|
|
33252
33649
|
init_repomap();
|
|
33253
33650
|
init_deadcode();
|
|
33651
|
+
init_literals2();
|
|
33254
33652
|
init_complexity();
|
|
33255
33653
|
init_viz();
|
|
33256
33654
|
init_sort();
|
|
@@ -33654,6 +34052,7 @@ init_grep();
|
|
|
33654
34052
|
init_coupling();
|
|
33655
34053
|
init_repomap();
|
|
33656
34054
|
init_deadcode();
|
|
34055
|
+
init_literals2();
|
|
33657
34056
|
init_complexity();
|
|
33658
34057
|
init_viz();
|
|
33659
34058
|
init_bm25();
|
|
@@ -33706,12 +34105,21 @@ Commands:
|
|
|
33706
34105
|
asset into the shared cache (sha256-verified,
|
|
33707
34106
|
atomic). Override the source with
|
|
33708
34107
|
CODEINDEX_GRAMMARS_URL
|
|
33709
|
-
rules Architecture rules (forbidden edges, cycles, orphans)
|
|
33710
|
-
against the link-graph: --config <codeindex.rules.json>;
|
|
33711
|
-
on any error-severity violation (a CI gate)
|
|
34108
|
+
rules Architecture rules (forbidden edges, cycles, orphans, literals)
|
|
34109
|
+
validated against the link-graph: --config <codeindex.rules.json>;
|
|
34110
|
+
exits 1 on any error-severity violation (a CI gate)
|
|
33712
34111
|
repomap Token-budgeted map of the highest-PageRank files (--budget-tokens)
|
|
33713
34112
|
hotspots Churn \xD7 size ranking of the files where work concentrates (JSON)
|
|
33714
34113
|
coupling Change coupling: files that change together (JSON; --since <ref>)
|
|
34114
|
+
literals Values with no single source of truth: one literal written out
|
|
34115
|
+
across many files, in three labeled tiers \u2014 'competing' (two or
|
|
34116
|
+
more exported constants hold it), 'bypassed' (a constant holds
|
|
34117
|
+
it and other files rewrite it anyway), 'uncentralized' (nothing
|
|
34118
|
+
holds it). Groups path-like values into namespace families so a
|
|
34119
|
+
whole route space reports once, not forty times. Reads code AND
|
|
34120
|
+
config files (JSON/YAML/TOML), because the duplications that hurt
|
|
34121
|
+
are the ones crossing a language boundary no compiler checks.
|
|
34122
|
+
(--min-files, --min-count, --include-tests)
|
|
33715
34123
|
deadcode Dead-code candidates in two labeled tiers: 'unreferenced' (no
|
|
33716
34124
|
call site binds AND nothing references the name) and 'uncalled'
|
|
33717
34125
|
(referenced \u2014 re-export, type position \u2014 but never called)
|
|
@@ -33732,7 +34140,7 @@ Commands:
|
|
|
33732
34140
|
and exits 0, or exits 1 when it has no opinion (run the original).
|
|
33733
34141
|
Deliberately conservative \u2014 any shell metacharacter or unknown
|
|
33734
34142
|
flag refuses the rewrite
|
|
33735
|
-
mcp Run as an MCP server over stdio (
|
|
34143
|
+
mcp Run as an MCP server over stdio (30 tools: scan_summary, graph,
|
|
33736
34144
|
symbols, callers, workspaces, churn, symbols_overview,
|
|
33737
34145
|
find_symbol, find_references, repo_map, hotspots, coupling,
|
|
33738
34146
|
dead_code, complexity, mermaid, grep, search, embed_status,
|
|
@@ -33786,6 +34194,10 @@ Flags (accepted before OR after the subcommand: '--repo X scan' and
|
|
|
33786
34194
|
each site corroborated|unique-name
|
|
33787
34195
|
--ignore-case \`grep\`: case-insensitive matching
|
|
33788
34196
|
--max-hits <n> \`grep\`: cap returned hits (default 200)
|
|
34197
|
+
--min-files <n> \`literals\`: distinct files a value must span (default 2)
|
|
34198
|
+
--min-count <n> \`literals\`: total occurrences required (default 3)
|
|
34199
|
+
--include-tests \`literals\`: count test files too. Off by default \u2014 a test
|
|
34200
|
+
restating a value is usually asserting it deliberately
|
|
33789
34201
|
`;
|
|
33790
34202
|
function parseFlags(args2) {
|
|
33791
34203
|
const flags2 = { repo: process.cwd(), include: [], exclude: [], gitignore: true, ignoreDirs: [], noAst: false, fuzzy: true, semantic: false };
|
|
@@ -33818,6 +34230,9 @@ function parseFlags(args2) {
|
|
|
33818
34230
|
else if (a === "--ignore-case") flags2.ignoreCase = true;
|
|
33819
34231
|
else if (a === "--max-hits") flags2.maxHits = num22();
|
|
33820
34232
|
else if (a === "--budget-tokens") flags2.budgetTokens = num22();
|
|
34233
|
+
else if (a === "--min-files") flags2.minFiles = num22();
|
|
34234
|
+
else if (a === "--min-count") flags2.minCount = num22();
|
|
34235
|
+
else if (a === "--include-tests") flags2.includeTests = true;
|
|
33821
34236
|
else if (a === "--no-ast") flags2.noAst = true;
|
|
33822
34237
|
else if (a === "--index") flags2.indexDir = next();
|
|
33823
34238
|
else if (a === "--no-index-cache") flags2.noIndexCache = true;
|
|
@@ -33911,6 +34326,8 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
|
33911
34326
|
"--max-calls",
|
|
33912
34327
|
"--max-hits",
|
|
33913
34328
|
"--budget-tokens",
|
|
34329
|
+
"--min-files",
|
|
34330
|
+
"--min-count",
|
|
33914
34331
|
"--since",
|
|
33915
34332
|
"--config",
|
|
33916
34333
|
"--limit",
|
|
@@ -34342,6 +34759,13 @@ async function runCli(rawArgv) {
|
|
|
34342
34759
|
emit(JSON.stringify({ ok, couplings }, null, 2) + "\n", flags2.out);
|
|
34343
34760
|
} else if (cmd === "deadcode") {
|
|
34344
34761
|
emit(JSON.stringify(findDeadCode(readScan()), null, 2) + "\n", flags2.out);
|
|
34762
|
+
} else if (cmd === "literals") {
|
|
34763
|
+
const report = findLiteralDuplications(readScan(), {
|
|
34764
|
+
minFiles: flags2.minFiles,
|
|
34765
|
+
minCount: flags2.minCount,
|
|
34766
|
+
includeTests: flags2.includeTests
|
|
34767
|
+
});
|
|
34768
|
+
emit(JSON.stringify(report, null, 2) + "\n", flags2.out);
|
|
34345
34769
|
} else if (cmd === "complexity") {
|
|
34346
34770
|
const scan2 = readScan();
|
|
34347
34771
|
emit(JSON.stringify(symbolComplexity(scan2, flags2.positional), null, 2) + "\n", flags2.out);
|