@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,262 @@
1
+ import { normalizeIdentityPath } from "./identity.js";
2
+ const GENERATED_ROOTS = ["dist", "build", "out"];
3
+ function toPosix(filePath) {
4
+ return normalizeIdentityPath(filePath);
5
+ }
6
+ /** Path is under a build output tree (dist/build/out). */
7
+ export function isBuildOutputPath(filePath) {
8
+ const normalized = toPosix(filePath);
9
+ return /(^|\/)(dist|build|out)(\/|$)/.test(normalized);
10
+ }
11
+ /**
12
+ * A symbol is treated as generated when its file is a build artefact, is a
13
+ * declaration file, or its provenance already marks it as generated.
14
+ */
15
+ export function isGeneratedSymbol(symbol) {
16
+ if (symbol.symbolProvenance === "generated_declaration" || symbol.symbolProvenance === "generated_only") {
17
+ return true;
18
+ }
19
+ const normalized = toPosix(symbol.filePath);
20
+ if (isBuildOutputPath(normalized)) {
21
+ return true;
22
+ }
23
+ return normalized.endsWith(".d.ts");
24
+ }
25
+ /**
26
+ * Strip a single source or generated root segment and the file extension so
27
+ * relative module topology can be compared (e.g. src/a/b.ts ↔ dist/a/b.d.ts).
28
+ */
29
+ export function moduleTopologyKey(filePath) {
30
+ let normalized = toPosix(filePath);
31
+ // Strip known source / emit roots (including nested package roots).
32
+ normalized = normalized.replace(/(^|\/)src\//, "$1");
33
+ for (const root of GENERATED_ROOTS) {
34
+ const pattern = new RegExp(`(^|/)${root}/`);
35
+ normalized = normalized.replace(pattern, "$1");
36
+ }
37
+ // .d.ts before .ts
38
+ if (normalized.endsWith(".d.ts")) {
39
+ normalized = normalized.slice(0, -".d.ts".length);
40
+ }
41
+ else {
42
+ normalized = normalized.replace(/\.[^.\/]+$/, "");
43
+ }
44
+ // Drop trailing /index for topology equivalence of package entry modules.
45
+ normalized = normalized.replace(/\/index$/, "");
46
+ return normalized;
47
+ }
48
+ /** Category B: authored and generated files share module topology after src↔emit transform. */
49
+ export function hasSourceOutputMapping(authored, generated) {
50
+ if (!isGeneratedSymbol(generated) || isGeneratedSymbol(authored)) {
51
+ return false;
52
+ }
53
+ const authoredPath = toPosix(authored.filePath);
54
+ const generatedPath = toPosix(generated.filePath);
55
+ // Generated must sit under a known emit root or be a .d.ts sibling of source topology.
56
+ const generatedUnderEmit = isBuildOutputPath(generatedPath) || generatedPath.endsWith(".d.ts");
57
+ if (!generatedUnderEmit) {
58
+ return false;
59
+ }
60
+ // Authored should not itself be under emit roots (unless only .d.ts-without-source case).
61
+ if (isBuildOutputPath(authoredPath)) {
62
+ return false;
63
+ }
64
+ const authoredKey = moduleTopologyKey(authoredPath);
65
+ const generatedKey = moduleTopologyKey(generatedPath);
66
+ if (!authoredKey || !generatedKey) {
67
+ return false;
68
+ }
69
+ return authoredKey === generatedKey;
70
+ }
71
+ /**
72
+ * Category A: declaration equivalence stronger than package+name+kind alone.
73
+ * Requires matching qualified declaration identity (container + name) and kind,
74
+ * plus compatible signature fingerprint when both are present.
75
+ */
76
+ export function categoryACompatible(a, b) {
77
+ if ((a.packageId ?? "package:unknown") !== (b.packageId ?? "package:unknown")) {
78
+ return false;
79
+ }
80
+ if (a.name !== b.name || a.kind !== b.kind) {
81
+ return false;
82
+ }
83
+ const aContainer = a.qualifiedContainerPath ?? "";
84
+ const bContainer = b.qualifiedContainerPath ?? "";
85
+ if (aContainer !== bContainer) {
86
+ return false;
87
+ }
88
+ // Prefer qualifiedName when both use the same logical module-qualified form after topology normalize.
89
+ // Do not require identical filePath-qualified strings (those differ across src/dist).
90
+ const aLogical = `${aContainer}\u0000${a.name}`;
91
+ const bLogical = `${bContainer}\u0000${b.name}`;
92
+ if (aLogical !== bLogical) {
93
+ return false;
94
+ }
95
+ if (a.signatureFingerprint && b.signatureFingerprint && a.signatureFingerprint !== b.signatureFingerprint) {
96
+ // Implementation vs declare-form fingerprints commonly differ; allow when exactly one side is generated.
97
+ const aGenerated = isGeneratedSymbol(a);
98
+ const bGenerated = isGeneratedSymbol(b);
99
+ if (aGenerated === bGenerated) {
100
+ return false;
101
+ }
102
+ }
103
+ return true;
104
+ }
105
+ /** Soft Category A grouping key used to find candidate pairs (not sufficient alone to collapse). */
106
+ function softGroupKey(symbol) {
107
+ const packageId = symbol.packageId ?? "package:unknown";
108
+ const container = symbol.qualifiedContainerPath ?? "";
109
+ return `${packageId}\u0000${container}\u0000${symbol.name}\u0000${symbol.kind}`;
110
+ }
111
+ export function validateSymbolProvenance(symbol) {
112
+ const provenance = symbol.symbolProvenance ?? "authored_source";
113
+ const canonical = symbol.canonicalSymbolId ?? symbol.id;
114
+ const duplicateOf = symbol.duplicateOf ?? null;
115
+ if (provenance === "authored_source") {
116
+ if (canonical !== symbol.id || duplicateOf !== null) {
117
+ return `authored_source requires self-canonical id and null duplicateOf (got canonical=${canonical}, duplicateOf=${duplicateOf})`;
118
+ }
119
+ }
120
+ else if (provenance === "generated_declaration") {
121
+ if (!duplicateOf || canonical !== duplicateOf) {
122
+ return `generated_declaration requires duplicateOf === canonicalSymbolId pointing at authored source`;
123
+ }
124
+ if (duplicateOf === symbol.id) {
125
+ return `generated_declaration must not duplicate itself`;
126
+ }
127
+ }
128
+ else if (provenance === "generated_only") {
129
+ if (canonical !== symbol.id || duplicateOf !== null) {
130
+ return `generated_only requires self-canonical id and null duplicateOf`;
131
+ }
132
+ }
133
+ return undefined;
134
+ }
135
+ function applyAuthoredDefaults(symbol) {
136
+ symbol.canonicalSymbolId = symbol.id;
137
+ symbol.duplicateOf = null;
138
+ if (isGeneratedSymbol(symbol)) {
139
+ symbol.symbolProvenance = "generated_only";
140
+ }
141
+ else {
142
+ symbol.symbolProvenance = "authored_source";
143
+ }
144
+ }
145
+ /**
146
+ * Assign canonical identities to symbols in place.
147
+ *
148
+ * Returns a map from collapsed duplicate symbol id to its canonical symbol id.
149
+ */
150
+ export function canonicaliseSymbols(symbols, diagnostics) {
151
+ const canonicalById = new Map();
152
+ for (const symbol of symbols) {
153
+ applyAuthoredDefaults(symbol);
154
+ }
155
+ const groups = new Map();
156
+ for (const symbol of symbols) {
157
+ const key = softGroupKey(symbol);
158
+ const bucket = groups.get(key);
159
+ if (bucket) {
160
+ bucket.push(symbol);
161
+ }
162
+ else {
163
+ groups.set(key, [symbol]);
164
+ }
165
+ }
166
+ for (const group of groups.values()) {
167
+ if (group.length < 2) {
168
+ continue;
169
+ }
170
+ const sources = group.filter((symbol) => !isGeneratedSymbol(symbol));
171
+ const generated = group.filter((symbol) => isGeneratedSymbol(symbol));
172
+ // Ambiguous: multiple authored sources in the soft group — never collapse.
173
+ if (sources.length !== 1) {
174
+ emitPossibleDuplicates(group, diagnostics, "ambiguous or missing authored source for Category B mapping");
175
+ continue;
176
+ }
177
+ const canonical = sources[0];
178
+ if (generated.length === 0) {
179
+ // Multiple soft-group members that are all authored — already handled by sources.length !== 1,
180
+ // but keep possible_generated_duplicate if somehow mixed without generated classification.
181
+ continue;
182
+ }
183
+ let collapsedAny = false;
184
+ for (const duplicate of generated) {
185
+ const aOk = categoryACompatible(canonical, duplicate);
186
+ const bOk = hasSourceOutputMapping(canonical, duplicate);
187
+ if (aOk && bOk) {
188
+ duplicate.canonicalSymbolId = canonical.id;
189
+ duplicate.duplicateOf = canonical.id;
190
+ duplicate.symbolProvenance = "generated_declaration";
191
+ canonicalById.set(duplicate.id, canonical.id);
192
+ collapsedAny = true;
193
+ continue;
194
+ }
195
+ // Category A alone or weak path proximity → diagnostic, leave self-canonical.
196
+ if (aOk && !bOk) {
197
+ diagnostics.push({
198
+ id: `diag:${duplicate.id}:possible_generated_duplicate`,
199
+ severity: "info",
200
+ category: "indexing",
201
+ code: "possible_generated_duplicate",
202
+ message: `Symbol ${duplicate.name} (${duplicate.kind}) in ${duplicate.filePath} matches authored ${canonical.filePath} on declaration identity but lacks a proven source↔output module mapping; not collapsed.`,
203
+ filePath: duplicate.filePath,
204
+ target: { kind: "symbol", id: duplicate.id },
205
+ provenance: { source: "canonicalisation" }
206
+ });
207
+ }
208
+ }
209
+ // Soft group shared package/name/kind with no successful Category B collapses.
210
+ if (!collapsedAny && generated.length > 0) {
211
+ // Diagnostics already emitted for A-without-B; if no A either, still warn once per generated.
212
+ for (const duplicate of generated) {
213
+ if (categoryACompatible(canonical, duplicate)) {
214
+ continue;
215
+ }
216
+ diagnostics.push({
217
+ id: `diag:${duplicate.id}:possible_generated_duplicate`,
218
+ severity: "info",
219
+ category: "indexing",
220
+ code: "possible_generated_duplicate",
221
+ message: `Symbol ${duplicate.name} (${duplicate.kind}) in ${duplicate.filePath} shares package/name/kind with other declarations but Category A+B mapping was not established; not collapsed.`,
222
+ filePath: duplicate.filePath,
223
+ target: { kind: "symbol", id: duplicate.id },
224
+ provenance: { source: "canonicalisation" }
225
+ });
226
+ }
227
+ }
228
+ }
229
+ for (const symbol of symbols) {
230
+ const error = validateSymbolProvenance(symbol);
231
+ if (error) {
232
+ diagnostics.push({
233
+ id: `diag:${symbol.id}:invalid_symbol_provenance`,
234
+ severity: "error",
235
+ category: "indexing",
236
+ code: "invalid_symbol_provenance",
237
+ message: error,
238
+ filePath: symbol.filePath,
239
+ target: { kind: "symbol", id: symbol.id },
240
+ provenance: { source: "canonicalisation" }
241
+ });
242
+ // Repair to a safe self-canonical generated_only / authored_source state.
243
+ applyAuthoredDefaults(symbol);
244
+ canonicalById.delete(symbol.id);
245
+ }
246
+ }
247
+ return canonicalById;
248
+ }
249
+ function emitPossibleDuplicates(group, diagnostics, reason) {
250
+ for (const symbol of group) {
251
+ diagnostics.push({
252
+ id: `diag:${symbol.id}:possible_generated_duplicate`,
253
+ severity: "info",
254
+ category: "indexing",
255
+ code: "possible_generated_duplicate",
256
+ message: `Symbol ${symbol.name} (${symbol.kind}) in ${symbol.filePath}: ${reason}; not collapsed.`,
257
+ filePath: symbol.filePath,
258
+ target: { kind: "symbol", id: symbol.id },
259
+ provenance: { source: "canonicalisation" }
260
+ });
261
+ }
262
+ }
@@ -0,0 +1,13 @@
1
+ import type { CatalogueConfig, CatalogueMode } from "./schema.js";
2
+ export declare const DEFAULT_EXCLUDES: string[];
3
+ export declare const DEFAULT_CONFIG: CatalogueConfig;
4
+ export interface LoadConfigOptions {
5
+ workspaceRoot: string;
6
+ configPath?: string;
7
+ mode?: CatalogueMode;
8
+ }
9
+ export declare function loadConfig(options: LoadConfigOptions): {
10
+ config: CatalogueConfig;
11
+ mode: CatalogueMode;
12
+ configPath?: string;
13
+ };
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ export const DEFAULT_EXCLUDES = [
4
+ ".git/**",
5
+ "node_modules/**",
6
+ "**/dist/**",
7
+ "**/build/**",
8
+ "**/out/**",
9
+ "**/coverage/**",
10
+ "**/.next/**",
11
+ "**/.expo/**",
12
+ "**/.pytest_cache/**",
13
+ ".vscode-test/**",
14
+ "__pycache__/**",
15
+ "**/*.egg-info/**",
16
+ ".env",
17
+ ".env.*",
18
+ "*.pem",
19
+ "*.key",
20
+ "*.p12",
21
+ "*.pfx"
22
+ ];
23
+ export const DEFAULT_CONFIG = {
24
+ schemaVersion: 2,
25
+ catalogueDir: ".mapwright",
26
+ committedCatalogue: true,
27
+ languages: ["typescript", "javascript", "python", "markdown", "json"],
28
+ include: ["src/**", "packages/**", "tests/**", "CATALOGUE.md", "README.md"],
29
+ exclude: DEFAULT_EXCLUDES,
30
+ documentation: {
31
+ requirePublicApiDocs: true,
32
+ requireCapabilityForPublicApi: true
33
+ },
34
+ validation: {
35
+ failOn: "warning"
36
+ },
37
+ limits: {
38
+ maxFileSizeBytes: 512000,
39
+ maxIndexedFiles: 10000,
40
+ maxSourcePreviewLines: 120
41
+ }
42
+ };
43
+ export function loadConfig(options) {
44
+ const resolvedConfigPath = options.configPath
45
+ ? path.resolve(options.workspaceRoot, options.configPath)
46
+ : path.resolve(options.workspaceRoot, "mapwright.config.json");
47
+ let userConfig = {};
48
+ let foundPath;
49
+ if (existsSync(resolvedConfigPath)) {
50
+ userConfig = JSON.parse(readFileSync(resolvedConfigPath, "utf8"));
51
+ foundPath = resolvedConfigPath;
52
+ }
53
+ const config = {
54
+ ...DEFAULT_CONFIG,
55
+ ...userConfig,
56
+ documentation: {
57
+ ...DEFAULT_CONFIG.documentation,
58
+ ...(userConfig.documentation ?? {})
59
+ },
60
+ validation: {
61
+ ...DEFAULT_CONFIG.validation,
62
+ ...(userConfig.validation ?? {})
63
+ },
64
+ limits: {
65
+ ...DEFAULT_CONFIG.limits,
66
+ ...(userConfig.limits ?? {})
67
+ },
68
+ include: userConfig.include ?? DEFAULT_CONFIG.include,
69
+ exclude: dedupe([...(DEFAULT_CONFIG.exclude ?? []), ...(userConfig.exclude ?? [])]),
70
+ languages: userConfig.languages ?? DEFAULT_CONFIG.languages
71
+ };
72
+ const mode = options.mode ?? (config.committedCatalogue ? "committed" : "local");
73
+ return { config, mode, configPath: foundPath };
74
+ }
75
+ function dedupe(values) {
76
+ return [...new Set(values)];
77
+ }
@@ -0,0 +1,3 @@
1
+ export declare function stableStringify(input: unknown, pretty?: boolean): string;
2
+ export declare function hashContent(content: string): string;
3
+ export declare function stableId(parts: string[]): string;
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ export function stableStringify(input, pretty = false) {
3
+ return JSON.stringify(sortValue(input), null, pretty ? 2 : 0) + "\n";
4
+ }
5
+ export function hashContent(content) {
6
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
7
+ }
8
+ export function stableId(parts) {
9
+ return hashContent(parts.join("|"));
10
+ }
11
+ function sortValue(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map(sortValue);
14
+ }
15
+ if (value && typeof value === "object") {
16
+ const obj = value;
17
+ const sortedKeys = Object.keys(obj).sort();
18
+ const out = {};
19
+ for (const key of sortedKeys) {
20
+ out[key] = sortValue(obj[key]);
21
+ }
22
+ return out;
23
+ }
24
+ return value;
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { CatalogueConfig, DiagnosticRecord, SymbolRecord } from "./schema.js";
2
+ export declare function buildDiagnostics(symbols: SymbolRecord[], config: CatalogueConfig): DiagnosticRecord[];
@@ -0,0 +1,34 @@
1
+ import { isExamplesTreePath } from "./scanner.js";
2
+ export function buildDiagnostics(symbols, config) {
3
+ const diagnostics = [];
4
+ for (const symbol of symbols) {
5
+ if (!symbol.exported || isExamplesTreePath(symbol.filePath)) {
6
+ continue;
7
+ }
8
+ if (config.documentation.requirePublicApiDocs && !symbol.doc?.present) {
9
+ diagnostics.push({
10
+ id: `diag:${symbol.id}:public_symbol_missing_catalogue_doc`,
11
+ severity: "warning",
12
+ category: "documentation",
13
+ code: "public_symbol_missing_catalogue_doc",
14
+ message: `Exported symbol ${symbol.name} is missing catalogue documentation.`,
15
+ filePath: symbol.filePath,
16
+ target: { kind: "symbol", id: symbol.id },
17
+ provenance: { source: "validation" }
18
+ });
19
+ }
20
+ if (config.documentation.requireCapabilityForPublicApi && symbol.capabilities.length === 0) {
21
+ diagnostics.push({
22
+ id: `diag:${symbol.id}:public_symbol_missing_capability`,
23
+ severity: "warning",
24
+ category: "documentation",
25
+ code: "public_symbol_missing_capability",
26
+ message: `Exported symbol ${symbol.name} is missing a @capability tag.`,
27
+ filePath: symbol.filePath,
28
+ target: { kind: "symbol", id: symbol.id },
29
+ provenance: { source: "validation" }
30
+ });
31
+ }
32
+ }
33
+ return diagnostics;
34
+ }
@@ -0,0 +1 @@
1
+ export declare function truncateEvidenceText(text: string | undefined, maxLength?: number): string | undefined;
@@ -0,0 +1,10 @@
1
+ export function truncateEvidenceText(text, maxLength = 300) {
2
+ if (!text) {
3
+ return undefined;
4
+ }
5
+ const normalized = text.replace(/\s+/g, " ").trim();
6
+ if (normalized.length <= maxLength) {
7
+ return normalized;
8
+ }
9
+ return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
10
+ }
@@ -0,0 +1,3 @@
1
+ export declare function normalizeExamplePath(value: string | undefined): string | undefined;
2
+ export declare function buildExampleIdentity(filePath: string | undefined, anchor: string | undefined, fallbackReference: string): string;
3
+ export declare function buildDeterministicExampleId(filePath: string | undefined, anchor: string | undefined, fallbackReference: string): string;
@@ -0,0 +1,20 @@
1
+ import { hashContent } from "./deterministic.js";
2
+ export function normalizeExamplePath(value) {
3
+ if (!value) {
4
+ return undefined;
5
+ }
6
+ const normalized = value.trim().replaceAll("\\", "/");
7
+ return normalized || undefined;
8
+ }
9
+ export function buildExampleIdentity(filePath, anchor, fallbackReference) {
10
+ const normalizedPath = normalizeExamplePath(filePath)?.toLowerCase();
11
+ const normalizedAnchor = anchor?.trim().toLowerCase() || undefined;
12
+ if (normalizedPath || normalizedAnchor) {
13
+ return `${normalizedPath ?? ""}#${normalizedAnchor ?? ""}`;
14
+ }
15
+ return fallbackReference.trim().toLowerCase();
16
+ }
17
+ export function buildDeterministicExampleId(filePath, anchor, fallbackReference) {
18
+ const identity = buildExampleIdentity(filePath, anchor, fallbackReference);
19
+ return `example:${hashContent(identity).replace(/^sha256:/, "")}`;
20
+ }
@@ -0,0 +1,61 @@
1
+ export declare const IDENTITY_NAMESPACE = "code-atlas-identity";
2
+ export declare const IDENTITY_SCHEME = "stable-sha256-v1";
3
+ export type IdentityDomain = "package" | "symbol" | "public-api" | "test" | "relationship" | "capability" | "example" | "diagnostic";
4
+ export type IdentityField = string | number | boolean | null;
5
+ /** Unicode NFC normalization for identity string inputs. */
6
+ export declare function normalizeIdentityString(value: string): string;
7
+ /** Repo-relative POSIX path for identity inputs (no absolute paths). */
8
+ export declare function normalizeIdentityPath(filePath: string): string;
9
+ /**
10
+ * Deterministic JSON array encoding: fixed order, null vs "", no omitted middle fields.
11
+ * Uses JSON.stringify on a plain array (stable for primitives).
12
+ */
13
+ export declare function encodeIdentityPreimage(domain: IdentityDomain, fields: IdentityField[]): string;
14
+ export declare function hashIdentityPreimage(preimageJson: string): string;
15
+ export declare function stableIdentityId(domain: IdentityDomain, fields: IdentityField[], prefix?: string): string;
16
+ export declare function packageIdentityId(input: {
17
+ ecosystem: "npm" | "python" | "unknown";
18
+ name: string;
19
+ root: string;
20
+ }): string;
21
+ export declare function symbolIdentityId(input: {
22
+ language: string;
23
+ kind: string;
24
+ packageId: string;
25
+ modulePath: string;
26
+ qualifiedContainerPath: string;
27
+ declaredName: string;
28
+ }): string;
29
+ export declare function publicApiIdentityId(input: {
30
+ packageId: string;
31
+ importPath: string;
32
+ exportedName: string;
33
+ exportKind: string;
34
+ }): string;
35
+ export declare function testIdentityId(input: {
36
+ framework: string;
37
+ filePath: string;
38
+ suitePath: string;
39
+ title: string;
40
+ parameterisationKey: string | null;
41
+ functionName: string | null;
42
+ localDeclarationKey: string | null;
43
+ }): string;
44
+ export declare function relationshipIdentityId(input: {
45
+ type: string;
46
+ fromKind: string;
47
+ fromId: string;
48
+ toKind: string;
49
+ toId: string;
50
+ discriminant: string | null;
51
+ }): string;
52
+ export declare function signatureFingerprint(parts: IdentityField[]): string;
53
+ export declare function implementationFingerprint(parts: IdentityField[]): string;
54
+ /** Detect duplicate full IDs with different preimages during indexing. */
55
+ export declare class IdentityCollisionTracker {
56
+ private readonly seen;
57
+ register(id: string, preimageJson: string): {
58
+ collision: boolean;
59
+ priorPreimage?: string;
60
+ };
61
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Canonical identity encoder for schema 2 (`stable-sha256-v1`).
3
+ * Preimage: JSON array ["code-atlas-identity", "stable-sha256-v1", domain, ...fields] as UTF-8 → SHA-256.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ // FROZEN: namespace string inherited from the pre-release Code Atlas era.
7
+ // Do not rename — changing it would change every stable identity ID in existing catalogues.
8
+ export const IDENTITY_NAMESPACE = "code-atlas-identity";
9
+ export const IDENTITY_SCHEME = "stable-sha256-v1";
10
+ /** Unicode NFC normalization for identity string inputs. */
11
+ export function normalizeIdentityString(value) {
12
+ return value.normalize("NFC");
13
+ }
14
+ /** Repo-relative POSIX path for identity inputs (no absolute paths). */
15
+ export function normalizeIdentityPath(filePath) {
16
+ const nfc = normalizeIdentityString(filePath);
17
+ let posix = nfc.replaceAll("\\", "/");
18
+ while (posix.startsWith("./")) {
19
+ posix = posix.slice(2);
20
+ }
21
+ if (posix.length > 1 && posix.endsWith("/")) {
22
+ posix = posix.slice(0, -1);
23
+ }
24
+ return posix === "" ? "." : posix;
25
+ }
26
+ /**
27
+ * Deterministic JSON array encoding: fixed order, null vs "", no omitted middle fields.
28
+ * Uses JSON.stringify on a plain array (stable for primitives).
29
+ */
30
+ export function encodeIdentityPreimage(domain, fields) {
31
+ const preimage = [IDENTITY_NAMESPACE, IDENTITY_SCHEME, domain, ...fields];
32
+ return JSON.stringify(preimage);
33
+ }
34
+ export function hashIdentityPreimage(preimageJson) {
35
+ return createHash("sha256").update(preimageJson, "utf8").digest("hex");
36
+ }
37
+ export function stableIdentityId(domain, fields, prefix) {
38
+ const digest = hashIdentityPreimage(encodeIdentityPreimage(domain, fields));
39
+ if (prefix) {
40
+ return `${prefix}${digest}`;
41
+ }
42
+ return `${domain}:${digest}`;
43
+ }
44
+ export function packageIdentityId(input) {
45
+ return stableIdentityId("package", [
46
+ normalizeIdentityString(input.ecosystem),
47
+ normalizeIdentityString(input.name || "unnamed"),
48
+ normalizeIdentityPath(input.root)
49
+ ]);
50
+ }
51
+ export function symbolIdentityId(input) {
52
+ const digest = hashIdentityPreimage(encodeIdentityPreimage("symbol", [
53
+ normalizeIdentityString(input.language),
54
+ normalizeIdentityString(input.kind),
55
+ normalizeIdentityString(input.packageId),
56
+ normalizeIdentityPath(input.modulePath),
57
+ normalizeIdentityString(input.qualifiedContainerPath),
58
+ normalizeIdentityString(input.declaredName)
59
+ ]));
60
+ return `symbol:${input.language}:${input.kind}:${digest}`;
61
+ }
62
+ export function publicApiIdentityId(input) {
63
+ return stableIdentityId("public-api", [
64
+ normalizeIdentityString(input.packageId),
65
+ normalizeIdentityString(input.importPath),
66
+ normalizeIdentityString(input.exportedName),
67
+ normalizeIdentityString(input.exportKind)
68
+ ]);
69
+ }
70
+ export function testIdentityId(input) {
71
+ const digest = hashIdentityPreimage(encodeIdentityPreimage("test", [
72
+ normalizeIdentityString(input.framework),
73
+ normalizeIdentityPath(input.filePath),
74
+ normalizeIdentityString(input.suitePath),
75
+ normalizeIdentityString(input.title),
76
+ input.parameterisationKey === null ? null : normalizeIdentityString(input.parameterisationKey),
77
+ input.functionName === null ? null : normalizeIdentityString(input.functionName),
78
+ input.localDeclarationKey === null ? null : normalizeIdentityString(input.localDeclarationKey)
79
+ ]));
80
+ return `test:${input.framework}:${digest}`;
81
+ }
82
+ export function relationshipIdentityId(input) {
83
+ return stableIdentityId("relationship", [
84
+ normalizeIdentityString(input.type),
85
+ normalizeIdentityString(input.fromKind),
86
+ normalizeIdentityString(input.fromId),
87
+ normalizeIdentityString(input.toKind),
88
+ normalizeIdentityString(input.toId),
89
+ input.discriminant === null ? null : normalizeIdentityString(input.discriminant)
90
+ ]);
91
+ }
92
+ // FROZEN: "code-atlas-fingerprint" preimage strings inherited from the pre-release Code Atlas era.
93
+ // Do not rename — changing them would change every fingerprint hash in existing catalogues.
94
+ export function signatureFingerprint(parts) {
95
+ return hashIdentityPreimage(JSON.stringify(["code-atlas-fingerprint", "signature-v1", ...parts]));
96
+ }
97
+ export function implementationFingerprint(parts) {
98
+ return hashIdentityPreimage(JSON.stringify(["code-atlas-fingerprint", "implementation-v1", ...parts]));
99
+ }
100
+ /** Detect duplicate full IDs with different preimages during indexing. */
101
+ export class IdentityCollisionTracker {
102
+ seen = new Map();
103
+ register(id, preimageJson) {
104
+ const prior = this.seen.get(id);
105
+ if (prior === undefined) {
106
+ this.seen.set(id, preimageJson);
107
+ return { collision: false };
108
+ }
109
+ if (prior === preimageJson) {
110
+ return { collision: false };
111
+ }
112
+ return { collision: true, priorPreimage: prior };
113
+ }
114
+ }
@@ -0,0 +1,4 @@
1
+ import type { CatalogueConfig } from "./schema.js";
2
+ export declare function createIgnoreMatcher(workspaceRoot: string, config: CatalogueConfig): (relativePath: string) => boolean;
3
+ export declare function isIncludedByGlob(relativePath: string, include: string[]): boolean;
4
+ export declare function normalize(filePath: string): string;
@@ -0,0 +1,29 @@
1
+ import path from "node:path";
2
+ import ignore from "ignore";
3
+ import { minimatch } from "minimatch";
4
+ export function createIgnoreMatcher(workspaceRoot, config) {
5
+ const ig = ignore();
6
+ ig.add(config.exclude);
7
+ return (relativePath) => {
8
+ const normalized = normalize(relativePath);
9
+ if (!normalized) {
10
+ return false;
11
+ }
12
+ if (ig.ignores(normalized)) {
13
+ return true;
14
+ }
15
+ // Path traversal and absolute path protection for scanner safety.
16
+ const absolute = path.resolve(workspaceRoot, normalized);
17
+ if (!absolute.startsWith(path.resolve(workspaceRoot))) {
18
+ return true;
19
+ }
20
+ return config.exclude.some((pattern) => minimatch(normalized, pattern, { dot: true }));
21
+ };
22
+ }
23
+ export function isIncludedByGlob(relativePath, include) {
24
+ const normalized = normalize(relativePath);
25
+ return include.some((pattern) => minimatch(normalized, pattern, { dot: true }));
26
+ }
27
+ export function normalize(filePath) {
28
+ return filePath.replaceAll(path.sep, "/").replace(/^\.\//, "");
29
+ }