@thomasminh1995/depverdict 0.6.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/bin/depverdict.js +7 -0
  4. package/bin/upgradelens.js +7 -0
  5. package/docs/GR-01-Semantic-Grounding-Failure-Analysis.md +309 -0
  6. package/docs/GR-02-Versioned-Action-Evaluation-Criteria.md +187 -0
  7. package/docs/GR-03-Extractive-Contract-Safety-Experiment.md +242 -0
  8. package/docs/GR-04-Versioned-Production-Extractive-Contract.md +154 -0
  9. package/docs/IA-01-Repository-Usage-Discovery.md +122 -0
  10. package/docs/IA-02-Repository-Impact-Analysis.md +118 -0
  11. package/docs/IA-03-Repository-Impact-Evidence.md +160 -0
  12. package/docs/IA-04-CLI-Orchestration.md +235 -0
  13. package/docs/IA-05-Real-Provider-Validation.md +235 -0
  14. package/docs/IA-05-VinGrade-Validation.md +339 -0
  15. package/docs/MVP-01.md +48 -0
  16. package/docs/MVP-02-Architecture.md +536 -0
  17. package/docs/MVP-02-CLI-HTTP-Runtime.md +50 -0
  18. package/docs/MVP-02-HTTP-Lifecycle.md +37 -0
  19. package/docs/MVP-02-Knowledge-Manifest-Generation.md +75 -0
  20. package/docs/MVP-02-Knowledge-Manifest.md +279 -0
  21. package/docs/MVP-02-Knowledge-Research-Orchestration.md +62 -0
  22. package/docs/MVP-02-Knowledge-Store.md +87 -0
  23. package/docs/MVP-02-PyPI-Registry-Adapter.md +97 -0
  24. package/docs/MVP-02-Research-Planning.md +196 -0
  25. package/docs/MVP-02-Source-Provenance.md +81 -0
  26. package/docs/MVP-02-npm-Registry-Adapter.md +99 -0
  27. package/docs/RR-01-End-to-End-and-Real-Provider-Validation.md +393 -0
  28. package/docs/RR-01-RERUN-Extractive-Contract-Validation.md +566 -0
  29. package/docs/RR-02-Full-Product-Workflow-and-Developer-CLI-UX-Review.md +35 -0
  30. package/docs/RR02-FIX-01-Persistent-Qualification-Resolution.md +32 -0
  31. package/docs/RR02-FIX-02-Stage-aware-CLI-Progress-and-Heartbeat.md +28 -0
  32. package/docs/RR02-FIX-03-npm-Capture-Evidence-Exclusion.md +33 -0
  33. package/docs/RR02-FIX-03A-Complete-Package-Exclusion-and-Evidence-Commit.md +223 -0
  34. package/docs/RR02-FIX-04-Event-loop-safe-Heartbeat.md +393 -0
  35. package/docs/RR02-FIX-05-Materialize-Persisted-Qualification.md +207 -0
  36. package/docs/RR02-RERUN-CLI-Qualification-Progress-UX-and-Package-Validation.md +342 -0
  37. package/docs/VinGrade-MVP-02-Validation.md +480 -0
  38. package/docs/VinGrade-RC02-Live-Validation.md +273 -0
  39. package/docs/ai-capability-discovery.md +573 -0
  40. package/docs/ai-engineering-production-readiness.md +390 -0
  41. package/docs/ai-engineering-review.md +251 -0
  42. package/docs/ai-runtime-governance-discovery.md +754 -0
  43. package/docs/architecture-overview.md +78 -0
  44. package/docs/cli-progress.md +111 -0
  45. package/docs/decisions/diff-01-brand-distribution-identity.md +450 -0
  46. package/docs/decisions/diff-02-identity-compatibility-contract.md +274 -0
  47. package/docs/decisions/diff-03-repository-docs-community-migration.md +165 -0
  48. package/docs/decisions/diff-04-release-evidence-gap-acceptance.md +110 -0
  49. package/docs/discovery/mvp-05-ai-migration-planning-discovery.md +350 -0
  50. package/docs/gateway-runtime-discovery.md +614 -0
  51. package/docs/live-ai-validation.md +299 -0
  52. package/docs/migration-planning-qualification-resolution.md +99 -0
  53. package/docs/migrations/upgradelens-to-depverdict.md +97 -0
  54. package/docs/mp-r03-deterministic-upgrade-decision-architecture.md +135 -0
  55. package/docs/mp-r04-evidence-bounded-migration-handoff-architecture.md +116 -0
  56. package/docs/mp-r05-product-completion-and-decision-first-cli-architecture.md +162 -0
  57. package/docs/mvp-05-deterministic-context-runtime.md +127 -0
  58. package/docs/mvp-05-migration-checklist-contract.md +166 -0
  59. package/docs/mvp-05-migration-checklist-orchestration.md +105 -0
  60. package/docs/mvp-05-migration-evaluation-and-qualification.md +135 -0
  61. package/docs/mvp-05-provider-neutral-generator.md +79 -0
  62. package/docs/ollama-local-smoke-validation.md +156 -0
  63. package/docs/openai-compatible-runtime-discovery.md +789 -0
  64. package/docs/openrouter-one-dependency-validation.md +205 -0
  65. package/docs/oss-02-package-guard-hardening-architecture.md +131 -0
  66. package/docs/oss-04-public-ci-package-metadata-architecture.md +142 -0
  67. package/docs/package-content-policy.md +98 -0
  68. package/docs/releases/v0.5.0-technical-preview.md +114 -0
  69. package/docs/releases/v0.6.0-alpha.1-depverdict-preview.md +101 -0
  70. package/docs/reviews/diff-02-identity-contract-compatibility.md +413 -0
  71. package/docs/reviews/diff-03-repository-docs-community-migration.md +364 -0
  72. package/docs/reviews/diff-04-depverdict-distribution-identity-readiness-rereview.md +453 -0
  73. package/docs/reviews/diff-04-fix-post-rename-identity-release-remediation.md +310 -0
  74. package/docs/reviews/diff-05-final-preview-distribution-qualification.md +561 -0
  75. package/docs/reviews/mvp-05-final-product-value-workflow-rereview.md +471 -0
  76. package/docs/reviews/mvp-05-product-workflow-review.md +605 -0
  77. package/docs/reviews/oss-01-duplicate-artifact-investigation-cleanup.md +411 -0
  78. package/docs/reviews/oss-02-package-guard-hardening.md +303 -0
  79. package/docs/reviews/oss-03-community-scaffolding.md +378 -0
  80. package/docs/reviews/oss-04-public-ci-package-metadata.md +439 -0
  81. package/docs/reviews/oss-05-technical-preview-qualification.md +482 -0
  82. package/docs/reviews/upgradelens-vs-upgradedepdetective-source-comparison.md +355 -0
  83. package/docs/reviews/v0.5.0-pre-release-smoke.md +263 -0
  84. package/docs/reviews/v0.5.0-version-bump-release-verification.md +306 -0
  85. package/docs/runtime-contract-discovery.md +521 -0
  86. package/docs/structured-output-compatibility-report.md +100 -0
  87. package/docs/ts-fix-01-exact-duplicate-occurrence-target-selection-architecture.md +111 -0
  88. package/docs/version-analysis-architecture.md +827 -0
  89. package/eval/README.md +86 -0
  90. package/eval/datasets/generic/declared-constraint.json +59 -0
  91. package/eval/datasets/generic/evidence-conflict.json +73 -0
  92. package/eval/datasets/generic/major-breaking-release.json +66 -0
  93. package/eval/datasets/generic/missing-evidence.json +46 -0
  94. package/eval/datasets/generic/patch-release-low.json +59 -0
  95. package/eval/datasets/node/axios-patch-low.json +59 -0
  96. package/eval/datasets/node/react-major-breaking.json +66 -0
  97. package/eval/datasets/node/react-minor-compatibility.json +66 -0
  98. package/eval/datasets/python/fastapi-deprecation.json +66 -0
  99. package/eval/datasets/python/pydantic-major-breaking.json +66 -0
  100. package/eval/migration-planning/golden-dataset-v2.json +306 -0
  101. package/eval/migration-planning/golden-dataset.json +214 -0
  102. package/eval/schemas/expected-result.schema.json +88 -0
  103. package/eval/schemas/golden-case.schema.json +181 -0
  104. package/package.json +57 -0
  105. package/schemas/ai-scorecard.schema.json +132 -0
  106. package/schemas/benchmark-report.schema.json +189 -0
  107. package/schemas/benchmark.schema.json +55 -0
  108. package/schemas/capability-profile.schema.json +44 -0
  109. package/schemas/conformance-report.schema.json +150 -0
  110. package/schemas/deployment-profile.schema.json +64 -0
  111. package/schemas/evaluation-report.schema.json +158 -0
  112. package/schemas/knowledge-evidence-bundle.schema.json +150 -0
  113. package/schemas/knowledge-manifest.schema.json +548 -0
  114. package/schemas/metrics.schema.json +178 -0
  115. package/schemas/migration-checklist-extractive-candidate.schema.json +42 -0
  116. package/schemas/migration-checklist.schema.json +706 -0
  117. package/schemas/migration-evaluation-dataset-v2.schema.json +208 -0
  118. package/schemas/migration-evaluation-dataset.schema.json +204 -0
  119. package/schemas/migration-planning-qualification-record.schema.json +234 -0
  120. package/schemas/project-manifest.schema.json +308 -0
  121. package/schemas/qualification-record.schema.json +56 -0
  122. package/schemas/repository-impact-evidence.schema.json +232 -0
  123. package/schemas/repository-impact.schema.json +202 -0
  124. package/schemas/upgrade-decision.schema.json +273 -0
  125. package/schemas/usage-index.schema.json +179 -0
  126. package/schemas/version-analysis.schema.json +449 -0
  127. package/src/ai-runtime-debug.js +325 -0
  128. package/src/ai-runtime-error.js +42 -0
  129. package/src/ai-runtime.js +174 -0
  130. package/src/ai-scorecard.js +204 -0
  131. package/src/ai-version-analysis.js +484 -0
  132. package/src/artifact-root-compatibility.js +91 -0
  133. package/src/benchmark-report.js +111 -0
  134. package/src/benchmark-runner.js +191 -0
  135. package/src/canonical-json.js +69 -0
  136. package/src/cli.js +1299 -0
  137. package/src/conformance-report.js +158 -0
  138. package/src/conformance-runner.js +253 -0
  139. package/src/constants.js +73 -0
  140. package/src/cooperative-scheduler.js +79 -0
  141. package/src/dependencies.js +44 -0
  142. package/src/dependency-ai-context.js +625 -0
  143. package/src/detectors.js +253 -0
  144. package/src/discovery.js +234 -0
  145. package/src/ecosystem-version-adapter.js +294 -0
  146. package/src/environment-compatibility.js +77 -0
  147. package/src/evaluation-comparator.js +158 -0
  148. package/src/evaluation-report.js +76 -0
  149. package/src/evaluation-runner.js +248 -0
  150. package/src/evidence-source-adapter.js +472 -0
  151. package/src/files.js +89 -0
  152. package/src/governance-diagnostics.js +64 -0
  153. package/src/governance-loader.js +63 -0
  154. package/src/governance-metadata.js +346 -0
  155. package/src/governance-validator.js +360 -0
  156. package/src/http/bounded-fetch.js +278 -0
  157. package/src/http/cli-http-runtime.js +44 -0
  158. package/src/impact/input-loader.js +157 -0
  159. package/src/impact/matcher.js +40 -0
  160. package/src/impact/repository-impact.js +199 -0
  161. package/src/impact/runtime.js +24 -0
  162. package/src/impact/status.js +62 -0
  163. package/src/impact/writer.js +30 -0
  164. package/src/impact-evidence/input-loader.js +202 -0
  165. package/src/impact-evidence/repository-impact-evidence.js +234 -0
  166. package/src/impact-evidence/runtime.js +16 -0
  167. package/src/impact-evidence/writer.js +30 -0
  168. package/src/index.js +563 -0
  169. package/src/installed-version-baseline.js +196 -0
  170. package/src/knowledge-cache.js +324 -0
  171. package/src/knowledge-evidence-bundle.js +101 -0
  172. package/src/knowledge-evidence-producer.js +233 -0
  173. package/src/knowledge-manifest-builder.js +188 -0
  174. package/src/knowledge-manifest-writer.js +32 -0
  175. package/src/knowledge-manifest.js +255 -0
  176. package/src/knowledge-research.js +615 -0
  177. package/src/metrics-engine.js +205 -0
  178. package/src/migration-checklist/ai-candidate.js +320 -0
  179. package/src/migration-checklist/assembler.js +37 -0
  180. package/src/migration-checklist/context-runtime.js +828 -0
  181. package/src/migration-checklist/evaluation/action-criteria.js +244 -0
  182. package/src/migration-checklist/evaluation/comparator-v2.js +332 -0
  183. package/src/migration-checklist/evaluation/comparator.js +279 -0
  184. package/src/migration-checklist/evaluation/dataset-v2.js +227 -0
  185. package/src/migration-checklist/evaluation/dataset.js +336 -0
  186. package/src/migration-checklist/evaluation/extractive-fixtures-v2.js +148 -0
  187. package/src/migration-checklist/evaluation/metrics-v2.js +226 -0
  188. package/src/migration-checklist/evaluation/metrics.js +158 -0
  189. package/src/migration-checklist/evaluation/qualification-v2.js +321 -0
  190. package/src/migration-checklist/evaluation/qualification.js +239 -0
  191. package/src/migration-checklist/evaluation/runner-v2.js +294 -0
  192. package/src/migration-checklist/evaluation/runner.js +194 -0
  193. package/src/migration-checklist/evaluation/scorecard-v2.js +106 -0
  194. package/src/migration-checklist/evaluation/scorecard.js +86 -0
  195. package/src/migration-checklist/extractive-candidate.js +166 -0
  196. package/src/migration-checklist/extractive-prompt.js +62 -0
  197. package/src/migration-checklist/generator.js +702 -0
  198. package/src/migration-checklist/grounding-policy.js +117 -0
  199. package/src/migration-checklist/input-loader.js +613 -0
  200. package/src/migration-checklist/migration-checklist.js +635 -0
  201. package/src/migration-checklist/presentation.js +292 -0
  202. package/src/migration-checklist/progress.js +96 -0
  203. package/src/migration-checklist/prompt.js +83 -0
  204. package/src/migration-checklist/qualification-guard.js +462 -0
  205. package/src/migration-checklist/qualification-resolution.js +122 -0
  206. package/src/migration-checklist/qualification-store.js +225 -0
  207. package/src/migration-checklist/runtime.js +205 -0
  208. package/src/migration-checklist/verification.js +134 -0
  209. package/src/migration-checklist/writer.js +35 -0
  210. package/src/openai-compatible-provider.js +451 -0
  211. package/src/orchestration/failure-log.js +32 -0
  212. package/src/orchestration/pipeline.js +200 -0
  213. package/src/orchestration/progress-events.js +337 -0
  214. package/src/orchestration/progress-reporter.js +131 -0
  215. package/src/orchestration/text-writer.js +22 -0
  216. package/src/portable.js +13 -0
  217. package/src/product-completion.js +249 -0
  218. package/src/project-manifest-input.js +90 -0
  219. package/src/project-manifest.js +141 -0
  220. package/src/python-requirements.js +137 -0
  221. package/src/registry/npm-packument.js +256 -0
  222. package/src/registry/npm-registry-adapter.js +262 -0
  223. package/src/registry/pypi-project.js +300 -0
  224. package/src/registry/pypi-registry-adapter.js +235 -0
  225. package/src/registry/sanitize-registry-body.js +51 -0
  226. package/src/renderers/console.js +160 -0
  227. package/src/renderers/impact-presentation.js +278 -0
  228. package/src/renderers/markdown.js +172 -0
  229. package/src/research-plan.js +455 -0
  230. package/src/runtime-conformance.js +275 -0
  231. package/src/source-provenance.js +393 -0
  232. package/src/source-url.js +62 -0
  233. package/src/structured-output-schema.js +66 -0
  234. package/src/target-selector.js +306 -0
  235. package/src/upgrade-decision/input-loader.js +107 -0
  236. package/src/upgrade-decision/presentation.js +43 -0
  237. package/src/upgrade-decision/runtime.js +21 -0
  238. package/src/upgrade-decision/upgrade-decision.js +626 -0
  239. package/src/upgrade-decision/writer.js +30 -0
  240. package/src/usage/analyzer-registry.js +63 -0
  241. package/src/usage/coverage.js +116 -0
  242. package/src/usage/input-loader.js +139 -0
  243. package/src/usage/js/analyzer.js +240 -0
  244. package/src/usage/js/parser.js +21 -0
  245. package/src/usage/runtime.js +187 -0
  246. package/src/usage/scope.js +44 -0
  247. package/src/usage/source-files.js +50 -0
  248. package/src/usage/usage-index.js +217 -0
  249. package/src/usage/writer.js +31 -0
  250. package/src/version-analysis-loader.js +203 -0
  251. package/src/version-analysis-manifest.js +314 -0
  252. package/src/version-analysis-writer.js +30 -0
@@ -0,0 +1,187 @@
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { DEFAULT_MANIFEST_PATH, DEFAULT_VERSION_ANALYSIS_PATH } from '../constants.js';
5
+ import { createCooperativeScheduler } from '../cooperative-scheduler.js';
6
+ import { relativePath } from '../files.js';
7
+ import { createUsageAnalyzerRegistry } from './analyzer-registry.js';
8
+ import { buildProjectUsageCoverage } from './coverage.js';
9
+ import { loadUsageDiscoveryInputs } from './input-loader.js';
10
+ import { createJavaScriptUsageAnalyzer } from './js/analyzer.js';
11
+ import { createUsageDiscoveryScope } from './scope.js';
12
+ import { collectUsageSourceFiles } from './source-files.js';
13
+ import { buildUsageIndex } from './usage-index.js';
14
+
15
+ async function repositoryDirectory(inputPath) {
16
+ const resolved = path.resolve(inputPath);
17
+ let details;
18
+ try {
19
+ details = await stat(resolved);
20
+ } catch (error) {
21
+ throw new Error(`Cannot access repository root "${inputPath}": ${error.message}`);
22
+ }
23
+ if (!details.isDirectory()) throw new Error(`Repository root is not a directory: ${inputPath}`);
24
+ return realpath(resolved);
25
+ }
26
+
27
+ function pathBelongsToProject(file, projectPath) {
28
+ if (projectPath === '.') return true;
29
+ return file === projectPath || file.startsWith(`${projectPath}/`);
30
+ }
31
+
32
+ function owningProject(file, scopes, registry) {
33
+ const candidates = scopes.filter((scope) => (
34
+ pathBelongsToProject(file, scope.projectPath) && registry.find(scope.ecosystem, file)
35
+ ));
36
+ return candidates.sort((left, right) => (
37
+ right.projectPath.split('/').length - left.projectPath.split('/').length
38
+ || left.projectId.localeCompare(right.projectId)
39
+ ))[0] ?? null;
40
+ }
41
+
42
+ function coverageOwner(warningPath, projects) {
43
+ return projects
44
+ .filter((project) => pathBelongsToProject(warningPath, project.projectPath))
45
+ .sort((left, right) => (
46
+ right.projectPath.split('/').length - left.projectPath.split('/').length
47
+ || left.projectId.localeCompare(right.projectId)
48
+ ))[0] ?? null;
49
+ }
50
+
51
+ export function createDefaultUsageAnalyzerRegistry() {
52
+ return createUsageAnalyzerRegistry([createJavaScriptUsageAnalyzer()]);
53
+ }
54
+
55
+ export async function discoverRepositoryUsage({
56
+ repositoryRoot,
57
+ projectManifest,
58
+ versionAnalysis,
59
+ input,
60
+ registry = createDefaultUsageAnalyzerRegistry(),
61
+ clock,
62
+ maxDepth,
63
+ signal,
64
+ cooperativeScheduler: injectedCooperativeScheduler,
65
+ cooperativeScheduling = true,
66
+ cooperativeBatchSize,
67
+ cooperativeMaxIntervalMs,
68
+ monotonicClock
69
+ }) {
70
+ const cooperativeScheduler = injectedCooperativeScheduler ?? createCooperativeScheduler({
71
+ signal,
72
+ enabled: cooperativeScheduling,
73
+ batchSize: cooperativeBatchSize,
74
+ maxIntervalMs: cooperativeMaxIntervalMs,
75
+ monotonicClock
76
+ });
77
+ cooperativeScheduler.checkpoint();
78
+ const root = await repositoryDirectory(repositoryRoot);
79
+ const scopes = createUsageDiscoveryScope(projectManifest, versionAnalysis);
80
+ const projectScopes = projectManifest.projects.map((project) => ({
81
+ projectId: project.id,
82
+ projectPath: project.path,
83
+ ecosystem: project.ecosystem
84
+ }));
85
+ const dependenciesByProject = new Map();
86
+ for (const scope of scopes) {
87
+ if (!dependenciesByProject.has(scope.projectId)) dependenciesByProject.set(scope.projectId, []);
88
+ dependenciesByProject.get(scope.projectId).push(scope);
89
+ await cooperativeScheduler.boundary();
90
+ }
91
+ const collected = await collectUsageSourceFiles(root, registry.extensions(), {
92
+ maxDepth,
93
+ cooperativeScheduler
94
+ });
95
+ const warnings = [...collected.warnings];
96
+ const usages = [];
97
+ let scannedFileCount = 0;
98
+ let analyzedFileCount = 0;
99
+ const coverageCounts = new Map(projectScopes.map((project) => [project.projectId, {
100
+ scannedFileCount: 0,
101
+ analyzedFileCount: 0,
102
+ parseFailureCount: 0,
103
+ analyzerFailureCount: 0,
104
+ unreadableFileCount: 0,
105
+ scanFailureCount: 0
106
+ }]));
107
+ for (const warning of collected.warnings) {
108
+ const owner = coverageOwner(warning.path, projectScopes);
109
+ if (owner) coverageCounts.get(owner.projectId).scanFailureCount += 1;
110
+ }
111
+
112
+ for (const absoluteFile of collected.files) {
113
+ try {
114
+ const file = relativePath(root, absoluteFile);
115
+ const project = owningProject(file, projectScopes, registry);
116
+ if (!project) continue;
117
+ if (!dependenciesByProject.has(project.projectId)) continue;
118
+ const analyzer = registry.find(project.ecosystem, file);
119
+ if (!analyzer) continue;
120
+ scannedFileCount += 1;
121
+ const counts = coverageCounts.get(project.projectId);
122
+ counts.scannedFileCount += 1;
123
+ let source;
124
+ try {
125
+ source = await readFile(absoluteFile, 'utf8');
126
+ } catch (error) {
127
+ warnings.push({
128
+ code: 'FILE_UNREADABLE',
129
+ path: file,
130
+ message: `Unable to read source file (${error.code ?? 'unknown error'}).`
131
+ });
132
+ counts.unreadableFileCount += 1;
133
+ continue;
134
+ }
135
+ try {
136
+ const discovered = await analyzer.analyze({
137
+ source,
138
+ file,
139
+ projectId: project.projectId,
140
+ dependencies: dependenciesByProject.get(project.projectId) ?? []
141
+ });
142
+ usages.push(...discovered.map((usage) => ({ ...usage, projectId: project.projectId })));
143
+ analyzedFileCount += 1;
144
+ counts.analyzedFileCount += 1;
145
+ } catch (error) {
146
+ warnings.push({
147
+ code: error instanceof SyntaxError ? 'SOURCE_PARSE_FAILED' : 'ANALYZER_FAILED',
148
+ path: file,
149
+ message: error instanceof SyntaxError
150
+ ? 'Unable to parse source file.'
151
+ : `Analyzer failed (${error.code ?? error.name ?? 'unknown error'}).`
152
+ });
153
+ if (error instanceof SyntaxError) counts.parseFailureCount += 1;
154
+ else counts.analyzerFailureCount += 1;
155
+ }
156
+ } finally {
157
+ await cooperativeScheduler.boundary();
158
+ }
159
+ }
160
+
161
+ cooperativeScheduler.checkpoint();
162
+ const coverage = projectScopes.map((project) => buildProjectUsageCoverage({
163
+ ...project,
164
+ analyzers: registry.forEcosystem(project.ecosystem),
165
+ ...coverageCounts.get(project.projectId)
166
+ }));
167
+ return buildUsageIndex({
168
+ input,
169
+ usages,
170
+ scannedFileCount,
171
+ analyzedFileCount,
172
+ analyzers: registry.analyzers(),
173
+ coverage,
174
+ warnings,
175
+ generatedAt: clock?.() ?? new Date()
176
+ });
177
+ }
178
+
179
+ export async function runUsageDiscovery({ repositoryRoot = '.', sources, ...options } = {}) {
180
+ const root = path.resolve(repositoryRoot);
181
+ const effectiveSources = sources ?? {
182
+ projectManifest: path.join(root, DEFAULT_MANIFEST_PATH),
183
+ versionAnalysis: path.join(root, DEFAULT_VERSION_ANALYSIS_PATH)
184
+ };
185
+ const artifacts = await loadUsageDiscoveryInputs(effectiveSources, options);
186
+ return discoverRepositoryUsage({ repositoryRoot, ...artifacts, ...options });
187
+ }
@@ -0,0 +1,44 @@
1
+ import { compareText } from '../portable.js';
2
+
3
+ function packageNameFromId(packageId) {
4
+ const separator = packageId.indexOf(':');
5
+ return separator === -1 ? packageId : packageId.slice(separator + 1);
6
+ }
7
+
8
+ export function createUsageDiscoveryScope(projectManifest, versionAnalysisManifest) {
9
+ const projects = new Map(projectManifest.projects.map((project) => [project.id, project]));
10
+ const scoped = new Map();
11
+
12
+ for (const result of versionAnalysisManifest.results) {
13
+ const project = projects.get(result.dependency.projectId);
14
+ if (!project) {
15
+ throw new Error(`Usage Discovery input error: unknown project ${result.dependency.projectId}.`);
16
+ }
17
+ if (project.ecosystem !== result.dependency.ecosystem) {
18
+ throw new Error(`Usage Discovery input error: ecosystem mismatch for ${project.id}.`);
19
+ }
20
+ const declared = project.dependencies.find((dependency) => (
21
+ dependency.normalizedName === result.dependency.normalizedName
22
+ && dependency.type === result.dependency.dependencyType
23
+ && dependency.manifest === result.dependency.manifest
24
+ ));
25
+ if (!declared) {
26
+ throw new Error(
27
+ `Usage Discovery input error: ${result.dependency.packageId} is not declared by ${project.id}.`
28
+ );
29
+ }
30
+ const key = `${project.id}\0${result.dependency.packageId}`;
31
+ scoped.set(key, {
32
+ projectId: project.id,
33
+ projectPath: project.path,
34
+ ecosystem: project.ecosystem,
35
+ packageId: result.dependency.packageId,
36
+ name: declared.name || packageNameFromId(result.dependency.packageId),
37
+ normalizedName: declared.normalizedName
38
+ });
39
+ }
40
+
41
+ return [...scoped.values()].sort((left, right) => (
42
+ compareText(left.projectId, right.projectId) || compareText(left.packageId, right.packageId)
43
+ ));
44
+ }
@@ -0,0 +1,50 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { DEFAULT_IGNORED_DIRECTORIES } from '../constants.js';
5
+ import { relativePath } from '../files.js';
6
+
7
+ export async function collectUsageSourceFiles(root, extensions, options = {}) {
8
+ const supportedExtensions = new Set(extensions.map((extension) => extension.toLowerCase()));
9
+ const maxDepth = options.maxDepth ?? Number.POSITIVE_INFINITY;
10
+ const cooperativeScheduler = options.cooperativeScheduler;
11
+ const files = [];
12
+ const warnings = [];
13
+
14
+ async function visit(directory, depth) {
15
+ cooperativeScheduler?.checkpoint();
16
+ let entries;
17
+ try {
18
+ entries = await readdir(directory, { withFileTypes: true });
19
+ } catch (error) {
20
+ warnings.push({
21
+ code: 'DIRECTORY_UNREADABLE',
22
+ path: relativePath(root, directory),
23
+ message: `Unable to read directory (${error.code ?? 'unknown error'}).`
24
+ });
25
+ return;
26
+ }
27
+
28
+ entries.sort((left, right) => left.name.localeCompare(right.name));
29
+ for (const entry of entries) {
30
+ const entryPath = path.join(directory, entry.name);
31
+ try {
32
+ if (entry.isSymbolicLink()) continue;
33
+ if (entry.isDirectory()) {
34
+ if (depth < maxDepth && !DEFAULT_IGNORED_DIRECTORIES.has(entry.name)) {
35
+ await visit(entryPath, depth + 1);
36
+ }
37
+ } else if (entry.isFile() && supportedExtensions.has(path.extname(entry.name).toLowerCase())) {
38
+ files.push(entryPath);
39
+ }
40
+ } finally {
41
+ await cooperativeScheduler?.boundary();
42
+ }
43
+ }
44
+ }
45
+
46
+ await visit(root, 0);
47
+ return { files, warnings };
48
+ }
49
+
50
+ export const collectRepositorySourceFiles = collectUsageSourceFiles;
@@ -0,0 +1,217 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ import Ajv2020 from 'ajv/dist/2020.js';
4
+ import addFormats from 'ajv-formats';
5
+
6
+ import {
7
+ ARTIFACT_GENERATOR_NAME,
8
+ USAGE_INDEX_SCHEMA_VERSION,
9
+ VERSION
10
+ } from '../constants.js';
11
+ import { compareText, isSorted } from '../portable.js';
12
+ import {
13
+ USAGE_COVERAGE_REASON_CODES,
14
+ USAGE_COVERAGE_STATUSES,
15
+ compareCoverage
16
+ } from './coverage.js';
17
+
18
+ const schema = JSON.parse(await readFile(
19
+ new URL('../../schemas/usage-index.schema.json', import.meta.url),
20
+ 'utf8'
21
+ ));
22
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
23
+ addFormats(ajv);
24
+ const validateSchema = ajv.compile(schema);
25
+
26
+ const compareDependencies = (left, right) => (
27
+ compareText(left.projectId, right.projectId) || compareText(left.packageId, right.packageId)
28
+ );
29
+ const compareSymbols = (left, right) => compareText(left.name, right.name);
30
+ const compareWarnings = (left, right) => (
31
+ compareText(left.path, right.path) || compareText(left.code, right.code) || compareText(left.message, right.message)
32
+ );
33
+ const compareAnalyzers = (left, right) => compareText(left.id, right.id) || compareText(left.version, right.version);
34
+
35
+ function indexUsages(usages) {
36
+ const dependencies = new Map();
37
+ for (const usage of usages) {
38
+ const dependencyKey = `${usage.projectId}\0${usage.packageId}`;
39
+ if (!dependencies.has(dependencyKey)) {
40
+ dependencies.set(dependencyKey, {
41
+ projectId: usage.projectId,
42
+ packageId: usage.packageId,
43
+ name: usage.dependency,
44
+ files: new Set(),
45
+ symbols: new Map()
46
+ });
47
+ }
48
+ const dependency = dependencies.get(dependencyKey);
49
+ dependency.files.add(usage.file);
50
+ if (usage.symbol === null) continue;
51
+ if (!dependency.symbols.has(usage.symbol)) dependency.symbols.set(usage.symbol, new Set());
52
+ dependency.symbols.get(usage.symbol).add(usage.file);
53
+ }
54
+
55
+ return [...dependencies.values()].map((dependency) => ({
56
+ projectId: dependency.projectId,
57
+ packageId: dependency.packageId,
58
+ name: dependency.name,
59
+ files: [...dependency.files].sort(compareText),
60
+ symbols: [...dependency.symbols].map(([name, files]) => ({
61
+ name,
62
+ files: [...files].sort(compareText)
63
+ })).sort(compareSymbols)
64
+ })).sort(compareDependencies);
65
+ }
66
+
67
+ function summary(dependencies, warningCount) {
68
+ const files = new Set(dependencies.flatMap((dependency) => dependency.files));
69
+ return {
70
+ dependencyCount: dependencies.length,
71
+ symbolCount: dependencies.reduce((count, dependency) => count + dependency.symbols.length, 0),
72
+ fileCount: files.size,
73
+ warningCount
74
+ };
75
+ }
76
+
77
+ export function validateUsageIndexInvariants(index) {
78
+ const errors = [];
79
+ if (!isSorted(index.dependencies, compareDependencies)) errors.push('dependencies must be sorted.');
80
+ if (!isSorted(index.analysis.analyzers, compareAnalyzers)) errors.push('analyzers must be sorted.');
81
+ if (index.analysis.coverage && !isSorted(index.analysis.coverage, compareCoverage)) {
82
+ errors.push('analysis.coverage must be sorted.');
83
+ }
84
+ if (new Set(index.analysis.analyzers.map((analyzer) => analyzer.id)).size !== index.analysis.analyzers.length) {
85
+ errors.push('analyzer ids must be unique.');
86
+ }
87
+ if (index.analysis.analyzedFileCount > index.analysis.scannedFileCount) {
88
+ errors.push('analysis.analyzedFileCount cannot exceed scannedFileCount.');
89
+ }
90
+ const coverageProjects = new Set();
91
+ let coverageScannedFileCount = 0;
92
+ let coverageAnalyzedFileCount = 0;
93
+ for (const coverage of index.analysis.coverage ?? []) {
94
+ if (coverageProjects.has(coverage.projectId)) {
95
+ errors.push(`duplicate coverage project ${coverage.projectId}.`);
96
+ }
97
+ coverageProjects.add(coverage.projectId);
98
+ if (!USAGE_COVERAGE_STATUSES.includes(coverage.status)) {
99
+ errors.push(`coverage ${coverage.projectId} has unsupported status.`);
100
+ }
101
+ if (!USAGE_COVERAGE_REASON_CODES.includes(coverage.reasonCode)) {
102
+ errors.push(`coverage ${coverage.projectId} has unsupported reasonCode.`);
103
+ }
104
+ if (coverage.analyzedFileCount > coverage.scannedFileCount) {
105
+ errors.push(`coverage ${coverage.projectId} analyzedFileCount cannot exceed scannedFileCount.`);
106
+ }
107
+ coverageScannedFileCount += coverage.scannedFileCount;
108
+ coverageAnalyzedFileCount += coverage.analyzedFileCount;
109
+ const processedFileCount = coverage.analyzedFileCount + coverage.parseFailureCount
110
+ + coverage.analyzerFailureCount + coverage.unreadableFileCount;
111
+ if (processedFileCount !== coverage.scannedFileCount) {
112
+ errors.push(`coverage ${coverage.projectId} file counts are inconsistent.`);
113
+ }
114
+ if ((coverage.status === 'unavailable') !== (coverage.analyzer === null)) {
115
+ errors.push(`coverage ${coverage.projectId} analyzer availability is inconsistent.`);
116
+ }
117
+ const failures = coverage.parseFailureCount + coverage.analyzerFailureCount
118
+ + coverage.unreadableFileCount + coverage.scanFailureCount;
119
+ if (coverage.status === 'complete' && failures !== 0) {
120
+ errors.push(`coverage ${coverage.projectId} complete status has failures.`);
121
+ }
122
+ if (coverage.status === 'complete' && coverage.reasonCode !== 'COVERAGE_COMPLETE') {
123
+ errors.push(`coverage ${coverage.projectId} complete reasonCode is inconsistent.`);
124
+ }
125
+ if (coverage.analyzer
126
+ && !index.analysis.analyzers.some((analyzer) => (
127
+ analyzer.id === coverage.analyzer.id && analyzer.version === coverage.analyzer.version
128
+ ))) {
129
+ errors.push(`coverage ${coverage.projectId} analyzer is not declared by analysis.`);
130
+ }
131
+ }
132
+ if ((index.analysis.coverage?.length ?? 0) > 0) {
133
+ if (coverageScannedFileCount !== index.analysis.scannedFileCount) {
134
+ errors.push('analysis.scannedFileCount does not equal project coverage total.');
135
+ }
136
+ if (coverageAnalyzedFileCount !== index.analysis.analyzedFileCount) {
137
+ errors.push('analysis.analyzedFileCount does not equal project coverage total.');
138
+ }
139
+ }
140
+ if (!isSorted(index.warnings, compareWarnings)) errors.push('warnings must be sorted.');
141
+ const identities = new Set();
142
+ for (const dependency of index.dependencies) {
143
+ const identity = `${dependency.projectId}\0${dependency.packageId}`;
144
+ if (identities.has(identity)) errors.push(`duplicate dependency ${dependency.packageId} in ${dependency.projectId}.`);
145
+ identities.add(identity);
146
+ if (!isSorted(dependency.files, compareText)) errors.push(`files for ${dependency.packageId} must be sorted.`);
147
+ if (new Set(dependency.files).size !== dependency.files.length) {
148
+ errors.push(`files for ${dependency.packageId} must be unique.`);
149
+ }
150
+ if (!isSorted(dependency.symbols, compareSymbols)) {
151
+ errors.push(`symbols for ${dependency.packageId} must be sorted.`);
152
+ }
153
+ if (new Set(dependency.symbols.map((symbol) => symbol.name)).size !== dependency.symbols.length) {
154
+ errors.push(`symbols for ${dependency.packageId} must be unique.`);
155
+ }
156
+ const dependencyFiles = new Set(dependency.files);
157
+ for (const symbol of dependency.symbols) {
158
+ if (!isSorted(symbol.files, compareText)) {
159
+ errors.push(`files for ${dependency.packageId}/${symbol.name} must be sorted.`);
160
+ }
161
+ if (new Set(symbol.files).size !== symbol.files.length) {
162
+ errors.push(`files for ${dependency.packageId}/${symbol.name} must be unique.`);
163
+ }
164
+ for (const file of symbol.files) {
165
+ if (!dependencyFiles.has(file)) {
166
+ errors.push(`file ${file} for ${dependency.packageId}/${symbol.name} is missing from dependency files.`);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ const expected = summary(index.dependencies, index.warnings.length);
172
+ for (const [field, value] of Object.entries(expected)) {
173
+ if (index.summary[field] !== value) {
174
+ errors.push(`summary.${field} is ${index.summary[field]}; expected ${value}.`);
175
+ }
176
+ }
177
+ return errors.sort(compareText);
178
+ }
179
+
180
+ export function validateUsageIndex(index) {
181
+ if (!validateSchema(index)) {
182
+ throw new Error(`Usage Index validation error: ${ajv.errorsText(validateSchema.errors, { separator: '; ' })}`);
183
+ }
184
+ const errors = validateUsageIndexInvariants(index);
185
+ if (errors.length > 0) throw new Error(`Usage Index invariant error: ${errors.join(' ')}`);
186
+ return index;
187
+ }
188
+
189
+ export function buildUsageIndex({
190
+ input,
191
+ usages,
192
+ scannedFileCount,
193
+ analyzedFileCount,
194
+ analyzers,
195
+ coverage = [],
196
+ warnings,
197
+ generatedAt = new Date()
198
+ }) {
199
+ const dependencies = indexUsages(usages);
200
+ const sortedWarnings = structuredClone(warnings).sort(compareWarnings);
201
+ const sortedAnalyzers = analyzers.map(({ id, version }) => ({ id, version })).sort(compareAnalyzers);
202
+ return validateUsageIndex({
203
+ schemaVersion: USAGE_INDEX_SCHEMA_VERSION,
204
+ generatedAt: generatedAt instanceof Date ? generatedAt.toISOString() : generatedAt,
205
+ generator: { name: ARTIFACT_GENERATOR_NAME, version: VERSION },
206
+ input: structuredClone(input),
207
+ analysis: {
208
+ analyzers: sortedAnalyzers,
209
+ scannedFileCount,
210
+ analyzedFileCount,
211
+ coverage: structuredClone(coverage).sort(compareCoverage)
212
+ },
213
+ summary: summary(dependencies, sortedWarnings.length),
214
+ dependencies,
215
+ warnings: sortedWarnings
216
+ });
217
+ }
@@ -0,0 +1,31 @@
1
+ import { mkdir, open, rename, rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { validateUsageIndex } from './usage-index.js';
5
+
6
+ export function serializeUsageIndex(index) {
7
+ validateUsageIndex(index);
8
+ return `${JSON.stringify(index, null, 2)}\n`;
9
+ }
10
+
11
+ export async function writeUsageIndex(outputPath, index) {
12
+ const target = path.resolve(outputPath);
13
+ const temporary = `${target}.${process.pid}.tmp`;
14
+ const contents = serializeUsageIndex(index);
15
+ await mkdir(path.dirname(target), { recursive: true });
16
+ let handle;
17
+ try {
18
+ handle = await open(temporary, 'w', 0o600);
19
+ await handle.writeFile(contents, 'utf8');
20
+ await handle.sync();
21
+ await handle.close();
22
+ handle = null;
23
+ await rename(temporary, target);
24
+ } catch (error) {
25
+ await handle?.close().catch(() => {});
26
+ await rm(temporary, { force: true }).catch(() => {});
27
+ throw error;
28
+ }
29
+ return target;
30
+ }
31
+