@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.
- package/LICENSE +201 -0
- package/README.md +30 -0
- package/dist/agent-practices.d.ts +22 -0
- package/dist/agent-practices.d.ts.map +1 -0
- package/dist/agent-practices.js +582 -0
- package/dist/agent-practices.js.map +1 -0
- package/dist/baseline.d.ts +40 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +172 -0
- package/dist/baseline.js.map +1 -0
- package/dist/canonical-owner.d.ts +3 -0
- package/dist/canonical-owner.d.ts.map +1 -0
- package/dist/canonical-owner.js +131 -0
- package/dist/canonical-owner.js.map +1 -0
- package/dist/cheap-model-routing.d.ts +4 -0
- package/dist/cheap-model-routing.d.ts.map +1 -0
- package/dist/cheap-model-routing.js +43 -0
- package/dist/cheap-model-routing.js.map +1 -0
- package/dist/ci-alignment.d.ts +10 -0
- package/dist/ci-alignment.d.ts.map +1 -0
- package/dist/ci-alignment.js +343 -0
- package/dist/ci-alignment.js.map +1 -0
- package/dist/cli-output-filter.d.ts +16 -0
- package/dist/cli-output-filter.d.ts.map +1 -0
- package/dist/cli-output-filter.js +88 -0
- package/dist/cli-output-filter.js.map +1 -0
- package/dist/command-validity.d.ts +12 -0
- package/dist/command-validity.d.ts.map +1 -0
- package/dist/command-validity.js +543 -0
- package/dist/command-validity.js.map +1 -0
- package/dist/conflicts.d.ts +49 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +1065 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/context-analysis.d.ts +15 -0
- package/dist/context-analysis.d.ts.map +1 -0
- package/dist/context-analysis.js +24 -0
- package/dist/context-analysis.js.map +1 -0
- package/dist/context-bloat.d.ts +17 -0
- package/dist/context-bloat.d.ts.map +1 -0
- package/dist/context-bloat.js +148 -0
- package/dist/context-bloat.js.map +1 -0
- package/dist/context-cost.d.ts +4 -0
- package/dist/context-cost.d.ts.map +1 -0
- package/dist/context-cost.js +70 -0
- package/dist/context-cost.js.map +1 -0
- package/dist/context-optimizer.d.ts +14 -0
- package/dist/context-optimizer.d.ts.map +1 -0
- package/dist/context-optimizer.js +219 -0
- package/dist/context-optimizer.js.map +1 -0
- package/dist/context-recommender.d.ts +3 -0
- package/dist/context-recommender.d.ts.map +1 -0
- package/dist/context-recommender.js +329 -0
- package/dist/context-recommender.js.map +1 -0
- package/dist/dead-references.d.ts +33 -0
- package/dist/dead-references.d.ts.map +1 -0
- package/dist/dead-references.js +607 -0
- package/dist/dead-references.js.map +1 -0
- package/dist/detectors.d.ts +11 -0
- package/dist/detectors.d.ts.map +1 -0
- package/dist/detectors.js +160 -0
- package/dist/detectors.js.map +1 -0
- package/dist/duplicates.d.ts +36 -0
- package/dist/duplicates.d.ts.map +1 -0
- package/dist/duplicates.js +436 -0
- package/dist/duplicates.js.map +1 -0
- package/dist/fix-engine.d.ts +16 -0
- package/dist/fix-engine.d.ts.map +1 -0
- package/dist/fix-engine.js +179 -0
- package/dist/fix-engine.js.map +1 -0
- package/dist/framework-packs.d.ts +8 -0
- package/dist/framework-packs.d.ts.map +1 -0
- package/dist/framework-packs.js +22 -0
- package/dist/framework-packs.js.map +1 -0
- package/dist/frameworks/dotnet.d.ts +4 -0
- package/dist/frameworks/dotnet.d.ts.map +1 -0
- package/dist/frameworks/dotnet.js +77 -0
- package/dist/frameworks/dotnet.js.map +1 -0
- package/dist/frameworks/go.d.ts +4 -0
- package/dist/frameworks/go.d.ts.map +1 -0
- package/dist/frameworks/go.js +39 -0
- package/dist/frameworks/go.js.map +1 -0
- package/dist/frameworks/nextjs.d.ts +4 -0
- package/dist/frameworks/nextjs.d.ts.map +1 -0
- package/dist/frameworks/nextjs.js +66 -0
- package/dist/frameworks/nextjs.js.map +1 -0
- package/dist/frameworks/python.d.ts +4 -0
- package/dist/frameworks/python.d.ts.map +1 -0
- package/dist/frameworks/python.js +66 -0
- package/dist/frameworks/python.js.map +1 -0
- package/dist/frameworks/rust.d.ts +4 -0
- package/dist/frameworks/rust.d.ts.map +1 -0
- package/dist/frameworks/rust.js +72 -0
- package/dist/frameworks/rust.js.map +1 -0
- package/dist/frameworks/unity.d.ts +4 -0
- package/dist/frameworks/unity.d.ts.map +1 -0
- package/dist/frameworks/unity.js +74 -0
- package/dist/frameworks/unity.js.map +1 -0
- package/dist/health-score.d.ts +37 -0
- package/dist/health-score.d.ts.map +1 -0
- package/dist/health-score.js +164 -0
- package/dist/health-score.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +6 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +67 -0
- package/dist/llm.js.map +1 -0
- package/dist/manifest-consistency.d.ts +37 -0
- package/dist/manifest-consistency.d.ts.map +1 -0
- package/dist/manifest-consistency.js +659 -0
- package/dist/manifest-consistency.js.map +1 -0
- package/dist/mcp-detector.d.ts +4 -0
- package/dist/mcp-detector.d.ts.map +1 -0
- package/dist/mcp-detector.js +131 -0
- package/dist/mcp-detector.js.map +1 -0
- package/dist/missing-context.d.ts +15 -0
- package/dist/missing-context.d.ts.map +1 -0
- package/dist/missing-context.js +517 -0
- package/dist/missing-context.js.map +1 -0
- package/dist/native-tool-preference.d.ts +20 -0
- package/dist/native-tool-preference.d.ts.map +1 -0
- package/dist/native-tool-preference.js +94 -0
- package/dist/native-tool-preference.js.map +1 -0
- package/dist/output-verbosity.d.ts +3 -0
- package/dist/output-verbosity.d.ts.map +1 -0
- package/dist/output-verbosity.js +83 -0
- package/dist/output-verbosity.js.map +1 -0
- package/dist/product-boundary.d.ts +11 -0
- package/dist/product-boundary.d.ts.map +1 -0
- package/dist/product-boundary.js +301 -0
- package/dist/product-boundary.js.map +1 -0
- package/dist/project-type.d.ts +15 -0
- package/dist/project-type.d.ts.map +1 -0
- package/dist/project-type.js +116 -0
- package/dist/project-type.js.map +1 -0
- package/dist/prompt-cache.d.ts +3 -0
- package/dist/prompt-cache.d.ts.map +1 -0
- package/dist/prompt-cache.js +151 -0
- package/dist/prompt-cache.js.map +1 -0
- package/dist/repo-context.d.ts +37 -0
- package/dist/repo-context.d.ts.map +1 -0
- package/dist/repo-context.js +221 -0
- package/dist/repo-context.js.map +1 -0
- package/dist/report.d.ts +87 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +558 -0
- package/dist/report.js.map +1 -0
- package/dist/scan.d.ts +7 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +43 -0
- package/dist/scan.js.map +1 -0
- package/dist/scanner.d.ts +3 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +216 -0
- package/dist/scanner.js.map +1 -0
- package/dist/security-pack.d.ts +4 -0
- package/dist/security-pack.d.ts.map +1 -0
- package/dist/security-pack.js +403 -0
- package/dist/security-pack.js.map +1 -0
- package/dist/stale-instructions.d.ts +20 -0
- package/dist/stale-instructions.d.ts.map +1 -0
- package/dist/stale-instructions.js +269 -0
- package/dist/stale-instructions.js.map +1 -0
- package/dist/suppression.d.ts +64 -0
- package/dist/suppression.d.ts.map +1 -0
- package/dist/suppression.js +338 -0
- package/dist/suppression.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/vague-guidance.d.ts +12 -0
- package/dist/vague-guidance.d.ts.map +1 -0
- package/dist/vague-guidance.js +235 -0
- package/dist/vague-guidance.js.map +1 -0
- 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
|