@mortice/mapwright 0.8.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 (166) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +262 -0
  4. package/dist/catalogue/canonicalisation.d.ts +36 -0
  5. package/dist/catalogue/canonicalisation.js +262 -0
  6. package/dist/catalogue/config.d.ts +13 -0
  7. package/dist/catalogue/config.js +77 -0
  8. package/dist/catalogue/deterministic.d.ts +3 -0
  9. package/dist/catalogue/deterministic.js +25 -0
  10. package/dist/catalogue/diagnostics.d.ts +2 -0
  11. package/dist/catalogue/diagnostics.js +34 -0
  12. package/dist/catalogue/evidence.d.ts +1 -0
  13. package/dist/catalogue/evidence.js +10 -0
  14. package/dist/catalogue/examples.d.ts +3 -0
  15. package/dist/catalogue/examples.js +20 -0
  16. package/dist/catalogue/identity.d.ts +61 -0
  17. package/dist/catalogue/identity.js +114 -0
  18. package/dist/catalogue/ignore.d.ts +4 -0
  19. package/dist/catalogue/ignore.js +29 -0
  20. package/dist/catalogue/indexer.d.ts +20 -0
  21. package/dist/catalogue/indexer.js +1393 -0
  22. package/dist/catalogue/linkage.d.ts +16 -0
  23. package/dist/catalogue/linkage.js +419 -0
  24. package/dist/catalogue/moduleResolution.d.ts +4 -0
  25. package/dist/catalogue/moduleResolution.js +67 -0
  26. package/dist/catalogue/ordering.d.ts +13 -0
  27. package/dist/catalogue/ordering.js +86 -0
  28. package/dist/catalogue/packages.d.ts +12 -0
  29. package/dist/catalogue/packages.js +475 -0
  30. package/dist/catalogue/pythonReExports.d.ts +7 -0
  31. package/dist/catalogue/pythonReExports.js +113 -0
  32. package/dist/catalogue/resolveRefs.d.ts +40 -0
  33. package/dist/catalogue/resolveRefs.js +284 -0
  34. package/dist/catalogue/scanner.d.ts +8 -0
  35. package/dist/catalogue/scanner.js +99 -0
  36. package/dist/catalogue/schema.d.ts +289 -0
  37. package/dist/catalogue/schema.js +1 -0
  38. package/dist/catalogue/store.d.ts +41 -0
  39. package/dist/catalogue/store.js +209 -0
  40. package/dist/catalogue/templates.d.ts +18 -0
  41. package/dist/catalogue/templates.js +158 -0
  42. package/dist/catalogue/version.d.ts +12 -0
  43. package/dist/catalogue/version.js +30 -0
  44. package/dist/cli/commands/adoptionPlan.d.ts +8 -0
  45. package/dist/cli/commands/adoptionPlan.js +24 -0
  46. package/dist/cli/commands/auditDocs.d.ts +8 -0
  47. package/dist/cli/commands/auditDocs.js +24 -0
  48. package/dist/cli/commands/changeImpact.d.ts +10 -0
  49. package/dist/cli/commands/changeImpact.js +36 -0
  50. package/dist/cli/commands/changes.d.ts +12 -0
  51. package/dist/cli/commands/changes.js +31 -0
  52. package/dist/cli/commands/check.d.ts +9 -0
  53. package/dist/cli/commands/check.js +4 -0
  54. package/dist/cli/commands/compatibility.d.ts +10 -0
  55. package/dist/cli/commands/compatibility.js +43 -0
  56. package/dist/cli/commands/diff.d.ts +31 -0
  57. package/dist/cli/commands/diff.js +118 -0
  58. package/dist/cli/commands/dogfood.d.ts +8 -0
  59. package/dist/cli/commands/dogfood.js +24 -0
  60. package/dist/cli/commands/drift.d.ts +8 -0
  61. package/dist/cli/commands/drift.js +13 -0
  62. package/dist/cli/commands/index.d.ts +9 -0
  63. package/dist/cli/commands/index.js +9 -0
  64. package/dist/cli/commands/indexWorkspace.d.ts +8 -0
  65. package/dist/cli/commands/indexWorkspace.js +23 -0
  66. package/dist/cli/commands/recommendConfig.d.ts +7 -0
  67. package/dist/cli/commands/recommendConfig.js +20 -0
  68. package/dist/cli/commands/schema.d.ts +1 -0
  69. package/dist/cli/commands/schema.js +37 -0
  70. package/dist/cli/commands/template.d.ts +5 -0
  71. package/dist/cli/commands/template.js +44 -0
  72. package/dist/cli/commands/validate.d.ts +9 -0
  73. package/dist/cli/commands/validate.js +85 -0
  74. package/dist/cli/index.d.ts +2 -0
  75. package/dist/cli/index.js +211 -0
  76. package/dist/comparison/artefactValidate.d.ts +19 -0
  77. package/dist/comparison/artefactValidate.js +334 -0
  78. package/dist/comparison/candidates.d.ts +12 -0
  79. package/dist/comparison/candidates.js +391 -0
  80. package/dist/comparison/compare.d.ts +15 -0
  81. package/dist/comparison/compare.js +240 -0
  82. package/dist/comparison/compatibility.d.ts +26 -0
  83. package/dist/comparison/compatibility.js +316 -0
  84. package/dist/comparison/drift.d.ts +34 -0
  85. package/dist/comparison/drift.js +71 -0
  86. package/dist/comparison/identity.d.ts +30 -0
  87. package/dist/comparison/identity.js +73 -0
  88. package/dist/comparison/impact.d.ts +14 -0
  89. package/dist/comparison/impact.js +291 -0
  90. package/dist/comparison/index.d.ts +6 -0
  91. package/dist/comparison/index.js +6 -0
  92. package/dist/comparison/integrity.d.ts +12 -0
  93. package/dist/comparison/integrity.js +199 -0
  94. package/dist/comparison/ordering.d.ts +4 -0
  95. package/dist/comparison/ordering.js +69 -0
  96. package/dist/comparison/projections.d.ts +19 -0
  97. package/dist/comparison/projections.js +185 -0
  98. package/dist/comparison/relationshipPolicy.d.ts +19 -0
  99. package/dist/comparison/relationshipPolicy.js +98 -0
  100. package/dist/comparison/schema.d.ts +178 -0
  101. package/dist/comparison/schema.js +16 -0
  102. package/dist/comparison/store.d.ts +58 -0
  103. package/dist/comparison/store.js +462 -0
  104. package/dist/comparison/validate.d.ts +23 -0
  105. package/dist/comparison/validate.js +137 -0
  106. package/dist/comparison/version.d.ts +9 -0
  107. package/dist/comparison/version.js +11 -0
  108. package/dist/extractors/docTags.d.ts +9 -0
  109. package/dist/extractors/docTags.js +106 -0
  110. package/dist/extractors/generic.d.ts +5 -0
  111. package/dist/extractors/generic.js +3 -0
  112. package/dist/extractors/javascript.d.ts +3 -0
  113. package/dist/extractors/javascript.js +4 -0
  114. package/dist/extractors/markdown.d.ts +7 -0
  115. package/dist/extractors/markdown.js +269 -0
  116. package/dist/extractors/python.d.ts +7 -0
  117. package/dist/extractors/python.js +314 -0
  118. package/dist/extractors/typescript.d.ts +7 -0
  119. package/dist/extractors/typescript.js +387 -0
  120. package/dist/index.d.ts +2 -0
  121. package/dist/index.js +2 -0
  122. package/dist/mcp/resources.d.ts +18 -0
  123. package/dist/mcp/resources.js +57 -0
  124. package/dist/mcp/server.d.ts +2 -0
  125. package/dist/mcp/server.js +95 -0
  126. package/dist/mcp/tools.d.ts +29 -0
  127. package/dist/mcp/tools.js +902 -0
  128. package/dist/queries/adoption.d.ts +62 -0
  129. package/dist/queries/adoption.js +366 -0
  130. package/dist/queries/capabilities.d.ts +45 -0
  131. package/dist/queries/capabilities.js +124 -0
  132. package/dist/queries/changes.d.ts +33 -0
  133. package/dist/queries/changes.js +53 -0
  134. package/dist/queries/dependencies.d.ts +17 -0
  135. package/dist/queries/dependencies.js +94 -0
  136. package/dist/queries/dogfood.d.ts +50 -0
  137. package/dist/queries/dogfood.js +116 -0
  138. package/dist/queries/editingContext.d.ts +84 -0
  139. package/dist/queries/editingContext.js +240 -0
  140. package/dist/queries/examples.d.ts +8 -0
  141. package/dist/queries/examples.js +16 -0
  142. package/dist/queries/files.d.ts +8 -0
  143. package/dist/queries/files.js +14 -0
  144. package/dist/queries/lists.d.ts +94 -0
  145. package/dist/queries/lists.js +184 -0
  146. package/dist/queries/overview.d.ts +2 -0
  147. package/dist/queries/overview.js +8 -0
  148. package/dist/queries/publicApis.d.ts +60 -0
  149. package/dist/queries/publicApis.js +120 -0
  150. package/dist/queries/ranking.d.ts +67 -0
  151. package/dist/queries/ranking.js +387 -0
  152. package/dist/queries/recommendConfig.d.ts +47 -0
  153. package/dist/queries/recommendConfig.js +123 -0
  154. package/dist/queries/relationships.d.ts +5 -0
  155. package/dist/queries/relationships.js +15 -0
  156. package/dist/queries/reuseCandidates.d.ts +34 -0
  157. package/dist/queries/reuseCandidates.js +181 -0
  158. package/dist/queries/symbols.d.ts +40 -0
  159. package/dist/queries/symbols.js +41 -0
  160. package/dist/security/pathPolicy.d.ts +12 -0
  161. package/dist/security/pathPolicy.js +46 -0
  162. package/dist/security/secretPatterns.d.ts +1 -0
  163. package/dist/security/secretPatterns.js +4 -0
  164. package/dist/security/sourceAccess.d.ts +12 -0
  165. package/dist/security/sourceAccess.js +35 -0
  166. package/package.json +60 -0
@@ -0,0 +1,1393 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { loadConfig } from "./config.js";
4
+ import { scanWorkspace } from "./scanner.js";
5
+ import { buildDiagnostics } from "./diagnostics.js";
6
+ import { computeCatalogueId, writeSnapshot } from "./store.js";
7
+ import { extractTypeScriptSymbols } from "../extractors/typescript.js";
8
+ import { extractJavaScriptSymbols } from "../extractors/javascript.js";
9
+ import { extractPythonSymbols } from "../extractors/python.js";
10
+ import { extractCatalogueMarkdown } from "../extractors/markdown.js";
11
+ import { firstTag, manyTags, parseDocTags } from "../extractors/docTags.js";
12
+ import { buildPackageModel, buildPublicImportPathBySymbolId, computePublicApiImportPath, findPackageForFile } from "./packages.js";
13
+ import { MAPWRIGHT_GENERATOR_NAME, MAPWRIGHT_GENERATOR_VERSION } from "./version.js";
14
+ import { buildDeterministicExampleId, normalizeExamplePath } from "./examples.js";
15
+ import { buildPublicApiRefIndex, buildSymbolRefIndex, resolvePublicApiReference, resolveSymbolReference } from "./resolveRefs.js";
16
+ import { IDENTITY_SCHEME, IdentityCollisionTracker, encodeIdentityPreimage, normalizeIdentityPath, normalizeIdentityString, publicApiIdentityId, relationshipIdentityId, testIdentityId } from "./identity.js";
17
+ import { canonicaliseSymbols } from "./canonicalisation.js";
18
+ import { buildLinkageRelationships } from "./linkage.js";
19
+ import { buildPythonInitReExports } from "./pythonReExports.js";
20
+ import { sortCapabilities, sortDiagnostics, sortExamples, sortFiles, sortPackages, sortPublicApis, sortRelationships, sortSymbols, sortTests } from "./ordering.js";
21
+ export function indexWorkspace(input) {
22
+ const workspaceRoot = path.resolve(input.workspaceRoot);
23
+ const { config, mode, configPath } = loadConfig(input);
24
+ const { files, diagnostics: scannerDiagnostics } = scanWorkspace(workspaceRoot, config);
25
+ const symbols = [];
26
+ const relationships = [];
27
+ const capabilitiesById = new Map();
28
+ const publicApis = [];
29
+ const examples = [];
30
+ const tests = [];
31
+ const documentationDiagnostics = [];
32
+ const fileByPath = new Map();
33
+ // Pass 1: collect authored packages/capabilities/examples from CATALOGUE.md only,
34
+ // so package identity is established before symbols reference it.
35
+ const markdownPackages = [];
36
+ for (const file of files) {
37
+ fileByPath.set(file.path, file);
38
+ if (/CATALOGUE\.md$/i.test(file.path)) {
39
+ const md = extractCatalogueMarkdown(workspaceRoot, file.path);
40
+ for (const pkg of md.packages) {
41
+ if (!markdownPackages.find((p) => p.id === pkg.id)) {
42
+ markdownPackages.push(pkg);
43
+ }
44
+ }
45
+ for (const capability of md.capabilities) {
46
+ upsertCapability(capabilitiesById, capability);
47
+ }
48
+ examples.push(...md.examples);
49
+ }
50
+ }
51
+ const packageModel = buildPackageModel(workspaceRoot, files, markdownPackages);
52
+ const packages = packageModel.packages;
53
+ // Pass 2: extract symbols/tests/examples with owning-package context.
54
+ for (const file of files) {
55
+ const packageRecord = findPackageForFile(packageModel, file.path);
56
+ if (file.language === "typescript") {
57
+ const result = extractTypeScriptSymbols(workspaceRoot, file, packageRecord);
58
+ symbols.push(...result.symbols);
59
+ relationships.push(...result.relationships);
60
+ documentationDiagnostics.push(...result.diagnostics);
61
+ }
62
+ else if (file.language === "javascript") {
63
+ const result = extractJavaScriptSymbols(workspaceRoot, file, packageRecord);
64
+ symbols.push(...result.symbols);
65
+ relationships.push(...result.relationships);
66
+ documentationDiagnostics.push(...result.diagnostics);
67
+ }
68
+ else if (file.language === "python") {
69
+ const result = extractPythonSymbols(workspaceRoot, file, packageRecord);
70
+ symbols.push(...result.symbols);
71
+ relationships.push(...result.relationships);
72
+ documentationDiagnostics.push(...result.diagnostics);
73
+ }
74
+ if (file.isTest) {
75
+ const extracted = extractTestRecords(workspaceRoot, file);
76
+ tests.push(...extracted.tests);
77
+ documentationDiagnostics.push(...extracted.diagnostics);
78
+ }
79
+ if (!file.isTest && (file.language === "typescript" || file.language === "javascript" || file.language === "python")) {
80
+ const extractedExamples = extractExampleRecords(workspaceRoot, file);
81
+ examples.push(...extractedExamples.examples);
82
+ documentationDiagnostics.push(...extractedExamples.diagnostics);
83
+ }
84
+ }
85
+ // Collapse generated declaration duplicates onto authored sources, then remap
86
+ // relationship endpoints to canonical symbol ids (keeping extracted ids as evidence).
87
+ const canonicalBySymbolId = canonicaliseSymbols(symbols, documentationDiagnostics);
88
+ remapRelationshipEndpoints(relationships, canonicalBySymbolId);
89
+ // Explicit Python __init__.py re-exports attribute the package-root import
90
+ // path to re-exported symbols, so they must exist before import-path resolution.
91
+ relationships.push(...buildPythonInitReExports({ workspaceRoot, files, symbols }));
92
+ const publicImportPathBySymbolId = buildPublicImportPathBySymbolId(packageModel, relationships);
93
+ const defaultExportSymbolIds = new Set(relationships
94
+ .filter((rel) => rel.type === "exports" && rel.to.kind === "symbol" && rel.localDiscriminant === "default")
95
+ .map((rel) => rel.to.id));
96
+ const publicApiBySymbolId = new Map();
97
+ for (const symbol of symbols) {
98
+ if (symbol.duplicateOf) {
99
+ continue;
100
+ }
101
+ for (const cap of symbol.capabilities) {
102
+ const capId = cap.startsWith("capability:") ? cap : `capability:${cap}`;
103
+ const record = ensureCapability(capabilitiesById, capId);
104
+ if (!record.symbols.includes(symbol.id))
105
+ record.symbols.push(symbol.id);
106
+ if (!record.files.includes(symbol.filePath))
107
+ record.files.push(symbol.filePath);
108
+ }
109
+ const docTags = symbol.doc?.tags ?? [];
110
+ const isPublicApiTag = Boolean(docTags.find((tag) => tag.tag === "publicApi")) || firstTag(docTags, "catalogue") === "public-api";
111
+ const symbolFile = fileByPath.get(symbol.filePath);
112
+ if ((symbol.exported || isPublicApiTag) && !symbolFile?.isTest && !symbolFile?.isExample) {
113
+ const packageRecord = findPackageForFile(packageModel, symbol.filePath);
114
+ const importPath = computePublicApiImportPath(workspaceRoot, symbol.filePath, packageRecord, symbol.language, publicImportPathBySymbolId.get(symbol.id));
115
+ const exportKind = defaultExportSymbolIds.has(symbol.id) ? "default" : "named";
116
+ const packageId = packageRecord?.id ?? "package:unknown";
117
+ // Public export identity is consumer-facing: package + import path + name + kind.
118
+ // The declaration is retained as a link (symbolId), not part of the identity.
119
+ const apiId = publicApiIdentityId({
120
+ packageId,
121
+ importPath: importPath ?? "",
122
+ exportedName: symbol.name,
123
+ exportKind
124
+ });
125
+ symbol.publicApi = apiId;
126
+ const capabilityIds = symbol.capabilities.map((c) => (c.startsWith("capability:") ? c : `capability:${c}`));
127
+ const api = {
128
+ id: apiId,
129
+ symbolId: symbol.id,
130
+ name: symbol.name,
131
+ importPath,
132
+ exportKind,
133
+ packageId,
134
+ signature: symbol.signature,
135
+ capabilityIds,
136
+ stability: firstTag(docTags, "stability") ?? "unknown",
137
+ intendedUse: firstTag(docTags, "intendedUse"),
138
+ inputs: firstTag(docTags, "inputs"),
139
+ returns: firstTag(docTags, "returns"),
140
+ contracts: manyTags(docTags, "contract"),
141
+ errors: manyTags(docTags, "errors"),
142
+ examples: [],
143
+ tests: manyTags(docTags, "tests"),
144
+ provenance: { source: symbol.language === "python" ? "python-docstring" : "jsdoc" }
145
+ };
146
+ const apiExamples = buildExamplesFromReferences(manyTags(docTags, "example"), {
147
+ sourceFilePath: symbol.filePath,
148
+ capabilityIds,
149
+ publicApiId: apiId,
150
+ symbolId: symbol.id,
151
+ provenanceSource: symbol.language === "python" ? "python-docstring" : "jsdoc"
152
+ });
153
+ api.examples = apiExamples.map((example) => example.id);
154
+ examples.push(...apiExamples);
155
+ publicApis.push(api);
156
+ publicApiBySymbolId.set(symbol.id, apiId);
157
+ const file = fileByPath.get(symbol.filePath);
158
+ if (file && !file.publicApis.includes(apiId)) {
159
+ file.publicApis.push(apiId);
160
+ }
161
+ for (const capId of capabilityIds) {
162
+ const capability = ensureCapability(capabilitiesById, capId);
163
+ if (!capability.publicApis.includes(api.id))
164
+ capability.publicApis.push(api.id);
165
+ }
166
+ }
167
+ }
168
+ const capabilities = [...capabilitiesById.values()]
169
+ .map((cap) => ({
170
+ ...cap,
171
+ files: [...new Set(cap.files)].sort(),
172
+ symbols: [...new Set(cap.symbols)].sort(),
173
+ publicApis: [...new Set(cap.publicApis)].sort()
174
+ }))
175
+ .sort((a, b) => a.id.localeCompare(b.id));
176
+ const symbolResolutionDiagnostics = resolveTestSymbolReferences(tests, symbols, packageModel);
177
+ documentationDiagnostics.push(...symbolResolutionDiagnostics);
178
+ const publicApiResolutionDiagnostics = resolveTestPublicApiReferences(tests, publicApis, symbols, packageModel);
179
+ documentationDiagnostics.push(...publicApiResolutionDiagnostics);
180
+ const capabilityPublicApiDiagnostics = resolveCapabilityPublicApiReferences(capabilities, publicApis, symbols, packageModel);
181
+ documentationDiagnostics.push(...capabilityPublicApiDiagnostics);
182
+ const testRelationships = buildTestRelationships(tests, symbols, publicApiBySymbolId, new Set(publicApis.map((api) => api.id)));
183
+ relationships.push(...testRelationships);
184
+ // Gate 3: conservative, provenanced consumer→API linkage (resolved direct
185
+ // import/usage only). Never relabels inferred references as coverage.
186
+ const linkageRelationships = buildLinkageRelationships({
187
+ workspaceRoot,
188
+ files,
189
+ symbols,
190
+ publicApis,
191
+ relationships,
192
+ packageModel
193
+ });
194
+ relationships.push(...linkageRelationships);
195
+ dedupeRelationshipsById(relationships);
196
+ for (const file of files) {
197
+ const packageRecord = findPackageForFile(packageModel, file.path);
198
+ if (!packageRecord) {
199
+ continue;
200
+ }
201
+ if (file.publicApis.length > 0) {
202
+ packageRecord.publicPaths = [...new Set([...(packageRecord.publicPaths ?? []), file.path])].sort();
203
+ }
204
+ else if (file.kind === "source") {
205
+ packageRecord.internalPaths = [...new Set([...(packageRecord.internalPaths ?? []), file.path])].sort();
206
+ }
207
+ }
208
+ const symbolById = new Map(symbols.map((symbol) => [symbol.id, symbol]));
209
+ for (const capability of capabilities) {
210
+ const linkedPackages = new Set(capability.packages);
211
+ for (const filePath of capability.files) {
212
+ const packageRecord = packageModel.filePackageByPath.get(filePath);
213
+ if (packageRecord) {
214
+ linkedPackages.add(packageRecord.id);
215
+ packageRecord.capabilities = [...new Set([...(packageRecord.capabilities ?? []), capability.id])].sort();
216
+ }
217
+ }
218
+ capability.packages = [...linkedPackages].sort();
219
+ }
220
+ for (const api of publicApis) {
221
+ const symbol = symbolById.get(api.symbolId);
222
+ const packageRecord = symbol ? packageModel.filePackageByPath.get(symbol.filePath) : undefined;
223
+ if (packageRecord) {
224
+ packageRecord.publicApis = [...new Set([...(packageRecord.publicApis ?? []), api.id])].sort();
225
+ }
226
+ }
227
+ for (const test of tests) {
228
+ const packageRecord = packageModel.filePackageByPath.get(test.filePath);
229
+ if (packageRecord) {
230
+ packageRecord.tests = [...new Set([...(packageRecord.tests ?? []), test.id])].sort();
231
+ }
232
+ }
233
+ const { examples: normalizedExamples, diagnostics: exampleResolutionDiagnostics } = normalizeExamples(examples, capabilities, publicApis, symbols, packageModel);
234
+ documentationDiagnostics.push(...exampleResolutionDiagnostics);
235
+ const boundaryDiagnostics = [];
236
+ for (const relationship of relationships) {
237
+ if (relationship.type !== "imports" || relationship.from.kind !== "file" || relationship.to.kind !== "file") {
238
+ continue;
239
+ }
240
+ const sourcePackage = packageModel.filePackageByPath.get(relationship.from.id);
241
+ const targetPackage = packageModel.filePackageByPath.get(relationship.to.id);
242
+ if (!sourcePackage || !targetPackage) {
243
+ boundaryDiagnostics.push({
244
+ id: `diag:${relationship.id}:package_boundary_unknown`,
245
+ severity: "info",
246
+ category: "resolution",
247
+ code: "package_boundary_unknown",
248
+ message: `Package boundary could not be determined for import ${relationship.from.id} -> ${relationship.to.id}.`,
249
+ filePath: relationship.from.id,
250
+ target: { kind: "file", id: relationship.from.id },
251
+ provenance: { source: "validation" }
252
+ });
253
+ continue;
254
+ }
255
+ if (sourcePackage.id !== targetPackage.id) {
256
+ boundaryDiagnostics.push({
257
+ id: `diag:${relationship.id}:cross_boundary_import`,
258
+ severity: "warning",
259
+ category: "resolution",
260
+ code: "cross_boundary_import",
261
+ message: `Import crosses package boundary from ${sourcePackage.id} to ${targetPackage.id}.`,
262
+ filePath: relationship.from.id,
263
+ target: { kind: "package", id: sourcePackage.id },
264
+ provenance: { source: "validation" }
265
+ });
266
+ }
267
+ }
268
+ for (const packageRecord of packages) {
269
+ if ((packageRecord.entrypoints?.length ?? 0) > 0 || (packageRecord.modules?.length ?? 0) === 0) {
270
+ continue;
271
+ }
272
+ if (packageRecord.provenance.warnings?.some((warning) => warning.startsWith("private-workspace-root"))) {
273
+ continue;
274
+ }
275
+ boundaryDiagnostics.push({
276
+ id: `diag:${packageRecord.id}:package_missing_entrypoint`,
277
+ severity: "info",
278
+ category: "documentation",
279
+ code: "package_missing_entrypoint",
280
+ message: `Package ${packageRecord.id} has no detected entrypoints.`,
281
+ target: { kind: "package", id: packageRecord.id },
282
+ provenance: { source: "validation" }
283
+ });
284
+ }
285
+ const diagnostics = sortDiagnostics([
286
+ ...scannerDiagnostics,
287
+ ...documentationDiagnostics,
288
+ ...buildDiagnostics(symbols, config),
289
+ ...duplicateIdDiagnostics(symbols, capabilities, publicApis, tests),
290
+ ...packageDocDiagnostics(files, packages),
291
+ ...capabilityResolutionDiagnostics(capabilities, symbols),
292
+ ...publicApiCapabilityDiagnostics(publicApis, capabilities),
293
+ ...reuseDocumentationDiagnostics(capabilities, publicApis, normalizedExamples, tests, symbols),
294
+ ...testResolutionDiagnostics(tests, capabilities, symbols, publicApis),
295
+ ...boundaryDiagnostics
296
+ ]);
297
+ // Fail closed on any full-id hash collision before emitting a snapshot.
298
+ assertNoIdentityCollisions(packages, symbols, publicApis, tests, relationships, capabilities, normalizedExamples, diagnostics);
299
+ const sortedFiles = sortFiles(files);
300
+ const sortedSymbols = sortSymbols(symbols);
301
+ const sortedPublicApis = sortPublicApis(publicApis);
302
+ const sortedExamples = sortExamples(normalizedExamples);
303
+ const sortedPackages = sortPackages(packages);
304
+ const sortedRelationships = sortRelationships(relationships);
305
+ const sortedTests = sortTests(tests);
306
+ const sortedCapabilities = sortCapabilities(capabilities);
307
+ const snapshotNoId = {
308
+ schemaVersion: 2,
309
+ identityScheme: IDENTITY_SCHEME,
310
+ generator: {
311
+ name: MAPWRIGHT_GENERATOR_NAME,
312
+ version: MAPWRIGHT_GENERATOR_VERSION
313
+ },
314
+ mode,
315
+ stats: {
316
+ fileCount: sortedFiles.length,
317
+ symbolCount: sortedSymbols.length,
318
+ capabilityCount: sortedCapabilities.length,
319
+ publicApiCount: sortedPublicApis.length,
320
+ exampleCount: sortedExamples.length,
321
+ diagnosticCount: diagnostics.length,
322
+ languages: languageStats(sortedFiles)
323
+ },
324
+ files: sortedFiles,
325
+ packages: sortedPackages,
326
+ capabilities: sortedCapabilities,
327
+ publicApis: sortedPublicApis,
328
+ examples: sortedExamples,
329
+ symbols: sortedSymbols,
330
+ relationships: sortedRelationships,
331
+ tests: sortedTests,
332
+ diagnostics,
333
+ provenance: {
334
+ source: "indexer",
335
+ extractors: ["scanner", "typescript", "python", "markdown"],
336
+ documentationSources: [],
337
+ warnings: []
338
+ }
339
+ };
340
+ const snapshot = {
341
+ ...snapshotNoId,
342
+ catalogueId: computeCatalogueId(snapshotNoId)
343
+ };
344
+ if (input.write ?? mode !== "memory") {
345
+ writeSnapshot(workspaceRoot, config.catalogueDir, snapshot, input.pretty ?? true);
346
+ }
347
+ return { snapshot, configPath };
348
+ }
349
+ function ensureCapability(map, id) {
350
+ const existing = map.get(id);
351
+ if (existing) {
352
+ return existing;
353
+ }
354
+ const name = id.replace(/^capability:/, "");
355
+ const created = {
356
+ id,
357
+ name,
358
+ summary: undefined,
359
+ description: undefined,
360
+ keywords: name.split(/[.\-]/).filter(Boolean),
361
+ status: "available",
362
+ stability: "unknown",
363
+ publicApis: [],
364
+ symbols: [],
365
+ files: [],
366
+ packages: [],
367
+ examples: [],
368
+ tests: [],
369
+ contracts: [],
370
+ limitations: [],
371
+ provenance: { source: "docs+symbols" }
372
+ };
373
+ map.set(id, created);
374
+ return created;
375
+ }
376
+ function upsertCapability(map, record) {
377
+ const existing = map.get(record.id);
378
+ if (!existing) {
379
+ map.set(record.id, record);
380
+ return;
381
+ }
382
+ existing.summary = existing.summary ?? record.summary;
383
+ existing.description = existing.description ?? record.description;
384
+ existing.intendedUse = existing.intendedUse ?? record.intendedUse;
385
+ existing.stability = existing.stability === "unknown" ? record.stability : existing.stability;
386
+ existing.keywords = [...new Set([...existing.keywords, ...record.keywords])];
387
+ existing.publicApis = [...new Set([...existing.publicApis, ...record.publicApis])];
388
+ existing.symbols = [...new Set([...existing.symbols, ...record.symbols])];
389
+ existing.files = [...new Set([...existing.files, ...record.files])];
390
+ existing.packages = [...new Set([...(existing.packages ?? []), ...(record.packages ?? [])])];
391
+ existing.examples = [...new Set([...(existing.examples ?? []), ...(record.examples ?? [])])];
392
+ existing.tests = [...new Set([...(existing.tests ?? []), ...(record.tests ?? [])])];
393
+ existing.contracts = [...new Set([...(existing.contracts ?? []), ...(record.contracts ?? [])])];
394
+ existing.limitations = [...new Set([...(existing.limitations ?? []), ...(record.limitations ?? [])])];
395
+ }
396
+ function resolveCapabilityPublicApiReferences(capabilities, publicApis, symbols, packageModel) {
397
+ const index = buildPublicApiRefIndex(publicApis, symbols, packageModel.filePackageByPath);
398
+ const diagnostics = [];
399
+ for (const capability of capabilities) {
400
+ const resolved = new Set();
401
+ for (const ref of capability.publicApis) {
402
+ const result = resolvePublicApiReference(ref, index);
403
+ if (result.status === "resolved") {
404
+ resolved.add(result.value);
405
+ continue;
406
+ }
407
+ if (result.status === "ambiguous") {
408
+ diagnostics.push({
409
+ id: `diag:${capability.id}:ambiguous_public_api_reference:${ref}`,
410
+ severity: "warning",
411
+ category: "resolution",
412
+ code: "ambiguous_public_api_reference",
413
+ message: `Capability ${capability.id} has ambiguous public API reference ${ref}.`,
414
+ target: { kind: "capability", id: capability.id },
415
+ provenance: { source: "validation" }
416
+ });
417
+ continue;
418
+ }
419
+ diagnostics.push({
420
+ id: `diag:${capability.id}:capability_references_unknown_public_api:${ref}`,
421
+ severity: "warning",
422
+ category: "resolution",
423
+ code: "capability_references_unknown_public_api",
424
+ message: `Capability ${capability.id} references unknown public API ${ref}.`,
425
+ target: { kind: "capability", id: capability.id },
426
+ provenance: { source: "validation" }
427
+ });
428
+ }
429
+ capability.publicApis = [...resolved].sort();
430
+ }
431
+ return diagnostics;
432
+ }
433
+ function exampleSourceRank(source) {
434
+ switch (source) {
435
+ case "example-docs":
436
+ return 1;
437
+ case "jsdoc":
438
+ case "python-docstring":
439
+ return 2;
440
+ case "catalogue-md":
441
+ return 3;
442
+ default:
443
+ return 99;
444
+ }
445
+ }
446
+ function normalizeExamples(examples, capabilities, publicApis, symbols, packageModel) {
447
+ const symbolIds = new Set(symbols.map((symbol) => symbol.id));
448
+ const index = buildPublicApiRefIndex(publicApis, symbols, packageModel.filePackageByPath);
449
+ const diagnostics = [];
450
+ // Prefer file-level annotations over symbol @example over CATALOGUE.md when merging
451
+ // the same identity (normalized filePath + optional anchor).
452
+ const ordered = [...examples].sort((left, right) => {
453
+ const rankDelta = exampleSourceRank(left.provenance.source) - exampleSourceRank(right.provenance.source);
454
+ if (rankDelta !== 0) {
455
+ return rankDelta;
456
+ }
457
+ return left.id.localeCompare(right.id);
458
+ });
459
+ const merged = new Map();
460
+ for (const example of ordered) {
461
+ const existing = merged.get(example.id);
462
+ if (!existing) {
463
+ const resolvedIds = resolveExamplePublicApiIds(example.id, example.publicApiIds, index, example.filePath, diagnostics);
464
+ merged.set(example.id, {
465
+ ...example,
466
+ filePath: normalizeExamplePath(example.filePath),
467
+ capabilityIds: [...new Set(example.capabilityIds.map(normalizeCap))].sort(),
468
+ publicApiIds: resolvedIds,
469
+ symbolIds: [...new Set(example.symbolIds)].filter((id) => symbolIds.has(id)).sort()
470
+ });
471
+ continue;
472
+ }
473
+ const preferIncoming = exampleSourceRank(example.provenance.source) < exampleSourceRank(existing.provenance.source);
474
+ existing.capabilityIds = [...new Set([...existing.capabilityIds, ...example.capabilityIds.map(normalizeCap)])]
475
+ .sort();
476
+ existing.publicApiIds = resolveExamplePublicApiIds(example.id, [...existing.publicApiIds, ...example.publicApiIds], index, example.filePath ?? existing.filePath, diagnostics);
477
+ existing.symbolIds = [...new Set([...existing.symbolIds, ...example.symbolIds])].filter((id) => symbolIds.has(id)).sort();
478
+ existing.filePath = existing.filePath ?? normalizeExamplePath(example.filePath);
479
+ if (preferIncoming) {
480
+ existing.anchor = example.anchor ?? existing.anchor;
481
+ existing.title = example.title ?? existing.title;
482
+ existing.summary = example.summary ?? existing.summary;
483
+ existing.provenance = {
484
+ ...example.provenance,
485
+ locations: [...new Set([...(example.provenance.locations ?? []), ...(existing.provenance.locations ?? [])])].sort(),
486
+ warnings: [...new Set([...(example.provenance.warnings ?? []), ...(existing.provenance.warnings ?? [])])].sort()
487
+ };
488
+ }
489
+ else {
490
+ existing.anchor = existing.anchor ?? example.anchor;
491
+ existing.title = existing.title ?? example.title;
492
+ existing.summary = existing.summary ?? example.summary;
493
+ existing.provenance = {
494
+ ...existing.provenance,
495
+ locations: [...new Set([...(existing.provenance.locations ?? []), ...(example.provenance.locations ?? [])])].sort(),
496
+ warnings: [...new Set([...(existing.provenance.warnings ?? []), ...(example.provenance.warnings ?? [])])].sort()
497
+ };
498
+ }
499
+ }
500
+ const normalizedExamples = [...merged.values()].sort((a, b) => a.id.localeCompare(b.id));
501
+ const exampleIdsByCapability = new Map();
502
+ for (const example of normalizedExamples) {
503
+ for (const capabilityId of example.capabilityIds) {
504
+ const current = exampleIdsByCapability.get(capabilityId) ?? [];
505
+ current.push(example.id);
506
+ exampleIdsByCapability.set(capabilityId, current.sort());
507
+ }
508
+ }
509
+ for (const capability of capabilities) {
510
+ const current = capability.examples ?? [];
511
+ const resolved = current.map((id) => id.trim()).filter(Boolean);
512
+ const mergedIds = [...new Set([...(exampleIdsByCapability.get(capability.id) ?? []), ...resolved])]
513
+ .filter((id) => normalizedExamples.some((example) => example.id === id))
514
+ .sort();
515
+ capability.examples = mergedIds;
516
+ }
517
+ for (const api of publicApis) {
518
+ const linked = normalizedExamples
519
+ .filter((example) => example.publicApiIds.includes(api.id))
520
+ .map((example) => example.id);
521
+ api.examples = [...new Set([...api.examples, ...linked])]
522
+ .filter((id) => normalizedExamples.some((example) => example.id === id))
523
+ .sort();
524
+ }
525
+ return { examples: normalizedExamples, diagnostics };
526
+ }
527
+ function resolveExamplePublicApiIds(exampleId, refs, index, filePath, diagnostics) {
528
+ const resolved = new Set();
529
+ for (const ref of refs) {
530
+ const result = resolvePublicApiReference(ref, index);
531
+ if (result.status === "resolved") {
532
+ resolved.add(result.value);
533
+ continue;
534
+ }
535
+ if (result.status === "ambiguous") {
536
+ diagnostics.push({
537
+ id: `diag:${exampleId}:example_references_ambiguous_public_api:${ref}`,
538
+ severity: "warning",
539
+ category: "resolution",
540
+ code: "example_references_ambiguous_public_api",
541
+ message: `Example ${exampleId} has ambiguous public API reference ${ref}.`,
542
+ filePath,
543
+ provenance: { source: "validation" }
544
+ });
545
+ continue;
546
+ }
547
+ resolved.add(ref);
548
+ }
549
+ return [...resolved].sort();
550
+ }
551
+ function buildExamplesFromReferences(references, options) {
552
+ return references
553
+ .map((reference) => reference.trim())
554
+ .filter(Boolean)
555
+ .map((reference) => {
556
+ const [filePathRaw, anchorRaw] = reference.split("#", 2);
557
+ const filePath = normalizeExamplePath(filePathRaw?.trim());
558
+ const anchor = anchorRaw?.trim();
559
+ return {
560
+ id: buildDeterministicExampleId(filePath, anchor, reference),
561
+ title: anchor ? anchor.replace(/[-_]/g, " ") : filePath ? path.basename(filePath) : undefined,
562
+ filePath,
563
+ anchor,
564
+ capabilityIds: [...new Set(options.capabilityIds.map(normalizeCap))].sort(),
565
+ publicApiIds: [options.publicApiId],
566
+ symbolIds: [options.symbolId],
567
+ summary: undefined,
568
+ provenance: { source: options.provenanceSource, locations: [options.sourceFilePath] }
569
+ };
570
+ })
571
+ .sort((a, b) => a.id.localeCompare(b.id));
572
+ }
573
+ function reuseDocumentationDiagnostics(capabilities, publicApis, examples, tests, symbols) {
574
+ const diagnostics = [];
575
+ const capabilityIds = new Set(capabilities.map((capability) => capability.id));
576
+ const publicApiIds = new Set(publicApis.map((api) => api.id));
577
+ const testsByCapability = new Map();
578
+ for (const test of tests) {
579
+ for (const capabilityId of test.coversCapabilities) {
580
+ const current = testsByCapability.get(capabilityId) ?? [];
581
+ current.push(test.id);
582
+ testsByCapability.set(capabilityId, current);
583
+ }
584
+ }
585
+ const examplesByCapability = new Map();
586
+ for (const example of examples) {
587
+ for (const capabilityId of example.capabilityIds) {
588
+ const current = examplesByCapability.get(capabilityId) ?? [];
589
+ current.push(example.id);
590
+ examplesByCapability.set(capabilityId, current);
591
+ }
592
+ }
593
+ const testsByPublicApi = new Map();
594
+ const publicApiBySymbolId = new Map(symbols
595
+ .filter((symbol) => Boolean(symbol.publicApi))
596
+ .map((symbol) => [symbol.id, symbol.publicApi]));
597
+ for (const test of tests) {
598
+ for (const symbolId of test.coversSymbols) {
599
+ const publicApiId = publicApiBySymbolId.get(symbolId);
600
+ if (!publicApiId) {
601
+ continue;
602
+ }
603
+ const current = testsByPublicApi.get(publicApiId) ?? [];
604
+ current.push(test.id);
605
+ testsByPublicApi.set(publicApiId, current);
606
+ }
607
+ for (const publicApiId of test.coversPublicApis) {
608
+ if (!publicApiIds.has(publicApiId)) {
609
+ continue;
610
+ }
611
+ const current = testsByPublicApi.get(publicApiId) ?? [];
612
+ current.push(test.id);
613
+ testsByPublicApi.set(publicApiId, current);
614
+ }
615
+ }
616
+ for (const capability of capabilities) {
617
+ const authoredCapability = capability.provenance.source === "catalogue-md";
618
+ if (!authoredCapability || capability.stability === "internal" || capability.stability === "test-only") {
619
+ continue;
620
+ }
621
+ if (!capability.summary?.trim()) {
622
+ diagnostics.push({
623
+ id: `diag:${capability.id}:capability_missing_summary`,
624
+ severity: "warning",
625
+ category: "documentation",
626
+ code: "capability_missing_summary",
627
+ message: `Capability ${capability.id} is missing a summary.`,
628
+ target: { kind: "capability", id: capability.id },
629
+ provenance: { source: "validation" }
630
+ });
631
+ }
632
+ if (capability.publicApis.length === 0) {
633
+ diagnostics.push({
634
+ id: `diag:${capability.id}:capability_missing_public_api`,
635
+ severity: "warning",
636
+ category: "documentation",
637
+ code: "capability_missing_public_api",
638
+ message: `Capability ${capability.id} has no linked public API.`,
639
+ target: { kind: "capability", id: capability.id },
640
+ provenance: { source: "validation" }
641
+ });
642
+ }
643
+ const hasTests = (capability.tests?.length ?? 0) > 0 || (testsByCapability.get(capability.id)?.length ?? 0) > 0;
644
+ if (!hasTests) {
645
+ diagnostics.push({
646
+ id: `diag:${capability.id}:capability_missing_tests`,
647
+ severity: "warning",
648
+ category: "documentation",
649
+ code: "capability_missing_tests",
650
+ message: `Capability ${capability.id} has no linked tests.`,
651
+ target: { kind: "capability", id: capability.id },
652
+ provenance: { source: "validation" }
653
+ });
654
+ }
655
+ const hasExamples = (capability.examples?.length ?? 0) > 0 || (examplesByCapability.get(capability.id)?.length ?? 0) > 0;
656
+ if (!hasExamples) {
657
+ diagnostics.push({
658
+ id: `diag:${capability.id}:capability_missing_examples`,
659
+ severity: "info",
660
+ category: "documentation",
661
+ code: "capability_missing_examples",
662
+ message: `Capability ${capability.id} has no linked examples.`,
663
+ target: { kind: "capability", id: capability.id },
664
+ provenance: { source: "validation" }
665
+ });
666
+ }
667
+ }
668
+ for (const api of publicApis) {
669
+ const symbol = symbols.find((item) => item.id === api.symbolId);
670
+ const internal = symbol?.visibility === "private"
671
+ || symbol?.visibility === "supported_internal"
672
+ || symbol?.visibility === "test_only"
673
+ || api.stability === "internal"
674
+ || api.stability === "test-only";
675
+ if (internal) {
676
+ continue;
677
+ }
678
+ // Avoid noisy diagnostics for APIs that are not connected to authored capabilities.
679
+ if (api.capabilityIds.length === 0) {
680
+ continue;
681
+ }
682
+ const hasExamples = api.examples.length > 0 || examples.some((example) => example.publicApiIds.includes(api.id));
683
+ if (!hasExamples) {
684
+ diagnostics.push({
685
+ id: `diag:${api.id}:public_api_missing_example`,
686
+ severity: "info",
687
+ category: "documentation",
688
+ code: "public_api_missing_example",
689
+ message: `Public API ${api.id} has no linked example.`,
690
+ target: { kind: "publicApi", id: api.id },
691
+ provenance: { source: "validation" }
692
+ });
693
+ }
694
+ const hasTests = api.tests.length > 0 || (testsByPublicApi.get(api.id)?.length ?? 0) > 0;
695
+ if (!hasTests) {
696
+ diagnostics.push({
697
+ id: `diag:${api.id}:public_api_missing_test_reference`,
698
+ severity: "warning",
699
+ category: "documentation",
700
+ code: "public_api_missing_test_reference",
701
+ message: `Public API ${api.id} has no linked test reference.`,
702
+ target: { kind: "publicApi", id: api.id },
703
+ provenance: { source: "validation" }
704
+ });
705
+ }
706
+ }
707
+ for (const example of examples) {
708
+ if (!example.filePath) {
709
+ diagnostics.push({
710
+ id: `diag:${example.id}:example_missing_file_path`,
711
+ severity: "warning",
712
+ category: "documentation",
713
+ code: "example_missing_file_path",
714
+ message: `Example ${example.id} is missing a file path reference.`,
715
+ provenance: { source: "validation" }
716
+ });
717
+ }
718
+ if ((example.provenance.warnings?.length ?? 0) > 0) {
719
+ diagnostics.push({
720
+ id: `diag:${example.id}:example_malformed_metadata`,
721
+ severity: "warning",
722
+ category: "documentation",
723
+ code: "example_malformed_metadata",
724
+ message: `Example ${example.id} contains malformed metadata tokens.`,
725
+ filePath: example.filePath,
726
+ provenance: { source: "validation" }
727
+ });
728
+ }
729
+ for (const capabilityId of example.capabilityIds) {
730
+ if (!capabilityIds.has(capabilityId)) {
731
+ diagnostics.push({
732
+ id: `diag:${example.id}:example_references_unknown_capability:${capabilityId}`,
733
+ severity: "warning",
734
+ category: "resolution",
735
+ code: "example_references_unknown_capability",
736
+ message: `Example ${example.id} references unknown capability ${capabilityId}.`,
737
+ filePath: example.filePath,
738
+ provenance: { source: "validation" }
739
+ });
740
+ }
741
+ }
742
+ for (const publicApiId of example.publicApiIds) {
743
+ if (!publicApiIds.has(publicApiId)) {
744
+ diagnostics.push({
745
+ id: `diag:${example.id}:example_references_unknown_public_api:${publicApiId}`,
746
+ severity: "warning",
747
+ category: "resolution",
748
+ code: "example_references_unknown_public_api",
749
+ message: `Example ${example.id} references unknown public API ${publicApiId}.`,
750
+ filePath: example.filePath,
751
+ provenance: { source: "validation" }
752
+ });
753
+ }
754
+ }
755
+ }
756
+ return diagnostics;
757
+ }
758
+ function languageStats(files) {
759
+ const out = {};
760
+ for (const file of files) {
761
+ out[file.language] = (out[file.language] ?? 0) + 1;
762
+ }
763
+ return out;
764
+ }
765
+ function capabilityResolutionDiagnostics(capabilities, symbols) {
766
+ const symbolIds = new Set(symbols.map((s) => s.id));
767
+ const diagnostics = [];
768
+ for (const capability of capabilities) {
769
+ for (const symbolId of capability.symbols) {
770
+ if (!symbolIds.has(symbolId)) {
771
+ diagnostics.push({
772
+ id: `diag:${capability.id}:capability_references_unknown_symbol:${symbolId}`,
773
+ severity: "error",
774
+ category: "resolution",
775
+ code: "capability_references_unknown_symbol",
776
+ message: `Capability ${capability.id} references unknown symbol ${symbolId}.`,
777
+ target: { kind: "capability", id: capability.id },
778
+ provenance: { source: "validation" }
779
+ });
780
+ }
781
+ }
782
+ }
783
+ return diagnostics;
784
+ }
785
+ function publicApiCapabilityDiagnostics(publicApis, capabilities) {
786
+ const capabilityIds = new Set(capabilities.map((capability) => capability.id));
787
+ const diagnostics = [];
788
+ for (const api of publicApis) {
789
+ for (const capabilityId of api.capabilityIds) {
790
+ if (!capabilityIds.has(capabilityId)) {
791
+ diagnostics.push({
792
+ id: `diag:${api.id}:public_api_references_unknown_capability:${capabilityId}`,
793
+ severity: "error",
794
+ category: "resolution",
795
+ code: "public_api_references_unknown_capability",
796
+ message: `Public API ${api.id} references unknown capability ${capabilityId}.`,
797
+ target: { kind: "publicApi", id: api.id },
798
+ provenance: { source: "validation" }
799
+ });
800
+ }
801
+ }
802
+ }
803
+ return diagnostics;
804
+ }
805
+ function testResolutionDiagnostics(tests, capabilities, symbols, publicApis) {
806
+ const capabilityIds = new Set(capabilities.map((c) => c.id));
807
+ const symbolIds = new Set(symbols.map((s) => s.id));
808
+ const publicApiIds = new Set(publicApis.map((api) => api.id));
809
+ const diagnostics = [];
810
+ for (const test of tests) {
811
+ for (const capId of test.coversCapabilities) {
812
+ if (!capabilityIds.has(capId)) {
813
+ diagnostics.push({
814
+ id: `diag:${test.id}:test_references_unknown_capability:${capId}`,
815
+ severity: "error",
816
+ category: "resolution",
817
+ code: "test_references_unknown_capability",
818
+ message: `Test ${test.id} references unknown capability ${capId}.`,
819
+ filePath: test.filePath,
820
+ target: { kind: "test", id: test.id },
821
+ provenance: { source: "validation" }
822
+ });
823
+ }
824
+ }
825
+ for (const symbolId of test.coversSymbols) {
826
+ if (!symbolIds.has(symbolId)) {
827
+ diagnostics.push({
828
+ id: `diag:${test.id}:test_references_unknown_symbol:${symbolId}`,
829
+ severity: "warning",
830
+ category: "resolution",
831
+ code: "test_references_unknown_symbol",
832
+ message: `Test ${test.id} references unknown symbol ${symbolId}.`,
833
+ filePath: test.filePath,
834
+ target: { kind: "test", id: test.id },
835
+ provenance: { source: "validation" }
836
+ });
837
+ }
838
+ }
839
+ for (const publicApiId of test.coversPublicApis) {
840
+ if (!publicApiIds.has(publicApiId)) {
841
+ diagnostics.push({
842
+ id: `diag:${test.id}:test_references_unknown_public_api:${publicApiId}`,
843
+ severity: "warning",
844
+ category: "resolution",
845
+ code: "test_references_unknown_public_api",
846
+ message: `Test ${test.id} references unknown public API ${publicApiId}.`,
847
+ filePath: test.filePath,
848
+ target: { kind: "test", id: test.id },
849
+ provenance: { source: "validation" }
850
+ });
851
+ }
852
+ }
853
+ }
854
+ return diagnostics;
855
+ }
856
+ function extractExampleRecords(workspaceRoot, file) {
857
+ const absolute = path.resolve(workspaceRoot, file.path);
858
+ const content = readFileSync(absolute, "utf8");
859
+ const examples = [];
860
+ const diagnostics = [];
861
+ for (const block of extractDocBlocks(content, file.language)) {
862
+ const parsed = parseExampleDocBlock(file.path, block.text);
863
+ if (!parsed) {
864
+ continue;
865
+ }
866
+ examples.push(parsed.example);
867
+ diagnostics.push(...parsed.diagnostics);
868
+ }
869
+ examples.sort((a, b) => a.id.localeCompare(b.id));
870
+ diagnostics.sort((a, b) => a.id.localeCompare(b.id));
871
+ return { examples, diagnostics };
872
+ }
873
+ function parseExampleDocBlock(filePath, blockText) {
874
+ const parsed = parseDocTags(blockText, filePath);
875
+ const tags = parsed.tags;
876
+ const isCatalogueExample = tags.some((tag) => tag.tag === "catalogue" && tag.value === "example");
877
+ if (!isCatalogueExample) {
878
+ return undefined;
879
+ }
880
+ const capabilityIds = tags.filter((tag) => tag.tag === "coversCapability").map((tag) => normalizeCap(tag.value));
881
+ const publicApiRefs = tags.filter((tag) => tag.tag === "coversPublicApi").map((tag) => tag.value);
882
+ const normalizedPath = normalizeExamplePath(filePath);
883
+ const title = normalizedPath ? path.basename(normalizedPath) : undefined;
884
+ return {
885
+ example: {
886
+ id: buildDeterministicExampleId(normalizedPath, undefined, filePath),
887
+ title,
888
+ filePath: normalizedPath,
889
+ anchor: undefined,
890
+ capabilityIds,
891
+ publicApiIds: publicApiRefs,
892
+ symbolIds: [],
893
+ summary: parsed.summary || undefined,
894
+ provenance: { source: "example-docs", locations: [filePath] }
895
+ },
896
+ diagnostics: parsed.diagnostics
897
+ };
898
+ }
899
+ function extractTestRecords(workspaceRoot, file) {
900
+ const absolute = path.resolve(workspaceRoot, file.path);
901
+ const content = readFileSync(absolute, "utf8");
902
+ const tests = [];
903
+ const diagnostics = [];
904
+ for (const block of extractDocBlocks(content, file.language)) {
905
+ const parsed = parseTestDocBlock(file.path, block.text, block.startLine);
906
+ if (!parsed) {
907
+ continue;
908
+ }
909
+ tests.push(parsed.test);
910
+ diagnostics.push(...parsed.diagnostics);
911
+ }
912
+ tests.sort((a, b) => a.id.localeCompare(b.id));
913
+ diagnostics.sort((a, b) => a.id.localeCompare(b.id));
914
+ return { tests, diagnostics };
915
+ }
916
+ function parseTestDocBlock(filePath, blockText, startLine) {
917
+ const parsed = parseDocTags(blockText, filePath);
918
+ const tags = parsed.tags;
919
+ const isCatalogueTest = tags.some((tag) => tag.tag === "catalogue" && tag.value === "test");
920
+ const hasCoverageTags = tags.some((tag) => tag.tag === "coversCapability" || tag.tag === "coversSymbol" || tag.tag === "coversPublicApi");
921
+ if (!isCatalogueTest && !hasCoverageTags) {
922
+ return undefined;
923
+ }
924
+ const capabilityIds = tags.filter((tag) => tag.tag === "coversCapability").map((tag) => normalizeCap(tag.value));
925
+ const symbolRefs = tags.filter((tag) => tag.tag === "coversSymbol").map((tag) => tag.value);
926
+ const publicApiRefs = tags.filter((tag) => tag.tag === "coversPublicApi").map((tag) => tag.value);
927
+ const contracts = tags.filter((tag) => tag.tag === "contract").map((tag) => tag.value);
928
+ const framework = detectTestFramework(filePath);
929
+ const name = `test-at-line-${startLine}`;
930
+ // Identity intentionally excludes testKind. The doc-block model carries no suite
931
+ // or title, so the derived local name acts as the stable within-file discriminator.
932
+ const id = testIdentityId({
933
+ framework,
934
+ filePath,
935
+ suitePath: "",
936
+ title: name,
937
+ parameterisationKey: null,
938
+ functionName: null,
939
+ localDeclarationKey: null
940
+ });
941
+ return {
942
+ test: {
943
+ id,
944
+ filePath,
945
+ name,
946
+ testKind: "unknown",
947
+ framework,
948
+ suitePath: "",
949
+ coversCapabilities: capabilityIds,
950
+ coversSymbols: symbolRefs,
951
+ coversPublicApis: publicApiRefs,
952
+ contracts,
953
+ provenance: { source: "test-docs" }
954
+ },
955
+ diagnostics: parsed.diagnostics
956
+ };
957
+ }
958
+ function detectTestFramework(filePath) {
959
+ const lower = filePath.toLowerCase();
960
+ if (lower.endsWith(".py")) {
961
+ return "pytest";
962
+ }
963
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(lower)) {
964
+ return "vitest";
965
+ }
966
+ return "unknown";
967
+ }
968
+ function resolveTestSymbolReferences(tests, symbols, packageModel) {
969
+ const index = buildSymbolRefIndex(symbols, packageModel.filePackageByPath);
970
+ const diagnostics = [];
971
+ for (const test of tests) {
972
+ const resolved = [];
973
+ for (const reference of test.coversSymbols) {
974
+ const result = resolveSymbolReference(reference, index);
975
+ if (result.status === "resolved") {
976
+ resolved.push(result.value);
977
+ continue;
978
+ }
979
+ if (result.status === "ambiguous") {
980
+ diagnostics.push({
981
+ id: `diag:${test.id}:ambiguous_symbol_reference:${reference}`,
982
+ severity: "warning",
983
+ category: "resolution",
984
+ code: "ambiguous_symbol_reference",
985
+ message: `Test ${test.id} has ambiguous symbol reference ${reference}.`,
986
+ filePath: test.filePath,
987
+ target: { kind: "test", id: test.id },
988
+ provenance: { source: "validation" }
989
+ });
990
+ }
991
+ resolved.push(reference);
992
+ }
993
+ test.coversSymbols = resolved;
994
+ }
995
+ return diagnostics;
996
+ }
997
+ function resolveTestPublicApiReferences(tests, publicApis, symbols, packageModel) {
998
+ const index = buildPublicApiRefIndex(publicApis, symbols, packageModel.filePackageByPath);
999
+ const diagnostics = [];
1000
+ for (const test of tests) {
1001
+ const resolved = [];
1002
+ for (const reference of test.coversPublicApis) {
1003
+ const result = resolvePublicApiReference(reference, index);
1004
+ if (result.status === "resolved") {
1005
+ resolved.push(result.value);
1006
+ continue;
1007
+ }
1008
+ if (result.status === "ambiguous") {
1009
+ diagnostics.push({
1010
+ id: `diag:${test.id}:ambiguous_public_api_reference:${reference}`,
1011
+ severity: "warning",
1012
+ category: "resolution",
1013
+ code: "ambiguous_public_api_reference",
1014
+ message: `Test ${test.id} has ambiguous public API reference ${reference}.`,
1015
+ filePath: test.filePath,
1016
+ target: { kind: "test", id: test.id },
1017
+ provenance: { source: "validation" }
1018
+ });
1019
+ }
1020
+ resolved.push(reference);
1021
+ }
1022
+ test.coversPublicApis = [...new Set(resolved)].sort();
1023
+ }
1024
+ return diagnostics;
1025
+ }
1026
+ function duplicateIdDiagnostics(symbols, capabilities, publicApis, tests) {
1027
+ const diagnostics = [];
1028
+ const symbolSeen = new Set();
1029
+ for (const symbol of symbols) {
1030
+ if (symbolSeen.has(symbol.id)) {
1031
+ diagnostics.push({
1032
+ id: `diag:${symbol.id}:duplicate_symbol_id`,
1033
+ severity: "error",
1034
+ category: "indexing",
1035
+ code: "duplicate_symbol_id",
1036
+ message: `Duplicate symbol id found: ${symbol.id}.`,
1037
+ filePath: symbol.filePath,
1038
+ target: { kind: "symbol", id: symbol.id },
1039
+ provenance: { source: "validation" }
1040
+ });
1041
+ continue;
1042
+ }
1043
+ symbolSeen.add(symbol.id);
1044
+ }
1045
+ const capabilitySeen = new Set();
1046
+ for (const capability of capabilities) {
1047
+ if (capabilitySeen.has(capability.id)) {
1048
+ diagnostics.push({
1049
+ id: `diag:${capability.id}:duplicate_capability_id`,
1050
+ severity: "error",
1051
+ category: "indexing",
1052
+ code: "duplicate_capability_id",
1053
+ message: `Duplicate capability id found: ${capability.id}.`,
1054
+ target: { kind: "capability", id: capability.id },
1055
+ provenance: { source: "validation" }
1056
+ });
1057
+ continue;
1058
+ }
1059
+ capabilitySeen.add(capability.id);
1060
+ }
1061
+ const publicApiSeen = new Set();
1062
+ for (const publicApi of publicApis) {
1063
+ if (publicApiSeen.has(publicApi.id)) {
1064
+ diagnostics.push({
1065
+ id: `diag:${publicApi.id}:duplicate_public_api_id`,
1066
+ severity: "error",
1067
+ category: "indexing",
1068
+ code: "duplicate_public_api_id",
1069
+ message: `Duplicate public API id found: ${publicApi.id}.`,
1070
+ target: { kind: "publicApi", id: publicApi.id },
1071
+ provenance: { source: "validation" }
1072
+ });
1073
+ continue;
1074
+ }
1075
+ publicApiSeen.add(publicApi.id);
1076
+ }
1077
+ const testSeen = new Set();
1078
+ for (const test of tests) {
1079
+ if (testSeen.has(test.id)) {
1080
+ diagnostics.push({
1081
+ id: `diag:${test.id}:duplicate_test_id`,
1082
+ severity: "error",
1083
+ category: "indexing",
1084
+ code: "duplicate_test_id",
1085
+ message: `Duplicate test id found: ${test.id}.`,
1086
+ filePath: test.filePath,
1087
+ target: { kind: "test", id: test.id },
1088
+ provenance: { source: "validation" }
1089
+ });
1090
+ continue;
1091
+ }
1092
+ testSeen.add(test.id);
1093
+ }
1094
+ return diagnostics;
1095
+ }
1096
+ function buildTestRelationships(tests, symbols, publicApiBySymbolId, publicApiIds) {
1097
+ const relationships = [];
1098
+ const symbolIds = new Set(symbols.map((symbol) => symbol.id));
1099
+ const pushEdge = (type, from, to, confidence) => {
1100
+ relationships.push({
1101
+ id: relationshipIdentityId({
1102
+ type,
1103
+ fromKind: from.kind,
1104
+ fromId: from.id,
1105
+ toKind: to.kind,
1106
+ toId: to.id,
1107
+ discriminant: null
1108
+ }),
1109
+ type,
1110
+ from,
1111
+ to,
1112
+ confidence,
1113
+ provenance: { source: "test-docs" }
1114
+ });
1115
+ };
1116
+ for (const test of tests) {
1117
+ for (const capabilityId of test.coversCapabilities) {
1118
+ pushEdge("covers", { kind: "test", id: test.id }, { kind: "capability", id: capabilityId }, 0.9);
1119
+ pushEdge("tests", { kind: "capability", id: capabilityId }, { kind: "test", id: test.id }, 0.9);
1120
+ }
1121
+ for (const symbolId of test.coversSymbols) {
1122
+ if (!symbolIds.has(symbolId)) {
1123
+ continue;
1124
+ }
1125
+ pushEdge("covers", { kind: "test", id: test.id }, { kind: "symbol", id: symbolId }, 0.9);
1126
+ pushEdge("tests", { kind: "symbol", id: symbolId }, { kind: "test", id: test.id }, 0.9);
1127
+ const publicApiId = publicApiBySymbolId.get(symbolId);
1128
+ if (publicApiId) {
1129
+ pushEdge("covers", { kind: "test", id: test.id }, { kind: "publicApi", id: publicApiId }, 0.85);
1130
+ pushEdge("tests", { kind: "publicApi", id: publicApiId }, { kind: "test", id: test.id }, 0.85);
1131
+ }
1132
+ }
1133
+ for (const publicApiId of test.coversPublicApis) {
1134
+ if (!publicApiIds.has(publicApiId)) {
1135
+ continue;
1136
+ }
1137
+ pushEdge("covers", { kind: "test", id: test.id }, { kind: "publicApi", id: publicApiId }, 0.9);
1138
+ pushEdge("tests", { kind: "publicApi", id: publicApiId }, { kind: "test", id: test.id }, 0.9);
1139
+ }
1140
+ }
1141
+ return sortRelationships(relationships);
1142
+ }
1143
+ /** Remap symbol endpoints of relationships onto canonical ids, preserving extracted ids as evidence. */
1144
+ function remapRelationshipEndpoints(relationships, canonicalBySymbolId) {
1145
+ if (canonicalBySymbolId.size === 0) {
1146
+ return;
1147
+ }
1148
+ for (const relationship of relationships) {
1149
+ const fromMapped = relationship.from.kind === "symbol" ? canonicalBySymbolId.get(relationship.from.id) : undefined;
1150
+ const toMapped = relationship.to.kind === "symbol" ? canonicalBySymbolId.get(relationship.to.id) : undefined;
1151
+ if (!fromMapped && !toMapped) {
1152
+ continue;
1153
+ }
1154
+ const extractedFromId = relationship.from.id;
1155
+ const extractedToId = relationship.to.id;
1156
+ if (fromMapped) {
1157
+ relationship.from = { ...relationship.from, id: fromMapped };
1158
+ }
1159
+ if (toMapped) {
1160
+ relationship.to = { ...relationship.to, id: toMapped };
1161
+ }
1162
+ const priorEvidence = relationship.evidence
1163
+ ? [{ ...relationship.evidence, extractedFromId, extractedToId }]
1164
+ : [{ extractedFromId, extractedToId }];
1165
+ relationship.evidenceRanges = mergeEvidenceRanges([...(relationship.evidenceRanges ?? []), ...priorEvidence]);
1166
+ relationship.id = relationshipIdentityId({
1167
+ type: relationship.type,
1168
+ fromKind: relationship.from.kind,
1169
+ fromId: relationship.from.id,
1170
+ toKind: relationship.to.kind,
1171
+ toId: relationship.to.id,
1172
+ discriminant: relationship.localDiscriminant ?? null
1173
+ });
1174
+ }
1175
+ // Collapse semantically identical edges created from source + generated counterparts.
1176
+ dedupeRelationshipsById(relationships);
1177
+ }
1178
+ function dedupeRelationshipsById(relationships) {
1179
+ const byId = new Map();
1180
+ const survivors = [];
1181
+ for (const relationship of relationships) {
1182
+ const existing = byId.get(relationship.id);
1183
+ if (!existing) {
1184
+ byId.set(relationship.id, relationship);
1185
+ survivors.push(relationship);
1186
+ continue;
1187
+ }
1188
+ existing.evidenceRanges = mergeEvidenceRanges([
1189
+ ...(existing.evidenceRanges ?? []),
1190
+ ...(relationship.evidenceRanges ?? []),
1191
+ ...(relationship.evidence ? [relationship.evidence] : []),
1192
+ ...(existing.evidence ? [existing.evidence] : [])
1193
+ ]);
1194
+ existing.confidence = Math.max(existing.confidence, relationship.confidence);
1195
+ }
1196
+ relationships.length = 0;
1197
+ relationships.push(...survivors);
1198
+ }
1199
+ function mergeEvidenceRanges(ranges) {
1200
+ const seen = new Set();
1201
+ const merged = [];
1202
+ for (const range of ranges) {
1203
+ if (!range)
1204
+ continue;
1205
+ const key = JSON.stringify({
1206
+ filePath: range.filePath ?? null,
1207
+ startLine: range.startLine ?? range.line ?? null,
1208
+ endLine: range.endLine ?? null,
1209
+ extractedFromId: range.extractedFromId ?? null,
1210
+ extractedToId: range.extractedToId ?? null,
1211
+ text: range.text ?? null
1212
+ });
1213
+ if (seen.has(key))
1214
+ continue;
1215
+ seen.add(key);
1216
+ merged.push(range);
1217
+ }
1218
+ return merged.sort((a, b) => {
1219
+ const aPath = a.filePath ?? "";
1220
+ const bPath = b.filePath ?? "";
1221
+ if (aPath !== bPath)
1222
+ return aPath.localeCompare(bPath);
1223
+ const aLine = a.startLine ?? a.line ?? 0;
1224
+ const bLine = b.startLine ?? b.line ?? 0;
1225
+ if (aLine !== bLine)
1226
+ return aLine - bLine;
1227
+ return JSON.stringify(a).localeCompare(JSON.stringify(b));
1228
+ });
1229
+ }
1230
+ /** Detect full-id hash collisions across all persisted entities and fail closed. */
1231
+ function assertNoIdentityCollisions(packages, symbols, publicApis, tests, relationships, capabilities, examples, diagnostics) {
1232
+ const tracker = new IdentityCollisionTracker();
1233
+ const check = (id, preimageJson) => {
1234
+ const result = tracker.register(id, preimageJson);
1235
+ if (result.collision) {
1236
+ const message = `Identity hash collision on ${id}: ${result.priorPreimage} vs ${preimageJson}.`;
1237
+ diagnostics.push({
1238
+ id: `diag:${id}:identity_hash_collision`,
1239
+ severity: "error",
1240
+ category: "indexing",
1241
+ code: "identity_hash_collision",
1242
+ message,
1243
+ provenance: { source: "identity" }
1244
+ });
1245
+ throw new IdentityCollisionError(message);
1246
+ }
1247
+ };
1248
+ for (const packageRecord of packages) {
1249
+ const identityName = packageRecord.name || "unnamed";
1250
+ check(packageRecord.id, encodeIdentityPreimage("package", [
1251
+ normalizeIdentityString(packageRecord.ecosystem ?? "unknown"),
1252
+ normalizeIdentityString(identityName),
1253
+ normalizeIdentityPath(packageRecord.path)
1254
+ ]));
1255
+ }
1256
+ for (const symbol of symbols) {
1257
+ check(symbol.id, encodeIdentityPreimage("symbol", [
1258
+ normalizeIdentityString(symbol.language),
1259
+ normalizeIdentityString(symbol.kind),
1260
+ normalizeIdentityString(symbol.packageId ?? "package:unknown"),
1261
+ normalizeIdentityPath(symbol.modulePath ?? symbol.filePath),
1262
+ normalizeIdentityString(symbol.qualifiedContainerPath ?? ""),
1263
+ normalizeIdentityString(symbol.name)
1264
+ ]));
1265
+ }
1266
+ for (const api of publicApis) {
1267
+ check(api.id, encodeIdentityPreimage("public-api", [
1268
+ normalizeIdentityString(api.packageId ?? "package:unknown"),
1269
+ normalizeIdentityString(api.importPath ?? ""),
1270
+ normalizeIdentityString(api.name),
1271
+ normalizeIdentityString(api.exportKind ?? "named")
1272
+ ]));
1273
+ }
1274
+ for (const test of tests) {
1275
+ check(test.id, encodeIdentityPreimage("test", [
1276
+ normalizeIdentityString(test.framework ?? "unknown"),
1277
+ normalizeIdentityPath(test.filePath),
1278
+ normalizeIdentityString(test.suitePath ?? ""),
1279
+ normalizeIdentityString(test.name),
1280
+ null,
1281
+ null,
1282
+ null
1283
+ ]));
1284
+ }
1285
+ for (const relationship of relationships) {
1286
+ check(relationship.id, encodeIdentityPreimage("relationship", [
1287
+ normalizeIdentityString(relationship.type),
1288
+ normalizeIdentityString(relationship.from.kind),
1289
+ normalizeIdentityString(relationship.from.id),
1290
+ normalizeIdentityString(relationship.to.kind),
1291
+ normalizeIdentityString(relationship.to.id),
1292
+ relationship.localDiscriminant === null || relationship.localDiscriminant === undefined
1293
+ ? null
1294
+ : normalizeIdentityString(relationship.localDiscriminant)
1295
+ ]));
1296
+ }
1297
+ for (const capability of capabilities) {
1298
+ check(capability.id, encodeIdentityPreimage("capability", [normalizeIdentityString(capability.name)]));
1299
+ }
1300
+ for (const example of examples) {
1301
+ check(example.id, encodeIdentityPreimage("example", [
1302
+ normalizeIdentityString(example.filePath ?? ""),
1303
+ normalizeIdentityString(example.anchor ?? ""),
1304
+ normalizeIdentityString(example.title ?? "")
1305
+ ]));
1306
+ }
1307
+ for (const diagnostic of diagnostics) {
1308
+ check(diagnostic.id, encodeIdentityPreimage("diagnostic", [
1309
+ normalizeIdentityString(diagnostic.code),
1310
+ normalizeIdentityString(diagnostic.filePath ?? ""),
1311
+ normalizeIdentityString(diagnostic.target?.kind ?? ""),
1312
+ normalizeIdentityString(diagnostic.target?.id ?? ""),
1313
+ normalizeIdentityString(diagnostic.message)
1314
+ ]));
1315
+ }
1316
+ }
1317
+ export class IdentityCollisionError extends Error {
1318
+ constructor(message) {
1319
+ super(message);
1320
+ this.name = "IdentityCollisionError";
1321
+ }
1322
+ }
1323
+ /** Exported for indexing-level collision tests. */
1324
+ export { assertNoIdentityCollisions };
1325
+ function extractDocBlocks(content, language) {
1326
+ const blocks = [];
1327
+ if (language === "typescript" || language === "javascript") {
1328
+ const pattern = /\/\*\*([\s\S]*?)\*\//g;
1329
+ let match;
1330
+ while ((match = pattern.exec(content)) !== null) {
1331
+ const before = content.slice(0, match.index);
1332
+ const startLine = before.split(/\r?\n/).length;
1333
+ blocks.push({ text: match[1] ?? "", startLine });
1334
+ }
1335
+ return blocks;
1336
+ }
1337
+ if (language === "python") {
1338
+ const pattern = /(["']{3})([\s\S]*?)\1/g;
1339
+ let match;
1340
+ while ((match = pattern.exec(content)) !== null) {
1341
+ const before = content.slice(0, match.index);
1342
+ const startLine = before.split(/\r?\n/).length;
1343
+ blocks.push({ text: match[2] ?? "", startLine });
1344
+ }
1345
+ return blocks;
1346
+ }
1347
+ return blocks;
1348
+ }
1349
+ function packageDocDiagnostics(files, packages) {
1350
+ if (packages.length > 0) {
1351
+ return [];
1352
+ }
1353
+ const hasCatalogueDoc = files.some((file) => /(^|\/)CATALOGUE\.md$/i.test(file.path));
1354
+ if (hasCatalogueDoc) {
1355
+ return [];
1356
+ }
1357
+ return [
1358
+ {
1359
+ id: "diag:package_missing_catalogue_doc",
1360
+ severity: "warning",
1361
+ category: "documentation",
1362
+ code: "package_missing_catalogue_doc",
1363
+ message: "No package-level CATALOGUE.md was found.",
1364
+ provenance: { source: "validation" }
1365
+ }
1366
+ ];
1367
+ }
1368
+ function normalizeCap(value) {
1369
+ return value.startsWith("capability:") ? value : `capability:${value}`;
1370
+ }
1371
+ export function shouldFailFromDiagnostics(mode, diagnostics) {
1372
+ if (mode === "never") {
1373
+ return false;
1374
+ }
1375
+ const threshold = mode === "error" ? 3 : mode === "warning" ? 2 : 1;
1376
+ for (const diagnostic of diagnostics) {
1377
+ const level = diagnostic.severity === "error" ? 3 : diagnostic.severity === "warning" ? 2 : 1;
1378
+ if (level >= threshold) {
1379
+ return true;
1380
+ }
1381
+ }
1382
+ return false;
1383
+ }
1384
+ export function staleCommittedCatalogue(workspaceRoot, snapshot, catalogueDir) {
1385
+ const currentPath = path.resolve(workspaceRoot, catalogueDir, "catalogue.json");
1386
+ try {
1387
+ const current = JSON.parse(readFileSync(currentPath, "utf8"));
1388
+ return current.catalogueId !== snapshot.catalogueId;
1389
+ }
1390
+ catch {
1391
+ return true;
1392
+ }
1393
+ }