@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,181 @@
1
+ import { nameMatchReasons } from "./ranking.js";
2
+ export function findReuseCandidates(snapshot, options) {
3
+ const query = options.query.trim();
4
+ const normalizedQuery = query.toLowerCase();
5
+ const tokens = tokenize(normalizedQuery);
6
+ const limit = Math.max(1, options.limit ?? 10);
7
+ const capabilityCandidates = snapshot.capabilities
8
+ .filter((capability) => {
9
+ if (options.capabilityId && capability.id !== options.capabilityId) {
10
+ return false;
11
+ }
12
+ if (options.packageId && !capability.packages.includes(options.packageId)) {
13
+ return false;
14
+ }
15
+ return true;
16
+ })
17
+ .map((capability) => scoreCapability(snapshot, capability, normalizedQuery, tokens));
18
+ const publicApiCandidates = snapshot.publicApis
19
+ .filter((api) => {
20
+ if (options.capabilityId && !api.capabilityIds.includes(options.capabilityId)) {
21
+ return false;
22
+ }
23
+ if (!options.packageId) {
24
+ return true;
25
+ }
26
+ const symbol = snapshot.symbols.find((item) => item.id === api.symbolId);
27
+ const ownerPackage = symbol ? findOwningPackageId(snapshot, symbol.filePath) : undefined;
28
+ return ownerPackage === options.packageId;
29
+ })
30
+ .map((api) => scorePublicApi(snapshot, api, normalizedQuery, tokens));
31
+ const candidates = [...capabilityCandidates, ...publicApiCandidates]
32
+ .filter((candidate) => candidate.score > 0)
33
+ .sort((a, b) => b.score - a.score || a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id))
34
+ .slice(0, limit);
35
+ return { query, tokens, candidates };
36
+ }
37
+ /**
38
+ * Thin wrapper over {@link findReuseCandidates} that annotates each candidate
39
+ * with closed-vocabulary match reasons derived from its name. Ordering and
40
+ * scoring are unchanged from the underlying reuse scorer.
41
+ */
42
+ export function rankReuseCandidates(snapshot, options) {
43
+ const result = findReuseCandidates(snapshot, options);
44
+ const candidates = result.candidates.map((candidate) => {
45
+ const name = candidate.kind === "capability"
46
+ ? snapshot.capabilities.find((item) => item.id === candidate.id)?.name
47
+ : snapshot.publicApis.find((item) => item.id === candidate.id)?.name;
48
+ const reasons = name ? nameMatchReasons(name, result.query, "exact_declared_name") : [];
49
+ return { ...candidate, matchReasons: reasons };
50
+ });
51
+ return { query: result.query, tokens: result.tokens, candidates };
52
+ }
53
+ function scoreCapability(snapshot, capability, query, tokens) {
54
+ let score = 0;
55
+ const reasons = [];
56
+ if (query && capability.id.toLowerCase() === query) {
57
+ score += 100;
58
+ reasons.push("matched capability id");
59
+ }
60
+ if (query && capability.name.toLowerCase().includes(query)) {
61
+ score += 35;
62
+ reasons.push("matched capability name");
63
+ }
64
+ if (tokens.some((token) => capability.keywords.map((keyword) => keyword.toLowerCase()).includes(token))) {
65
+ score += 20;
66
+ reasons.push("matched capability keywords");
67
+ }
68
+ const linkedApis = snapshot.publicApis.filter((api) => capability.publicApis.includes(api.id));
69
+ if (query && linkedApis.some((api) => api.name.toLowerCase().includes(query))) {
70
+ score += 25;
71
+ reasons.push("matched public API name");
72
+ }
73
+ const linkedSymbols = snapshot.symbols.filter((symbol) => capability.symbols.includes(symbol.id));
74
+ if (query && linkedSymbols.some((symbol) => symbol.name.toLowerCase().includes(query))) {
75
+ score += 20;
76
+ reasons.push("matched symbol name");
77
+ }
78
+ const linkedPackages = snapshot.packages.filter((pkg) => capability.packages.includes(pkg.id));
79
+ if (query && linkedPackages.some((pkg) => pkg.name.toLowerCase().includes(query))) {
80
+ score += 15;
81
+ reasons.push("matched package name");
82
+ }
83
+ const searchableText = [
84
+ capability.summary ?? "",
85
+ capability.intendedUse ?? "",
86
+ ...capability.contracts,
87
+ ...capability.limitations,
88
+ ...capability.tests,
89
+ ...capability.examples
90
+ ].join(" ").toLowerCase();
91
+ if (query && searchableText.includes(query)) {
92
+ score += 12;
93
+ reasons.push("matched capability documentation");
94
+ }
95
+ if (score > 0
96
+ && (capability.tests.length > 0 || linkedApis.some((api) => api.tests.length > 0))
97
+ && (capability.examples.length > 0 || linkedApis.some((api) => api.examples.length > 0))) {
98
+ score += 6;
99
+ reasons.push("has tests and examples");
100
+ }
101
+ return {
102
+ kind: "capability",
103
+ id: capability.id,
104
+ score: normalizeScore(score, 200),
105
+ reasons: dedupe(reasons)
106
+ };
107
+ }
108
+ function scorePublicApi(snapshot, api, query, tokens) {
109
+ let score = 0;
110
+ const reasons = [];
111
+ if (query && api.id.toLowerCase() === query) {
112
+ score += 100;
113
+ reasons.push("matched public API id");
114
+ }
115
+ if (query && api.name.toLowerCase().includes(query)) {
116
+ score += 45;
117
+ reasons.push("matched public API name");
118
+ }
119
+ if (tokens.some((token) => api.capabilityIds.some((capabilityId) => capabilityId.toLowerCase().includes(token)))) {
120
+ score += 20;
121
+ reasons.push("matched linked capability");
122
+ }
123
+ const symbol = snapshot.symbols.find((item) => item.id === api.symbolId);
124
+ if (query && symbol?.name.toLowerCase().includes(query)) {
125
+ score += 20;
126
+ reasons.push("matched implementation symbol");
127
+ }
128
+ if (query) {
129
+ const owningPackageId = symbol ? findOwningPackageId(snapshot, symbol.filePath) : undefined;
130
+ const owningPackage = owningPackageId ? snapshot.packages.find((pkg) => pkg.id === owningPackageId) : undefined;
131
+ if (owningPackage?.name.toLowerCase().includes(query)) {
132
+ score += 15;
133
+ reasons.push("matched owning package");
134
+ }
135
+ }
136
+ const searchableText = [
137
+ api.intendedUse ?? "",
138
+ api.inputs ?? "",
139
+ api.returns ?? "",
140
+ ...api.contracts,
141
+ ...api.tests,
142
+ ...api.examples
143
+ ].join(" ").toLowerCase();
144
+ if (query && searchableText.includes(query)) {
145
+ score += 12;
146
+ reasons.push("matched API documentation");
147
+ }
148
+ if (score > 0 && api.tests.length > 0 && api.examples.length > 0) {
149
+ score += 6;
150
+ reasons.push("has tests and examples");
151
+ }
152
+ return {
153
+ kind: "publicApi",
154
+ id: api.id,
155
+ score: normalizeScore(score, 200),
156
+ reasons: dedupe(reasons)
157
+ };
158
+ }
159
+ function findOwningPackageId(snapshot, filePath) {
160
+ const candidates = snapshot.packages
161
+ .filter((pkg) => pkg.path === "." || filePath === pkg.path || filePath.startsWith(`${pkg.path}/`))
162
+ .sort((a, b) => b.path.length - a.path.length || a.id.localeCompare(b.id));
163
+ return candidates[0]?.id;
164
+ }
165
+ function tokenize(input) {
166
+ return input
167
+ .split(/\s+/)
168
+ .map((token) => token.trim())
169
+ .filter(Boolean)
170
+ .sort();
171
+ }
172
+ function normalizeScore(score, max) {
173
+ if (score <= 0) {
174
+ return 0;
175
+ }
176
+ const normalized = Math.min(1, score / max);
177
+ return Number(normalized.toFixed(4));
178
+ }
179
+ function dedupe(values) {
180
+ return [...new Set(values)].sort();
181
+ }
@@ -0,0 +1,40 @@
1
+ import type { CatalogueSnapshot, SymbolRecord } from "../catalogue/schema.js";
2
+ import { type RankedEnvelope } from "./ranking.js";
3
+ import { type EditingContext } from "./editingContext.js";
4
+ export interface SearchSymbolsOptions {
5
+ query: string;
6
+ kind?: SymbolRecord["kind"];
7
+ language?: string;
8
+ packageId?: string;
9
+ publicOnly?: boolean;
10
+ capabilityId?: string;
11
+ hasExamples?: boolean;
12
+ hasTests?: boolean;
13
+ limit?: number;
14
+ offset?: number;
15
+ }
16
+ export interface SymbolCompact {
17
+ id: string;
18
+ name: string;
19
+ qualifiedName: string;
20
+ kind: SymbolRecord["kind"];
21
+ language: string;
22
+ filePath: string;
23
+ packageId?: string;
24
+ visibility: SymbolRecord["visibility"];
25
+ exported: boolean;
26
+ }
27
+ export declare function toSymbolCompact(symbol: SymbolRecord): SymbolCompact;
28
+ export declare function searchSymbols(snapshot: CatalogueSnapshot, options: SearchSymbolsOptions): RankedEnvelope<SymbolCompact>;
29
+ export interface GetSymbolContextOptions {
30
+ /** Opt-in bounded editing context (Gate 4). Default false. */
31
+ includeEditingContext?: boolean;
32
+ /** Per-section budget for editing context when included. */
33
+ editingContextLimit?: number;
34
+ }
35
+ export interface SymbolContextResult {
36
+ symbol: SymbolRecord;
37
+ /** Bounded, advisory editing context; present only when opted in. */
38
+ editingContext?: EditingContext;
39
+ }
40
+ export declare function getSymbolContext(snapshot: CatalogueSnapshot, symbolId: string, options?: GetSymbolContextOptions): SymbolContextResult | undefined;
@@ -0,0 +1,41 @@
1
+ import { buildRankedEnvelope, rankSymbols } from "./ranking.js";
2
+ import { buildEditingContext } from "./editingContext.js";
3
+ export function toSymbolCompact(symbol) {
4
+ return {
5
+ id: symbol.id,
6
+ name: symbol.name,
7
+ qualifiedName: symbol.qualifiedName,
8
+ kind: symbol.kind,
9
+ language: symbol.language,
10
+ filePath: symbol.filePath,
11
+ packageId: symbol.packageId,
12
+ visibility: symbol.visibility,
13
+ exported: symbol.exported
14
+ };
15
+ }
16
+ export function searchSymbols(snapshot, options) {
17
+ const filters = {
18
+ kind: options.kind,
19
+ language: options.language,
20
+ packageId: options.packageId,
21
+ publicOnly: options.publicOnly,
22
+ capabilityId: options.capabilityId,
23
+ hasExamples: options.hasExamples,
24
+ hasTests: options.hasTests
25
+ };
26
+ const ranked = rankSymbols(options.query, snapshot.symbols, filters, snapshot);
27
+ return buildRankedEnvelope(options.query, ranked, toSymbolCompact, filters, {
28
+ offset: options.offset,
29
+ limit: options.limit
30
+ });
31
+ }
32
+ export function getSymbolContext(snapshot, symbolId, options = {}) {
33
+ const symbol = snapshot.symbols.find((item) => item.id === symbolId);
34
+ if (!symbol) {
35
+ return undefined;
36
+ }
37
+ const editingContext = options.includeEditingContext
38
+ ? buildEditingContext(snapshot, { symbol }, { perSectionLimit: options.editingContextLimit })
39
+ : undefined;
40
+ return { symbol, editingContext };
41
+ }
@@ -0,0 +1,12 @@
1
+ export declare function toWorkspaceRelative(workspaceRoot: string, targetPath: string): string;
2
+ export declare function isWithinWorkspace(workspaceRoot: string, targetPath: string): boolean;
3
+ /**
4
+ * Resolve a filesystem path to its canonical target.
5
+ * Returns undefined on missing paths, broken symlinks, or inaccessible targets.
6
+ */
7
+ export declare function resolveCanonicalPath(input: string): string | undefined;
8
+ /**
9
+ * True when candidatePath's canonical target is inside workspaceRoot's canonical root.
10
+ * False when either path cannot be canonicalised or the target escapes the root.
11
+ */
12
+ export declare function isCanonicalWithinWorkspace(workspaceRoot: string, candidatePath: string): boolean;
@@ -0,0 +1,46 @@
1
+ import path from "node:path";
2
+ import { realpathSync } from "node:fs";
3
+ export function toWorkspaceRelative(workspaceRoot, targetPath) {
4
+ const relative = path.relative(path.resolve(workspaceRoot), path.resolve(targetPath));
5
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
6
+ return "";
7
+ }
8
+ return relative.replaceAll("\\", "/");
9
+ }
10
+ export function isWithinWorkspace(workspaceRoot, targetPath) {
11
+ const resolvedRoot = path.resolve(workspaceRoot);
12
+ const resolvedTarget = path.resolve(targetPath);
13
+ return isRelativeWithinRoot(resolvedRoot, resolvedTarget);
14
+ }
15
+ /**
16
+ * Resolve a filesystem path to its canonical target.
17
+ * Returns undefined on missing paths, broken symlinks, or inaccessible targets.
18
+ */
19
+ export function resolveCanonicalPath(input) {
20
+ try {
21
+ const realpathNative = realpathSync.native;
22
+ if (typeof realpathNative === "function") {
23
+ return realpathNative(input);
24
+ }
25
+ return realpathSync(input);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ /**
32
+ * True when candidatePath's canonical target is inside workspaceRoot's canonical root.
33
+ * False when either path cannot be canonicalised or the target escapes the root.
34
+ */
35
+ export function isCanonicalWithinWorkspace(workspaceRoot, candidatePath) {
36
+ const canonicalRoot = resolveCanonicalPath(workspaceRoot);
37
+ const canonicalTarget = resolveCanonicalPath(candidatePath);
38
+ if (!canonicalRoot || !canonicalTarget) {
39
+ return false;
40
+ }
41
+ return isRelativeWithinRoot(canonicalRoot, canonicalTarget);
42
+ }
43
+ function isRelativeWithinRoot(root, target) {
44
+ const relative = path.relative(root, target);
45
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
46
+ }
@@ -0,0 +1 @@
1
+ export declare function isSecretLikeFile(filePath: string): boolean;
@@ -0,0 +1,4 @@
1
+ const SECRET_FILE_PATTERNS = [/^\.env(\..+)?$/i, /\.pem$/i, /\.key$/i, /\.p12$/i, /\.pfx$/i];
2
+ export function isSecretLikeFile(filePath) {
3
+ return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
4
+ }
@@ -0,0 +1,12 @@
1
+ export interface SourceAccessOptions {
2
+ workspaceRoot: string;
3
+ relativePath: string;
4
+ includeSource?: boolean;
5
+ maxSourceLines: number;
6
+ maxFileSizeBytes: number;
7
+ }
8
+ export declare function getBoundedSourcePreview(options: SourceAccessOptions): {
9
+ allowed: boolean;
10
+ reason?: string;
11
+ source?: string;
12
+ };
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import { readFileSync } from "node:fs";
3
+ import { isSecretLikeFile } from "./secretPatterns.js";
4
+ import { isCanonicalWithinWorkspace, isWithinWorkspace, resolveCanonicalPath } from "./pathPolicy.js";
5
+ export function getBoundedSourcePreview(options) {
6
+ if (!options.includeSource) {
7
+ return { allowed: false, reason: "source_disabled" };
8
+ }
9
+ if (isSecretLikeFile(path.basename(options.relativePath))) {
10
+ return { allowed: false, reason: "raw_source_excluded" };
11
+ }
12
+ const absolute = path.resolve(options.workspaceRoot, options.relativePath);
13
+ if (!isWithinWorkspace(options.workspaceRoot, absolute)) {
14
+ return { allowed: false, reason: "path_outside_workspace" };
15
+ }
16
+ const canonicalRoot = resolveCanonicalPath(options.workspaceRoot);
17
+ const canonicalTarget = resolveCanonicalPath(absolute);
18
+ if (!canonicalRoot || !canonicalTarget) {
19
+ // Missing path, broken symlink, or inaccessible target — fail closed without throwing.
20
+ return { allowed: false, reason: "source_missing" };
21
+ }
22
+ if (!isCanonicalWithinWorkspace(options.workspaceRoot, absolute)) {
23
+ return { allowed: false, reason: "path_outside_workspace" };
24
+ }
25
+ const buffer = readFileSync(canonicalTarget);
26
+ if (buffer.includes(0)) {
27
+ return { allowed: false, reason: "binary_source_excluded" };
28
+ }
29
+ const content = buffer.toString("utf8");
30
+ if (Buffer.byteLength(content, "utf8") > options.maxFileSizeBytes) {
31
+ return { allowed: false, reason: "raw_source_excluded" };
32
+ }
33
+ const lines = content.split(/\r?\n/).slice(0, options.maxSourceLines);
34
+ return { allowed: true, source: lines.join("\n") };
35
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@mortice/mapwright",
3
+ "version": "0.8.0",
4
+ "description": "Mortice Mapwright — a deterministic map of what a repository contains, what changed, and what it affects",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "mapwright": "dist/cli/index.js",
10
+ "mapwright-mcp": "dist/mcp/server.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "clean": "node -e \"import('node:fs').then(fs=>fs.rmSync('dist',{recursive:true,force:true}))\"",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "cli": "tsx src/cli/index.ts",
25
+ "mcp": "tsx src/mcp/server.ts"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "mapwright",
30
+ "mortice",
31
+ "catalogue",
32
+ "change-intelligence"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/andrewmuir/Mapwright.git"
42
+ },
43
+ "homepage": "https://github.com/andrewmuir/Mapwright#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/andrewmuir/Mapwright/issues"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.29.0",
49
+ "fast-glob": "^3.3.3",
50
+ "ignore": "^7.0.5",
51
+ "minimatch": "^10.2.5",
52
+ "typescript": "^6.0.3",
53
+ "zod": "^4.4.3"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^26.1.0",
57
+ "tsx": "^4.23.0",
58
+ "vitest": "^4.1.9"
59
+ }
60
+ }