@promptci/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/action-pinning.d.ts +37 -0
  2. package/dist/action-pinning.d.ts.map +1 -0
  3. package/dist/action-pinning.js +277 -0
  4. package/dist/action-pinning.js.map +1 -0
  5. package/dist/agent-practices.d.ts +1 -1
  6. package/dist/agent-practices.d.ts.map +1 -1
  7. package/dist/agent-practices.js +107 -43
  8. package/dist/agent-practices.js.map +1 -1
  9. package/dist/ai-config.d.ts +42 -3
  10. package/dist/ai-config.d.ts.map +1 -1
  11. package/dist/ai-config.js +210 -12
  12. package/dist/ai-config.js.map +1 -1
  13. package/dist/alias-files.d.ts +33 -0
  14. package/dist/alias-files.d.ts.map +1 -0
  15. package/dist/alias-files.js +60 -0
  16. package/dist/alias-files.js.map +1 -0
  17. package/dist/buried-critical.d.ts.map +1 -1
  18. package/dist/buried-critical.js +4 -9
  19. package/dist/buried-critical.js.map +1 -1
  20. package/dist/canonical-owner.d.ts.map +1 -1
  21. package/dist/canonical-owner.js +45 -25
  22. package/dist/canonical-owner.js.map +1 -1
  23. package/dist/ci-alignment.d.ts.map +1 -1
  24. package/dist/ci-alignment.js +3 -2
  25. package/dist/ci-alignment.js.map +1 -1
  26. package/dist/command-validity.d.ts +16 -0
  27. package/dist/command-validity.d.ts.map +1 -1
  28. package/dist/command-validity.js +337 -63
  29. package/dist/command-validity.js.map +1 -1
  30. package/dist/conflicts.d.ts.map +1 -1
  31. package/dist/conflicts.js +3 -2
  32. package/dist/conflicts.js.map +1 -1
  33. package/dist/context-bloat.d.ts +1 -1
  34. package/dist/context-bloat.d.ts.map +1 -1
  35. package/dist/context-bloat.js +18 -7
  36. package/dist/context-bloat.js.map +1 -1
  37. package/dist/context-optimizer.d.ts.map +1 -1
  38. package/dist/context-optimizer.js +5 -2
  39. package/dist/context-optimizer.js.map +1 -1
  40. package/dist/custom-rules.d.ts.map +1 -1
  41. package/dist/custom-rules.js +6 -1
  42. package/dist/custom-rules.js.map +1 -1
  43. package/dist/dead-references.d.ts +8 -2
  44. package/dist/dead-references.d.ts.map +1 -1
  45. package/dist/dead-references.js +306 -61
  46. package/dist/dead-references.js.map +1 -1
  47. package/dist/dependency-staleness.d.ts +35 -0
  48. package/dist/dependency-staleness.d.ts.map +1 -0
  49. package/dist/dependency-staleness.js +1001 -0
  50. package/dist/dependency-staleness.js.map +1 -0
  51. package/dist/detectors.d.ts.map +1 -1
  52. package/dist/detectors.js +31 -6
  53. package/dist/detectors.js.map +1 -1
  54. package/dist/duplicates.d.ts.map +1 -1
  55. package/dist/duplicates.js +5 -4
  56. package/dist/duplicates.js.map +1 -1
  57. package/dist/evidence.d.ts +11 -0
  58. package/dist/evidence.d.ts.map +1 -1
  59. package/dist/evidence.js +40 -0
  60. package/dist/evidence.js.map +1 -1
  61. package/dist/finding-id.d.ts +21 -0
  62. package/dist/finding-id.d.ts.map +1 -0
  63. package/dist/finding-id.js +31 -0
  64. package/dist/finding-id.js.map +1 -0
  65. package/dist/frameworks/dotnet.js +2 -2
  66. package/dist/frameworks/dotnet.js.map +1 -1
  67. package/dist/frameworks/rust.d.ts.map +1 -1
  68. package/dist/frameworks/rust.js +2 -1
  69. package/dist/frameworks/rust.js.map +1 -1
  70. package/dist/git-symlinks.d.ts +30 -0
  71. package/dist/git-symlinks.d.ts.map +1 -0
  72. package/dist/git-symlinks.js +161 -0
  73. package/dist/git-symlinks.js.map +1 -0
  74. package/dist/gitignore.d.ts +22 -0
  75. package/dist/gitignore.d.ts.map +1 -0
  76. package/dist/gitignore.js +157 -0
  77. package/dist/gitignore.js.map +1 -0
  78. package/dist/health-score.d.ts.map +1 -1
  79. package/dist/health-score.js +34 -5
  80. package/dist/health-score.js.map +1 -1
  81. package/dist/index.d.ts +9 -3
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +5 -2
  84. package/dist/index.js.map +1 -1
  85. package/dist/makefile.d.ts +37 -0
  86. package/dist/makefile.d.ts.map +1 -0
  87. package/dist/makefile.js +156 -0
  88. package/dist/makefile.js.map +1 -0
  89. package/dist/manifest-consistency.d.ts.map +1 -1
  90. package/dist/manifest-consistency.js +52 -8
  91. package/dist/manifest-consistency.js.map +1 -1
  92. package/dist/negative-instructions.d.ts.map +1 -1
  93. package/dist/negative-instructions.js +4 -9
  94. package/dist/negative-instructions.js.map +1 -1
  95. package/dist/path-containment.d.ts +29 -0
  96. package/dist/path-containment.d.ts.map +1 -1
  97. package/dist/path-containment.js +70 -0
  98. package/dist/path-containment.js.map +1 -1
  99. package/dist/prompt-cache.d.ts.map +1 -1
  100. package/dist/prompt-cache.js +8 -6
  101. package/dist/prompt-cache.js.map +1 -1
  102. package/dist/repo-context.d.ts +53 -0
  103. package/dist/repo-context.d.ts.map +1 -1
  104. package/dist/repo-context.js +134 -11
  105. package/dist/repo-context.js.map +1 -1
  106. package/dist/report.d.ts +7 -0
  107. package/dist/report.d.ts.map +1 -1
  108. package/dist/report.js +36 -7
  109. package/dist/report.js.map +1 -1
  110. package/dist/scan.d.ts.map +1 -1
  111. package/dist/scan.js +41 -2
  112. package/dist/scan.js.map +1 -1
  113. package/dist/scanner.d.ts +49 -1
  114. package/dist/scanner.d.ts.map +1 -1
  115. package/dist/scanner.js +323 -38
  116. package/dist/scanner.js.map +1 -1
  117. package/dist/security-pack.d.ts.map +1 -1
  118. package/dist/security-pack.js +6 -4
  119. package/dist/security-pack.js.map +1 -1
  120. package/dist/skill-supply-chain.d.ts +132 -0
  121. package/dist/skill-supply-chain.d.ts.map +1 -0
  122. package/dist/skill-supply-chain.js +2954 -0
  123. package/dist/skill-supply-chain.js.map +1 -0
  124. package/dist/skills-detector.d.ts +7 -3
  125. package/dist/skills-detector.d.ts.map +1 -1
  126. package/dist/skills-detector.js +98 -22
  127. package/dist/skills-detector.js.map +1 -1
  128. package/dist/stale-instructions.d.ts.map +1 -1
  129. package/dist/stale-instructions.js +2 -1
  130. package/dist/stale-instructions.js.map +1 -1
  131. package/dist/suppression.d.ts +2 -0
  132. package/dist/suppression.d.ts.map +1 -1
  133. package/dist/suppression.js +5 -2
  134. package/dist/suppression.js.map +1 -1
  135. package/dist/types.d.ts +35 -2
  136. package/dist/types.d.ts.map +1 -1
  137. package/dist/types.js +22 -0
  138. package/dist/types.js.map +1 -1
  139. package/dist/vague-guidance.d.ts.map +1 -1
  140. package/dist/vague-guidance.js +2 -1
  141. package/dist/vague-guidance.js.map +1 -1
  142. package/dist/windsurf-detector.d.ts.map +1 -1
  143. package/dist/windsurf-detector.js +15 -6
  144. package/dist/windsurf-detector.js.map +1 -1
  145. package/dist/within-section-dedup.d.ts.map +1 -1
  146. package/dist/within-section-dedup.js +4 -9
  147. package/dist/within-section-dedup.js.map +1 -1
  148. package/dist/workspace-manifests.d.ts +80 -0
  149. package/dist/workspace-manifests.d.ts.map +1 -0
  150. package/dist/workspace-manifests.js +333 -0
  151. package/dist/workspace-manifests.js.map +1 -0
  152. package/package.json +1 -1
@@ -0,0 +1,1001 @@
1
+ /**
2
+ * Dependency and framework-API staleness against the repo's manifests
3
+ * (pcic-2b6.11).
4
+ *
5
+ * Instruction files age: they keep recommending a package that was swapped out,
6
+ * a library the ecosystem has abandoned, or an API the installed major version
7
+ * no longer has. An agent follows that text literally. Three rules, all
8
+ * deterministic and offline, all deliberately conservative (a missed finding
9
+ * costs less than a wrong one):
10
+ *
11
+ * (a) MISSING PACKAGE — an npm-shaped package named by an install command
12
+ * (`npm i x`, `pnpm add x`) or an import/require line that no package.json
13
+ * in the repo declares. Low confidence (0.5): the text may describe an
14
+ * optional or external tool. Prose that merely calls a code span a
15
+ * "package" is not enough, and an install after a `cd` (or with
16
+ * `--prefix`/`-C`) targets a directory this check cannot resolve.
17
+ * (b) DEPRECATED PACKAGE — a short list of packages their maintainers have
18
+ * deprecated or put in maintenance mode, recommended by the instructions.
19
+ * `info` when a manifest declares it (the instructions are consistent
20
+ * with the repo, the dependency is just old), `warning` when no manifest
21
+ * does (the instructions point at a deprecated package that is not even
22
+ * installed). Reported instead of (a) for those names.
23
+ * (c) VERSION-GATED API — `ReactDOM.render` against React >= 18 (warning) or
24
+ * >= 19 (removed: high), `getInitialProps` in a Next.js app that uses the
25
+ * app/ router, and NgModule-centric guidance for an Angular >= 17 project
26
+ * that has no `*.module.ts` files.
27
+ *
28
+ * Only agent-facing instruction files are read (not README/docs, which mostly
29
+ * describe how end users install the project). Prose that tells the agent NOT
30
+ * to use something, or narrates a migration away from it, is never flagged.
31
+ */
32
+ import * as crypto from 'node:crypto';
33
+ import * as fs from 'node:fs';
34
+ import * as path from 'node:path';
35
+ import fg from 'fast-glob';
36
+ import { INSTRUCTION_FILE_TYPES } from './types.js';
37
+ import { isExcludedPath } from './repo-context.js';
38
+ import { scanFencedLines } from './markdown-fences.js';
39
+ import { snippet } from './evidence.js';
40
+ import { resolveReadableWithinRoot } from './path-containment.js';
41
+ import { isKnownBinary, parseWorkspaceManifest } from './workspace-manifests.js';
42
+ import { MAX_FILE_SIZE } from './scanner.js';
43
+ // ── Name filters ──────────────────────────────────────────────────────────────
44
+ /** Node.js core modules (static: output must not depend on the Node version that runs the scan). */
45
+ const NODE_BUILTINS = new Set([
46
+ 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto',
47
+ 'dgram', 'diagnostics_channel', 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https',
48
+ 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring',
49
+ 'readline', 'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events', 'tty',
50
+ 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib', 'test', 'sqlite',
51
+ ]);
52
+ /** Package managers, runtimes and version managers: commands, not dependencies. */
53
+ const WELL_KNOWN_CLIS = new Set([
54
+ 'npm', 'pnpm', 'yarn', 'npx', 'bun', 'bunx', 'deno', 'node', 'corepack', 'nvm', 'fnm', 'volta', 'git',
55
+ ]);
56
+ const PLACEHOLDER_NAMES = new Set([
57
+ 'package', 'package-name', 'pkg', 'name', 'module', 'module-name', 'lib', 'library', 'dependency', 'dep',
58
+ 'foo', 'bar', 'baz', 'example', 'sample', 'x', 'y', 'z', 'a', 'b', 'c', 'my-package', 'my-lib', 'my-app',
59
+ 'some-package', 'some-lib', 'your-package', 'package-a', 'package-b', 'new-package', 'new-dependency',
60
+ ]);
61
+ const PLACEHOLDER_PREFIX_RE = /^(?:your|my|some|example|sample|foo|bar|baz|pkg|package)(?:-|$)/;
62
+ const PLACEHOLDER_SCOPES = new Set([
63
+ 'scope', 'your-scope', 'my-scope', 'org', 'your-org', 'my-org', 'yourorg', 'myorg', 'company', 'your-company',
64
+ 'acme', 'example', 'project', 'repo', 'workspace', 'name', 'user', 'username', 'namespace', 'team', 'monorepo',
65
+ 'scope-name', 'org-name', 'your-username',
66
+ ]);
67
+ /**
68
+ * Words that are almost always a path alias or a local directory when they
69
+ * appear as an import's first segment (`import x from 'components/Button'`),
70
+ * not a package.
71
+ */
72
+ const ALIAS_LIKE_NAMES = new Set([
73
+ 'src', 'app', 'lib', 'libs', 'components', 'component', 'utils', 'util', 'hooks', 'types', 'config', 'styles',
74
+ 'assets', 'public', 'test', 'tests', 'shared', 'common', 'core', 'store', 'stores', 'services', 'service',
75
+ 'pages', 'features', 'modules', 'server', 'client', 'api', 'db', 'models', 'helpers', 'constants', 'context',
76
+ 'contexts', 'layouts', 'routes', 'views', 'data', 'mocks', 'fixtures', 'schemas', 'icons', 'images', 'ui',
77
+ 'actions', 'reducers', 'providers', 'middleware', 'controllers', 'entities', 'domain',
78
+ // Words that read as a package but name a dependency group or an adjective.
79
+ 'dev', 'prod', 'optional', 'peer', 'build', 'lint', 'main', 'default', 'latest', 'local', 'root', 'runtime',
80
+ 'required', 'direct', 'transitive', 'external', 'internal', 'vendor',
81
+ ]);
82
+ /**
83
+ * A line that tells the reader NOT to use something, or narrates moving away
84
+ * from it. Prose like this is correct guidance and must never be flagged.
85
+ */
86
+ const NEGATION_RE = /\b(?:not|never|don'?t|do\s+not|doesn'?t|avoid|instead\s+of|rather\s+than|no\s+longer|replace[sd]?|replacing|migrat\w+|deprecated|removed|legacy|without|except|unlike|vs\.?|versus|older|previously|formerly|used\s+to|drop(?:ped)?|stop\s+using|forbidden|banned|prohibited|phas(?:e[sd]?|ing)\s+out|being\s+replaced|sunset\w*)\b/i;
87
+ const DEPRECATED_PACKAGES = {
88
+ moment: {
89
+ status: 'is a legacy project in maintenance mode',
90
+ replacement: 'date-fns, Luxon, Day.js, or the Temporal API',
91
+ distinctive: true,
92
+ command: false,
93
+ },
94
+ request: {
95
+ status: 'was deprecated by its maintainers in 2020',
96
+ replacement: 'the built-in fetch, undici, got, or axios',
97
+ distinctive: false,
98
+ command: false,
99
+ },
100
+ tslint: {
101
+ status: 'was deprecated in favor of ESLint',
102
+ replacement: 'ESLint with typescript-eslint',
103
+ distinctive: true,
104
+ command: true,
105
+ },
106
+ 'node-sass': {
107
+ status: 'is deprecated (LibSass is no longer maintained)',
108
+ replacement: 'sass (Dart Sass)',
109
+ distinctive: true,
110
+ command: true,
111
+ },
112
+ 'react-scripts': {
113
+ status: 'belongs to Create React App, which the React team has deprecated',
114
+ replacement: 'Vite, Next.js, or another maintained React toolchain',
115
+ distinctive: true,
116
+ command: true,
117
+ },
118
+ enzyme: {
119
+ status: 'is unmaintained and has no official adapter for current React versions',
120
+ replacement: '@testing-library/react',
121
+ distinctive: true,
122
+ command: false,
123
+ },
124
+ };
125
+ const CD_RE = /(?:^|[\s;&|(])(?:cd|pushd)\s+\S/;
126
+ const SHELL_LANGS = new Set([
127
+ '', 'sh', 'bash', 'shell', 'zsh', 'console', 'shellsession', 'terminal', 'powershell', 'ps1', 'pwsh', 'cmd',
128
+ ]);
129
+ const JS_LANGS = new Set([
130
+ '', 'js', 'jsx', 'ts', 'tsx', 'javascript', 'typescript', 'mjs', 'cjs', 'mts', 'cts', 'vue', 'svelte', 'astro',
131
+ ]);
132
+ const INLINE_CODE_RE = /(`+)([^`\n]+)\1/g;
133
+ /** Code segments (fenced lines and inline spans) and the plain prose lines of one file. */
134
+ function readFile(file) {
135
+ const segments = [];
136
+ const prose = [];
137
+ let sawCd = false;
138
+ for (const fl of scanFencedLines(file.content)) {
139
+ const text = fl.text.replace(/\r$/, '');
140
+ if (fl.kind === 'open')
141
+ sawCd = false;
142
+ if (fl.kind === 'content') {
143
+ segments.push({ text, line: fl.lineNumber, fenced: true, lang: fl.lang, lineText: text, afterCd: sawCd });
144
+ if (CD_RE.test(text))
145
+ sawCd = true;
146
+ }
147
+ else if (fl.kind === 'text') {
148
+ prose.push({ line: fl.lineNumber, text });
149
+ const re = new RegExp(INLINE_CODE_RE.source, INLINE_CODE_RE.flags);
150
+ let m;
151
+ while ((m = re.exec(text)) !== null) {
152
+ segments.push({ text: m[2].trim(), line: fl.lineNumber, fenced: false, lang: '', lineText: text, afterCd: false });
153
+ }
154
+ }
155
+ }
156
+ return { segments, prose };
157
+ }
158
+ const NPM_NAME_RE = /^(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
159
+ // `.js` is deliberately absent: `chart.js` and `three.js` are real package names.
160
+ const FILE_EXTENSION_RE = /\.(?:json|jsonc|ts|tsx|mts|cts|jsx|mjs|cjs|md|mdx|ya?ml|toml|lock|css|scss|sass|less|styl|html|sh|txt|env|config|dart|go|py|rs|rb|java|kt|swift|vue|svelte|astro|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|otf|mp[34]|wasm|graphql|gql|sql)$/i;
161
+ /** `github.com/x/y`, `golang.org/x/net`: a Go/Dart-style module path, not an npm package. */
162
+ const DOMAIN_FIRST_SEGMENT_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.(?:com|org|net|io|dev|co|app|ai|me|sh|xyz|gg|tv|cc|us|uk|de|fr|ru|cn|jp|to|in|so|cloud)\//i;
163
+ /** `name`, `name@1.2.3`, `@scope/name@latest` -> the package name; undefined when it is not npm-shaped. */
164
+ function nameFromInstallSpec(raw) {
165
+ const spec = raw.replace(/^['"]|['"]$/g, '');
166
+ const m = /^(@[^/@\s]+\/[^/@\s]+|[^/@\s][^@/\s]*)(?:@\S*)?$/.exec(spec);
167
+ if (!m)
168
+ return undefined;
169
+ const name = m[1];
170
+ return NPM_NAME_RE.test(name) && !FILE_EXTENSION_RE.test(name) ? name : undefined;
171
+ }
172
+ /** `pkg`, `pkg/sub/path`, `@scope/pkg/sub` -> the package name; undefined for relative/absolute/alias/protocol specifiers. */
173
+ function nameFromImportSpecifier(spec) {
174
+ if (/^[./~#$]|^@\/|:|\s|\*|<|\{/.test(spec))
175
+ return undefined;
176
+ if (DOMAIN_FIRST_SEGMENT_RE.test(spec))
177
+ return undefined;
178
+ const m = /^(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._-]*)(?:\/.*)?$/.exec(spec);
179
+ if (!m)
180
+ return undefined;
181
+ // `uno.css`, `foo_bar.dart`, `logo.svg`: a file or virtual-module id, not a package.
182
+ return FILE_EXTENSION_RE.test(m[1]) ? undefined : m[1];
183
+ }
184
+ /** Flags that point the package manager at another directory. */
185
+ const CWD_FLAGS = new Set(['-C', '--cwd', '--prefix', '--dir']);
186
+ const PRE_VERB_VALUE_FLAGS = new Set(['--filter', '-F', '--workspace', ...CWD_FLAGS]);
187
+ const POST_VERB_BARE_FLAGS = new Set([
188
+ '-D', '--save-dev', '-S', '--save', '-E', '--save-exact', '-O', '--save-optional', '-P', '--save-prod',
189
+ '--dev', '--exact', '--optional', '--peer', '--no-save', '--force', '--legacy-peer-deps', '--ignore-scripts',
190
+ '--silent', '--no-audit', '--no-fund', '--prefer-offline', '--workspace-root', '-W',
191
+ ]);
192
+ const INSTALL_VERBS = {
193
+ npm: new Set(['install', 'i', 'add']),
194
+ pnpm: new Set(['add', 'install', 'i']),
195
+ yarn: new Set(['add']),
196
+ bun: new Set(['add', 'install', 'i']),
197
+ };
198
+ /**
199
+ * Package names an install command adds: `npm i x`, `pnpm add -D y z`,
200
+ * `yarn workspace web add w`, `pnpm --filter web add v`. Global installs
201
+ * (`-g`) are tools, not dependencies, and are skipped; so is any command with a
202
+ * flag this parser does not know (it cannot tell a flag's value from a package).
203
+ */
204
+ function installedPackageNames(text, afterCd) {
205
+ if (afterCd)
206
+ return []; // an earlier `cd` in the block: the install lands in a directory we cannot resolve
207
+ const names = [];
208
+ for (const command of text.split(/&&|\|\||[;|]/)) {
209
+ // Drop a shell prompt (`$ `) or a subshell opener (`(cd x && ...`) before the command word.
210
+ const tokens = command.replace(/^\s*(?:[$>%]\s+|[({]\s*)+/, '').trim().split(/\s+/);
211
+ const manager = tokens[0];
212
+ if (manager === 'cd' || manager === 'pushd')
213
+ break; // the rest of the line runs somewhere else
214
+ const verbs = manager ? INSTALL_VERBS[manager] : undefined;
215
+ if (!verbs)
216
+ continue;
217
+ // The install targets a directory given by a flag, whose manifest is not necessarily the root's.
218
+ if (tokens.some((t) => CWD_FLAGS.has(t) || /^--(?:prefix|cwd|dir)=/.test(t)))
219
+ continue;
220
+ let i = 1;
221
+ let foundVerb = false;
222
+ for (; i < tokens.length; i++) {
223
+ const tok = tokens[i];
224
+ if (verbs.has(tok)) {
225
+ foundVerb = true;
226
+ i++;
227
+ break;
228
+ }
229
+ if (PRE_VERB_VALUE_FLAGS.has(tok) || (manager === 'npm' && tok === '-w') || (manager === 'yarn' && tok === 'workspace')) {
230
+ i++; // its value
231
+ continue;
232
+ }
233
+ if (tok.startsWith('-'))
234
+ continue;
235
+ break; // another subcommand (run, dlx, exec, ...)
236
+ }
237
+ if (!foundVerb)
238
+ continue;
239
+ const found = [];
240
+ let bail = false;
241
+ for (; i < tokens.length; i++) {
242
+ const tok = tokens[i];
243
+ if (tok.startsWith('#'))
244
+ break;
245
+ if (tok === '-g' || tok === '--global' || tok === '--location=global') {
246
+ bail = true;
247
+ break;
248
+ }
249
+ if (tok.startsWith('-')) {
250
+ if (POST_VERB_BARE_FLAGS.has(tok) || (manager === 'pnpm' && tok === '-w') || tok.includes('='))
251
+ continue;
252
+ if (PRE_VERB_VALUE_FLAGS.has(tok) || (manager === 'npm' && tok === '-w')) {
253
+ i++;
254
+ continue;
255
+ }
256
+ bail = true; // unknown flag
257
+ break;
258
+ }
259
+ const name = nameFromInstallSpec(tok);
260
+ if (name === undefined) {
261
+ bail = true; // a path, URL, tag or variable: not a plain registry name
262
+ break;
263
+ }
264
+ found.push(name);
265
+ }
266
+ if (!bail)
267
+ names.push(...found);
268
+ }
269
+ return names;
270
+ }
271
+ /** Forms only JavaScript uses: safe in an unlabeled fence or an inline span, where Go/Dart/Python also appear. */
272
+ const STRICT_IMPORT_PATTERNS = [
273
+ /\bimport\s+(?:type\s+)?[^'"`;]*?\sfrom\s+['"]([^'"]+)['"]/g,
274
+ /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
275
+ /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g,
276
+ /^\s*\}?\s*from\s+['"]([^'"]+)['"]/g,
277
+ ];
278
+ /** `import 'x'` (side-effect import) reads as Go/Dart too, so it only counts in a JS-labeled fence. */
279
+ const SIDE_EFFECT_IMPORT_PATTERN = /\bimport\s+['"]([^'"]+)['"]/g;
280
+ function importedPackageNames(text, strictOnly) {
281
+ const names = [];
282
+ const patterns = strictOnly ? STRICT_IMPORT_PATTERNS : [...STRICT_IMPORT_PATTERNS, SIDE_EFFECT_IMPORT_PATTERN];
283
+ for (const pattern of patterns) {
284
+ const re = new RegExp(pattern.source, pattern.flags);
285
+ let m;
286
+ while ((m = re.exec(text)) !== null) {
287
+ const name = nameFromImportSpecifier(m[1]);
288
+ if (name !== undefined)
289
+ names.push(name);
290
+ }
291
+ }
292
+ return names;
293
+ }
294
+ /**
295
+ * "the `x` package", "library `x`": a code span the prose itself calls a package.
296
+ * Only trusted for the deprecated-package list: as a trigger for an arbitrary
297
+ * name it is too loose ("the `playwright-core` package root", "install
298
+ * `cloudflared`" are not npm dependencies the repo should declare).
299
+ */
300
+ const QUALIFIED_SPAN_PATTERNS = [
301
+ /\b(?:package|library|dependency)\s+`([^`\s]+)`/gi,
302
+ /`([^`\s]+)`\s+(?:npm\s+)?(?:package|library|dependency)\b/gi,
303
+ ];
304
+ function qualifiedSpanNames(line) {
305
+ const names = [];
306
+ for (const pattern of QUALIFIED_SPAN_PATTERNS) {
307
+ const re = new RegExp(pattern.source, pattern.flags);
308
+ let m;
309
+ while ((m = re.exec(line)) !== null) {
310
+ const name = nameFromInstallSpec(m[1]);
311
+ if (name !== undefined)
312
+ names.push(name);
313
+ }
314
+ }
315
+ return names;
316
+ }
317
+ const DEPRECATED_COMMAND_RE = /(?:^|[\s;&|(])(?:(?:npx|bunx|yarn|pnpm\s+(?:exec|dlx)|npm\s+exec)\s+)?(react-scripts|tslint|node-sass)(?=\s|$)/g;
318
+ // The trailing comment of a code line (hash, double-slash, slash-star or SQL-style), '' when there is none.
319
+ function commentOf(line) {
320
+ const m = /(?:^|\s)(?:#|\/\/|\/\*|--)\s*(.*)$/.exec(line);
321
+ return m ? m[1] : '';
322
+ }
323
+ function collectMentions(file) {
324
+ const mentions = [];
325
+ const { segments, prose } = readFile(file);
326
+ const add = (pkg, kind, line, text) => {
327
+ mentions.push({ pkg, kind, file, line, text });
328
+ };
329
+ for (const seg of segments) {
330
+ // In a fence only the trailing comment can carry prose; an identifier like
331
+ // `import { without } from 'lodash'` is not a negation.
332
+ if (NEGATION_RE.test(seg.fenced ? commentOf(seg.lineText) : seg.lineText))
333
+ continue;
334
+ const text = seg.text;
335
+ if (!seg.fenced || SHELL_LANGS.has(seg.lang)) {
336
+ for (const pkg of installedPackageNames(text, seg.afterCd))
337
+ add(pkg, 'install', seg.line, seg.lineText);
338
+ const cmd = new RegExp(DEPRECATED_COMMAND_RE.source, 'g');
339
+ let m;
340
+ // A deprecated tool invoked as a command; the `command: true` entries only.
341
+ while ((m = cmd.exec(text)) !== null) {
342
+ if (DEPRECATED_PACKAGES[m[1]]?.command)
343
+ add(m[1], 'command', seg.line, seg.lineText);
344
+ }
345
+ }
346
+ if (!seg.fenced || JS_LANGS.has(seg.lang)) {
347
+ // An unlabeled fence or an inline span could be Go, Dart, Python...: JS-only forms there.
348
+ const strictOnly = !seg.fenced || seg.lang === '';
349
+ for (const pkg of importedPackageNames(text, strictOnly))
350
+ add(pkg, 'import', seg.line, seg.lineText);
351
+ }
352
+ // A span that is nothing but a distinctive deprecated package name.
353
+ if (!seg.fenced && NPM_NAME_RE.test(text) && DEPRECATED_PACKAGES[text]?.distinctive) {
354
+ add(text, 'span', seg.line, seg.lineText);
355
+ }
356
+ }
357
+ for (const { line, text } of prose) {
358
+ if (NEGATION_RE.test(text))
359
+ continue;
360
+ for (const pkg of qualifiedSpanNames(text))
361
+ add(pkg, 'qualified', line, text);
362
+ }
363
+ return mentions;
364
+ }
365
+ // ── Manifest facts ────────────────────────────────────────────────────────────
366
+ const EXTRA_MANIFEST_LIMIT = 200;
367
+ // Dot directories (`.github/actions/x`) are included on purpose; only VCS and dependency trees are not.
368
+ const EXTRA_MANIFEST_IGNORE = [
369
+ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/.next/**', '**/worktrees/**', '**/.worktrees/**',
370
+ '**/vendor/**', '**/target/**', '**/.turbo/**', '**/coverage/**', '**/Library/**', '**/Temp/**',
371
+ ];
372
+ /** Modules the host application provides at runtime; they are never in a package.json. */
373
+ const HOST_PROVIDED_MODULES = new Set(['vscode', 'k6']);
374
+ /**
375
+ * Tools manifest-consistency already reports as a "tooling mismatch" when the
376
+ * instructions name one and the repo uses the other: skip them here so one
377
+ * stale mention is not reported twice.
378
+ */
379
+ const TOOLING_PAIRS = {
380
+ jest: ['vitest'],
381
+ vitest: ['jest'],
382
+ '@playwright/test': ['cypress'],
383
+ cypress: ['@playwright/test'],
384
+ };
385
+ function addManifest(into, ws) {
386
+ into.any = true;
387
+ if (ws.name)
388
+ into.declared.add(ws.name);
389
+ for (const dep of ws.dependencies)
390
+ into.declared.add(dep);
391
+ for (const [dep, spec] of Object.entries(ws.versions ?? {})) {
392
+ into.specs.set(dep, [...(into.specs.get(dep) ?? []), spec]);
393
+ }
394
+ if (ws.dependencies.includes('next'))
395
+ into.nextDirs.push(ws.dir);
396
+ into.dirs.add(ws.dir);
397
+ if (ws.dir !== '.')
398
+ into.localNames.add(path.posix.basename(ws.dir));
399
+ if (ws.name)
400
+ into.localNames.add(ws.name.replace(/^@[^/]+\//, ''));
401
+ }
402
+ function contextManifests(context) {
403
+ const out = {
404
+ declared: new Set(),
405
+ specs: new Map(),
406
+ any: false,
407
+ dirs: new Set(),
408
+ nextDirs: [],
409
+ localNames: new Set(),
410
+ aliasNames: new Set(),
411
+ aliasScopes: new Set(),
412
+ catalogs: { default: {}, named: {} },
413
+ truncated: context.workspacesTruncated === true,
414
+ lazyLoaded: false,
415
+ };
416
+ const rootRaw = context.manifests.packageJson;
417
+ const rootWorkspace = (context.workspaces ?? []).find((ws) => ws.dir === '.');
418
+ if (rootRaw !== undefined && rootWorkspace === undefined) {
419
+ // A hand-built context: only the parsed root facts exist.
420
+ const { dependencies, devDependencies, peerDependencies } = context.packageJson;
421
+ const root = parseWorkspaceManifest('.', JSON.stringify({ dependencies, devDependencies, peerDependencies }));
422
+ if (root)
423
+ addManifest(out, root);
424
+ }
425
+ for (const ws of context.workspaces ?? [])
426
+ addManifest(out, ws);
427
+ return out;
428
+ }
429
+ /** Read a repo-relative file through the symlink-aware containment check; undefined when absent or unsafe. */
430
+ function readContained(repoRoot, rel) {
431
+ try {
432
+ const abs = resolveReadableWithinRoot(repoRoot, rel);
433
+ if (!abs)
434
+ return undefined;
435
+ const stat = fs.statSync(abs);
436
+ if (!stat.isFile() || stat.size > MAX_FILE_SIZE)
437
+ return undefined;
438
+ return fs.readFileSync(abs, 'utf-8');
439
+ }
440
+ catch {
441
+ return undefined;
442
+ }
443
+ }
444
+ /** Comments and trailing commas removed, so tsconfig.json / deno.jsonc parse as JSON. */
445
+ function parseJsonc(raw) {
446
+ let out = '';
447
+ let inString = false;
448
+ for (let i = 0; i < raw.length; i++) {
449
+ const c = raw[i];
450
+ if (inString) {
451
+ out += c;
452
+ if (c === '\\')
453
+ out += raw[++i] ?? '';
454
+ else if (c === '"')
455
+ inString = false;
456
+ }
457
+ else if (c === '"') {
458
+ inString = true;
459
+ out += c;
460
+ }
461
+ else if (c === '/' && raw[i + 1] === '/') {
462
+ while (i < raw.length && raw[i] !== '\n')
463
+ i++;
464
+ out += '\n';
465
+ }
466
+ else if (c === '/' && raw[i + 1] === '*') {
467
+ i += 2;
468
+ while (i < raw.length && !(raw[i] === '*' && raw[i + 1] === '/'))
469
+ i++;
470
+ i++;
471
+ }
472
+ else {
473
+ out += c;
474
+ }
475
+ }
476
+ try {
477
+ return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
478
+ }
479
+ catch {
480
+ return undefined;
481
+ }
482
+ }
483
+ function addAlias(into, key) {
484
+ if (key.startsWith('@/') || key.startsWith('~') || key.startsWith('#') || key === '*')
485
+ return;
486
+ const prefix = key.replace(/\/?\*.*$/, '');
487
+ if (!prefix)
488
+ return;
489
+ if (prefix.startsWith('@')) {
490
+ if (prefix.includes('/'))
491
+ into.aliasNames.add(prefix);
492
+ else
493
+ into.aliasScopes.add(prefix);
494
+ }
495
+ else {
496
+ into.aliasNames.add(prefix.split('/')[0]);
497
+ }
498
+ }
499
+ /** tsconfig/jsconfig `paths` keys are local import aliases; a Deno import map's keys are declared dependencies. */
500
+ function loadAuxConfigs(context, into) {
501
+ for (const dir of [...new Set(['.', ...into.dirs])]) {
502
+ const at = (file) => (dir === '.' ? file : `${dir}/${file}`);
503
+ for (const file of ['tsconfig.json', 'jsconfig.json']) {
504
+ const raw = readContained(context.repoRoot, at(file));
505
+ const parsed = raw === undefined ? undefined : parseJsonc(raw);
506
+ for (const key of Object.keys(parsed?.compilerOptions?.paths ?? {}))
507
+ addAlias(into, key);
508
+ }
509
+ for (const file of ['deno.json', 'deno.jsonc']) {
510
+ const raw = readContained(context.repoRoot, at(file));
511
+ const parsed = raw === undefined ? undefined : parseJsonc(raw);
512
+ for (const key of Object.keys(parsed?.imports ?? {}))
513
+ into.declared.add(key.replace(/\/$/, ''));
514
+ }
515
+ }
516
+ }
517
+ const CATALOG_ENTRY_RE = /^\s+(['"]?)(@?[^'":\s]+(?:\/[^'":\s]+)?)\1\s*:\s*['"]?([^'"#\s]+)['"]?/;
518
+ /** `catalog:` and `catalogs:` of pnpm-workspace.yaml (the small subset of YAML they use). */
519
+ function parseCatalogs(rawYaml) {
520
+ const out = { default: {}, named: {} };
521
+ let section = null;
522
+ let named = null;
523
+ for (const line of rawYaml.split(/\r?\n/)) {
524
+ if (/^\s*(?:#.*)?$/.test(line))
525
+ continue;
526
+ if (/^catalog:\s*(?:#.*)?$/.test(line)) {
527
+ section = 'catalog';
528
+ continue;
529
+ }
530
+ if (/^catalogs:\s*(?:#.*)?$/.test(line)) {
531
+ section = 'catalogs';
532
+ named = null;
533
+ continue;
534
+ }
535
+ if (/^\S/.test(line)) {
536
+ section = null;
537
+ continue;
538
+ }
539
+ if (section === 'catalog') {
540
+ const m = CATALOG_ENTRY_RE.exec(line);
541
+ if (m)
542
+ out.default[m[2]] = m[3];
543
+ }
544
+ else if (section === 'catalogs') {
545
+ const header = /^ {1,3}(['"]?)([^'":\s]+)\1\s*:\s*(?:#.*)?$/.exec(line);
546
+ if (header) {
547
+ named = header[2];
548
+ out.named[named] ??= {};
549
+ continue;
550
+ }
551
+ const m = CATALOG_ENTRY_RE.exec(line);
552
+ if (m && named)
553
+ out.named[named][m[2]] = m[3];
554
+ }
555
+ }
556
+ return out;
557
+ }
558
+ /** The spec a `catalog:` / `catalog:<name>` reference points at, or the spec itself; undefined when unresolvable. */
559
+ function resolveSpec(manifests, dep, spec) {
560
+ if (!spec.startsWith('catalog:'))
561
+ return spec;
562
+ const name = spec.slice('catalog:'.length).trim();
563
+ return name === '' || name === 'default' ? manifests.catalogs.default[dep] : manifests.catalogs.named[name]?.[dep];
564
+ }
565
+ /**
566
+ * Everything beyond the context's own manifests, read once and only when a
567
+ * mention needs it: other package.json files (a service in `services/api`, a
568
+ * sample app), tsconfig/deno configs, and pnpm catalogs.
569
+ */
570
+ function loadLazyFacts(context, into) {
571
+ if (into.lazyLoaded)
572
+ return;
573
+ into.lazyLoaded = true;
574
+ const exclude = context.exclude ?? [];
575
+ let files = [];
576
+ try {
577
+ files = fg.sync(['**/package.json'], {
578
+ cwd: context.repoRoot,
579
+ ignore: EXTRA_MANIFEST_IGNORE,
580
+ dot: true,
581
+ onlyFiles: true,
582
+ followSymbolicLinks: false,
583
+ suppressErrors: true,
584
+ });
585
+ }
586
+ catch {
587
+ // cannot walk: the facts we have are all we get
588
+ into.truncated = true;
589
+ }
590
+ files = files.filter((rel) => !isExcludedPath(rel, exclude) && !into.dirs.has(path.posix.dirname(rel))).sort();
591
+ // Past the cap a declaration may live in a manifest we did not read: rule (a) must not guess.
592
+ if (files.length > EXTRA_MANIFEST_LIMIT)
593
+ into.truncated = true;
594
+ for (const rel of files.slice(0, EXTRA_MANIFEST_LIMIT)) {
595
+ const raw = readContained(context.repoRoot, rel);
596
+ const manifest = raw === undefined ? undefined : parseWorkspaceManifest(path.posix.dirname(rel), raw);
597
+ if (manifest)
598
+ addManifest(into, manifest);
599
+ }
600
+ loadAuxConfigs(context, into);
601
+ const yaml = readContained(context.repoRoot, 'pnpm-workspace.yaml');
602
+ if (yaml !== undefined)
603
+ into.catalogs = parseCatalogs(yaml);
604
+ }
605
+ function scopeOf(name) {
606
+ return name.startsWith('@') ? name.slice(0, name.indexOf('/')) : undefined;
607
+ }
608
+ /** Major version of the lowest range a spec allows, or null when it cannot be read statically. */
609
+ function majorOf(spec) {
610
+ const s = spec.trim();
611
+ if (/^(?:workspace|catalog|link|file|git|github|http|https|npm):/.test(s) || s.startsWith('<'))
612
+ return null;
613
+ const m = /^(?:\^|~|>=|>|=)?\s*v?(\d+)(?:[.x*]|\b)/.exec(s);
614
+ return m ? parseInt(m[1], 10) : null;
615
+ }
616
+ /** The lowest declared major across every manifest, or null when any spec is unreadable or none exists. */
617
+ function lowestMajor(manifests, names) {
618
+ let best = null;
619
+ for (const name of names) {
620
+ for (const raw of manifests.specs.get(name) ?? []) {
621
+ const spec = resolveSpec(manifests, name, raw);
622
+ const major = spec === undefined ? null : majorOf(spec);
623
+ if (spec === undefined || major === null)
624
+ return null;
625
+ if (best === null || major < best.major)
626
+ best = { major, spec };
627
+ }
628
+ }
629
+ return best;
630
+ }
631
+ // ── Findings ──────────────────────────────────────────────────────────────────
632
+ function issueId(rule, key) {
633
+ const hash = crypto.createHash('sha1').update(`dep-staleness:${rule}:${key}`).digest('hex').slice(0, 12);
634
+ return `dep-staleness-${rule}-${hash}`;
635
+ }
636
+ /** First mention per file, in file order. */
637
+ function firstPerFile(mentions) {
638
+ const seen = new Set();
639
+ const out = [];
640
+ for (const m of mentions) {
641
+ if (seen.has(m.file.path))
642
+ continue;
643
+ seen.add(m.file.path);
644
+ out.push(m);
645
+ }
646
+ return out;
647
+ }
648
+ function locationsOf(mentions) {
649
+ return mentions.map((m) => ({ filePath: m.file.path, startLine: m.line, endLine: m.line }));
650
+ }
651
+ function evidenceLine(m) {
652
+ return `${m.file.relativePath ?? path.basename(m.file.path)}:${m.line}: "${snippet(m.text, 100)}"`;
653
+ }
654
+ function isPlaceholder(name) {
655
+ const scope = scopeOf(name);
656
+ if (scope !== undefined)
657
+ return PLACEHOLDER_SCOPES.has(scope.slice(1));
658
+ return PLACEHOLDER_NAMES.has(name) || PLACEHOLDER_PREFIX_RE.test(name);
659
+ }
660
+ /** `<repo>/<dir>/<base>/<name>` through the symlink-aware containment check, or null. */
661
+ function containedPath(repoRoot, ...parts) {
662
+ return resolveReadableWithinRoot(repoRoot, path.posix.join(...parts));
663
+ }
664
+ function pathExists(repoRoot, ...parts) {
665
+ const abs = containedPath(repoRoot, ...parts);
666
+ if (abs === null)
667
+ return false;
668
+ try {
669
+ return fs.existsSync(abs);
670
+ }
671
+ catch {
672
+ return false;
673
+ }
674
+ }
675
+ /** A directory or file named `name` at the root of the repo or of any workspace (or in its src/). */
676
+ function localDirectoryExists(repoRoot, manifests, name) {
677
+ for (const dir of new Set(['.', ...manifests.dirs])) {
678
+ for (const base of ['.', 'src']) {
679
+ if (pathExists(repoRoot, dir, base, name))
680
+ return true;
681
+ }
682
+ }
683
+ return false;
684
+ }
685
+ /** `@types/foo` declares `foo`; `@types/scope__name` declares `@scope/name`. */
686
+ function typesPackageFor(name) {
687
+ return name.startsWith('@') ? `@types/${name.slice(1).replace('/', '__')}` : `@types/${name}`;
688
+ }
689
+ function checkPackages(context, files, manifests) {
690
+ const byPackage = new Map();
691
+ for (const file of files) {
692
+ for (const mention of collectMentions(file)) {
693
+ byPackage.set(mention.pkg, [...(byPackage.get(mention.pkg) ?? []), mention]);
694
+ }
695
+ }
696
+ if (byPackage.size === 0)
697
+ return [];
698
+ const issues = [];
699
+ const isDeclared = (name) => {
700
+ const has = (n) => manifests.declared.has(n) || manifests.declared.has(typesPackageFor(n));
701
+ if (has(name))
702
+ return true;
703
+ loadLazyFacts(context, manifests);
704
+ return has(name) || isKnownBinary(context, name);
705
+ };
706
+ for (const [pkg, mentions] of [...byPackage].sort(([a], [b]) => a.localeCompare(b))) {
707
+ const bare = pkg.startsWith('@') ? pkg : pkg.split('/')[0];
708
+ if (NODE_BUILTINS.has(bare) || WELL_KNOWN_CLIS.has(bare) || HOST_PROVIDED_MODULES.has(bare) || isPlaceholder(pkg))
709
+ continue;
710
+ const deprecated = DEPRECATED_PACKAGES[pkg];
711
+ if (deprecated) {
712
+ const declared = isDeclared(pkg);
713
+ const severity = declared || manifests.truncated ? 'info' : 'warning';
714
+ const where = firstPerFile(mentions);
715
+ issues.push({
716
+ id: issueId('deprecated', pkg),
717
+ severity,
718
+ category: 'stale_instruction',
719
+ title: `Instructions point at a deprecated package: ${pkg}`,
720
+ summary: `Instruction files mention \`${pkg}\`, which ${deprecated.status}. ` +
721
+ (declared
722
+ ? 'A package.json in this repo still declares it, so an agent will keep extending the old dependency. '
723
+ : 'No package.json in this repo declares it, so an agent following the instructions would add a deprecated dependency. ') +
724
+ `Consider ${deprecated.replacement} instead, or confirm the repo intends to keep using it.`,
725
+ filePaths: where.map((m) => m.file.path),
726
+ locations: locationsOf(where),
727
+ evidence: [
728
+ ...where.slice(0, 3).map(evidenceLine),
729
+ declared ? `${pkg} is declared in a package.json` : `${pkg} is not declared in any package.json`,
730
+ ],
731
+ recommendation: `Update the instructions to recommend ${deprecated.replacement}${declared ? ', and plan the dependency migration' : ''}; if the repo deliberately keeps ${pkg}, note why.`,
732
+ confidence: 0.7,
733
+ });
734
+ continue;
735
+ }
736
+ // Rule (a): only an install command or an import line counts. Prose that merely
737
+ // calls something a package/library is not enough for an arbitrary name.
738
+ const explicit = mentions.filter((m) => m.kind === 'install' || m.kind === 'import');
739
+ if (explicit.length === 0)
740
+ continue;
741
+ if (isDeclared(pkg))
742
+ continue;
743
+ // Declarations may live in manifests we could not read: do not guess.
744
+ if (manifests.truncated)
745
+ continue;
746
+ // manifest-consistency already reports a Jest/Vitest or Playwright/Cypress mix-up.
747
+ if (TOOLING_PAIRS[pkg]?.some((other) => isDeclared(other)))
748
+ continue;
749
+ const scope = scopeOf(pkg);
750
+ const viaInstall = explicit.some((m) => m.kind === 'install');
751
+ if (!viaInstall) {
752
+ // An import is weaker evidence than an install command: path aliases
753
+ // (`@lib/x`, `components/x`, tsconfig `paths`) look exactly like packages.
754
+ if (scope === undefined && (ALIAS_LIKE_NAMES.has(pkg) || localDirectoryExists(context.repoRoot, manifests, pkg)))
755
+ continue;
756
+ if (manifests.aliasNames.has(pkg) || (scope !== undefined && manifests.aliasScopes.has(scope)))
757
+ continue;
758
+ // An import of one of this monorepo's own packages by its directory or short name.
759
+ if (manifests.localNames.has(pkg))
760
+ continue;
761
+ // A scoped import is only believed when the repo already uses that scope
762
+ // (`@tanstack/react-query` absent, `@tanstack/react-table` present).
763
+ if (scope !== undefined && ![...manifests.declared].some((d) => scopeOf(d) === scope))
764
+ continue;
765
+ }
766
+ const where = firstPerFile(explicit);
767
+ issues.push({
768
+ id: issueId('missing-package', pkg),
769
+ // An install command is explicit; an import line alone is weaker evidence.
770
+ severity: viaInstall ? 'warning' : 'info',
771
+ category: 'stale_instruction',
772
+ title: `Package named in instructions is not in any package.json: ${pkg}`,
773
+ summary: `Instruction files refer to the npm package \`${pkg}\`, but no package.json in this repo declares it. ` +
774
+ 'It may have been removed or renamed since the instructions were written. ' +
775
+ 'Treat this as a prompt to check, not a certainty: it may be an optional or external tool.',
776
+ filePaths: where.map((m) => m.file.path),
777
+ locations: locationsOf(where),
778
+ evidence: [...where.slice(0, 3).map(evidenceLine), `${pkg} is not declared in any package.json`],
779
+ recommendation: `Remove or update the mention of ${pkg}, or add it to package.json if the repo really depends on it.`,
780
+ confidence: 0.5,
781
+ });
782
+ }
783
+ return issues;
784
+ }
785
+ /** "Before:", "old", "legacy", "anti-pattern": the example is shown as what NOT to write. */
786
+ const OLD_LABEL_RE = /\b(?:before|old(?:er)?|previously|legacy|prior|outdated|anti-?patterns?|bad|wrong)\b|❌/i;
787
+ const HEADING_RE = /^\s{0,3}#{1,6}\s/;
788
+ /**
789
+ * Lines (prose or code) that match `re` and read as guidance to follow. Skipped:
790
+ * negated or migration prose, `diff`/`patch` fences (a before/after listing),
791
+ * anything labeled Before/Old, and anything in a section that also names the
792
+ * replacement API (`replacement`) — that is a migration example, not an instruction.
793
+ */
794
+ function apiMentions(files, re, skipLine, replacement) {
795
+ const hits = [];
796
+ for (const file of files) {
797
+ const lines = scanFencedLines(file.content);
798
+ const sectionOf = [];
799
+ const sectionText = [''];
800
+ const headings = [''];
801
+ const openOf = [];
802
+ let section = 0;
803
+ let open = -1;
804
+ lines.forEach((fl, i) => {
805
+ if (fl.kind === 'text' && HEADING_RE.test(fl.text)) {
806
+ section++;
807
+ sectionText[section] = '';
808
+ headings[section] = fl.text;
809
+ }
810
+ if (fl.kind === 'open')
811
+ open = i;
812
+ openOf[i] = fl.inFence ? open : -1;
813
+ sectionOf[i] = section;
814
+ sectionText[section] += `${fl.text}\n`;
815
+ });
816
+ /** The nearest non-blank line before this one, or before the fence it sits in. */
817
+ const lineBefore = (i) => {
818
+ for (let j = (openOf[i] >= 0 ? openOf[i] : i) - 1; j >= 0; j--) {
819
+ if (lines[j].text.trim() !== '')
820
+ return lines[j].text;
821
+ }
822
+ return '';
823
+ };
824
+ lines.forEach((fl, i) => {
825
+ if (fl.kind === 'open' || fl.kind === 'close')
826
+ return;
827
+ if (fl.inFence && (fl.lang === 'diff' || fl.lang === 'patch'))
828
+ return;
829
+ const text = fl.text.replace(/\r$/, '');
830
+ if (!re.test(text) || NEGATION_RE.test(text) || skipLine.test(text))
831
+ return;
832
+ if (replacement.test(sectionText[sectionOf[i]]))
833
+ return;
834
+ if (OLD_LABEL_RE.test(text) || OLD_LABEL_RE.test(headings[sectionOf[i]]) || OLD_LABEL_RE.test(lineBefore(i)))
835
+ return;
836
+ hits.push({ file, line: fl.lineNumber, text });
837
+ });
838
+ }
839
+ return hits;
840
+ }
841
+ const REACT_RENDER_RE = /\bReactDOM\.render\b/;
842
+ // Explicitly about an older React.
843
+ const REACT_RENDER_SKIP_RE = /\bReact\s*(?:v\.?\s*)?(?:0|1[0-7])\b/i;
844
+ const REACT_ROOT_API_RE = /\b(?:createRoot|hydrateRoot)\b/;
845
+ function checkReactRender(context, files, manifests) {
846
+ const hits = firstPerFile(apiMentions(files, REACT_RENDER_RE, REACT_RENDER_SKIP_RE, REACT_ROOT_API_RE));
847
+ if (hits.length === 0)
848
+ return [];
849
+ loadLazyFacts(context, manifests);
850
+ const lowest = lowestMajor(manifests, ['react', 'react-dom']);
851
+ if (lowest === null || lowest.major < 18)
852
+ return [];
853
+ const removed = lowest.major >= 19;
854
+ return [
855
+ {
856
+ id: issueId('react-dom-render', 'ReactDOM.render'),
857
+ severity: removed ? 'high' : 'warning',
858
+ category: 'stale_instruction',
859
+ title: removed
860
+ ? 'Instructions use ReactDOM.render, which React 19 removed'
861
+ : 'Instructions use ReactDOM.render, which is the legacy React 17 root API',
862
+ summary: `Instruction files mention \`ReactDOM.render\`, but package.json declares React ${lowest.spec}. ` +
863
+ (removed
864
+ ? '`ReactDOM.render` was removed in React 19, so code an agent writes from this guidance will fail.'
865
+ : 'In React 18 it still runs but only in legacy mode (with a console warning) and without concurrent features.') +
866
+ ' Use `createRoot` from `react-dom/client` instead.',
867
+ filePaths: hits.map((h) => h.file.path),
868
+ locations: locationsOf(hits),
869
+ evidence: [...hits.slice(0, 3).map(evidenceLine), `package.json declares react ${lowest.spec}`],
870
+ recommendation: 'Replace the `ReactDOM.render(...)` guidance with `createRoot(container).render(...)` from `react-dom/client`.',
871
+ confidence: 0.8,
872
+ },
873
+ ];
874
+ }
875
+ const GET_INITIAL_PROPS_RE = /\bgetInitialProps\b/;
876
+ // Prose that scopes the API to the pages router.
877
+ const PAGES_ROUTER_RE = /\bpages\s*(?:router|dir(?:ectory)?)\b|\bpages\//i;
878
+ const APP_ROUTER_API_RE = /\bServer Components?\b|\broute handlers?\b|\bapp\s+router\b/i;
879
+ const APP_ROUTER_FILE_RE = /^(?:layout|page)\.(?:tsx|ts|jsx|js|mdx)$/;
880
+ function dirHasAppRouter(repoRoot, dir) {
881
+ for (const app of ['app', 'src/app']) {
882
+ try {
883
+ const abs = containedPath(repoRoot, dir, app);
884
+ if (abs !== null && fs.readdirSync(abs).some((f) => APP_ROUTER_FILE_RE.test(f)))
885
+ return true;
886
+ }
887
+ catch {
888
+ // no such directory
889
+ }
890
+ }
891
+ return false;
892
+ }
893
+ function dirHasPagesRouter(repoRoot, dir) {
894
+ for (const pages of ['pages', 'src/pages']) {
895
+ try {
896
+ const abs = containedPath(repoRoot, dir, pages);
897
+ if (abs !== null && fs.statSync(abs).isDirectory())
898
+ return true;
899
+ }
900
+ catch {
901
+ // no such directory
902
+ }
903
+ }
904
+ return false;
905
+ }
906
+ function checkGetInitialProps(context, files, manifests) {
907
+ const hits = firstPerFile(apiMentions(files, GET_INITIAL_PROPS_RE, PAGES_ROUTER_RE, APP_ROUTER_API_RE));
908
+ if (hits.length === 0)
909
+ return [];
910
+ // A Next app may live outside the conventional workspace directories.
911
+ loadLazyFacts(context, manifests);
912
+ const nextDirs = manifests.nextDirs;
913
+ if (nextDirs.length === 0)
914
+ return [];
915
+ // Hybrid apps still need getInitialProps in pages/; only a pure app-router project makes it dead guidance.
916
+ if (nextDirs.some((dir) => dirHasPagesRouter(context.repoRoot, dir)))
917
+ return [];
918
+ if (!nextDirs.some((dir) => dirHasAppRouter(context.repoRoot, dir)))
919
+ return [];
920
+ return [
921
+ {
922
+ id: issueId('get-initial-props', 'getInitialProps'),
923
+ severity: 'warning',
924
+ category: 'stale_instruction',
925
+ title: 'Instructions use getInitialProps in a Next.js app-router project',
926
+ summary: 'Instruction files mention `getInitialProps`, but this project uses the Next.js `app/` router ' +
927
+ '(and has no `pages/` directory). `getInitialProps` only applies to the pages router, so an ' +
928
+ 'agent following this guidance would write data fetching that never runs.',
929
+ filePaths: hits.map((h) => h.file.path),
930
+ locations: locationsOf(hits),
931
+ evidence: [...hits.slice(0, 3).map(evidenceLine), 'an app/ directory with layout/page files exists; no pages/ directory'],
932
+ recommendation: 'Describe data fetching with Server Components (async components, `fetch`) or route handlers instead, ' +
933
+ 'or scope the `getInitialProps` note to the pages router.',
934
+ confidence: 0.65,
935
+ },
936
+ ];
937
+ }
938
+ const NGMODULE_RE = /\bNgModules?\b/;
939
+ const STANDALONE_RE = /\bstandalone\b/i;
940
+ function hasNgModuleFiles(repoRoot) {
941
+ try {
942
+ const found = fg.sync(['**/*.module.ts'], {
943
+ cwd: repoRoot,
944
+ ignore: EXTRA_MANIFEST_IGNORE,
945
+ deep: 8,
946
+ onlyFiles: true,
947
+ followSymbolicLinks: false,
948
+ suppressErrors: true,
949
+ });
950
+ return found.some((rel) => resolveReadableWithinRoot(repoRoot, rel) !== null);
951
+ }
952
+ catch {
953
+ return true; // cannot tell: stay silent
954
+ }
955
+ }
956
+ function checkNgModule(context, files, manifests) {
957
+ const hits = firstPerFile(apiMentions(files, NGMODULE_RE, STANDALONE_RE, STANDALONE_RE));
958
+ if (hits.length === 0)
959
+ return [];
960
+ loadLazyFacts(context, manifests);
961
+ const angular = lowestMajor(manifests, ['@angular/core']);
962
+ if (angular === null || angular.major < 17)
963
+ return [];
964
+ // A project that still has NgModule files is genuinely module-based; the guidance is right for it.
965
+ if (hasNgModuleFiles(context.repoRoot))
966
+ return [];
967
+ return [
968
+ {
969
+ id: issueId('ngmodule', 'NgModule'),
970
+ severity: 'info',
971
+ category: 'stale_instruction',
972
+ title: 'Instructions are NgModule-centric in an Angular 17+ project with no modules',
973
+ summary: `Instruction files describe NgModules, but package.json declares @angular/core ${angular.spec} ` +
974
+ 'and the repo has no `*.module.ts` files. New Angular projects use standalone components, so an agent ' +
975
+ 'following this guidance may scaffold NgModules the project does not use.',
976
+ filePaths: hits.map((h) => h.file.path),
977
+ locations: locationsOf(hits),
978
+ evidence: [...hits.slice(0, 3).map(evidenceLine), `package.json declares @angular/core ${angular.spec}; no *.module.ts files`],
979
+ recommendation: 'Describe standalone components (`standalone: true`, `bootstrapApplication`) instead of NgModules.',
980
+ confidence: 0.6,
981
+ },
982
+ ];
983
+ }
984
+ // ── Public detector ───────────────────────────────────────────────────────────
985
+ export function detectDependencyStaleness(context) {
986
+ // Agent-facing instruction prose only: README/docs mostly tell end users how to install the project.
987
+ const files = context.files.filter((f) => INSTRUCTION_FILE_TYPES.has(f.fileType));
988
+ if (files.length === 0)
989
+ return [];
990
+ const manifests = contextManifests(context);
991
+ // Not a JavaScript project (as far as the manifests say): nothing to compare against.
992
+ if (!manifests.any)
993
+ return [];
994
+ return [
995
+ ...checkPackages(context, files, manifests),
996
+ ...checkReactRender(context, files, manifests),
997
+ ...checkGetInitialProps(context, files, manifests),
998
+ ...checkNgModule(context, files, manifests),
999
+ ].sort((a, b) => a.id.localeCompare(b.id));
1000
+ }
1001
+ //# sourceMappingURL=dependency-staleness.js.map