@promptci/core 0.0.1

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 (179) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +30 -0
  3. package/dist/agent-practices.d.ts +22 -0
  4. package/dist/agent-practices.d.ts.map +1 -0
  5. package/dist/agent-practices.js +582 -0
  6. package/dist/agent-practices.js.map +1 -0
  7. package/dist/baseline.d.ts +40 -0
  8. package/dist/baseline.d.ts.map +1 -0
  9. package/dist/baseline.js +172 -0
  10. package/dist/baseline.js.map +1 -0
  11. package/dist/canonical-owner.d.ts +3 -0
  12. package/dist/canonical-owner.d.ts.map +1 -0
  13. package/dist/canonical-owner.js +131 -0
  14. package/dist/canonical-owner.js.map +1 -0
  15. package/dist/cheap-model-routing.d.ts +4 -0
  16. package/dist/cheap-model-routing.d.ts.map +1 -0
  17. package/dist/cheap-model-routing.js +43 -0
  18. package/dist/cheap-model-routing.js.map +1 -0
  19. package/dist/ci-alignment.d.ts +10 -0
  20. package/dist/ci-alignment.d.ts.map +1 -0
  21. package/dist/ci-alignment.js +343 -0
  22. package/dist/ci-alignment.js.map +1 -0
  23. package/dist/cli-output-filter.d.ts +16 -0
  24. package/dist/cli-output-filter.d.ts.map +1 -0
  25. package/dist/cli-output-filter.js +88 -0
  26. package/dist/cli-output-filter.js.map +1 -0
  27. package/dist/command-validity.d.ts +12 -0
  28. package/dist/command-validity.d.ts.map +1 -0
  29. package/dist/command-validity.js +543 -0
  30. package/dist/command-validity.js.map +1 -0
  31. package/dist/conflicts.d.ts +49 -0
  32. package/dist/conflicts.d.ts.map +1 -0
  33. package/dist/conflicts.js +1065 -0
  34. package/dist/conflicts.js.map +1 -0
  35. package/dist/context-analysis.d.ts +15 -0
  36. package/dist/context-analysis.d.ts.map +1 -0
  37. package/dist/context-analysis.js +24 -0
  38. package/dist/context-analysis.js.map +1 -0
  39. package/dist/context-bloat.d.ts +17 -0
  40. package/dist/context-bloat.d.ts.map +1 -0
  41. package/dist/context-bloat.js +148 -0
  42. package/dist/context-bloat.js.map +1 -0
  43. package/dist/context-cost.d.ts +4 -0
  44. package/dist/context-cost.d.ts.map +1 -0
  45. package/dist/context-cost.js +70 -0
  46. package/dist/context-cost.js.map +1 -0
  47. package/dist/context-optimizer.d.ts +14 -0
  48. package/dist/context-optimizer.d.ts.map +1 -0
  49. package/dist/context-optimizer.js +219 -0
  50. package/dist/context-optimizer.js.map +1 -0
  51. package/dist/context-recommender.d.ts +3 -0
  52. package/dist/context-recommender.d.ts.map +1 -0
  53. package/dist/context-recommender.js +329 -0
  54. package/dist/context-recommender.js.map +1 -0
  55. package/dist/dead-references.d.ts +33 -0
  56. package/dist/dead-references.d.ts.map +1 -0
  57. package/dist/dead-references.js +607 -0
  58. package/dist/dead-references.js.map +1 -0
  59. package/dist/detectors.d.ts +11 -0
  60. package/dist/detectors.d.ts.map +1 -0
  61. package/dist/detectors.js +160 -0
  62. package/dist/detectors.js.map +1 -0
  63. package/dist/duplicates.d.ts +36 -0
  64. package/dist/duplicates.d.ts.map +1 -0
  65. package/dist/duplicates.js +436 -0
  66. package/dist/duplicates.js.map +1 -0
  67. package/dist/fix-engine.d.ts +16 -0
  68. package/dist/fix-engine.d.ts.map +1 -0
  69. package/dist/fix-engine.js +179 -0
  70. package/dist/fix-engine.js.map +1 -0
  71. package/dist/framework-packs.d.ts +8 -0
  72. package/dist/framework-packs.d.ts.map +1 -0
  73. package/dist/framework-packs.js +22 -0
  74. package/dist/framework-packs.js.map +1 -0
  75. package/dist/frameworks/dotnet.d.ts +4 -0
  76. package/dist/frameworks/dotnet.d.ts.map +1 -0
  77. package/dist/frameworks/dotnet.js +77 -0
  78. package/dist/frameworks/dotnet.js.map +1 -0
  79. package/dist/frameworks/go.d.ts +4 -0
  80. package/dist/frameworks/go.d.ts.map +1 -0
  81. package/dist/frameworks/go.js +39 -0
  82. package/dist/frameworks/go.js.map +1 -0
  83. package/dist/frameworks/nextjs.d.ts +4 -0
  84. package/dist/frameworks/nextjs.d.ts.map +1 -0
  85. package/dist/frameworks/nextjs.js +66 -0
  86. package/dist/frameworks/nextjs.js.map +1 -0
  87. package/dist/frameworks/python.d.ts +4 -0
  88. package/dist/frameworks/python.d.ts.map +1 -0
  89. package/dist/frameworks/python.js +66 -0
  90. package/dist/frameworks/python.js.map +1 -0
  91. package/dist/frameworks/rust.d.ts +4 -0
  92. package/dist/frameworks/rust.d.ts.map +1 -0
  93. package/dist/frameworks/rust.js +72 -0
  94. package/dist/frameworks/rust.js.map +1 -0
  95. package/dist/frameworks/unity.d.ts +4 -0
  96. package/dist/frameworks/unity.d.ts.map +1 -0
  97. package/dist/frameworks/unity.js +74 -0
  98. package/dist/frameworks/unity.js.map +1 -0
  99. package/dist/health-score.d.ts +37 -0
  100. package/dist/health-score.d.ts.map +1 -0
  101. package/dist/health-score.js +164 -0
  102. package/dist/health-score.js.map +1 -0
  103. package/dist/index.d.ts +31 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +23 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/llm.d.ts +6 -0
  108. package/dist/llm.d.ts.map +1 -0
  109. package/dist/llm.js +67 -0
  110. package/dist/llm.js.map +1 -0
  111. package/dist/manifest-consistency.d.ts +37 -0
  112. package/dist/manifest-consistency.d.ts.map +1 -0
  113. package/dist/manifest-consistency.js +659 -0
  114. package/dist/manifest-consistency.js.map +1 -0
  115. package/dist/mcp-detector.d.ts +4 -0
  116. package/dist/mcp-detector.d.ts.map +1 -0
  117. package/dist/mcp-detector.js +131 -0
  118. package/dist/mcp-detector.js.map +1 -0
  119. package/dist/missing-context.d.ts +15 -0
  120. package/dist/missing-context.d.ts.map +1 -0
  121. package/dist/missing-context.js +517 -0
  122. package/dist/missing-context.js.map +1 -0
  123. package/dist/native-tool-preference.d.ts +20 -0
  124. package/dist/native-tool-preference.d.ts.map +1 -0
  125. package/dist/native-tool-preference.js +94 -0
  126. package/dist/native-tool-preference.js.map +1 -0
  127. package/dist/output-verbosity.d.ts +3 -0
  128. package/dist/output-verbosity.d.ts.map +1 -0
  129. package/dist/output-verbosity.js +83 -0
  130. package/dist/output-verbosity.js.map +1 -0
  131. package/dist/product-boundary.d.ts +11 -0
  132. package/dist/product-boundary.d.ts.map +1 -0
  133. package/dist/product-boundary.js +301 -0
  134. package/dist/product-boundary.js.map +1 -0
  135. package/dist/project-type.d.ts +15 -0
  136. package/dist/project-type.d.ts.map +1 -0
  137. package/dist/project-type.js +116 -0
  138. package/dist/project-type.js.map +1 -0
  139. package/dist/prompt-cache.d.ts +3 -0
  140. package/dist/prompt-cache.d.ts.map +1 -0
  141. package/dist/prompt-cache.js +151 -0
  142. package/dist/prompt-cache.js.map +1 -0
  143. package/dist/repo-context.d.ts +37 -0
  144. package/dist/repo-context.d.ts.map +1 -0
  145. package/dist/repo-context.js +221 -0
  146. package/dist/repo-context.js.map +1 -0
  147. package/dist/report.d.ts +87 -0
  148. package/dist/report.d.ts.map +1 -0
  149. package/dist/report.js +558 -0
  150. package/dist/report.js.map +1 -0
  151. package/dist/scan.d.ts +7 -0
  152. package/dist/scan.d.ts.map +1 -0
  153. package/dist/scan.js +43 -0
  154. package/dist/scan.js.map +1 -0
  155. package/dist/scanner.d.ts +3 -0
  156. package/dist/scanner.d.ts.map +1 -0
  157. package/dist/scanner.js +216 -0
  158. package/dist/scanner.js.map +1 -0
  159. package/dist/security-pack.d.ts +4 -0
  160. package/dist/security-pack.d.ts.map +1 -0
  161. package/dist/security-pack.js +403 -0
  162. package/dist/security-pack.js.map +1 -0
  163. package/dist/stale-instructions.d.ts +20 -0
  164. package/dist/stale-instructions.d.ts.map +1 -0
  165. package/dist/stale-instructions.js +269 -0
  166. package/dist/stale-instructions.js.map +1 -0
  167. package/dist/suppression.d.ts +64 -0
  168. package/dist/suppression.d.ts.map +1 -0
  169. package/dist/suppression.js +338 -0
  170. package/dist/suppression.js.map +1 -0
  171. package/dist/types.d.ts +128 -0
  172. package/dist/types.d.ts.map +1 -0
  173. package/dist/types.js +3 -0
  174. package/dist/types.js.map +1 -0
  175. package/dist/vague-guidance.d.ts +12 -0
  176. package/dist/vague-guidance.d.ts.map +1 -0
  177. package/dist/vague-guidance.js +235 -0
  178. package/dist/vague-guidance.js.map +1 -0
  179. package/package.json +54 -0
@@ -0,0 +1,160 @@
1
+ import { detectAgentPractices } from './agent-practices.js';
2
+ import { detectCanonicalOwner } from './canonical-owner.js';
3
+ import { detectCiAlignment } from './ci-alignment.js';
4
+ import { detectProductBoundary } from './product-boundary.js';
5
+ import { detectCompetingTechConflicts, detectConflicts, detectVersionConflicts } from './conflicts.js';
6
+ import { detectCommandValidity } from './command-validity.js';
7
+ import { detectContextBloat } from './context-bloat.js';
8
+ import { detectContextCost } from './context-cost.js';
9
+ import { detectDeadReferences } from './dead-references.js';
10
+ import { detectDuplicates, detectDuplicateHeadings } from './duplicates.js';
11
+ import { detectManifestConsistency } from './manifest-consistency.js';
12
+ import { detectMissingContext } from './missing-context.js';
13
+ import { detectStaleInstructions } from './stale-instructions.js';
14
+ import { detectVagueGuidance } from './vague-guidance.js';
15
+ import { detectSecurityPack } from './security-pack.js';
16
+ import { runFrameworkPacks } from './framework-packs.js';
17
+ import { detectPromptCacheFriendliness } from './prompt-cache.js';
18
+ import { detectCheapModelRouting } from './cheap-model-routing.js';
19
+ import { detectOutputVerbosity } from './output-verbosity.js';
20
+ import { detectMcpTooling } from './mcp-detector.js';
21
+ import { detectCliOutputFilter } from './cli-output-filter.js';
22
+ import { detectNativeToolPreference } from './native-tool-preference.js';
23
+ export const DETECTORS = [
24
+ {
25
+ id: 'duplicates',
26
+ group: 'scan',
27
+ run: (context) => detectDuplicates(context.files),
28
+ },
29
+ {
30
+ id: 'duplicate-headings',
31
+ group: 'scan',
32
+ run: (context) => detectDuplicateHeadings(context.files),
33
+ },
34
+ {
35
+ id: 'conflicts',
36
+ group: 'scan',
37
+ run: (context) => detectConflicts(context.files),
38
+ },
39
+ {
40
+ id: 'version-conflicts',
41
+ group: 'scan',
42
+ run: (context) => detectVersionConflicts(context.files),
43
+ },
44
+ {
45
+ id: 'competing-tech-conflicts',
46
+ group: 'scan',
47
+ run: (context) => detectCompetingTechConflicts(context.files),
48
+ },
49
+ {
50
+ id: 'context-bloat',
51
+ group: 'scan',
52
+ run: (context) => detectContextBloat(context.files, {
53
+ totalWarning: context.contextBudget,
54
+ totalHigh: context.contextBudget ? context.contextBudget * 2 : undefined,
55
+ fileWarning: context.fileContextBudget,
56
+ fileHigh: context.fileContextBudget ? context.fileContextBudget * 2.5 : undefined,
57
+ }),
58
+ },
59
+ {
60
+ id: 'stale-instructions',
61
+ group: 'scan',
62
+ run: (context) => detectStaleInstructions(context.files),
63
+ },
64
+ {
65
+ id: 'command-validity',
66
+ group: 'scan',
67
+ run: (context) => detectCommandValidity(context),
68
+ },
69
+ {
70
+ id: 'missing-context',
71
+ group: 'scan',
72
+ run: (context) => detectMissingContext(context.files, context.projectType),
73
+ },
74
+ {
75
+ id: 'manifest-consistency',
76
+ group: 'scan',
77
+ run: (context) => detectManifestConsistency(context),
78
+ },
79
+ {
80
+ id: 'vague-guidance',
81
+ group: 'scan',
82
+ run: (context) => detectVagueGuidance(context.files),
83
+ },
84
+ {
85
+ id: 'dead-references',
86
+ group: 'scan',
87
+ run: (context) => detectDeadReferences(context.files, context.repoRoot),
88
+ },
89
+ {
90
+ id: 'agent-practices',
91
+ group: 'scan',
92
+ run: (context) => detectAgentPractices(context.files),
93
+ },
94
+ {
95
+ id: 'canonical-owner',
96
+ group: 'scan',
97
+ run: (context) => detectCanonicalOwner(context.files),
98
+ },
99
+ {
100
+ id: 'ci-alignment',
101
+ group: 'scan',
102
+ run: (context) => detectCiAlignment(context),
103
+ },
104
+ {
105
+ id: 'product-boundary',
106
+ group: 'scan',
107
+ run: (context) => detectProductBoundary(context),
108
+ },
109
+ {
110
+ id: 'security-pack',
111
+ group: 'scan',
112
+ run: (context) => detectSecurityPack(context),
113
+ },
114
+ {
115
+ id: 'framework-packs',
116
+ group: 'scan',
117
+ run: (context) => runFrameworkPacks(context),
118
+ },
119
+ {
120
+ id: 'prompt-cache-friendliness',
121
+ group: 'scan',
122
+ run: (context) => detectPromptCacheFriendliness(context.files),
123
+ },
124
+ {
125
+ id: 'cheap-model-routing',
126
+ group: 'scan',
127
+ run: (context) => detectCheapModelRouting(context),
128
+ },
129
+ {
130
+ id: 'output-verbosity',
131
+ group: 'scan',
132
+ run: (context) => detectOutputVerbosity(context.files),
133
+ },
134
+ {
135
+ id: 'mcp-tooling',
136
+ group: 'scan',
137
+ run: (context) => detectMcpTooling(context),
138
+ },
139
+ {
140
+ id: 'cli-output-filter',
141
+ group: 'scan',
142
+ run: (context) => detectCliOutputFilter(context),
143
+ },
144
+ {
145
+ id: 'native-tool-preference',
146
+ group: 'scan',
147
+ run: (context) => detectNativeToolPreference(context.files),
148
+ },
149
+ {
150
+ id: 'context-cost',
151
+ group: 'context',
152
+ run: detectContextCost,
153
+ },
154
+ ];
155
+ export function runDetectors(context, group) {
156
+ return DETECTORS
157
+ .filter((detector) => detector.group === group)
158
+ .flatMap((detector) => detector.run(context));
159
+ }
160
+ //# sourceMappingURL=detectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectors.js","sourceRoot":"","sources":["../src/detectors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Duplicate section detector for PromptCI.
3
+ *
4
+ * Compares sections across all instruction files and flags groups that are
5
+ * exact matches or near-duplicates (Jaccard ≥ 0.8).
6
+ *
7
+ * When the same section appears in N files, ONE issue is emitted listing
8
+ * all N files — not N*(N-1)/2 pairwise issues.
9
+ *
10
+ * Wording is intentionally cautious — all findings are heuristic.
11
+ */
12
+ import type { InstructionFile, PromptCiIssue } from './types.js';
13
+ /** Normalise text for comparison: lowercase, collapse whitespace, strip markdown syntax. */
14
+ export declare function normalizeSection(text: string): string;
15
+ /**
16
+ * Detect duplicate or near-duplicate sections across all instruction files.
17
+ *
18
+ * Groups all N-way duplicates into a single issue instead of N*(N-1)/2 pairs.
19
+ *
20
+ * - Exact normalised match → severity "high", confidence 0.95
21
+ * - Jaccard ≥ 0.80 → severity "warning", confidence 0.75
22
+ */
23
+ export declare function detectDuplicates(files: InstructionFile[]): PromptCiIssue[];
24
+ /**
25
+ * Detect when the same section heading appears more than once in a single
26
+ * instruction file. This is almost always a structural mistake — accidental
27
+ * copy/paste, merged conflicts, or two incremental additions that were never
28
+ * consolidated.
29
+ *
30
+ * Unlike the Jaccard-based detector above, this fires on heading identity alone
31
+ * (case-insensitive, markdown stripped) regardless of content similarity.
32
+ *
33
+ * Severity: warning, confidence 0.85.
34
+ */
35
+ export declare function detectDuplicateHeadings(files: InstructionFile[]): PromptCiIssue[];
36
+ //# sourceMappingURL=duplicates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicates.d.ts","sourceRoot":"","sources":["../src/duplicates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAsB,aAAa,EAAE,MAAM,YAAY,CAAC;AAuDrF,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQrD;AAwED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE,CA8L1E;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE,CAsHjF"}
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Duplicate section detector for PromptCI.
3
+ *
4
+ * Compares sections across all instruction files and flags groups that are
5
+ * exact matches or near-duplicates (Jaccard ≥ 0.8).
6
+ *
7
+ * When the same section appears in N files, ONE issue is emitted listing
8
+ * all N files — not N*(N-1)/2 pairwise issues.
9
+ *
10
+ * Wording is intentionally cautious — all findings are heuristic.
11
+ */
12
+ import * as crypto from 'node:crypto';
13
+ /**
14
+ * Cross-file: sections shorter than this (normalised chars) are too noisy to flag.
15
+ * Within-file: we use a lower bar so short but verbatim-repeated bash blocks are caught.
16
+ */
17
+ const MIN_SECTION_LENGTH = 150; // cross-file minimum
18
+ const WITHIN_FILE_MIN_SECTION_LENGTH = 80; // BUG-001: within-file exact/near-dups need less content
19
+ const MIN_TOKEN_COUNT = 10; // lowered from 20 — short blocks can have fewer tokens
20
+ /**
21
+ * Jaccard similarity threshold for near-duplicate detection.
22
+ * Within the same file a lower threshold is appropriate because a repeated
23
+ * section (e.g. condensed bullet-list restatement of a prose section) is
24
+ * almost always an accidental duplicate rather than intentional.
25
+ *
26
+ * BUG-003: A second, lower cross-file threshold catches borderline near-duplicates
27
+ * (e.g. "Getting Started" vs "Environment Setup" with ~60% overlap) at info severity.
28
+ * These are worth flagging but with lower confidence than clear duplicates.
29
+ */
30
+ const CROSS_FILE_JACCARD_THRESHOLD = 0.8;
31
+ const CROSS_FILE_JACCARD_THRESHOLD_INFO = 0.60; // BUG-003: info-level borderline duplicates
32
+ const WITHIN_FILE_JACCARD_THRESHOLD = 0.6; // BUG-001
33
+ /**
34
+ * Generic boilerplate headings that commonly appear in every instruction file
35
+ * but don't represent actionable duplicate content.
36
+ * BUG-010: Added summary/description terms so short project-preamble sections
37
+ * in multiple instruction files are not flagged as duplicates.
38
+ */
39
+ const BOILERPLATE_HEADINGS = new Set([
40
+ 'introduction',
41
+ 'overview',
42
+ 'notes',
43
+ 'note',
44
+ 'about',
45
+ 'background',
46
+ 'context',
47
+ 'disclaimer',
48
+ 'warning',
49
+ 'important',
50
+ 'readme',
51
+ // BUG-010 additions
52
+ 'summary',
53
+ 'project summary',
54
+ 'project overview',
55
+ 'project description',
56
+ 'description',
57
+ 'what is this',
58
+ 'table of contents',
59
+ 'contents',
60
+ 'license',
61
+ 'changelog',
62
+ ]);
63
+ /** Normalise text for comparison: lowercase, collapse whitespace, strip markdown syntax. */
64
+ export function normalizeSection(text) {
65
+ return text
66
+ .toLowerCase()
67
+ .replace(/^#{1,6}\s+/gm, '')
68
+ .replace(/^[\s]*[-*+]\s+/gm, '')
69
+ .replace(/^[\s]*\d+\.\s+/gm, '')
70
+ .replace(/\s+/g, ' ')
71
+ .trim();
72
+ }
73
+ /** Tokenise text into a Set of lowercase words for Jaccard calculation. */
74
+ function tokenize(text) {
75
+ const words = text.match(/\b\w+\b/g) ?? [];
76
+ return new Set(words);
77
+ }
78
+ /** Jaccard similarity between two token sets. Returns 0–1. */
79
+ function jaccardSimilarity(a, b) {
80
+ if (a.size === 0 && b.size === 0)
81
+ return 1;
82
+ if (a.size === 0 || b.size === 0)
83
+ return 0;
84
+ let intersectionSize = 0;
85
+ for (const token of a) {
86
+ if (b.has(token))
87
+ intersectionSize++;
88
+ }
89
+ return intersectionSize / (a.size + b.size - intersectionSize);
90
+ }
91
+ /** First 200 chars of a string, trimmed. */
92
+ function snippet(text) {
93
+ return text.length <= 200 ? text : `${text.slice(0, 200)}…`;
94
+ }
95
+ function isBoilerplateHeading(heading) {
96
+ if (!heading)
97
+ return false;
98
+ const lower = heading.toLowerCase().trim();
99
+ if (BOILERPLATE_HEADINGS.has(lower))
100
+ return true;
101
+ // BUG-010: also match headings that START WITH a boilerplate term (e.g.
102
+ // "Project Summary / NovaSaaS — Claude Code Instructions" starts with "project summary").
103
+ for (const term of BOILERPLATE_HEADINGS) {
104
+ if (lower.startsWith(term))
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ function normalizedHeading(heading) {
110
+ return heading?.toLowerCase().replace(/\s+/g, ' ').trim() ?? '';
111
+ }
112
+ function isActionableShortExactDuplicate(a, b) {
113
+ const headingA = normalizedHeading(a.heading);
114
+ const headingB = normalizedHeading(b.heading);
115
+ return headingA.length > 0 && headingA === headingB;
116
+ }
117
+ // ── Union-Find ────────────────────────────────────────────────────────────────
118
+ function find(parent, key) {
119
+ if (!parent.has(key))
120
+ return key;
121
+ const root = find(parent, parent.get(key));
122
+ parent.set(key, root); // path compression
123
+ return root;
124
+ }
125
+ function union(parent, a, b) {
126
+ const ra = find(parent, a);
127
+ const rb = find(parent, b);
128
+ if (ra !== rb)
129
+ parent.set(ra, rb);
130
+ }
131
+ // ── ID helpers ────────────────────────────────────────────────────────────────
132
+ /** Deterministic ID for a cluster of section keys. */
133
+ function clusterIssueId(keys) {
134
+ const sorted = [...keys].sort().join('|');
135
+ const hash = crypto.createHash('sha1').update(sorted).digest('hex').slice(0, 12);
136
+ return `duplicate-${hash}`;
137
+ }
138
+ // ── Detector ─────────────────────────────────────────────────────────────────
139
+ /**
140
+ * Detect duplicate or near-duplicate sections across all instruction files.
141
+ *
142
+ * Groups all N-way duplicates into a single issue instead of N*(N-1)/2 pairs.
143
+ *
144
+ * - Exact normalised match → severity "high", confidence 0.95
145
+ * - Jaccard ≥ 0.80 → severity "warning", confidence 0.75
146
+ */
147
+ export function detectDuplicates(files) {
148
+ // Flatten eligible sections.
149
+ // Use the lower within-file threshold here; the cross-file threshold is
150
+ // enforced per-pair in the comparison loop below.
151
+ const eligible = [];
152
+ for (const file of files) {
153
+ for (const section of file.sections) {
154
+ const norm = normalizeSection(section.text);
155
+ if (norm.length < WITHIN_FILE_MIN_SECTION_LENGTH)
156
+ continue; // BUG-001: lower global floor
157
+ if (tokenize(norm).size < MIN_TOKEN_COUNT)
158
+ continue;
159
+ eligible.push({ ...section, normalizedText: norm });
160
+ }
161
+ }
162
+ // Union-Find parent map and per-section metadata
163
+ const parent = new Map();
164
+ // sectionByKey: key → section
165
+ const sectionByKey = new Map();
166
+ /**
167
+ * DU1: every matching pair's severity/confidence, recorded WITHOUT trying to
168
+ * attribute it to a cluster root yet. Union-find roots can change after a
169
+ * pair is processed — e.g. A==B (root becomes keyB) unions with A~C later,
170
+ * which re-roots the WHOLE {A,B} cluster onto keyC. The old code recorded
171
+ * severity keyed by "the root at the moment this pair was unioned"
172
+ * (`clusterSeverity.set(find(parent, keyA), severity)`), so a later
173
+ * re-rooting orphaned that entry — the final lookup by the NEW root missed
174
+ * it entirely and silently fell back to the pair processed last, even if
175
+ * an earlier pair in the same cluster had a stronger severity.
176
+ * Deferring root attribution to a second pass (after ALL unions are done,
177
+ * see below) makes this immune to re-rooting.
178
+ */
179
+ const edges = [];
180
+ for (let i = 0; i < eligible.length; i++) {
181
+ for (let j = i + 1; j < eligible.length; j++) {
182
+ const a = eligible[i];
183
+ const b = eligible[j];
184
+ if (a.filePath === b.filePath && a.startLine === b.startLine)
185
+ continue;
186
+ const keyA = `${a.filePath}:${a.startLine}`;
187
+ const keyB = `${b.filePath}:${b.startLine}`;
188
+ const normA = a.normalizedText;
189
+ const normB = b.normalizedText;
190
+ let severity = null;
191
+ let confidence = 0;
192
+ // BUG-010: Apply boilerplate heading filter to ALL comparisons, including exact
193
+ // matches. "Project Summary" and similar sections are intentionally duplicated
194
+ // across instruction files and should never be flagged.
195
+ if (isBoilerplateHeading(a.heading) || isBoilerplateHeading(b.heading))
196
+ continue;
197
+ const sameFile = a.filePath === b.filePath;
198
+ if (normA === normB) {
199
+ if (!sameFile &&
200
+ (normA.length < MIN_SECTION_LENGTH || normB.length < MIN_SECTION_LENGTH) &&
201
+ !isActionableShortExactDuplicate(a, b)) {
202
+ continue;
203
+ }
204
+ severity = 'high';
205
+ confidence = 0.95;
206
+ }
207
+ else {
208
+ // BUG-001: apply a stricter length floor for cross-file near-duplicate
209
+ // comparisons so short same-file repeated blocks are caught while
210
+ // cross-file noise is kept low. Short exact duplicates are handled
211
+ // above only when their matching headings make them actionable.
212
+ if (!sameFile && (normA.length < MIN_SECTION_LENGTH || normB.length < MIN_SECTION_LENGTH)) {
213
+ continue;
214
+ }
215
+ // BUG-001: within-file near-duplicates use a lower Jaccard threshold because
216
+ // a repeated (possibly condensed) section in the same file is almost certainly
217
+ // an accidental duplicate, not intentional.
218
+ const jaccardThreshold = sameFile
219
+ ? WITHIN_FILE_JACCARD_THRESHOLD
220
+ : CROSS_FILE_JACCARD_THRESHOLD;
221
+ const sim = jaccardSimilarity(tokenize(normA), tokenize(normB));
222
+ if (sim >= jaccardThreshold) {
223
+ severity = 'warning';
224
+ confidence = sameFile ? 0.8 : 0.75;
225
+ }
226
+ else if (!sameFile && sim >= CROSS_FILE_JACCARD_THRESHOLD_INFO) {
227
+ // BUG-003: Borderline near-duplicate — same topic, moderate text overlap (65–79%).
228
+ // Flag at info severity with lower confidence rather than silently ignoring.
229
+ severity = 'info';
230
+ confidence = 0.5;
231
+ }
232
+ }
233
+ if (severity === null)
234
+ continue;
235
+ sectionByKey.set(keyA, a);
236
+ sectionByKey.set(keyB, b);
237
+ union(parent, keyA, keyB);
238
+ edges.push({ keyA, keyB, severity, confidence });
239
+ }
240
+ }
241
+ // Group section keys by cluster root — all unions are complete by this
242
+ // point, so every `find()` call here returns the TRUE final root.
243
+ const clusters = new Map();
244
+ for (const key of sectionByKey.keys()) {
245
+ const root = find(parent, key);
246
+ if (!clusters.has(root))
247
+ clusters.set(root, []);
248
+ clusters.get(root).push(key);
249
+ }
250
+ // DU1: attribute each recorded edge's severity to its FINAL root (re-computed
251
+ // now, after every union has already happened) and keep the highest-ranked
252
+ // one per cluster. This is what makes the fix immune to re-rooting — an
253
+ // edge processed early (e.g. the exact-duplicate A==B pair) is looked up
254
+ // under the SAME final root as a later edge that merged its cluster into a
255
+ // bigger one (e.g. A~C), so the strongest severity in the whole cluster
256
+ // always wins regardless of processing order.
257
+ const SEVERITY_RANK = {
258
+ info: 0, warning: 1, high: 2, critical: 3,
259
+ };
260
+ const clusterSeverity = new Map();
261
+ const clusterConfidence = new Map();
262
+ for (const edge of edges) {
263
+ const root = find(parent, edge.keyA); // same final root as edge.keyB
264
+ const prev = clusterSeverity.get(root);
265
+ if (!prev || SEVERITY_RANK[edge.severity] > SEVERITY_RANK[prev]) {
266
+ clusterSeverity.set(root, edge.severity);
267
+ clusterConfidence.set(root, edge.confidence);
268
+ }
269
+ }
270
+ // Emit one issue per cluster
271
+ const issues = [];
272
+ for (const [root, keys] of clusters) {
273
+ if (keys.length < 2)
274
+ continue;
275
+ const sections = keys.map((k) => sectionByKey.get(k));
276
+ const severity = clusterSeverity.get(root) ?? 'warning';
277
+ const confidence = clusterConfidence.get(root) ?? 0.75;
278
+ // Deduplicate file paths while preserving order
279
+ const filePaths = [...new Set(sections.map((s) => s.filePath))];
280
+ // Build a concise heading label
281
+ const uniqueHeadings = [...new Set(sections.map((s) => s.heading ?? '(untitled)'))];
282
+ const headingLabel = uniqueHeadings.length === 1
283
+ ? uniqueHeadings[0]
284
+ : uniqueHeadings.slice(0, 2).join(' / ');
285
+ const fileCount = filePaths.length;
286
+ const title = fileCount > 2
287
+ ? `Possible duplicate: ${headingLabel} (${fileCount} files)`
288
+ : `Possible duplicate: ${headingLabel}`;
289
+ const duplicateTokens = sections.slice(1).reduce((sum, s) => sum + Math.round(s.text.length / 4), 0);
290
+ const summary = fileCount > 2
291
+ ? `This section appears highly similar in ${fileCount} files. Consider consolidating into one canonical location.`
292
+ : 'These sections appear highly similar. Consider consolidating them into one canonical location.';
293
+ issues.push({
294
+ id: clusterIssueId(keys),
295
+ severity,
296
+ category: 'duplicate',
297
+ title,
298
+ summary,
299
+ filePaths,
300
+ locations: sections.map((s) => ({
301
+ filePath: s.filePath,
302
+ startLine: s.startLine,
303
+ endLine: s.endLine,
304
+ })),
305
+ evidence: [
306
+ snippet(sections[0].normalizedText),
307
+ `Duplicated size: ~${duplicateTokens} tokens`,
308
+ ],
309
+ recommendation: `Consolidate these sections into one canonical location (e.g. AGENTS.md) and use forwarding pointers to save ~${duplicateTokens} tokens per session. If both are intentional, add a comment clarifying the distinction.`,
310
+ confidence,
311
+ });
312
+ }
313
+ return issues;
314
+ }
315
+ // ── BUG-004: Same-heading duplicate within a single file ─────────────────────
316
+ /**
317
+ * Detect when the same section heading appears more than once in a single
318
+ * instruction file. This is almost always a structural mistake — accidental
319
+ * copy/paste, merged conflicts, or two incremental additions that were never
320
+ * consolidated.
321
+ *
322
+ * Unlike the Jaccard-based detector above, this fires on heading identity alone
323
+ * (case-insensitive, markdown stripped) regardless of content similarity.
324
+ *
325
+ * Severity: warning, confidence 0.85.
326
+ */
327
+ export function detectDuplicateHeadings(files) {
328
+ const issues = [];
329
+ for (const file of files) {
330
+ // heading (normalised) → sections that carry it
331
+ const headingMap = new Map();
332
+ for (const section of file.sections) {
333
+ if (!section.heading)
334
+ continue;
335
+ if (isBoilerplateHeading(section.heading))
336
+ continue;
337
+ const key = section.heading
338
+ .toLowerCase()
339
+ .replace(/^#+\s*/, '')
340
+ .replace(/[^\w\s]/g, '')
341
+ .replace(/\s+/g, ' ')
342
+ .trim();
343
+ if (key.length < 3)
344
+ continue;
345
+ if (!headingMap.has(key))
346
+ headingMap.set(key, []);
347
+ headingMap.get(key).push(section);
348
+ }
349
+ for (const [, sections] of headingMap) {
350
+ if (sections.length < 2)
351
+ continue;
352
+ // Don't flag headings that appear at different nesting levels (e.g. "## Troubleshooting"
353
+ // and "### Troubleshooting" in the same file are in distinct structural contexts).
354
+ const levels = new Set(sections.map((s) => {
355
+ const m = /^(#{1,6})\s/.exec(s.text);
356
+ return m ? m[1].length : 0;
357
+ }));
358
+ if (levels.size > 1)
359
+ continue;
360
+ const keys = sections.map((s) => `${file.path}:${s.startLine}`);
361
+ const id = clusterIssueId(keys);
362
+ const headingLabel = sections[0].heading ?? '(untitled)';
363
+ issues.push({
364
+ id,
365
+ severity: 'warning',
366
+ category: 'duplicate',
367
+ title: `Duplicate section heading in one file: "${headingLabel}"`,
368
+ summary: `The heading "${headingLabel}" appears ${sections.length} times in this file. ` +
369
+ `This is likely an accidental duplicate — consider consolidating into one section.`,
370
+ filePaths: [file.path],
371
+ locations: sections.map((s) => ({ filePath: file.path, startLine: s.startLine, endLine: s.endLine })),
372
+ evidence: [`Heading "${headingLabel}" found at lines: ${sections.map((s) => s.startLine).join(', ')}`],
373
+ recommendation: 'Merge the duplicate sections into one, or rename one if the distinction is intentional.',
374
+ confidence: 0.85,
375
+ });
376
+ }
377
+ // BUG-006: Also check for headings where one is a prefix of another within
378
+ // the same file (e.g. "Accessibility" and "Accessibility Standards").
379
+ // These are almost always overlapping sections that should be merged.
380
+ const headingKeys = [...headingMap.keys()];
381
+ const prefixReported = new Set();
382
+ for (let i = 0; i < headingKeys.length; i++) {
383
+ for (let j = i + 1; j < headingKeys.length; j++) {
384
+ const keyA = headingKeys[i];
385
+ const keyB = headingKeys[j];
386
+ // One must be a strict prefix of the other, and both must be meaningful
387
+ const shorter = keyA.length <= keyB.length ? keyA : keyB;
388
+ const longer = keyA.length <= keyB.length ? keyB : keyA;
389
+ if (shorter.length < 4)
390
+ continue;
391
+ if (!longer.startsWith(shorter))
392
+ continue;
393
+ // DU2: the longer heading must have at least one extra WORD after the
394
+ // prefix, not just a mid-word continuation. The old check called
395
+ // `.trimStart()` before testing anything, which threw away the one
396
+ // signal it needed — whether a space actually followed the shared
397
+ // prefix — so "Lint" → "Linting" (extra = "ing", no space) passed the
398
+ // same `/^\w/` test as "accessibility" → "accessibility standards"
399
+ // (extra = " standards", a real space). Test the RAW (untrimmed)
400
+ // remainder for a leading space first; only trim after that check.
401
+ const extraRaw = longer.slice(shorter.length);
402
+ if (!/^\s/.test(extraRaw))
403
+ continue; // mid-word continuation — skip
404
+ const extra = extraRaw.trimStart();
405
+ if (!extra)
406
+ continue; // trailing whitespace only, nothing real followed
407
+ const pairKey = [keyA, keyB].sort().join('|');
408
+ if (prefixReported.has(pairKey))
409
+ continue;
410
+ prefixReported.add(pairKey);
411
+ const secA = headingMap.get(keyA);
412
+ const secB = headingMap.get(keyB);
413
+ const allSections = [...secA, ...secB].sort((a, b) => a.startLine - b.startLine);
414
+ const allKeys = allSections.map((s) => `${file.path}:${s.startLine}`);
415
+ issues.push({
416
+ id: clusterIssueId(allKeys),
417
+ severity: 'warning',
418
+ category: 'duplicate',
419
+ title: `Overlapping section headings in one file: "${secA[0].heading}" / "${secB[0].heading}"`,
420
+ summary: `These section headings appear to cover the same topic — one is a prefix/subset of the other. ` +
421
+ `This is likely an accidental split. Consider merging into one canonical section.`,
422
+ filePaths: [file.path],
423
+ locations: allSections.map((s) => ({ filePath: file.path, startLine: s.startLine, endLine: s.endLine })),
424
+ evidence: [
425
+ `"${secA[0].heading}" at line ${secA[0].startLine}`,
426
+ `"${secB[0].heading}" at line ${secB[0].startLine}`,
427
+ ],
428
+ recommendation: 'Consolidate these sections into one. If the distinction is intentional, rename one to make the difference clear.',
429
+ confidence: 0.75,
430
+ });
431
+ }
432
+ }
433
+ }
434
+ return issues;
435
+ }
436
+ //# sourceMappingURL=duplicates.js.map