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 +1 -1
- package/src/config/index.ts +14 -4
- package/src/context/adr.ts +44 -13
package/package.json
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -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
|
|
28
|
-
adrPath
|
|
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
|
|
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
|
}
|
package/src/context/adr.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
*
|
|
183
|
-
* -
|
|
184
|
-
* -
|
|
185
|
-
* -
|
|
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) {
|