debtlens 0.1.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +21 -0
  3. package/README.md +244 -0
  4. package/dist/cli/index.d.ts +2 -0
  5. package/dist/cli/index.js +153 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/init.d.ts +10 -0
  8. package/dist/cli/init.js +18 -0
  9. package/dist/cli/init.js.map +1 -0
  10. package/dist/cli/parseList.d.ts +2 -0
  11. package/dist/cli/parseList.js +29 -0
  12. package/dist/cli/parseList.js.map +1 -0
  13. package/dist/config/defaults.d.ts +2 -0
  14. package/dist/config/defaults.js +40 -0
  15. package/dist/config/defaults.js.map +1 -0
  16. package/dist/config/loadConfig.d.ts +2 -0
  17. package/dist/config/loadConfig.js +23 -0
  18. package/dist/config/loadConfig.js.map +1 -0
  19. package/dist/config/mergeConfig.d.ts +2 -0
  20. package/dist/config/mergeConfig.js +26 -0
  21. package/dist/config/mergeConfig.js.map +1 -0
  22. package/dist/config/schema.d.ts +7 -0
  23. package/dist/config/schema.js +63 -0
  24. package/dist/config/schema.js.map +1 -0
  25. package/dist/config/template.d.ts +10 -0
  26. package/dist/config/template.js +32 -0
  27. package/dist/config/template.js.map +1 -0
  28. package/dist/core/baseline.d.ts +23 -0
  29. package/dist/core/baseline.js +118 -0
  30. package/dist/core/baseline.js.map +1 -0
  31. package/dist/core/scan.d.ts +2 -0
  32. package/dist/core/scan.js +129 -0
  33. package/dist/core/scan.js.map +1 -0
  34. package/dist/core/severity.d.ts +7 -0
  35. package/dist/core/severity.js +26 -0
  36. package/dist/core/severity.js.map +1 -0
  37. package/dist/core/types.d.ts +96 -0
  38. package/dist/core/types.js +2 -0
  39. package/dist/core/types.js.map +1 -0
  40. package/dist/detectors/deadAbstraction.d.ts +2 -0
  41. package/dist/detectors/deadAbstraction.js +115 -0
  42. package/dist/detectors/deadAbstraction.js.map +1 -0
  43. package/dist/detectors/duplicateLogic.d.ts +2 -0
  44. package/dist/detectors/duplicateLogic.js +81 -0
  45. package/dist/detectors/duplicateLogic.js.map +1 -0
  46. package/dist/detectors/effectComplexity.d.ts +2 -0
  47. package/dist/detectors/effectComplexity.js +64 -0
  48. package/dist/detectors/effectComplexity.js.map +1 -0
  49. package/dist/detectors/index.d.ts +3 -0
  50. package/dist/detectors/index.js +20 -0
  51. package/dist/detectors/index.js.map +1 -0
  52. package/dist/detectors/largeComponent.d.ts +2 -0
  53. package/dist/detectors/largeComponent.js +46 -0
  54. package/dist/detectors/largeComponent.js.map +1 -0
  55. package/dist/detectors/namingDrift.d.ts +10 -0
  56. package/dist/detectors/namingDrift.js +82 -0
  57. package/dist/detectors/namingDrift.js.map +1 -0
  58. package/dist/detectors/propDrilling.d.ts +2 -0
  59. package/dist/detectors/propDrilling.js +97 -0
  60. package/dist/detectors/propDrilling.js.map +1 -0
  61. package/dist/detectors/stateSprawl.d.ts +2 -0
  62. package/dist/detectors/stateSprawl.js +47 -0
  63. package/dist/detectors/stateSprawl.js.map +1 -0
  64. package/dist/detectors/todoComment.d.ts +2 -0
  65. package/dist/detectors/todoComment.js +45 -0
  66. package/dist/detectors/todoComment.js.map +1 -0
  67. package/dist/reporters/index.d.ts +4 -0
  68. package/dist/reporters/index.js +14 -0
  69. package/dist/reporters/index.js.map +1 -0
  70. package/dist/reporters/jsonReporter.d.ts +2 -0
  71. package/dist/reporters/jsonReporter.js +4 -0
  72. package/dist/reporters/jsonReporter.js.map +1 -0
  73. package/dist/reporters/markdownReporter.d.ts +2 -0
  74. package/dist/reporters/markdownReporter.js +52 -0
  75. package/dist/reporters/markdownReporter.js.map +1 -0
  76. package/dist/reporters/sarifReporter.d.ts +7 -0
  77. package/dist/reporters/sarifReporter.js +77 -0
  78. package/dist/reporters/sarifReporter.js.map +1 -0
  79. package/dist/reporters/terminalReporter.d.ts +4 -0
  80. package/dist/reporters/terminalReporter.js +39 -0
  81. package/dist/reporters/terminalReporter.js.map +1 -0
  82. package/dist/utils/ast.d.ts +23 -0
  83. package/dist/utils/ast.js +132 -0
  84. package/dist/utils/ast.js.map +1 -0
  85. package/dist/utils/color.d.ts +8 -0
  86. package/dist/utils/color.js +27 -0
  87. package/dist/utils/color.js.map +1 -0
  88. package/dist/utils/createIssue.d.ts +13 -0
  89. package/dist/utils/createIssue.js +28 -0
  90. package/dist/utils/createIssue.js.map +1 -0
  91. package/dist/utils/git.d.ts +15 -0
  92. package/dist/utils/git.js +68 -0
  93. package/dist/utils/git.js.map +1 -0
  94. package/dist/utils/hostComponents.d.ts +12 -0
  95. package/dist/utils/hostComponents.js +57 -0
  96. package/dist/utils/hostComponents.js.map +1 -0
  97. package/dist/utils/identifiers.d.ts +3 -0
  98. package/dist/utils/identifiers.js +20 -0
  99. package/dist/utils/identifiers.js.map +1 -0
  100. package/dist/utils/lines.d.ts +8 -0
  101. package/dist/utils/lines.js +19 -0
  102. package/dist/utils/lines.js.map +1 -0
  103. package/dist/utils/similarity.d.ts +8 -0
  104. package/dist/utils/similarity.js +63 -0
  105. package/dist/utils/similarity.js.map +1 -0
  106. package/docs/architecture.md +68 -0
  107. package/docs/example-report.md +141 -0
  108. package/docs/good-first-issues.md +63 -0
  109. package/docs/rules.md +139 -0
  110. package/docs/showcase-expensify-app.md +119 -0
  111. package/package.json +60 -0
  112. package/schema/debtlens.config.schema.json +116 -0
@@ -0,0 +1,68 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ function git(cwd, args) {
4
+ return execFileSync("git", args, {
5
+ cwd,
6
+ encoding: "utf8",
7
+ stdio: ["ignore", "pipe", "ignore"],
8
+ }).trim();
9
+ }
10
+ function gitSafe(cwd, args) {
11
+ try {
12
+ return git(cwd, args);
13
+ }
14
+ catch {
15
+ return "";
16
+ }
17
+ }
18
+ export function isGitRepo(cwd) {
19
+ try {
20
+ return git(cwd, ["rev-parse", "--is-inside-work-tree"]) === "true";
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * Collect changed files as absolute paths. Returns `null` when `cwd` is not inside a
28
+ * git work tree, so the caller can degrade gracefully.
29
+ *
30
+ * - With `base`, includes the committed diff `base...HEAD` (merge-base — the PR's own
31
+ * changes). An unknown `base` throws a clear error.
32
+ * - Always unions in uncommitted working-tree changes vs HEAD and untracked files.
33
+ * - Deletions are excluded (`--diff-filter=d`) since deleted files cannot be scanned.
34
+ */
35
+ export function getChangedFiles(cwd, base) {
36
+ if (!isGitRepo(cwd))
37
+ return null;
38
+ let root;
39
+ try {
40
+ root = git(cwd, ["rev-parse", "--show-toplevel"]);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ const relative = new Set();
46
+ const add = (output) => {
47
+ for (const line of output.split("\n")) {
48
+ const trimmed = line.trim();
49
+ if (trimmed)
50
+ relative.add(trimmed);
51
+ }
52
+ };
53
+ if (base) {
54
+ try {
55
+ add(git(cwd, ["diff", "--name-only", "--diff-filter=d", `${base}...HEAD`]));
56
+ }
57
+ catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ throw new Error(`Could not diff against base ref "${base}": ${message}`);
60
+ }
61
+ }
62
+ // Uncommitted changes vs HEAD (staged + unstaged) and untracked files. Best-effort:
63
+ // a brand-new repo with no commits has no HEAD, which is fine.
64
+ add(gitSafe(cwd, ["diff", "--name-only", "--diff-filter=d", "HEAD"]));
65
+ add(gitSafe(cwd, ["ls-files", "--others", "--exclude-standard"]));
66
+ return { root, files: [...relative].map((path) => resolve(root, path)) };
67
+ }
68
+ //# sourceMappingURL=git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,SAAS,GAAG,CAAC,GAAW,EAAE,IAAc;IACtC,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;QAC/B,GAAG;QACH,QAAQ,EAAE,MAAM;QAChB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;KACpC,CAAC,CAAC,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,OAAO,CAAC,GAAW,EAAE,IAAc;IAC1C,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,OAAO,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC,CAAC,KAAK,MAAM,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAOD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,IAAa;IACxD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEjC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,EAAE;QAC7B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,OAAO;gBAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC;YACH,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,GAAG,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,IAAI,KAAK,CAAC,oCAAoC,IAAI,MAAM,OAAO,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,+DAA+D;IAC/D,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IACtE,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAElE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Built-in UI primitives that are PascalCase but are *not* user-defined child
3
+ * components. Forwarding props to these (e.g. `onPress` to a `Pressable`, `style` to a
4
+ * `View`) is ordinary composition, not prop drilling. Covers React Native core plus the
5
+ * common icon/gradient/blur libraries used across the RN/Expo ecosystem.
6
+ */
7
+ export declare const HOST_COMPONENTS: Set<string>;
8
+ /**
9
+ * Returns true if a JSX tag name refers to a host primitive (so it should be ignored
10
+ * as a "child component"). Handles namespaced tags like `Animated.View`.
11
+ */
12
+ export declare function isHostComponent(tagName: string): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Built-in UI primitives that are PascalCase but are *not* user-defined child
3
+ * components. Forwarding props to these (e.g. `onPress` to a `Pressable`, `style` to a
4
+ * `View`) is ordinary composition, not prop drilling. Covers React Native core plus the
5
+ * common icon/gradient/blur libraries used across the RN/Expo ecosystem.
6
+ */
7
+ export const HOST_COMPONENTS = new Set([
8
+ // React Native core
9
+ "View",
10
+ "Text",
11
+ "Image",
12
+ "ImageBackground",
13
+ "ScrollView",
14
+ "FlatList",
15
+ "SectionList",
16
+ "VirtualizedList",
17
+ "Pressable",
18
+ "TouchableOpacity",
19
+ "TouchableHighlight",
20
+ "TouchableWithoutFeedback",
21
+ "TouchableNativeFeedback",
22
+ "TextInput",
23
+ "Switch",
24
+ "Modal",
25
+ "ActivityIndicator",
26
+ "Button",
27
+ "RefreshControl",
28
+ "StatusBar",
29
+ "SafeAreaView",
30
+ "KeyboardAvoidingView",
31
+ "Animated",
32
+ // Common ecosystem libraries
33
+ "LinearGradient",
34
+ "BlurView",
35
+ "MaterialIcons",
36
+ "MaterialCommunityIcons",
37
+ "Ionicons",
38
+ "Feather",
39
+ "FontAwesome",
40
+ "FontAwesome5",
41
+ "AntDesign",
42
+ "Entypo",
43
+ "Octicons",
44
+ "SimpleLineIcons",
45
+ "Fontisto",
46
+ "Foundation",
47
+ "Zocial",
48
+ ]);
49
+ /**
50
+ * Returns true if a JSX tag name refers to a host primitive (so it should be ignored
51
+ * as a "child component"). Handles namespaced tags like `Animated.View`.
52
+ */
53
+ export function isHostComponent(tagName) {
54
+ const base = tagName.split(".").pop() ?? tagName;
55
+ return HOST_COMPONENTS.has(base) || HOST_COMPONENTS.has(tagName);
56
+ }
57
+ //# sourceMappingURL=hostComponents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hostComponents.js","sourceRoot":"","sources":["../../src/utils/hostComponents.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,GAAG,CAAS;IAC7C,oBAAoB;IACpB,MAAM;IACN,MAAM;IACN,OAAO;IACP,iBAAiB;IACjB,YAAY;IACZ,UAAU;IACV,aAAa;IACb,iBAAiB;IACjB,WAAW;IACX,kBAAkB;IAClB,oBAAoB;IACpB,0BAA0B;IAC1B,yBAAyB;IACzB,WAAW;IACX,QAAQ;IACR,OAAO;IACP,mBAAmB;IACnB,QAAQ;IACR,gBAAgB;IAChB,WAAW;IACX,cAAc;IACd,sBAAsB;IACtB,UAAU;IACV,6BAA6B;IAC7B,gBAAgB;IAChB,UAAU;IACV,eAAe;IACf,wBAAwB;IACxB,UAAU;IACV,SAAS;IACT,aAAa;IACb,cAAc;IACd,WAAW;IACX,QAAQ;IACR,UAAU;IACV,iBAAiB;IACjB,UAAU;IACV,YAAY;IACZ,QAAQ;CACT,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC;IACjD,OAAO,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function splitIdentifier(value: string): string[];
2
+ export declare function isPascalCase(value: string): boolean;
3
+ export declare function isHookName(value: string): boolean;
@@ -0,0 +1,20 @@
1
+ const stopWords = new Set([
2
+ "get", "set", "use", "is", "has", "with", "from", "to", "by", "for", "and", "or", "the", "a", "an",
3
+ "component", "props", "state", "data", "item", "items", "value", "values", "result", "results"
4
+ ]);
5
+ export function splitIdentifier(value) {
6
+ return value
7
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
8
+ .replace(/[_\-./]/g, " ")
9
+ .toLowerCase()
10
+ .split(/\s+/)
11
+ .map((part) => part.trim())
12
+ .filter((part) => part.length > 2 && !stopWords.has(part));
13
+ }
14
+ export function isPascalCase(value) {
15
+ return /^[A-Z][A-Za-z0-9]*$/.test(value);
16
+ }
17
+ export function isHookName(value) {
18
+ return /^use[A-Z0-9]/.test(value);
19
+ }
20
+ //# sourceMappingURL=identifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identifiers.js","sourceRoot":"","sources":["../../src/utils/identifiers.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI;IAClG,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS;CAC/F,CAAC,CAAC;AAEH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,KAAK;SACT,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,WAAW,EAAE;SACb,KAAK,CAAC,KAAK,CAAC;SACZ,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Node } from "ts-morph";
2
+ export declare function nodeLineSpan(node: Node): {
3
+ startLine: number;
4
+ endLine: number;
5
+ lines: number;
6
+ };
7
+ export declare function lineAt(content: string, lineNumber: number): string;
8
+ export declare function countLines(text: string): number;
@@ -0,0 +1,19 @@
1
+ export function nodeLineSpan(node) {
2
+ const source = node.getSourceFile();
3
+ const start = source.getLineAndColumnAtPos(node.getStart());
4
+ const end = source.getLineAndColumnAtPos(node.getEnd());
5
+ return {
6
+ startLine: start.line,
7
+ endLine: end.line,
8
+ lines: Math.max(1, end.line - start.line + 1),
9
+ };
10
+ }
11
+ export function lineAt(content, lineNumber) {
12
+ return content.split(/\r?\n/)[lineNumber - 1] ?? "";
13
+ }
14
+ export function countLines(text) {
15
+ if (!text)
16
+ return 0;
17
+ return text.split(/\r?\n/).length;
18
+ }
19
+ //# sourceMappingURL=lines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lines.js","sourceRoot":"","sources":["../../src/utils/lines.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5D,MAAM,GAAG,GAAG,MAAM,CAAC,qBAAqB,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,IAAI;QACrB,OAAO,EAAE,GAAG,CAAC,IAAI;QACjB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,OAAe,EAAE,UAAkB;IACxD,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;AACpC,CAAC"}
@@ -0,0 +1,8 @@
1
+ export declare function normalizeSnippet(source: string): string;
2
+ export declare function shingle(value: string, size?: number): Set<string>;
3
+ export declare function jaccard(a: Set<string>, b: Set<string>): number;
4
+ /**
5
+ * Cosine similarity between two token-count vectors (multisets). Used to compare the
6
+ * structural fingerprints of two functions before the more expensive text-shingle step.
7
+ */
8
+ export declare function cosineSimilarity(a: Map<string, number>, b: Map<string, number>): number;
@@ -0,0 +1,63 @@
1
+ export function normalizeSnippet(source) {
2
+ return source
3
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
4
+ .replace(/\/\/.*$/gm, " ")
5
+ .replace(/(['"`])(?:\\.|(?!\1).)*\1/g, "__str__")
6
+ .replace(/\b\d+(?:\.\d+)?\b/g, "__num__")
7
+ .replace(/\b[A-Za-z_$][\w$]*\b/g, (token) => keywordSet.has(token) ? token : "__id__")
8
+ .replace(/\s+/g, " ")
9
+ .trim();
10
+ }
11
+ export function shingle(value, size = 5) {
12
+ const tokens = value.split(/\s+/).filter(Boolean);
13
+ const shingles = new Set();
14
+ for (let i = 0; i <= tokens.length - size; i += 1) {
15
+ shingles.add(tokens.slice(i, i + size).join(" "));
16
+ }
17
+ if (shingles.size === 0 && tokens.length > 0) {
18
+ shingles.add(tokens.join(" "));
19
+ }
20
+ return shingles;
21
+ }
22
+ export function jaccard(a, b) {
23
+ if (a.size === 0 && b.size === 0)
24
+ return 1;
25
+ let intersection = 0;
26
+ for (const item of a) {
27
+ if (b.has(item))
28
+ intersection += 1;
29
+ }
30
+ const union = a.size + b.size - intersection;
31
+ return union === 0 ? 0 : intersection / union;
32
+ }
33
+ /**
34
+ * Cosine similarity between two token-count vectors (multisets). Used to compare the
35
+ * structural fingerprints of two functions before the more expensive text-shingle step.
36
+ */
37
+ export function cosineSimilarity(a, b) {
38
+ if (a.size === 0 && b.size === 0)
39
+ return 1;
40
+ if (a.size === 0 || b.size === 0)
41
+ return 0;
42
+ let dot = 0;
43
+ let normA = 0;
44
+ let normB = 0;
45
+ for (const [key, value] of a) {
46
+ normA += value * value;
47
+ const other = b.get(key);
48
+ if (other)
49
+ dot += value * other;
50
+ }
51
+ for (const value of b.values()) {
52
+ normB += value * value;
53
+ }
54
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
55
+ return denominator === 0 ? 0 : dot / denominator;
56
+ }
57
+ const keywordSet = new Set([
58
+ "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "return", "const", "let", "var",
59
+ "function", "class", "extends", "implements", "interface", "type", "import", "export", "default", "from", "async",
60
+ "await", "try", "catch", "finally", "throw", "new", "this", "super", "true", "false", "null", "undefined",
61
+ "useState", "useEffect", "useMemo", "useCallback", "useReducer", "useRef"
62
+ ]);
63
+ //# sourceMappingURL=similarity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"similarity.js","sourceRoot":"","sources":["../../src/utils/similarity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ # DebtLens Architecture
2
+
3
+ DebtLens has four layers:
4
+
5
+ 1. CLI input parsing
6
+ 2. scan orchestration
7
+ 3. detectors
8
+ 4. reporters
9
+
10
+ ## CLI
11
+
12
+ `src/cli/index.ts` uses Commander to parse command-line options. It loads JSON config, merges CLI overrides, runs the scanner, renders the selected report format, and handles `--fail-on` exit behavior.
13
+
14
+ ## Scanner
15
+
16
+ `src/core/scan.ts` resolves files with `fast-glob`, creates a `ts-morph` project, loads source files, runs selected detectors, filters by minimum severity, and returns a stable `ScanResult` object.
17
+
18
+ The scanner does not execute project code.
19
+
20
+ ## Detectors
21
+
22
+ A detector implements:
23
+
24
+ ```ts
25
+ export interface Detector {
26
+ id: string;
27
+ name: string;
28
+ description: string;
29
+ defaultSeverity: Severity;
30
+ tags: string[];
31
+ detect: (context: DetectorContext) => Promise<DebtIssue[]> | DebtIssue[];
32
+ }
33
+ ```
34
+
35
+ Each detector receives parsed `ts-morph` source files plus threshold helpers. The rule should return reviewable issues with evidence and a suggestion.
36
+
37
+ Current detectors live in `src/detectors`.
38
+
39
+ ## Reporters
40
+
41
+ Reporters convert `ScanResult` to terminal text, JSON, or Markdown.
42
+
43
+ Future reporters should use the same `ScanResult` object so downstream integrations can remain stable.
44
+
45
+ Planned reporters:
46
+
47
+ - SARIF
48
+ - GitHub PR comment
49
+ - compact CI summary
50
+
51
+ ## Why JSON config only?
52
+
53
+ DebtLens intentionally starts with JSON config rather than JavaScript config. Static-analysis tools should avoid executing arbitrary project code by default. A plugin API can come later with clear security boundaries.
54
+
55
+ ## Future plugin model
56
+
57
+ Potential design:
58
+
59
+ ```ts
60
+ export default defineDebtLensPlugin({
61
+ rules: [myDetector],
62
+ vocabulary: {
63
+ "commerce-entity": ["product", "sku", "item", "listing"]
64
+ }
65
+ });
66
+ ```
67
+
68
+ Plugin loading should be explicit and disabled in untrusted CI contexts.
@@ -0,0 +1,141 @@
1
+ # DebtLens Report
2
+
3
+ Scanned **3** files with **8** rules in **174ms**.
4
+
5
+ ## Summary
6
+
7
+ - Total issues: **10**
8
+ - High: **2**
9
+ - Medium: **2**
10
+ - Low: **4**
11
+ - Info: **2**
12
+
13
+ ## High severity
14
+
15
+ ### Prop drilling — `src/Dashboard.tsx:13`
16
+
17
+ Dashboard forwards 7 props across 3 child components.
18
+
19
+ Confidence: **73%**
20
+
21
+ Evidence:
22
+ - ReleaseHero: movie, userId, region, theme, onSelect, onSave
23
+ - ReleaseGrid: movie, userId, region, theme, onSelect, onSave, onShare
24
+ - ReleaseFooter: movie, userId, region, theme, onShare
25
+
26
+ Suggestion: Consider colocating the data owner closer to consumers, using a composition slot, or extracting a focused context for stable cross-cutting values.
27
+
28
+ ### Duplicate logic — `src/duplicateOne.ts:1`
29
+
30
+ normalizeMovieRelease is 100% structurally similar to normalizeGameRelease.
31
+
32
+ Confidence: **100%**
33
+
34
+ Evidence:
35
+ - src/duplicateOne.ts:1-18 (18 lines)
36
+ - src/duplicateTwo.ts:1-18 (18 lines)
37
+
38
+ Suggestion: Compare the two implementations. Extract shared behavior only if the variation is intentional and stable; otherwise delete the weaker duplicate.
39
+
40
+
41
+ ## Medium severity
42
+
43
+ ### State sprawl — `src/Dashboard.tsx:13`
44
+
45
+ Dashboard manages 7 stateful hook calls. This often means one component is coordinating several unrelated workflows.
46
+
47
+ Confidence: **82%**
48
+
49
+ Evidence:
50
+ - Stateful hooks: useState, useState, useState, useState, useState, useState, useState
51
+
52
+ Suggestion: Group related state in a reducer, extract state machines into a hook, or move server/cache state out of component state.
53
+
54
+ ### Effect complexity — `src/Dashboard.tsx:23`
55
+
56
+ This useEffect spans 26 lines, has 9 dependencies, 3 branches, and 14 nested calls.
57
+
58
+ Confidence: **80%**
59
+
60
+ Evidence:
61
+ - Lines: 26 / 30
62
+ - Dependencies: 9 / 8
63
+ - Branches: 3
64
+ - Contains async work
65
+
66
+ Suggestion: Split unrelated effects, move imperative workflows into named functions, or replace derived state effects with memoized values.
67
+
68
+
69
+ ## Low severity
70
+
71
+ ### Debt marker comment — `src/Dashboard.tsx:22`
72
+
73
+ Comment contains a todo marker.
74
+
75
+ Confidence: **90%**
76
+
77
+ Evidence:
78
+ - // TODO: split this when the launch rush is over.
79
+
80
+ Suggestion: Convert the marker into a tracked issue, add a removal condition, or fix it before more code depends on it.
81
+
82
+ ### Dead abstraction — `src/Dashboard.tsx:67`
83
+
84
+ ReleaseHero looks like a thin wrapper: it only forwards to a single JSX element.
85
+
86
+ Confidence: **68%**
87
+
88
+ Evidence:
89
+ - { return <section>{props.movie.title}</section>; }
90
+
91
+ Suggestion: Keep the wrapper only if it creates a stable domain boundary. Otherwise inline it or add the missing behavior where this abstraction belongs.
92
+
93
+ ### Dead abstraction — `src/Dashboard.tsx:71`
94
+
95
+ ReleaseGrid looks like a thin wrapper: it only forwards to a single JSX element.
96
+
97
+ Confidence: **68%**
98
+
99
+ Evidence:
100
+ - { return <section>{props.movie.releaseDate}</section>; }
101
+
102
+ Suggestion: Keep the wrapper only if it creates a stable domain boundary. Otherwise inline it or add the missing behavior where this abstraction belongs.
103
+
104
+ ### Dead abstraction — `src/Dashboard.tsx:75`
105
+
106
+ ReleaseFooter looks like a thin wrapper: it only forwards to a single JSX element.
107
+
108
+ Confidence: **68%**
109
+
110
+ Evidence:
111
+ - { return <footer>{props.region}</footer>; }
112
+
113
+ Suggestion: Keep the wrapper only if it creates a stable domain boundary. Otherwise inline it or add the missing behavior where this abstraction belongs.
114
+
115
+
116
+ ## Info severity
117
+
118
+ ### Naming drift — `src/duplicateOne.ts:1`
119
+
120
+ This file uses 4 competing terms for release timing: date, air, launch, release.
121
+
122
+ Confidence: **62%**
123
+
124
+ Evidence:
125
+ - Concept group: time-of-release
126
+ - Variants found: date, air, launch, release
127
+
128
+ Suggestion: Pick a canonical domain name and rename adapters at system boundaries instead of mixing boundary names throughout the feature.
129
+
130
+ ### Naming drift — `src/duplicateTwo.ts:1`
131
+
132
+ This file uses 4 competing terms for release timing: date, air, launch, release.
133
+
134
+ Confidence: **62%**
135
+
136
+ Evidence:
137
+ - Concept group: time-of-release
138
+ - Variants found: date, air, launch, release
139
+
140
+ Suggestion: Pick a canonical domain name and rename adapters at system boundaries instead of mixing boundary names throughout the feature.
141
+
@@ -0,0 +1,63 @@
1
+ # Good first issues
2
+
3
+ Scoped, self-contained tasks for new contributors. Each lists where to start and how to
4
+ verify. Open these as GitHub issues (label `good-first-rule` / `good-first-issue`) once
5
+ the repo is public. See [`CONTRIBUTING.md`](../CONTRIBUTING.md) for setup.
6
+
7
+ ## Rules
8
+
9
+ ### 1. Make the `prop-drilling` host-component list configurable
10
+ The ignore list of UI primitives lives in [`src/utils/hostComponents.ts`](../src/utils/hostComponents.ts).
11
+ Add an optional config key (e.g. `propDrilling.ignoreComponents`) that extends it, so
12
+ teams can register their own design-system primitives.
13
+ - Touch: `src/core/types.ts`, `src/config/*`, `src/detectors/propDrilling.ts`, schema.
14
+ - Verify: a test where a custom-ignored component is not counted.
15
+
16
+ ### 2. Teach `large-component` to recognize more component forms
17
+ Today it only classifies PascalCase function/arrow components
18
+ ([`src/utils/ast.ts`](../src/utils/ast.ts) `collectFunctionLikes`). It misses
19
+ `memo(function X(){})`, `forwardRef(...)`, and class components.
20
+ - Verify: fixtures for each form in `tests/detectors/largeComponent.test.ts`.
21
+
22
+ ### 3. Reduce `naming-drift` false positives on domain-rich apps
23
+ The built-in media vocabulary treats distinct domain entities (e.g. `movie` vs `show`)
24
+ as "competing names." Options: raise the default `minVariants`, add a config switch to
25
+ disable the built-in pack, or require co-occurrence in the same identifier.
26
+ - Touch: [`src/detectors/namingDrift.ts`](../src/detectors/namingDrift.ts).
27
+ - Verify: a media-style fixture that should NOT fire by default.
28
+
29
+ ### 4. Configurable markers for `todo-comment`
30
+ Allow projects to add/replace the marker patterns
31
+ ([`src/detectors/todoComment.ts`](../src/detectors/todoComment.ts)) via config.
32
+ - Verify: a custom marker fires; a removed default does not.
33
+
34
+ ## Reporters & integrations
35
+
36
+ ### 5. Add `helpUri` to SARIF rules
37
+ In [`src/reporters/sarifReporter.ts`](../src/reporters/sarifReporter.ts), point each
38
+ rule's `helpUri` at its section in `docs/rules.md` so code-scanning links to docs.
39
+ - Verify: extend `tests/reporters/sarifReporter.test.ts`.
40
+
41
+ ### 6. Snapshot test for the Markdown reporter
42
+ Add a fixture-based test that scans `examples/react` and asserts the Markdown matches a
43
+ committed snapshot (normalizing the elapsed-ms line), guarding `docs/example-report.md`.
44
+
45
+ ### 7. Publish the config JSON schema to a stable URL
46
+ The schema is generated to `schema/debtlens.config.schema.json`
47
+ ([`src/config/schema.ts`](../src/config/schema.ts)). Wire up hosting (e.g. GitHub Pages
48
+ or SchemaStore) and confirm the `$schema` URL in the `init` template resolves.
49
+
50
+ ## CLI / DX
51
+
52
+ ### 8. Add a summary-only / `--quiet` output mode
53
+ Print just the counts line and exit code, useful for CI logs.
54
+ - Touch: `src/cli/index.ts`, `src/reporters/terminalReporter.ts`.
55
+
56
+ ### 9. Respect `.gitignore` when resolving files
57
+ Optionally skip files ignored by git during a scan, in addition to the configured
58
+ `exclude` globs.
59
+ - Touch: `src/core/scan.ts` (file resolution).
60
+
61
+ ### 10. Document each rule's false-positive guidance
62
+ Expand [`docs/rules.md`](./rules.md) with a "When this is a false positive" note per
63
+ rule, mirroring the guards in the detector tests.