specvector 0.1.4 → 0.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specvector",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Context-aware AI code review using Model Context Protocol (MCP)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -24,8 +24,8 @@ export interface SpecVectorConfig {
24
24
  maxFileSize: number;
25
25
  /** Maximum iterations for agent */
26
26
  maxIterations: number;
27
- /** Path to ADR directory (relative to project root), null to disable */
28
- adrPath: string | null;
27
+ /** Path to ADR directory: string = use path, null = disabled, undefined = auto-detect */
28
+ adrPath?: string | null;
29
29
  }
30
30
 
31
31
  /** Default configuration */
@@ -49,7 +49,7 @@ export const DEFAULT_CONFIG: SpecVectorConfig = {
49
49
  strictness: "normal",
50
50
  maxFileSize: 100 * 1024, // 100KB
51
51
  maxIterations: 15,
52
- adrPath: null, // Disabled by default, set to "docs/adr" to enable
52
+ // adrPath omitted = auto-detect; set to null to explicitly disable
53
53
  };
54
54
 
55
55
  /** Config file name */
@@ -115,7 +115,7 @@ function parseYamlConfig(content: string): Partial<SpecVectorConfig> {
115
115
  const lines = content.split("\n");
116
116
  const warnings: string[] = [];
117
117
 
118
- const validKeys = ["provider", "model", "strictness", "maxFileSize", "maxIterations", "ignore"];
118
+ const validKeys = ["provider", "model", "strictness", "maxFileSize", "maxIterations", "ignore", "adrPath"];
119
119
 
120
120
  let currentKey: string | null = null;
121
121
  let currentArray: string[] = [];
@@ -202,6 +202,16 @@ function parseYamlConfig(content: string): Partial<SpecVectorConfig> {
202
202
  }
203
203
  break;
204
204
  }
205
+ case "adrPath": {
206
+ // Support explicit disable with null, false, none, or empty
207
+ const lower = cleanValue.toLowerCase();
208
+ if (lower === "null" || lower === "false" || lower === "none" || lower === "") {
209
+ result.adrPath = null; // Explicitly disabled
210
+ } else {
211
+ result.adrPath = cleanValue;
212
+ }
213
+ break;
214
+ }
205
215
  }
206
216
  }
207
217
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { readdir, readFile, stat } from "fs/promises";
9
- import { join, basename } from "path";
9
+ import { join, basename, resolve, isAbsolute, relative } from "path";
10
10
  import type { Result } from "../types/result";
11
11
  import { ok, err } from "../types/result";
12
12
 
@@ -70,7 +70,33 @@ export async function getADRContext(
70
70
  workingDir: string,
71
71
  adrPath: string
72
72
  ): Promise<Result<ADRContext, ADRContextError>> {
73
- const fullPath = join(workingDir, adrPath);
73
+ // Security: reject absolute paths
74
+ if (isAbsolute(adrPath)) {
75
+ return err({
76
+ code: "PATH_NOT_FOUND",
77
+ message: `ADR path must be relative, not absolute: ${adrPath}`,
78
+ });
79
+ }
80
+
81
+ // Security: reject path traversal attempts
82
+ if (adrPath.includes("..")) {
83
+ return err({
84
+ code: "PATH_NOT_FOUND",
85
+ message: `ADR path cannot contain '..': ${adrPath}`,
86
+ });
87
+ }
88
+
89
+ const fullPath = resolve(workingDir, adrPath);
90
+ const resolvedWorkingDir = resolve(workingDir);
91
+
92
+ // Security: use path.relative for portable containment check
93
+ const relativePath = relative(resolvedWorkingDir, fullPath);
94
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
95
+ return err({
96
+ code: "PATH_NOT_FOUND",
97
+ message: `ADR path must be within project directory: ${adrPath}`,
98
+ });
99
+ }
74
100
 
75
101
  // Check if directory exists
76
102
  try {
@@ -99,11 +125,10 @@ export async function getADRContext(
99
125
  });
100
126
  }
101
127
 
102
- // Filter to markdown files
128
+ // Filter to markdown files and sort
103
129
  const mdFiles = entries
104
130
  .filter((f) => ADR_EXTENSIONS.some((ext) => f.toLowerCase().endsWith(ext)))
105
- .sort() // Sort alphabetically (ADRs often numbered: 001-xxx.md)
106
- .slice(0, MAX_ADR_FILES);
131
+ .sort(); // Sort alphabetically (ADRs often numbered: 001-xxx.md)
107
132
 
108
133
  if (mdFiles.length === 0) {
109
134
  return err({
@@ -112,9 +137,11 @@ export async function getADRContext(
112
137
  });
113
138
  }
114
139
 
115
- // Read each file
140
+ // Read files until we have MAX_ADR_FILES (don't slice upfront)
116
141
  const files: ADRFile[] = [];
117
142
  for (const filename of mdFiles) {
143
+ if (files.length >= MAX_ADR_FILES) break;
144
+
118
145
  const filePath = join(fullPath, filename);
119
146
  try {
120
147
  const fileStats = await stat(filePath);
@@ -179,18 +206,22 @@ export function formatADRContext(context: ADRContext): string {
179
206
  * Get ADR context for a review, with graceful error handling.
180
207
  * Returns null if ADRs are not available (no error thrown).
181
208
  *
182
- * If adrPath is not set, auto-detects common paths:
183
- * - _bmad-output/planning-artifacts (BMAD projects)
184
- * - docs/adr (standard ADR location)
185
- * - docs/architecture (common alternative)
186
- * - adr (simple ADR folder)
209
+ * Behavior:
210
+ * - adrPath = string: use the specified path
211
+ * - adrPath = null: explicitly disabled, no ADR context
212
+ * - adrPath = undefined: auto-detect common paths
187
213
  */
188
214
  export async function getADRContextForReview(
189
215
  workingDir: string,
190
216
  adrPath: string | null | undefined
191
217
  ): Promise<{ context: ADRContext; formatted: string } | null> {
218
+ // Explicitly disabled with null
219
+ if (adrPath === null) {
220
+ return null;
221
+ }
222
+
192
223
  // If path is explicitly set, use it
193
- if (adrPath) {
224
+ if (adrPath !== undefined) {
194
225
  const result = await getADRContext(workingDir, adrPath);
195
226
  if (!result.ok) {
196
227
  console.log(`⚠️ ADR context unavailable: ${result.error.message}`);
@@ -202,7 +233,7 @@ export async function getADRContextForReview(
202
233
  };
203
234
  }
204
235
 
205
- // Auto-detect: try common paths in priority order
236
+ // Auto-detect: try common paths in priority order (adrPath is undefined)
206
237
  for (const path of AUTO_DETECT_PATHS) {
207
238
  const result = await getADRContext(workingDir, path);
208
239
  if (result.ok) {