@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,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest-consistency detector for PromptCI.
|
|
3
|
+
*
|
|
4
|
+
* Cross-references instruction file content against project manifest files
|
|
5
|
+
* (pyproject.toml, package.json) and flags mismatches that would cause an
|
|
6
|
+
* agent to follow an instruction that conflicts with the actual build setup.
|
|
7
|
+
*
|
|
8
|
+
* BUG-001: Tooling mismatch — instruction says `pip install -r requirements.txt`
|
|
9
|
+
* but pyproject.toml declares Poetry as the build backend and there is
|
|
10
|
+
* no requirements.txt in the repo.
|
|
11
|
+
*
|
|
12
|
+
* BUG-002: Python version mismatch — instruction file claims an older Python
|
|
13
|
+
* version than the one pinned in pyproject.toml's `python` constraint.
|
|
14
|
+
*
|
|
15
|
+
* BUG-003: Library version mismatch — instruction file claims compatibility
|
|
16
|
+
* with an older major library version than the one pinned in pyproject.toml.
|
|
17
|
+
*
|
|
18
|
+
* Findings are heuristic — wording is intentionally cautious.
|
|
19
|
+
*/
|
|
20
|
+
import * as crypto from 'node:crypto';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
24
|
+
function issueId(key) {
|
|
25
|
+
const hash = crypto.createHash('sha1').update(key).digest('hex').slice(0, 12);
|
|
26
|
+
return `manifest-${hash}`;
|
|
27
|
+
}
|
|
28
|
+
// ── BUG-001: pip vs Poetry tooling mismatch ───────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* Detect when instruction files tell the agent to run `pip install -r requirements.txt`
|
|
31
|
+
* but the project actually uses Poetry (pyproject.toml with [tool.poetry]) and has no
|
|
32
|
+
* requirements.txt. Running the pip command would immediately fail.
|
|
33
|
+
*/
|
|
34
|
+
function checkPipVsPoetry(files, pyprojectContent, repoRoot) {
|
|
35
|
+
const isPoetry = /\[tool\.poetry\]/.test(pyprojectContent) ||
|
|
36
|
+
/build-backend\s*=\s*["'][^"']*poetry[^"']*["']/i.test(pyprojectContent);
|
|
37
|
+
if (!isPoetry)
|
|
38
|
+
return null;
|
|
39
|
+
// Only flag when requirements.txt is genuinely absent — some Poetry projects
|
|
40
|
+
// export a requirements.txt as an artefact; if it exists the instruction is valid.
|
|
41
|
+
const reqTxtPath = path.join(repoRoot, 'requirements.txt');
|
|
42
|
+
if (fs.existsSync(reqTxtPath))
|
|
43
|
+
return null;
|
|
44
|
+
const PIP_INSTALL_REQUIREMENTS_RE = /pip\s+install\s+-r\s+requirements\.txt/i;
|
|
45
|
+
const offendingFiles = files.filter((f) => PIP_INSTALL_REQUIREMENTS_RE.test(f.content));
|
|
46
|
+
if (offendingFiles.length === 0)
|
|
47
|
+
return null;
|
|
48
|
+
return {
|
|
49
|
+
id: issueId('pip-vs-poetry'),
|
|
50
|
+
severity: 'high',
|
|
51
|
+
category: 'conflict',
|
|
52
|
+
title: 'Tooling mismatch: instruction uses pip but project uses Poetry',
|
|
53
|
+
summary: 'Instruction files tell the agent to run `pip install -r requirements.txt`, but this ' +
|
|
54
|
+
'project uses Poetry as its build system (pyproject.toml with [tool.poetry]) and has no ' +
|
|
55
|
+
'requirements.txt. Running this command would fail immediately.',
|
|
56
|
+
filePaths: offendingFiles.map((f) => f.path),
|
|
57
|
+
locations: offendingFiles.map((f) => ({ filePath: f.path })),
|
|
58
|
+
evidence: [
|
|
59
|
+
'`pip install -r requirements.txt` found in instruction file(s)',
|
|
60
|
+
'pyproject.toml declares Poetry as the build system',
|
|
61
|
+
'requirements.txt does not exist in the repo',
|
|
62
|
+
],
|
|
63
|
+
recommendation: 'Update the instruction files to use Poetry commands instead: ' +
|
|
64
|
+
'`poetry install` (or `uv sync` if the project has migrated to uv). ' +
|
|
65
|
+
'Remove the `pip install -r requirements.txt` reference.',
|
|
66
|
+
confidence: 0.9,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ── BUG-002: Python version mismatch ─────────────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Detect when an instruction file states a Python version that is lower than
|
|
72
|
+
* the minimum declared in pyproject.toml's `python` constraint.
|
|
73
|
+
*
|
|
74
|
+
* Example: CLAUDE.md says "We target Python 3.9" but pyproject.toml declares
|
|
75
|
+
* `python = "^3.11"` — an agent following the instruction would target the
|
|
76
|
+
* wrong interpreter and miss 3.10–3.11 language features and APIs.
|
|
77
|
+
*/
|
|
78
|
+
function checkPythonVersionMismatch(files, pyprojectContent) {
|
|
79
|
+
// Extract the minimum Python version from pyproject.toml.
|
|
80
|
+
// Handles: python = "^3.11", ">=3.10", "~=3.9", "3.11.*"
|
|
81
|
+
const manifestMatch = /^python\s*=\s*["'][\^~>=<*]*(\d+)\.(\d+)/m.exec(pyprojectContent) ||
|
|
82
|
+
/^requires-python\s*=\s*["'][\^~>=<*]*(\d+)\.(\d+)/m.exec(pyprojectContent);
|
|
83
|
+
if (!manifestMatch)
|
|
84
|
+
return [];
|
|
85
|
+
const manifestMajor = parseInt(manifestMatch[1], 10);
|
|
86
|
+
const manifestMinor = parseInt(manifestMatch[2], 10);
|
|
87
|
+
const issues = [];
|
|
88
|
+
const reported = new Set();
|
|
89
|
+
// Match "Python X.Y" or "python X.Y" in instruction files (prose mentions)
|
|
90
|
+
const PYTHON_VERSION_RE = /\bPython\s+(\d+)\.(\d+)\b/gi;
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
let m;
|
|
93
|
+
const re = new RegExp(PYTHON_VERSION_RE.source, PYTHON_VERSION_RE.flags);
|
|
94
|
+
while ((m = re.exec(file.content)) !== null) {
|
|
95
|
+
const instrMajor = parseInt(m[1], 10);
|
|
96
|
+
const instrMinor = parseInt(m[2], 10);
|
|
97
|
+
if (isNegationContext(file.content, m.index))
|
|
98
|
+
continue;
|
|
99
|
+
// Only flag when the instruction version is strictly older than the manifest minimum
|
|
100
|
+
const isOlder = instrMajor < manifestMajor ||
|
|
101
|
+
(instrMajor === manifestMajor && instrMinor < manifestMinor);
|
|
102
|
+
if (!isOlder)
|
|
103
|
+
continue;
|
|
104
|
+
const instrVersion = `${instrMajor}.${instrMinor}`;
|
|
105
|
+
const manifestVersion = `${manifestMajor}.${manifestMinor}`;
|
|
106
|
+
const dedupeKey = `${file.path}:${instrVersion}`;
|
|
107
|
+
if (reported.has(dedupeKey))
|
|
108
|
+
continue;
|
|
109
|
+
reported.add(dedupeKey);
|
|
110
|
+
issues.push({
|
|
111
|
+
id: issueId(`python-version:${file.path}:${instrVersion}`),
|
|
112
|
+
severity: 'high',
|
|
113
|
+
category: 'stale_instruction',
|
|
114
|
+
title: `Python version mismatch: instruction says ${instrVersion}, pyproject.toml requires ≥${manifestVersion}`,
|
|
115
|
+
summary: `An instruction file refers to Python ${instrVersion}, but pyproject.toml declares ` +
|
|
116
|
+
`\`python = "^${manifestVersion}"\` (or newer). An agent following this instruction ` +
|
|
117
|
+
`would target an older interpreter and may avoid language features or APIs that are ` +
|
|
118
|
+
`safe to use in this project.`,
|
|
119
|
+
filePaths: [file.path],
|
|
120
|
+
locations: [{ filePath: file.path }],
|
|
121
|
+
evidence: [
|
|
122
|
+
`Instruction says "Python ${instrVersion}"; pyproject.toml requires ≥${manifestVersion}`,
|
|
123
|
+
],
|
|
124
|
+
recommendation: `Update the instruction file to reference Python ${manifestVersion} (or remove the ` +
|
|
125
|
+
`version claim and rely on pyproject.toml as the authoritative source).`,
|
|
126
|
+
confidence: 0.85,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return issues;
|
|
131
|
+
}
|
|
132
|
+
// ── BUG-003: Library version mismatch ────────────────────────────────────────
|
|
133
|
+
const TRACKED_PYTHON_LIBRARIES = [
|
|
134
|
+
'pandas',
|
|
135
|
+
'numpy',
|
|
136
|
+
'sqlalchemy',
|
|
137
|
+
'fastapi',
|
|
138
|
+
'django',
|
|
139
|
+
'flask',
|
|
140
|
+
'pydantic',
|
|
141
|
+
];
|
|
142
|
+
function displayLibraryName(library) {
|
|
143
|
+
if (library === 'sqlalchemy')
|
|
144
|
+
return 'SQLAlchemy';
|
|
145
|
+
if (library === 'fastapi')
|
|
146
|
+
return 'FastAPI';
|
|
147
|
+
return library[0].toUpperCase() + library.slice(1);
|
|
148
|
+
}
|
|
149
|
+
function escapeRegExp(value) {
|
|
150
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
151
|
+
}
|
|
152
|
+
function parseAuthoritativeMajorVersion(versionSpec) {
|
|
153
|
+
const spec = versionSpec.trim();
|
|
154
|
+
// Upper-bound-only constraints do not prove the project has moved to that major.
|
|
155
|
+
if (/^<=?\s*\d+/.test(spec))
|
|
156
|
+
return null;
|
|
157
|
+
const match = /^(?:\^|~=|==|>=|>|=)?\s*(\d+)(?:\.\d+)?/.exec(spec);
|
|
158
|
+
if (!match)
|
|
159
|
+
return null;
|
|
160
|
+
return parseInt(match[1], 10);
|
|
161
|
+
}
|
|
162
|
+
function extractManifestLibraryMajors(pyprojectContent) {
|
|
163
|
+
const versions = new Map();
|
|
164
|
+
for (const library of TRACKED_PYTHON_LIBRARIES) {
|
|
165
|
+
const name = escapeRegExp(library);
|
|
166
|
+
// [tool.poetry.dependencies]
|
|
167
|
+
// pandas = "^2.1"
|
|
168
|
+
const poetryLineRe = new RegExp(`^\\s*${name}\\s*=\\s*["']([^"']+)["']`, 'im');
|
|
169
|
+
const poetryMatch = poetryLineRe.exec(pyprojectContent);
|
|
170
|
+
if (poetryMatch) {
|
|
171
|
+
const major = parseAuthoritativeMajorVersion(poetryMatch[1]);
|
|
172
|
+
if (major !== null) {
|
|
173
|
+
versions.set(library, major);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// [project]
|
|
178
|
+
// dependencies = ["pandas>=2.1", "numpy==1.26", "pandas[performance]>=2.1"]
|
|
179
|
+
const projectDependencyRe = new RegExp(`["']${name}(?:\\[[^\\]]+\\])?\\s*([~^=<>!]{1,2}\\s*\\d+(?:\\.\\d+)?)`, 'gim');
|
|
180
|
+
let match;
|
|
181
|
+
while ((match = projectDependencyRe.exec(pyprojectContent)) !== null) {
|
|
182
|
+
const major = parseAuthoritativeMajorVersion(match[1]);
|
|
183
|
+
if (major === null)
|
|
184
|
+
continue;
|
|
185
|
+
versions.set(library, major);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return versions;
|
|
190
|
+
}
|
|
191
|
+
function hasCompatibilityIntent(content, index) {
|
|
192
|
+
const windowStart = Math.max(0, index - 80);
|
|
193
|
+
const windowEnd = Math.min(content.length, index + 120);
|
|
194
|
+
const window = content.slice(windowStart, windowEnd);
|
|
195
|
+
return /\b(target|support|supports|compatibility|compatible|avoid|maintain|maintains)\b/i.test(window);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Detect when instruction files reference an older major version of a common
|
|
199
|
+
* Python data/web library than the major version declared in pyproject.toml.
|
|
200
|
+
*
|
|
201
|
+
* Example: CLAUDE.md says "Target Pandas 1.5 compatibility" but pyproject.toml
|
|
202
|
+
* declares `pandas = "^2.1"` — an agent following the instruction may avoid
|
|
203
|
+
* APIs that are safe for this project.
|
|
204
|
+
*/
|
|
205
|
+
function checkLibraryVersionMismatch(files, pyprojectContent) {
|
|
206
|
+
const manifestVersions = extractManifestLibraryMajors(pyprojectContent);
|
|
207
|
+
if (manifestVersions.size === 0)
|
|
208
|
+
return [];
|
|
209
|
+
const issues = [];
|
|
210
|
+
const reported = new Set();
|
|
211
|
+
for (const [library, manifestMajor] of manifestVersions) {
|
|
212
|
+
const displayName = displayLibraryName(library);
|
|
213
|
+
const libraryMentionRe = new RegExp(`\\b${escapeRegExp(displayName)}\\s+(?:v(?:ersion)?\\s*)?(\\d+)(?:\\.(\\d+|x))?\\b`, 'gi');
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
let match;
|
|
216
|
+
const re = new RegExp(libraryMentionRe.source, libraryMentionRe.flags);
|
|
217
|
+
while ((match = re.exec(file.content)) !== null) {
|
|
218
|
+
if (!hasCompatibilityIntent(file.content, match.index))
|
|
219
|
+
continue;
|
|
220
|
+
const instructionMajor = parseInt(match[1], 10);
|
|
221
|
+
if (instructionMajor >= manifestMajor)
|
|
222
|
+
continue;
|
|
223
|
+
const instructionVersion = match[2]
|
|
224
|
+
? `${instructionMajor}.${match[2]}`
|
|
225
|
+
: `${instructionMajor}.x`;
|
|
226
|
+
const dedupeKey = `${file.path}:${library}:${instructionVersion}`;
|
|
227
|
+
if (reported.has(dedupeKey))
|
|
228
|
+
continue;
|
|
229
|
+
reported.add(dedupeKey);
|
|
230
|
+
issues.push({
|
|
231
|
+
id: issueId(`library-version:${file.path}:${library}:${instructionVersion}`),
|
|
232
|
+
severity: 'high',
|
|
233
|
+
category: 'stale_instruction',
|
|
234
|
+
title: `${displayName} version mismatch: instruction says ${instructionVersion}, ` +
|
|
235
|
+
`pyproject.toml uses ${manifestMajor}.x`,
|
|
236
|
+
summary: `An instruction file refers to ${displayName} ${instructionVersion}, but ` +
|
|
237
|
+
`pyproject.toml declares ${library} ${manifestMajor}.x. An agent following ` +
|
|
238
|
+
`this instruction may avoid APIs that are valid for the installed dependency ` +
|
|
239
|
+
`or target compatibility behavior that no longer matches the project manifest.`,
|
|
240
|
+
filePaths: [file.path],
|
|
241
|
+
locations: [{ filePath: file.path }],
|
|
242
|
+
evidence: [
|
|
243
|
+
`Instruction says "${displayName} ${instructionVersion}"; pyproject.toml uses ${library} ${manifestMajor}.x`,
|
|
244
|
+
],
|
|
245
|
+
recommendation: `Update the instruction file to match ${displayName} ${manifestMajor}.x, or remove ` +
|
|
246
|
+
`the version-specific guidance and rely on pyproject.toml as the authoritative source.`,
|
|
247
|
+
confidence: 0.85,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return issues;
|
|
253
|
+
}
|
|
254
|
+
// ── Public detector ───────────────────────────────────────────────────────────
|
|
255
|
+
// ── JavaScript / TypeScript manifest checks ──────────────────────────────────
|
|
256
|
+
const TRACKED_JS_LIBRARIES = [
|
|
257
|
+
'react',
|
|
258
|
+
'next',
|
|
259
|
+
'typescript',
|
|
260
|
+
'vite',
|
|
261
|
+
'vitest',
|
|
262
|
+
'jest',
|
|
263
|
+
];
|
|
264
|
+
const JS_DISPLAY_NAMES = {
|
|
265
|
+
react: 'React',
|
|
266
|
+
next: 'Next.js',
|
|
267
|
+
typescript: 'TypeScript',
|
|
268
|
+
vite: 'Vite',
|
|
269
|
+
vitest: 'Vitest',
|
|
270
|
+
jest: 'Jest',
|
|
271
|
+
};
|
|
272
|
+
function parsePackageJsonVersions(packageJsonContent) {
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
parsed = JSON.parse(packageJsonContent);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return new Map();
|
|
279
|
+
}
|
|
280
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
281
|
+
return new Map();
|
|
282
|
+
}
|
|
283
|
+
const obj = parsed;
|
|
284
|
+
const versions = new Map();
|
|
285
|
+
const dependencyBlocks = [obj.dependencies, obj.devDependencies, obj.peerDependencies];
|
|
286
|
+
for (const block of dependencyBlocks) {
|
|
287
|
+
if (typeof block !== 'object' || block === null || Array.isArray(block))
|
|
288
|
+
continue;
|
|
289
|
+
const deps = block;
|
|
290
|
+
for (const library of TRACKED_JS_LIBRARIES) {
|
|
291
|
+
const spec = deps[library];
|
|
292
|
+
if (typeof spec !== 'string')
|
|
293
|
+
continue;
|
|
294
|
+
const major = parseAuthoritativeMajorVersion(spec);
|
|
295
|
+
if (major !== null)
|
|
296
|
+
versions.set(library, major);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return versions;
|
|
300
|
+
}
|
|
301
|
+
function checkJsLibraryVersionMismatch(files, packageJsonContent) {
|
|
302
|
+
const manifestVersions = parsePackageJsonVersions(packageJsonContent);
|
|
303
|
+
if (manifestVersions.size === 0)
|
|
304
|
+
return [];
|
|
305
|
+
const issues = [];
|
|
306
|
+
const reported = new Set();
|
|
307
|
+
for (const [library, manifestMajor] of manifestVersions) {
|
|
308
|
+
const displayName = JS_DISPLAY_NAMES[library];
|
|
309
|
+
const libraryMentionRe = new RegExp(`\\b${escapeRegExp(displayName)}\\s+(?:v(?:ersion)?\\s*)?(\\d+)(?:\\.(\\d+|x))?\\b`, 'gi');
|
|
310
|
+
for (const file of files) {
|
|
311
|
+
let match;
|
|
312
|
+
const re = new RegExp(libraryMentionRe.source, libraryMentionRe.flags);
|
|
313
|
+
while ((match = re.exec(file.content)) !== null) {
|
|
314
|
+
if (!hasJsVersionIntent(file.content, match.index))
|
|
315
|
+
continue;
|
|
316
|
+
const instructionMajor = parseInt(match[1], 10);
|
|
317
|
+
if (instructionMajor >= manifestMajor)
|
|
318
|
+
continue;
|
|
319
|
+
const instructionVersion = match[2]
|
|
320
|
+
? `${instructionMajor}.${match[2]}`
|
|
321
|
+
: `${instructionMajor}.x`;
|
|
322
|
+
const dedupeKey = `${file.path}:${library}:${instructionVersion}`;
|
|
323
|
+
if (reported.has(dedupeKey))
|
|
324
|
+
continue;
|
|
325
|
+
reported.add(dedupeKey);
|
|
326
|
+
issues.push({
|
|
327
|
+
id: issueId(`js-library-version:${file.path}:${library}:${instructionVersion}`),
|
|
328
|
+
severity: 'high',
|
|
329
|
+
category: 'stale_instruction',
|
|
330
|
+
title: `${displayName} version mismatch: instruction says ${instructionVersion}, ` +
|
|
331
|
+
`package.json uses ${manifestMajor}.x`,
|
|
332
|
+
summary: `An instruction file refers to ${displayName} ${instructionVersion}, but ` +
|
|
333
|
+
`package.json declares ${library} ${manifestMajor}.x. An agent following ` +
|
|
334
|
+
`this instruction may target compatibility behavior or APIs that no longer ` +
|
|
335
|
+
`match the installed dependency.`,
|
|
336
|
+
filePaths: [file.path],
|
|
337
|
+
locations: [{ filePath: file.path }],
|
|
338
|
+
evidence: [
|
|
339
|
+
`Instruction says "${displayName} ${instructionVersion}"; package.json uses ${library} ${manifestMajor}.x`,
|
|
340
|
+
],
|
|
341
|
+
recommendation: `Update the instruction file to match ${displayName} ${manifestMajor}.x, or remove ` +
|
|
342
|
+
`the version-specific guidance and rely on package.json as the authoritative source.`,
|
|
343
|
+
confidence: 0.85,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return issues;
|
|
349
|
+
}
|
|
350
|
+
function hasJsVersionIntent(content, index) {
|
|
351
|
+
const windowStart = Math.max(0, index - 100);
|
|
352
|
+
const windowEnd = Math.min(content.length, index + 140);
|
|
353
|
+
const window = content.slice(windowStart, windowEnd);
|
|
354
|
+
return /\b(use|using|target|support|supports|compatibility|compatible|avoid|maintain|migration|migrate|features?|apis?)\b/i.test(window);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Returns true when a line around the match position reads as a negation or
|
|
358
|
+
* advisory non-use (e.g. "do not use npm", "avoid npm", "don't use npm").
|
|
359
|
+
* Used to avoid false-positives on correct guidance.
|
|
360
|
+
*/
|
|
361
|
+
function isNegationContext(content, matchIndex) {
|
|
362
|
+
const lineStart = content.lastIndexOf('\n', matchIndex - 1) + 1;
|
|
363
|
+
const lineEnd = content.indexOf('\n', matchIndex);
|
|
364
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
365
|
+
return /\b(?:do\s+not|don'?t|avoid|never|instead\s+of|not\s+use|refrain\s+from)\b/i.test(line);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Detect when instructions tell the agent to use a different package manager
|
|
369
|
+
* than the one indicated by the repo's lockfile/package.json.
|
|
370
|
+
*
|
|
371
|
+
* Negation context ("do not use npm", "avoid yarn") is intentionally skipped
|
|
372
|
+
* because that kind of advisory guidance is correct in a pnpm project, not a
|
|
373
|
+
* mismatch.
|
|
374
|
+
*/
|
|
375
|
+
function checkPackageManagerMismatch(context) {
|
|
376
|
+
const { packageJson, files } = context;
|
|
377
|
+
const preferred = packageJson.packageManagerName;
|
|
378
|
+
if (preferred === 'unknown')
|
|
379
|
+
return null;
|
|
380
|
+
const alternatives = [
|
|
381
|
+
{ name: 'npm', label: 'npm' },
|
|
382
|
+
{ name: 'pnpm', label: 'pnpm' },
|
|
383
|
+
{ name: 'yarn', label: 'Yarn' },
|
|
384
|
+
{ name: 'bun', label: 'Bun' },
|
|
385
|
+
].filter((a) => a.name !== preferred);
|
|
386
|
+
const offendingFiles = [];
|
|
387
|
+
for (const file of files) {
|
|
388
|
+
for (const alt of alternatives) {
|
|
389
|
+
// Match commands like "npm install", "yarn add", "bun run"
|
|
390
|
+
const re = new RegExp(`\\b${alt.name}\\s+(?:install|add|run|exec|dlx|i)\\b`, 'gi');
|
|
391
|
+
let match;
|
|
392
|
+
let positiveHit = false;
|
|
393
|
+
while ((match = re.exec(file.content)) !== null) {
|
|
394
|
+
const lineStart = file.content.lastIndexOf('\n', match.index - 1) + 1;
|
|
395
|
+
const lineEnd = file.content.indexOf('\n', match.index);
|
|
396
|
+
const line = file.content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
397
|
+
if (preferred === 'pnpm' && /\bnpm\s+(?:install|i)\s+-g\s+pnpm\b/i.test(line))
|
|
398
|
+
continue;
|
|
399
|
+
if (!isNegationContext(file.content, match.index)) {
|
|
400
|
+
positiveHit = true;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (positiveHit) {
|
|
405
|
+
offendingFiles.push(file);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (offendingFiles.length === 0)
|
|
411
|
+
return null;
|
|
412
|
+
return {
|
|
413
|
+
id: 'manifest-package-manager-mismatch',
|
|
414
|
+
severity: 'warning',
|
|
415
|
+
category: 'conflict',
|
|
416
|
+
title: `Possible package manager mismatch: instructions use alternatives to ${preferred}`,
|
|
417
|
+
summary: `Instruction files refer to other package managers, but this project appears to use ${preferred} ` +
|
|
418
|
+
`(based on ${packageJson.lockfiles.join(', ') || 'package.json'}). ` +
|
|
419
|
+
`Mixing package managers can lead to inconsistent lockfiles and installation errors.`,
|
|
420
|
+
filePaths: offendingFiles.map((f) => f.path),
|
|
421
|
+
locations: offendingFiles.map((f) => ({ filePath: f.path })),
|
|
422
|
+
evidence: [
|
|
423
|
+
`Project appears to use ${preferred}`,
|
|
424
|
+
`Instruction files mention other package managers in command-like contexts`,
|
|
425
|
+
],
|
|
426
|
+
recommendation: `Update instruction files to consistently use ${preferred} commands.`,
|
|
427
|
+
fixRecipe: `This repo uses ${preferred}. Use \`${preferred} install\`, \`${preferred} test\`, and \`${preferred} build\`; do not substitute npm/yarn commands.`,
|
|
428
|
+
confidence: 0.8,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Detect when instructions reference scripts that do not exist in package.json.
|
|
433
|
+
*/
|
|
434
|
+
function checkMissingScripts(context) {
|
|
435
|
+
const { packageJson, files } = context;
|
|
436
|
+
const availableScripts = new Set(Object.keys(packageJson.scripts));
|
|
437
|
+
if (availableScripts.size === 0)
|
|
438
|
+
return [];
|
|
439
|
+
const issues = [];
|
|
440
|
+
// MF2: '_' was excluded from the script-name char class, so `pnpm build_all`
|
|
441
|
+
// could never match at all (the regex has nowhere to complete once it hits
|
|
442
|
+
// the underscore) — always silently skipped, never validated.
|
|
443
|
+
const SCRIPT_RUN_RE = /\b(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?([a-z0-9][a-z0-9:_-]*)\b/gi;
|
|
444
|
+
const reported = new Set();
|
|
445
|
+
// MF1: skip list was missing common built-in subcommands/prose verbs, so
|
|
446
|
+
// "npm ci" reported a missing script "ci", and bare "pnpm run" (prose, no
|
|
447
|
+
// actual script name following it) reported a missing script "run".
|
|
448
|
+
const SKIP_SUBCOMMANDS = new Set([
|
|
449
|
+
'install', 'add', 'remove', 'init', 'test', 'start', 'stop', 'restart',
|
|
450
|
+
'exec', 'dlx', 'i', 'help', 'run', 'ci', 'audit', 'update', 'outdated',
|
|
451
|
+
'publish', 'create', 'link', 'why', 'workspace', 'workspaces',
|
|
452
|
+
'store', 'ls', 'list', 'pack', 'unlink', 'uninstall', 'up', 'prune',
|
|
453
|
+
'package', 'packages', 'project', 'repo', 'repository',
|
|
454
|
+
]);
|
|
455
|
+
const ENGLISH_FILLER_WORDS = new Set([
|
|
456
|
+
'instead', 'of', 'packages', 'package', 'scripts', 'script', 'ecosystem',
|
|
457
|
+
'are', 'is', 'the', 'a', 'an', 'that', 'this', 'for', 'to', 'and', 'or',
|
|
458
|
+
'not', 'with', 'without', 'via', 'because', 'since', 'before', 'after',
|
|
459
|
+
'than', 'as', 'like', 'but', 'so',
|
|
460
|
+
]);
|
|
461
|
+
for (const file of files) {
|
|
462
|
+
let match;
|
|
463
|
+
const re = new RegExp(SCRIPT_RUN_RE.source, SCRIPT_RUN_RE.flags);
|
|
464
|
+
while ((match = re.exec(file.content)) !== null) {
|
|
465
|
+
const scriptName = match[1];
|
|
466
|
+
if (scriptName.startsWith('-')) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
// Skip common standard subcommands that aren't necessarily scripts
|
|
470
|
+
if (SKIP_SUBCOMMANDS.has(scriptName)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (ENGLISH_FILLER_WORDS.has(scriptName.toLowerCase())) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (!availableScripts.has(scriptName)) {
|
|
477
|
+
const dedupeKey = `${file.path}:${scriptName}`;
|
|
478
|
+
if (reported.has(dedupeKey))
|
|
479
|
+
continue;
|
|
480
|
+
reported.add(dedupeKey);
|
|
481
|
+
issues.push({
|
|
482
|
+
id: `manifest-missing-script:${file.path}:${scriptName}`,
|
|
483
|
+
severity: 'warning',
|
|
484
|
+
category: 'stale_instruction',
|
|
485
|
+
title: `Reference to missing script: "${scriptName}"`,
|
|
486
|
+
summary: `Instructions tell the agent to run \`${scriptName}\`, but this script is not defined in package.json. ` +
|
|
487
|
+
`The agent might attempt to run a command that fails.`,
|
|
488
|
+
filePaths: [file.path],
|
|
489
|
+
locations: [{ filePath: file.path }],
|
|
490
|
+
evidence: [`Found reference to \`${scriptName}\` in ${file.path}`],
|
|
491
|
+
recommendation: `Verify if the script name is correct or add the missing script to package.json.`,
|
|
492
|
+
confidence: 0.7,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return issues;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Detect when instructions mention a Node.js version lower than engines.node.
|
|
501
|
+
*/
|
|
502
|
+
function checkNodeVersionMismatch(context) {
|
|
503
|
+
const { packageJson, files } = context;
|
|
504
|
+
if (!packageJson.enginesNode)
|
|
505
|
+
return [];
|
|
506
|
+
// Simplified version parsing: look for >= or ^ followed by a number
|
|
507
|
+
const minVersionMatch = />=\s*(\d+)/.exec(packageJson.enginesNode) || /\^\s*(\d+)/.exec(packageJson.enginesNode);
|
|
508
|
+
if (!minVersionMatch)
|
|
509
|
+
return [];
|
|
510
|
+
const minNodeVersion = parseInt(minVersionMatch[1], 10);
|
|
511
|
+
const issues = [];
|
|
512
|
+
// MF3 / B2: no dedupe set, unlike every sibling check in this file — "Node 12"
|
|
513
|
+
// mentioned twice in one file emitted two issues sharing the identical id
|
|
514
|
+
// `manifest-node-version-mismatch:<path>:12`, corrupting downstream id-based
|
|
515
|
+
// dedup/trend logic.
|
|
516
|
+
const reported = new Set();
|
|
517
|
+
const NODE_VERSION_RE = /\bNode(?:\.js)?\s*(?:v)?(\d+)\b/gi;
|
|
518
|
+
for (const file of files) {
|
|
519
|
+
let match;
|
|
520
|
+
const re = new RegExp(NODE_VERSION_RE.source, NODE_VERSION_RE.flags);
|
|
521
|
+
while ((match = re.exec(file.content)) !== null) {
|
|
522
|
+
const instrVersion = parseInt(match[1], 10);
|
|
523
|
+
// MF3: no sibling check fires on a negated mention ("Do not use Node
|
|
524
|
+
// 14") — this one did, since it had no negation-context guard at all.
|
|
525
|
+
if (isNegationContext(file.content, match.index))
|
|
526
|
+
continue;
|
|
527
|
+
if (instrVersion < minNodeVersion) {
|
|
528
|
+
const dedupeKey = `${file.path}:${instrVersion}`;
|
|
529
|
+
if (reported.has(dedupeKey))
|
|
530
|
+
continue;
|
|
531
|
+
reported.add(dedupeKey);
|
|
532
|
+
issues.push({
|
|
533
|
+
id: `manifest-node-version-mismatch:${file.path}:${instrVersion}`,
|
|
534
|
+
severity: 'warning',
|
|
535
|
+
category: 'stale_instruction',
|
|
536
|
+
title: `Node.js version mismatch: instruction says ${instrVersion}, project requires ≥${minNodeVersion}`,
|
|
537
|
+
summary: `Instructions mention Node.js ${instrVersion}, but package.json specifies a minimum version of ${minNodeVersion} in \`engines.node\`.`,
|
|
538
|
+
filePaths: [file.path],
|
|
539
|
+
locations: [{ filePath: file.path }],
|
|
540
|
+
evidence: [`Instruction says Node ${instrVersion}; engines.node says ${packageJson.enginesNode}`],
|
|
541
|
+
recommendation: `Update instructions to reflect the minimum supported Node.js version (${minNodeVersion}).`,
|
|
542
|
+
confidence: 0.8,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return issues;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Returns true when the tool mention at `matchIndex` appears to be a negation
|
|
551
|
+
* or migration-history reference rather than an active instruction to use it.
|
|
552
|
+
*
|
|
553
|
+
* Examples that should NOT produce a finding:
|
|
554
|
+
* - "Do not use Jest; we use Vitest."
|
|
555
|
+
* - "We migrated from Jest to Vitest last quarter."
|
|
556
|
+
* - "Avoid Cypress — we use Playwright instead."
|
|
557
|
+
*/
|
|
558
|
+
function isToolNegationOrHistoryContext(content, matchIndex, toolName) {
|
|
559
|
+
const lineStart = content.lastIndexOf('\n', matchIndex - 1) + 1;
|
|
560
|
+
const lineEnd = content.indexOf('\n', matchIndex);
|
|
561
|
+
const line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
|
|
562
|
+
// Negation phrases before the tool name
|
|
563
|
+
if (/\b(?:do\s+not|don'?t|avoid|never|refrain\s+from|not\s+use)\b/i.test(line.slice(0, matchIndex - lineStart + toolName.length + 5))) {
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
// Migration/history phrases anywhere in the line
|
|
567
|
+
if (/\b(?:migrat(?:ed?|ing)|replaced?|switched?\s+(?:from|away)|moved?\s+(?:from|away)|no\s+longer\s+use|previously\s+used?)\b/i.test(line)) {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Detect when instructions reference one JS tool when another similar tool is used.
|
|
574
|
+
*
|
|
575
|
+
* Skips negation contexts ("do not use Jest") and migration-history prose
|
|
576
|
+
* ("migrated from Jest to Vitest") to avoid false positives on correct
|
|
577
|
+
* advisory guidance.
|
|
578
|
+
*/
|
|
579
|
+
function checkJsToolingMismatch(context) {
|
|
580
|
+
const { packageJson, files } = context;
|
|
581
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
582
|
+
const issues = [];
|
|
583
|
+
const toolPairs = [
|
|
584
|
+
{ name: 'Jest', package: 'jest', alternatives: ['vitest'] },
|
|
585
|
+
{ name: 'Vitest', package: 'vitest', alternatives: ['jest'] },
|
|
586
|
+
{ name: 'Playwright', package: '@playwright/test', alternatives: ['cypress'] },
|
|
587
|
+
{ name: 'Cypress', package: 'cypress', alternatives: ['@playwright/test'] },
|
|
588
|
+
];
|
|
589
|
+
for (const tool of toolPairs) {
|
|
590
|
+
if (!deps[tool.package]) {
|
|
591
|
+
// Tool is NOT in dependencies. Check if any alternative IS.
|
|
592
|
+
const usedAlt = tool.alternatives.find((alt) => deps[alt]);
|
|
593
|
+
if (usedAlt) {
|
|
594
|
+
// Tool not used, but alternative is. Check if instructions positively mention the unused tool.
|
|
595
|
+
const toolRe = new RegExp(`\\b${tool.name}\\b`, 'gi');
|
|
596
|
+
const offendingFiles = files.filter((f) => {
|
|
597
|
+
let match;
|
|
598
|
+
const re = new RegExp(toolRe.source, toolRe.flags);
|
|
599
|
+
while ((match = re.exec(f.content)) !== null) {
|
|
600
|
+
if (!isToolNegationOrHistoryContext(f.content, match.index, tool.name)) {
|
|
601
|
+
return true; // At least one positive mention found
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
});
|
|
606
|
+
if (offendingFiles.length > 0) {
|
|
607
|
+
issues.push({
|
|
608
|
+
id: `manifest-js-tooling-mismatch:${tool.package}`,
|
|
609
|
+
severity: 'warning',
|
|
610
|
+
category: 'conflict',
|
|
611
|
+
title: `Possible tooling mismatch: instructions mention ${tool.name} but project uses ${usedAlt}`,
|
|
612
|
+
summary: `Instructions appear to reference ${tool.name}, but this package is not in dependencies. ` +
|
|
613
|
+
`${usedAlt} is present instead. Consider reviewing whether the instructions are current.`,
|
|
614
|
+
filePaths: offendingFiles.map((f) => f.path),
|
|
615
|
+
locations: offendingFiles.map((f) => ({ filePath: f.path })),
|
|
616
|
+
evidence: [
|
|
617
|
+
`Instructions mention ${tool.name}`,
|
|
618
|
+
`${usedAlt} found in dependencies`,
|
|
619
|
+
`${tool.package} not found in dependencies`,
|
|
620
|
+
],
|
|
621
|
+
recommendation: `Update instructions to reference ${usedAlt} instead of ${tool.name}, or confirm migration is complete.`,
|
|
622
|
+
confidence: 0.75,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return issues;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Cross-reference instruction files against project manifests and flag
|
|
632
|
+
* inconsistencies that would mislead an agent about the build setup.
|
|
633
|
+
*
|
|
634
|
+
* @param files Scanned instruction files.
|
|
635
|
+
* @param manifests Raw text of any manifest files that were read by the scanner.
|
|
636
|
+
* @param repoRoot Absolute path to the repo root (used for file existence checks).
|
|
637
|
+
*/
|
|
638
|
+
export function detectManifestConsistency(context) {
|
|
639
|
+
const { files, manifests, repoRoot } = context;
|
|
640
|
+
const issues = [];
|
|
641
|
+
if (manifests.packageJson) {
|
|
642
|
+
issues.push(...checkJsLibraryVersionMismatch(files, manifests.packageJson));
|
|
643
|
+
issues.push(...checkMissingScripts(context));
|
|
644
|
+
issues.push(...checkNodeVersionMismatch(context));
|
|
645
|
+
issues.push(...checkJsToolingMismatch(context));
|
|
646
|
+
}
|
|
647
|
+
const pmIssue = checkPackageManagerMismatch(context);
|
|
648
|
+
if (pmIssue)
|
|
649
|
+
issues.push(pmIssue);
|
|
650
|
+
if (manifests.pyproject) {
|
|
651
|
+
const toolingIssue = checkPipVsPoetry(files, manifests.pyproject, repoRoot);
|
|
652
|
+
if (toolingIssue)
|
|
653
|
+
issues.push(toolingIssue);
|
|
654
|
+
issues.push(...checkPythonVersionMismatch(files, manifests.pyproject));
|
|
655
|
+
issues.push(...checkLibraryVersionMismatch(files, manifests.pyproject));
|
|
656
|
+
}
|
|
657
|
+
return issues;
|
|
658
|
+
}
|
|
659
|
+
//# sourceMappingURL=manifest-consistency.js.map
|