@sudocode-ai/integration-speckit 0.1.14
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/dist/id-generator.d.ts +149 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +197 -0
- package/dist/id-generator.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1017 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/index.d.ts +11 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +16 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/markdown-utils.d.ts +138 -0
- package/dist/parser/markdown-utils.d.ts.map +1 -0
- package/dist/parser/markdown-utils.js +283 -0
- package/dist/parser/markdown-utils.js.map +1 -0
- package/dist/parser/plan-parser.d.ts +97 -0
- package/dist/parser/plan-parser.d.ts.map +1 -0
- package/dist/parser/plan-parser.js +286 -0
- package/dist/parser/plan-parser.js.map +1 -0
- package/dist/parser/spec-parser.d.ts +95 -0
- package/dist/parser/spec-parser.d.ts.map +1 -0
- package/dist/parser/spec-parser.js +250 -0
- package/dist/parser/spec-parser.js.map +1 -0
- package/dist/parser/supporting-docs.d.ts +119 -0
- package/dist/parser/supporting-docs.d.ts.map +1 -0
- package/dist/parser/supporting-docs.js +324 -0
- package/dist/parser/supporting-docs.js.map +1 -0
- package/dist/parser/tasks-parser.d.ts +171 -0
- package/dist/parser/tasks-parser.d.ts.map +1 -0
- package/dist/parser/tasks-parser.js +281 -0
- package/dist/parser/tasks-parser.js.map +1 -0
- package/dist/relationship-mapper.d.ts +165 -0
- package/dist/relationship-mapper.d.ts.map +1 -0
- package/dist/relationship-mapper.js +238 -0
- package/dist/relationship-mapper.js.map +1 -0
- package/dist/watcher.d.ts +137 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +599 -0
- package/dist/watcher.js.map +1 -0
- package/dist/writer/index.d.ts +8 -0
- package/dist/writer/index.d.ts.map +1 -0
- package/dist/writer/index.js +10 -0
- package/dist/writer/index.js.map +1 -0
- package/dist/writer/spec-writer.d.ts +70 -0
- package/dist/writer/spec-writer.d.ts.map +1 -0
- package/dist/writer/spec-writer.js +261 -0
- package/dist/writer/spec-writer.js.map +1 -0
- package/dist/writer/tasks-writer.d.ts +47 -0
- package/dist/writer/tasks-writer.d.ts.map +1 -0
- package/dist/writer/tasks-writer.js +161 -0
- package/dist/writer/tasks-writer.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-Kit Integration Plugin for sudocode
|
|
3
|
+
*
|
|
4
|
+
* Provides integration with spec-kit - a markdown-based specification system.
|
|
5
|
+
* Syncs specs, plans, and tasks to sudocode's specs and issues.
|
|
6
|
+
*/
|
|
7
|
+
// Re-export ID generation utilities
|
|
8
|
+
export { generateSpecId, generateTaskIssueId, parseSpecId, isValidSpecKitId, extractFeatureNumber, extractFileType, getFeatureSpecId, getFeaturePlanId, getFeatureTasksId, } from "./id-generator.js";
|
|
9
|
+
// Re-export relationship mapping utilities
|
|
10
|
+
export { mapFeatureRelationships, mapTaskDependencies, mapSupportingDocRelationships, mapPlanToSpecRelationship, mapTaskToPlanRelationship, getStandardSupportingDocTypes, createContractDocInfo, } from "./relationship-mapper.js";
|
|
11
|
+
// Re-export writer utilities
|
|
12
|
+
export { updateTaskStatus, getTaskStatus, getAllTaskStatuses, updateSpecContent, getSpecTitle, getSpecStatus, } from "./writer/index.js";
|
|
13
|
+
// Re-export watcher
|
|
14
|
+
export { SpecKitWatcher, } from "./watcher.js";
|
|
15
|
+
// Re-export parser utilities
|
|
16
|
+
export {
|
|
17
|
+
// Markdown utilities
|
|
18
|
+
PATTERNS, extractMetadata, extractTitle, extractTitleWithPrefixRemoval, extractMetadataValue, extractCrossReferences, findContentStartIndex, extractSection, parseDate, escapeRegex, cleanTaskDescription, normalizeStatus,
|
|
19
|
+
// Spec parser
|
|
20
|
+
parseSpec, parseSpecContent, isSpecFile, getSpecFileTitle, getSpecFileStatus,
|
|
21
|
+
// Plan parser
|
|
22
|
+
parsePlan, parsePlanContent, isPlanFile, getPlanFileTitle, getPlanFileStatus,
|
|
23
|
+
// Tasks parser
|
|
24
|
+
parseTasks, parseTasksContent, getAllTasks, getTaskById, getIncompleteTasks, getParallelizableTasks, getTasksByPhase, getTasksByUserStory, isTasksFile, getTaskStats,
|
|
25
|
+
// Supporting documents parser
|
|
26
|
+
parseResearch, parseDataModel, parseSupportingDoc, parseContract, parseContractsDirectory, discoverSupportingDocs, detectDocType, } from "./parser/index.js";
|
|
27
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
28
|
+
import * as path from "path";
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
import { parseSpecId, generateSpecId, generateTaskIssueId, getFeatureSpecId, getFeaturePlanId, } from "./id-generator.js";
|
|
31
|
+
import { updateTaskStatus, updateSpecContent } from "./writer/index.js";
|
|
32
|
+
import { parseSpec } from "./parser/spec-parser.js";
|
|
33
|
+
import { parsePlan } from "./parser/plan-parser.js";
|
|
34
|
+
import { parseTasks } from "./parser/tasks-parser.js";
|
|
35
|
+
import { discoverSupportingDocs, } from "./parser/supporting-docs.js";
|
|
36
|
+
import { SpecKitWatcher } from "./watcher.js";
|
|
37
|
+
/**
|
|
38
|
+
* Configuration schema for UI form generation
|
|
39
|
+
*/
|
|
40
|
+
const configSchema = {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
path: {
|
|
44
|
+
type: "string",
|
|
45
|
+
title: "Spec-Kit Path",
|
|
46
|
+
description: "Path to the .specify directory (relative to project root)",
|
|
47
|
+
default: ".specify",
|
|
48
|
+
required: true,
|
|
49
|
+
},
|
|
50
|
+
spec_prefix: {
|
|
51
|
+
type: "string",
|
|
52
|
+
title: "Spec Prefix",
|
|
53
|
+
description: "Prefix for spec IDs imported from spec-kit",
|
|
54
|
+
default: "sk",
|
|
55
|
+
},
|
|
56
|
+
task_prefix: {
|
|
57
|
+
type: "string",
|
|
58
|
+
title: "Task Prefix",
|
|
59
|
+
description: "Prefix for task IDs imported from spec-kit",
|
|
60
|
+
default: "skt",
|
|
61
|
+
},
|
|
62
|
+
include_supporting_docs: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
title: "Include Supporting Docs",
|
|
65
|
+
description: "Include research.md, data-model.md, and contracts/*.md",
|
|
66
|
+
default: true,
|
|
67
|
+
},
|
|
68
|
+
include_constitution: {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
title: "Include Constitution",
|
|
71
|
+
description: "Include constitution.md as root project spec",
|
|
72
|
+
default: true,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ["path"],
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Spec-kit integration plugin
|
|
79
|
+
*/
|
|
80
|
+
const specKitPlugin = {
|
|
81
|
+
name: "spec-kit",
|
|
82
|
+
displayName: "Spec-Kit",
|
|
83
|
+
version: "0.1.0",
|
|
84
|
+
description: "Integration with spec-kit markdown-based specification system",
|
|
85
|
+
configSchema,
|
|
86
|
+
validateConfig(options) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
const warnings = [];
|
|
89
|
+
// Check required path field
|
|
90
|
+
if (!options.path || typeof options.path !== "string") {
|
|
91
|
+
errors.push("spec-kit.options.path is required");
|
|
92
|
+
}
|
|
93
|
+
// Validate spec_prefix if provided
|
|
94
|
+
if (options.spec_prefix !== undefined) {
|
|
95
|
+
if (typeof options.spec_prefix !== "string") {
|
|
96
|
+
errors.push("spec-kit.options.spec_prefix must be a string");
|
|
97
|
+
}
|
|
98
|
+
else if (!/^[a-z]{1,4}$/i.test(options.spec_prefix)) {
|
|
99
|
+
warnings.push("spec-kit.options.spec_prefix should be 1-4 alphabetic characters");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Validate task_prefix if provided
|
|
103
|
+
if (options.task_prefix !== undefined) {
|
|
104
|
+
if (typeof options.task_prefix !== "string") {
|
|
105
|
+
errors.push("spec-kit.options.task_prefix must be a string");
|
|
106
|
+
}
|
|
107
|
+
else if (!/^[a-z]{1,4}$/i.test(options.task_prefix)) {
|
|
108
|
+
warnings.push("spec-kit.options.task_prefix should be 1-4 alphabetic characters");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Validate boolean options
|
|
112
|
+
if (options.include_supporting_docs !== undefined &&
|
|
113
|
+
typeof options.include_supporting_docs !== "boolean") {
|
|
114
|
+
errors.push("spec-kit.options.include_supporting_docs must be a boolean");
|
|
115
|
+
}
|
|
116
|
+
if (options.include_constitution !== undefined &&
|
|
117
|
+
typeof options.include_constitution !== "boolean") {
|
|
118
|
+
errors.push("spec-kit.options.include_constitution must be a boolean");
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
valid: errors.length === 0,
|
|
122
|
+
errors,
|
|
123
|
+
warnings,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
async testConnection(options, projectPath) {
|
|
127
|
+
const specKitPath = options.path;
|
|
128
|
+
if (!specKitPath) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
configured: true,
|
|
132
|
+
enabled: true,
|
|
133
|
+
error: "Spec-kit path is not configured",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const resolvedPath = path.resolve(projectPath, specKitPath);
|
|
137
|
+
if (!existsSync(resolvedPath)) {
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
configured: true,
|
|
141
|
+
enabled: true,
|
|
142
|
+
error: `Spec-kit directory not found: ${resolvedPath}`,
|
|
143
|
+
details: { path: specKitPath, resolvedPath },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Check for specs subdirectory
|
|
147
|
+
const specsPath = path.join(resolvedPath, "specs");
|
|
148
|
+
const hasSpecs = existsSync(specsPath);
|
|
149
|
+
// Check for common spec-kit files
|
|
150
|
+
const hasConstitution = existsSync(path.join(resolvedPath, "constitution.md"));
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
configured: true,
|
|
154
|
+
enabled: true,
|
|
155
|
+
details: {
|
|
156
|
+
path: specKitPath,
|
|
157
|
+
resolvedPath,
|
|
158
|
+
hasSpecsDirectory: hasSpecs,
|
|
159
|
+
hasConstitution,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
createProvider(options, projectPath) {
|
|
164
|
+
return new SpecKitProvider(options, projectPath);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Spec-kit provider implementation
|
|
169
|
+
*/
|
|
170
|
+
class SpecKitProvider {
|
|
171
|
+
name = "spec-kit";
|
|
172
|
+
supportsWatch = true;
|
|
173
|
+
supportsPolling = true;
|
|
174
|
+
options;
|
|
175
|
+
projectPath;
|
|
176
|
+
resolvedPath;
|
|
177
|
+
// Change tracking for getChangesSince
|
|
178
|
+
entityHashes = new Map();
|
|
179
|
+
// File watcher instance
|
|
180
|
+
watcher = null;
|
|
181
|
+
constructor(options, projectPath) {
|
|
182
|
+
this.options = options;
|
|
183
|
+
this.projectPath = projectPath;
|
|
184
|
+
this.resolvedPath = path.resolve(projectPath, options.path);
|
|
185
|
+
}
|
|
186
|
+
async initialize() {
|
|
187
|
+
console.log(`[spec-kit] Initializing provider for path: ${this.resolvedPath}`);
|
|
188
|
+
if (!existsSync(this.resolvedPath)) {
|
|
189
|
+
throw new Error(`Spec-kit directory not found: ${this.resolvedPath}`);
|
|
190
|
+
}
|
|
191
|
+
// Check for specs subdirectory
|
|
192
|
+
const specsDir = path.join(this.resolvedPath, "specs");
|
|
193
|
+
if (!existsSync(specsDir)) {
|
|
194
|
+
console.warn(`[spec-kit] Note: specs directory does not exist yet at ${specsDir}`);
|
|
195
|
+
}
|
|
196
|
+
// Note: We intentionally do NOT pre-populate entityHashes here.
|
|
197
|
+
// This allows getChangesSince to detect all existing entities as "new" on first sync,
|
|
198
|
+
// enabling auto-import of existing spec-kit entities into sudocode.
|
|
199
|
+
// The hash cache gets populated as entities are synced/detected.
|
|
200
|
+
console.log(`[spec-kit] Provider initialized successfully (hash cache empty for fresh import)`);
|
|
201
|
+
}
|
|
202
|
+
async validate() {
|
|
203
|
+
const errors = [];
|
|
204
|
+
if (!existsSync(this.resolvedPath)) {
|
|
205
|
+
errors.push(`Spec-kit directory not found: ${this.resolvedPath}`);
|
|
206
|
+
return { valid: false, errors };
|
|
207
|
+
}
|
|
208
|
+
// Check for specs subdirectory
|
|
209
|
+
const specsDir = path.join(this.resolvedPath, "specs");
|
|
210
|
+
if (!existsSync(specsDir)) {
|
|
211
|
+
// Not an error - directory might not exist yet, but warn
|
|
212
|
+
console.log(`[spec-kit] Note: specs directory does not exist yet at ${specsDir}`);
|
|
213
|
+
}
|
|
214
|
+
const valid = errors.length === 0;
|
|
215
|
+
console.log(`[spec-kit] Validation result: valid=${valid}, errors=${errors.length}`);
|
|
216
|
+
return { valid, errors };
|
|
217
|
+
}
|
|
218
|
+
async dispose() {
|
|
219
|
+
console.log(`[spec-kit] Disposing provider`);
|
|
220
|
+
this.stopWatching();
|
|
221
|
+
// Clear entity state cache
|
|
222
|
+
this.entityHashes.clear();
|
|
223
|
+
console.log(`[spec-kit] Provider disposed successfully`);
|
|
224
|
+
}
|
|
225
|
+
async fetchEntity(externalId) {
|
|
226
|
+
console.log(`[spec-kit] fetchEntity called for: ${externalId}`);
|
|
227
|
+
// Parse the ID to determine file type and path
|
|
228
|
+
const parsed = parseSpecId(externalId);
|
|
229
|
+
if (!parsed) {
|
|
230
|
+
console.warn(`[spec-kit] Invalid ID format: ${externalId}`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const prefix = this.options.spec_prefix || "sk";
|
|
234
|
+
const taskPrefix = this.options.task_prefix || "skt";
|
|
235
|
+
// Check if this is a task (issue) or spec
|
|
236
|
+
if (parsed.isTask && parsed.featureNumber) {
|
|
237
|
+
// Task entity - find it in the tasks.md file
|
|
238
|
+
const tasksFilePath = this.getTasksFilePath(parsed.featureNumber);
|
|
239
|
+
if (!existsSync(tasksFilePath)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const tasksFile = parseTasks(tasksFilePath);
|
|
243
|
+
if (!tasksFile) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const task = tasksFile.tasks.find((t) => t.taskId === parsed.fileType);
|
|
247
|
+
if (!task) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return this.taskToExternalEntity(task, parsed.featureNumber, externalId, tasksFilePath);
|
|
251
|
+
}
|
|
252
|
+
// Spec entity - determine file path based on feature number and file type
|
|
253
|
+
if (parsed.featureNumber) {
|
|
254
|
+
const filePath = this.getSpecFilePath(parsed.featureNumber, parsed.fileType);
|
|
255
|
+
if (!existsSync(filePath)) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
// Get the feature directory name for title generation
|
|
259
|
+
const featureDirName = this.getFeatureDirName(parsed.featureNumber);
|
|
260
|
+
// Parse based on file type
|
|
261
|
+
if (parsed.fileType === "spec") {
|
|
262
|
+
const spec = parseSpec(filePath);
|
|
263
|
+
if (!spec)
|
|
264
|
+
return null;
|
|
265
|
+
return this.specToExternalEntity(spec, externalId, featureDirName || undefined, "spec");
|
|
266
|
+
}
|
|
267
|
+
else if (parsed.fileType === "plan") {
|
|
268
|
+
const plan = parsePlan(filePath);
|
|
269
|
+
if (!plan)
|
|
270
|
+
return null;
|
|
271
|
+
return this.planToExternalEntity(plan, externalId, featureDirName || undefined, parsed.featureNumber || undefined);
|
|
272
|
+
}
|
|
273
|
+
else if (parsed.fileType === "research" ||
|
|
274
|
+
parsed.fileType === "data-model") {
|
|
275
|
+
const featureDir = path.dirname(filePath);
|
|
276
|
+
const docs = discoverSupportingDocs(featureDir);
|
|
277
|
+
const doc = parsed.fileType === "research" ? docs.research : docs.dataModel;
|
|
278
|
+
if (!doc)
|
|
279
|
+
return null;
|
|
280
|
+
return this.supportingDocToExternalEntity(doc, externalId, featureDirName || undefined, parsed.featureNumber || undefined);
|
|
281
|
+
}
|
|
282
|
+
else if (parsed.fileType.startsWith("contract-")) {
|
|
283
|
+
const featureDir = path.dirname(filePath);
|
|
284
|
+
const contractsDir = path.join(featureDir, "contracts");
|
|
285
|
+
const contractName = parsed.fileType.replace("contract-", "");
|
|
286
|
+
const docs = discoverSupportingDocs(featureDir);
|
|
287
|
+
const contract = docs.contracts.find((c) => c.name === contractName);
|
|
288
|
+
if (!contract)
|
|
289
|
+
return null;
|
|
290
|
+
return this.contractToExternalEntity(contract, externalId, featureDirName || undefined, parsed.featureNumber || undefined);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Non-feature file (e.g., constitution.md)
|
|
295
|
+
if (parsed.fileType === "constitution") {
|
|
296
|
+
const constitutionPath = path.join(this.resolvedPath, "memory", "constitution.md");
|
|
297
|
+
if (!existsSync(constitutionPath)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const spec = parseSpec(constitutionPath);
|
|
301
|
+
if (!spec)
|
|
302
|
+
return null;
|
|
303
|
+
return this.specToExternalEntity(spec, externalId, undefined, "constitution");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
async searchEntities(query) {
|
|
309
|
+
console.log(`[spec-kit] searchEntities called with query: ${query}`);
|
|
310
|
+
const entities = [];
|
|
311
|
+
const prefix = this.options.spec_prefix || "sk";
|
|
312
|
+
const taskPrefix = this.options.task_prefix || "skt";
|
|
313
|
+
const includeSupportingDocs = this.options.include_supporting_docs !== false;
|
|
314
|
+
const includeConstitution = this.options.include_constitution !== false;
|
|
315
|
+
// Scan specs directory for feature directories
|
|
316
|
+
const specsDir = path.join(this.resolvedPath, "specs");
|
|
317
|
+
if (existsSync(specsDir)) {
|
|
318
|
+
try {
|
|
319
|
+
const entries = readdirSync(specsDir, { withFileTypes: true });
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
if (!entry.isDirectory())
|
|
322
|
+
continue;
|
|
323
|
+
// Feature directories match pattern like "001-auth", "002-payments"
|
|
324
|
+
const featureMatch = entry.name.match(/^(\d+)-/);
|
|
325
|
+
if (!featureMatch)
|
|
326
|
+
continue;
|
|
327
|
+
const featureNumber = featureMatch[1];
|
|
328
|
+
const featureDir = path.join(specsDir, entry.name);
|
|
329
|
+
// Parse spec.md
|
|
330
|
+
const specPath = path.join(featureDir, "spec.md");
|
|
331
|
+
if (existsSync(specPath)) {
|
|
332
|
+
const spec = parseSpec(specPath);
|
|
333
|
+
if (spec) {
|
|
334
|
+
const specId = generateSpecId(`specs/${entry.name}/spec.md`, prefix);
|
|
335
|
+
const entity = this.specToExternalEntity(spec, specId, entry.name, "spec");
|
|
336
|
+
if (this.matchesQuery(entity, query)) {
|
|
337
|
+
entities.push(entity);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Parse plan.md
|
|
342
|
+
const planPath = path.join(featureDir, "plan.md");
|
|
343
|
+
if (existsSync(planPath)) {
|
|
344
|
+
const plan = parsePlan(planPath);
|
|
345
|
+
if (plan) {
|
|
346
|
+
const planId = generateSpecId(`specs/${entry.name}/plan.md`, prefix);
|
|
347
|
+
const entity = this.planToExternalEntity(plan, planId, entry.name, featureNumber);
|
|
348
|
+
if (this.matchesQuery(entity, query)) {
|
|
349
|
+
entities.push(entity);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Parse tasks.md - each task becomes an Issue
|
|
354
|
+
const tasksPath = path.join(featureDir, "tasks.md");
|
|
355
|
+
if (existsSync(tasksPath)) {
|
|
356
|
+
const tasksFile = parseTasks(tasksPath);
|
|
357
|
+
if (tasksFile) {
|
|
358
|
+
for (const task of tasksFile.tasks) {
|
|
359
|
+
const taskIssueId = generateTaskIssueId(featureNumber, task.taskId, taskPrefix);
|
|
360
|
+
const entity = this.taskToExternalEntity(task, featureNumber, taskIssueId, tasksPath);
|
|
361
|
+
if (this.matchesQuery(entity, query)) {
|
|
362
|
+
entities.push(entity);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Optionally include supporting docs
|
|
368
|
+
if (includeSupportingDocs) {
|
|
369
|
+
const docs = discoverSupportingDocs(featureDir);
|
|
370
|
+
if (docs.research) {
|
|
371
|
+
const docId = generateSpecId(`specs/${entry.name}/research.md`, prefix);
|
|
372
|
+
const entity = this.supportingDocToExternalEntity(docs.research, docId, entry.name, featureNumber);
|
|
373
|
+
if (this.matchesQuery(entity, query)) {
|
|
374
|
+
entities.push(entity);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (docs.dataModel) {
|
|
378
|
+
const docId = generateSpecId(`specs/${entry.name}/data-model.md`, prefix);
|
|
379
|
+
const entity = this.supportingDocToExternalEntity(docs.dataModel, docId, entry.name, featureNumber);
|
|
380
|
+
if (this.matchesQuery(entity, query)) {
|
|
381
|
+
entities.push(entity);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const contract of docs.contracts) {
|
|
385
|
+
const docId = `${prefix}-${featureNumber}-contract-${contract.name}`;
|
|
386
|
+
const entity = this.contractToExternalEntity(contract, docId, entry.name, featureNumber);
|
|
387
|
+
if (this.matchesQuery(entity, query)) {
|
|
388
|
+
entities.push(entity);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const other of docs.other) {
|
|
392
|
+
const docId = generateSpecId(`specs/${entry.name}/${other.fileName}.md`, prefix);
|
|
393
|
+
const entity = this.supportingDocToExternalEntity(other, docId, entry.name, featureNumber);
|
|
394
|
+
if (this.matchesQuery(entity, query)) {
|
|
395
|
+
entities.push(entity);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
console.error(`[spec-kit] Error scanning specs directory:`, error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Include constitution.md if configured
|
|
406
|
+
if (includeConstitution) {
|
|
407
|
+
const constitutionPath = path.join(this.resolvedPath, "memory", "constitution.md");
|
|
408
|
+
if (existsSync(constitutionPath)) {
|
|
409
|
+
const spec = parseSpec(constitutionPath);
|
|
410
|
+
if (spec) {
|
|
411
|
+
const constitutionId = `${prefix}-constitution`;
|
|
412
|
+
// Use "constitution" as title (no feature dir for global files)
|
|
413
|
+
const entity = this.specToExternalEntity(spec, constitutionId, undefined, "constitution");
|
|
414
|
+
if (this.matchesQuery(entity, query)) {
|
|
415
|
+
entities.push(entity);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
console.log(`[spec-kit] searchEntities found ${entities.length} entities`);
|
|
421
|
+
return entities;
|
|
422
|
+
}
|
|
423
|
+
async createEntity(entity) {
|
|
424
|
+
// Spec-kit uses file-based storage, so creating entities is not directly supported
|
|
425
|
+
// Entities are created by adding files to the spec-kit directory structure
|
|
426
|
+
console.log(`[spec-kit] createEntity called:`, entity.title);
|
|
427
|
+
throw new Error("createEntity not supported: spec-kit entities are created by adding files to the .specify directory");
|
|
428
|
+
}
|
|
429
|
+
async updateEntity(externalId, entity) {
|
|
430
|
+
console.log(`[spec-kit] updateEntity called for ${externalId}:`, entity);
|
|
431
|
+
// Parse the external ID to determine what type of entity this is
|
|
432
|
+
const parsed = parseSpecId(externalId);
|
|
433
|
+
if (!parsed) {
|
|
434
|
+
throw new Error(`Invalid spec-kit ID format: ${externalId}`);
|
|
435
|
+
}
|
|
436
|
+
// Handle task updates (issues that map to tasks.md entries)
|
|
437
|
+
if (parsed.isTask && parsed.featureNumber) {
|
|
438
|
+
await this.updateTaskEntity(parsed.featureNumber, parsed.fileType, entity);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// Handle spec/plan updates
|
|
442
|
+
if (parsed.featureNumber) {
|
|
443
|
+
await this.updateSpecEntity(parsed.featureNumber, parsed.fileType, entity);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Handle non-feature files (e.g., constitution.md)
|
|
447
|
+
await this.updateNonFeatureEntity(parsed.fileType, entity);
|
|
448
|
+
}
|
|
449
|
+
async deleteEntity(externalId) {
|
|
450
|
+
// Spec-kit uses file-based storage, so deleting entities is not directly supported
|
|
451
|
+
// Entities are deleted by removing files from the spec-kit directory structure
|
|
452
|
+
console.log(`[spec-kit] deleteEntity called for: ${externalId}`);
|
|
453
|
+
throw new Error("deleteEntity not supported: spec-kit entities are deleted by removing files from the .specify directory");
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Update a task entity (corresponds to a line in tasks.md)
|
|
457
|
+
*/
|
|
458
|
+
async updateTaskEntity(featureNumber, taskId, entity) {
|
|
459
|
+
const tasksFilePath = this.getTasksFilePath(featureNumber);
|
|
460
|
+
if (!existsSync(tasksFilePath)) {
|
|
461
|
+
throw new Error(`Tasks file not found: ${tasksFilePath}`);
|
|
462
|
+
}
|
|
463
|
+
// Check if this is an issue with status
|
|
464
|
+
const issue = entity;
|
|
465
|
+
if (issue.status !== undefined) {
|
|
466
|
+
const completed = issue.status === "closed";
|
|
467
|
+
const result = updateTaskStatus(tasksFilePath, taskId, completed);
|
|
468
|
+
if (!result.success) {
|
|
469
|
+
throw new Error(result.error || "Failed to update task status");
|
|
470
|
+
}
|
|
471
|
+
// Update hash cache to prevent false change detection
|
|
472
|
+
this.updateHashCache(tasksFilePath);
|
|
473
|
+
console.log(`[spec-kit] Updated task ${taskId} in feature ${featureNumber}: ${result.previousStatus} -> ${result.newStatus}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Update a spec/plan entity
|
|
478
|
+
*/
|
|
479
|
+
async updateSpecEntity(featureNumber, fileType, entity) {
|
|
480
|
+
const filePath = this.getSpecFilePath(featureNumber, fileType);
|
|
481
|
+
if (!existsSync(filePath)) {
|
|
482
|
+
throw new Error(`Spec file not found: ${filePath}`);
|
|
483
|
+
}
|
|
484
|
+
const spec = entity;
|
|
485
|
+
const updates = {};
|
|
486
|
+
if (spec.title !== undefined) {
|
|
487
|
+
updates.title = spec.title;
|
|
488
|
+
}
|
|
489
|
+
// Map sudocode priority/status to spec-kit status if applicable
|
|
490
|
+
// Note: spec-kit doesn't have a standard status field, but we support it if present
|
|
491
|
+
const issue = entity;
|
|
492
|
+
if (issue.status !== undefined) {
|
|
493
|
+
updates.status = this.mapStatusToSpecKit(issue.status);
|
|
494
|
+
}
|
|
495
|
+
if (spec.content !== undefined) {
|
|
496
|
+
updates.content = spec.content;
|
|
497
|
+
}
|
|
498
|
+
const result = updateSpecContent(filePath, updates);
|
|
499
|
+
if (!result.success) {
|
|
500
|
+
throw new Error(result.error || "Failed to update spec content");
|
|
501
|
+
}
|
|
502
|
+
// Update hash cache to prevent false change detection
|
|
503
|
+
this.updateHashCache(filePath);
|
|
504
|
+
console.log(`[spec-kit] Updated ${fileType} for feature ${featureNumber}:`, result.changes);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Update a non-feature entity (e.g., constitution.md)
|
|
508
|
+
*/
|
|
509
|
+
async updateNonFeatureEntity(fileType, entity) {
|
|
510
|
+
// Map file type to path
|
|
511
|
+
let filePath;
|
|
512
|
+
if (fileType === "constitution") {
|
|
513
|
+
filePath = path.join(this.resolvedPath, "memory", "constitution.md");
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
filePath = path.join(this.resolvedPath, `${fileType}.md`);
|
|
517
|
+
}
|
|
518
|
+
if (!existsSync(filePath)) {
|
|
519
|
+
throw new Error(`Spec file not found: ${filePath}`);
|
|
520
|
+
}
|
|
521
|
+
const spec = entity;
|
|
522
|
+
const updates = {};
|
|
523
|
+
if (spec.title !== undefined) {
|
|
524
|
+
updates.title = spec.title;
|
|
525
|
+
}
|
|
526
|
+
if (spec.content !== undefined) {
|
|
527
|
+
updates.content = spec.content;
|
|
528
|
+
}
|
|
529
|
+
const result = updateSpecContent(filePath, updates);
|
|
530
|
+
if (!result.success) {
|
|
531
|
+
throw new Error(result.error || "Failed to update spec content");
|
|
532
|
+
}
|
|
533
|
+
// Update hash cache to prevent false change detection
|
|
534
|
+
this.updateHashCache(filePath);
|
|
535
|
+
console.log(`[spec-kit] Updated ${fileType}:`, result.changes);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Get the path to a feature's tasks.md file
|
|
539
|
+
*/
|
|
540
|
+
getTasksFilePath(featureNumber) {
|
|
541
|
+
// Find the feature directory by looking for XXX-* pattern
|
|
542
|
+
const specsDir = path.join(this.resolvedPath, "specs");
|
|
543
|
+
if (!existsSync(specsDir)) {
|
|
544
|
+
return path.join(specsDir, `${featureNumber}-unknown`, "tasks.md");
|
|
545
|
+
}
|
|
546
|
+
const dirs = readdirSync(specsDir, { withFileTypes: true })
|
|
547
|
+
.filter((d) => d.isDirectory())
|
|
548
|
+
.map((d) => d.name);
|
|
549
|
+
const featureDir = dirs.find((dir) => dir.startsWith(`${featureNumber}-`));
|
|
550
|
+
if (featureDir) {
|
|
551
|
+
return path.join(specsDir, featureDir, "tasks.md");
|
|
552
|
+
}
|
|
553
|
+
// Fallback: return best guess path
|
|
554
|
+
return path.join(specsDir, `${featureNumber}-unknown`, "tasks.md");
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Get the path to a feature's spec file
|
|
558
|
+
*/
|
|
559
|
+
getSpecFilePath(featureNumber, fileType) {
|
|
560
|
+
const specsDir = path.join(this.resolvedPath, "specs");
|
|
561
|
+
if (!existsSync(specsDir)) {
|
|
562
|
+
return path.join(specsDir, `${featureNumber}-unknown`, `${fileType}.md`);
|
|
563
|
+
}
|
|
564
|
+
const dirs = readdirSync(specsDir, { withFileTypes: true })
|
|
565
|
+
.filter((d) => d.isDirectory())
|
|
566
|
+
.map((d) => d.name);
|
|
567
|
+
const featureDir = dirs.find((dir) => dir.startsWith(`${featureNumber}-`));
|
|
568
|
+
if (featureDir) {
|
|
569
|
+
// Handle contract files
|
|
570
|
+
if (fileType.startsWith("contract-")) {
|
|
571
|
+
const contractName = fileType.replace("contract-", "");
|
|
572
|
+
return path.join(specsDir, featureDir, "contracts", `${contractName}.json`);
|
|
573
|
+
}
|
|
574
|
+
return path.join(specsDir, featureDir, `${fileType}.md`);
|
|
575
|
+
}
|
|
576
|
+
// Fallback
|
|
577
|
+
return path.join(specsDir, `${featureNumber}-unknown`, `${fileType}.md`);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get the feature directory name (e.g., "001-test-feature") from a feature number
|
|
581
|
+
*/
|
|
582
|
+
getFeatureDirName(featureNumber) {
|
|
583
|
+
const specsDir = path.join(this.resolvedPath, "specs");
|
|
584
|
+
if (!existsSync(specsDir)) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const dirs = readdirSync(specsDir, { withFileTypes: true })
|
|
588
|
+
.filter((d) => d.isDirectory())
|
|
589
|
+
.map((d) => d.name);
|
|
590
|
+
return dirs.find((dir) => dir.startsWith(`${featureNumber}-`)) || null;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Update the hash cache for a file after writing
|
|
594
|
+
*/
|
|
595
|
+
updateHashCache(filePath) {
|
|
596
|
+
try {
|
|
597
|
+
const content = readFileSync(filePath, "utf-8");
|
|
598
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
599
|
+
this.entityHashes.set(filePath, hash);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// Ignore errors updating cache
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Map sudocode status to spec-kit status string
|
|
607
|
+
*/
|
|
608
|
+
mapStatusToSpecKit(status) {
|
|
609
|
+
const statusMap = {
|
|
610
|
+
open: "Open",
|
|
611
|
+
in_progress: "In Progress",
|
|
612
|
+
blocked: "Blocked",
|
|
613
|
+
needs_review: "Needs Review",
|
|
614
|
+
closed: "Complete",
|
|
615
|
+
};
|
|
616
|
+
return statusMap[status] || "Open";
|
|
617
|
+
}
|
|
618
|
+
async getChangesSince(timestamp) {
|
|
619
|
+
console.log(`[spec-kit] getChangesSince called for: ${timestamp.toISOString()}`);
|
|
620
|
+
console.log(`[spec-kit] getChangesSince: current entityHashes count: ${this.entityHashes.size}`);
|
|
621
|
+
const changes = [];
|
|
622
|
+
const currentEntities = await this.searchEntities();
|
|
623
|
+
console.log(`[spec-kit] getChangesSince: searchEntities returned ${currentEntities.length} entities`);
|
|
624
|
+
const currentIds = new Set();
|
|
625
|
+
// Check for created and updated entities
|
|
626
|
+
for (const entity of currentEntities) {
|
|
627
|
+
currentIds.add(entity.id);
|
|
628
|
+
const newHash = this.computeEntityHash(entity);
|
|
629
|
+
const cachedHash = this.entityHashes.get(entity.id);
|
|
630
|
+
if (!cachedHash) {
|
|
631
|
+
// New entity
|
|
632
|
+
console.log(`[spec-kit] getChangesSince: NEW entity detected: ${entity.id} (type=${entity.type})`);
|
|
633
|
+
changes.push({
|
|
634
|
+
entity_id: entity.id,
|
|
635
|
+
entity_type: entity.type,
|
|
636
|
+
change_type: "created",
|
|
637
|
+
timestamp: entity.created_at || new Date().toISOString(),
|
|
638
|
+
data: entity,
|
|
639
|
+
});
|
|
640
|
+
this.entityHashes.set(entity.id, newHash);
|
|
641
|
+
}
|
|
642
|
+
else if (newHash !== cachedHash) {
|
|
643
|
+
// Updated entity
|
|
644
|
+
console.log(`[spec-kit] getChangesSince: UPDATED entity detected: ${entity.id}`);
|
|
645
|
+
changes.push({
|
|
646
|
+
entity_id: entity.id,
|
|
647
|
+
entity_type: entity.type,
|
|
648
|
+
change_type: "updated",
|
|
649
|
+
timestamp: entity.updated_at || new Date().toISOString(),
|
|
650
|
+
data: entity,
|
|
651
|
+
});
|
|
652
|
+
this.entityHashes.set(entity.id, newHash);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Check for deleted entities
|
|
656
|
+
const now = new Date().toISOString();
|
|
657
|
+
for (const [id, _hash] of this.entityHashes) {
|
|
658
|
+
if (!currentIds.has(id)) {
|
|
659
|
+
// Determine entity type from ID
|
|
660
|
+
const parsed = parseSpecId(id);
|
|
661
|
+
const entityType = parsed?.isTask ? "issue" : "spec";
|
|
662
|
+
console.log(`[spec-kit] getChangesSince: DELETED entity detected: ${id}`);
|
|
663
|
+
changes.push({
|
|
664
|
+
entity_id: id,
|
|
665
|
+
entity_type: entityType,
|
|
666
|
+
change_type: "deleted",
|
|
667
|
+
timestamp: now,
|
|
668
|
+
});
|
|
669
|
+
this.entityHashes.delete(id);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
console.log(`[spec-kit] getChangesSince found ${changes.length} changes:`, changes.map(c => `${c.entity_id}(${c.change_type})`).join(", "));
|
|
673
|
+
return changes;
|
|
674
|
+
}
|
|
675
|
+
startWatching(callback) {
|
|
676
|
+
console.log(`[spec-kit] startWatching called`);
|
|
677
|
+
if (this.watcher) {
|
|
678
|
+
console.warn(`[spec-kit] Watcher already running`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
this.watcher = new SpecKitWatcher({
|
|
682
|
+
specifyPath: this.resolvedPath,
|
|
683
|
+
specPrefix: this.options.spec_prefix || "sk",
|
|
684
|
+
taskPrefix: this.options.task_prefix || "skt",
|
|
685
|
+
includeSupportingDocs: this.options.include_supporting_docs !== false,
|
|
686
|
+
includeConstitution: this.options.include_constitution !== false,
|
|
687
|
+
});
|
|
688
|
+
this.watcher.start(callback);
|
|
689
|
+
console.log(`[spec-kit] Watcher started for ${this.resolvedPath}`);
|
|
690
|
+
}
|
|
691
|
+
stopWatching() {
|
|
692
|
+
console.log(`[spec-kit] stopWatching called`);
|
|
693
|
+
if (this.watcher) {
|
|
694
|
+
this.watcher.stop();
|
|
695
|
+
this.watcher = null;
|
|
696
|
+
console.log(`[spec-kit] Watcher stopped`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
mapToSudocode(external) {
|
|
700
|
+
console.log(`[spec-kit] mapToSudocode: external.type=${external.type}, title=${external.title}`);
|
|
701
|
+
if (external.type === "issue") {
|
|
702
|
+
const result = {
|
|
703
|
+
issue: {
|
|
704
|
+
title: external.title,
|
|
705
|
+
content: external.description || "",
|
|
706
|
+
priority: external.priority ?? 2,
|
|
707
|
+
status: this.mapStatus(external.status),
|
|
708
|
+
},
|
|
709
|
+
};
|
|
710
|
+
console.log(`[spec-kit] mapToSudocode: returning issue with status=${result.issue.status}`);
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
const result = {
|
|
714
|
+
spec: {
|
|
715
|
+
title: external.title,
|
|
716
|
+
content: external.description || "",
|
|
717
|
+
priority: external.priority ?? 2,
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
console.log(`[spec-kit] mapToSudocode: returning spec`);
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
mapFromSudocode(entity) {
|
|
724
|
+
const isIssue = "status" in entity;
|
|
725
|
+
return {
|
|
726
|
+
type: isIssue ? "issue" : "spec",
|
|
727
|
+
title: entity.title,
|
|
728
|
+
description: entity.content,
|
|
729
|
+
priority: entity.priority,
|
|
730
|
+
status: isIssue ? entity.status : undefined,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
mapStatus(externalStatus) {
|
|
734
|
+
if (!externalStatus)
|
|
735
|
+
return "open";
|
|
736
|
+
const statusMap = {
|
|
737
|
+
open: "open",
|
|
738
|
+
in_progress: "in_progress",
|
|
739
|
+
blocked: "blocked",
|
|
740
|
+
needs_review: "needs_review",
|
|
741
|
+
closed: "closed",
|
|
742
|
+
done: "closed",
|
|
743
|
+
completed: "closed",
|
|
744
|
+
};
|
|
745
|
+
return statusMap[externalStatus.toLowerCase()] || "open";
|
|
746
|
+
}
|
|
747
|
+
// ===========================================================================
|
|
748
|
+
// Entity Conversion Helpers
|
|
749
|
+
// ===========================================================================
|
|
750
|
+
/**
|
|
751
|
+
* Capture current entity state for change detection
|
|
752
|
+
*/
|
|
753
|
+
async captureEntityState() {
|
|
754
|
+
console.log(`[spec-kit] captureEntityState: capturing initial entity state...`);
|
|
755
|
+
const entities = await this.searchEntities();
|
|
756
|
+
this.entityHashes.clear();
|
|
757
|
+
for (const entity of entities) {
|
|
758
|
+
const hash = this.computeEntityHash(entity);
|
|
759
|
+
this.entityHashes.set(entity.id, hash);
|
|
760
|
+
}
|
|
761
|
+
console.log(`[spec-kit] captureEntityState: captured ${this.entityHashes.size} entities`);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Compute a hash for an entity to detect changes
|
|
765
|
+
*/
|
|
766
|
+
computeEntityHash(entity) {
|
|
767
|
+
// Create a canonical representation for hashing
|
|
768
|
+
const canonical = JSON.stringify({
|
|
769
|
+
id: entity.id,
|
|
770
|
+
type: entity.type,
|
|
771
|
+
title: entity.title,
|
|
772
|
+
description: entity.description,
|
|
773
|
+
status: entity.status,
|
|
774
|
+
priority: entity.priority,
|
|
775
|
+
});
|
|
776
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Check if an entity matches a query string
|
|
780
|
+
*/
|
|
781
|
+
matchesQuery(entity, query) {
|
|
782
|
+
if (!query)
|
|
783
|
+
return true;
|
|
784
|
+
const lowerQuery = query.toLowerCase();
|
|
785
|
+
return (entity.title.toLowerCase().includes(lowerQuery) ||
|
|
786
|
+
(entity.description?.toLowerCase().includes(lowerQuery) ?? false));
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Convert a parsed spec to ExternalEntity
|
|
790
|
+
* @param spec - Parsed spec data
|
|
791
|
+
* @param id - External entity ID
|
|
792
|
+
* @param featureDirName - Feature directory name (e.g., "001-test-feature")
|
|
793
|
+
* @param fileType - File type for title suffix (e.g., "spec", "plan")
|
|
794
|
+
*/
|
|
795
|
+
specToExternalEntity(spec, id, featureDirName, fileType = "spec") {
|
|
796
|
+
// Use directory-based title: "001-test-feature (spec)" instead of extracted title
|
|
797
|
+
const title = featureDirName
|
|
798
|
+
? `${featureDirName} (${fileType})`
|
|
799
|
+
: spec.title;
|
|
800
|
+
// Read raw file content (including frontmatter) instead of processed content
|
|
801
|
+
let rawContent = spec.content;
|
|
802
|
+
try {
|
|
803
|
+
rawContent = readFileSync(spec.filePath, "utf-8");
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// Fall back to parsed content if file read fails
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
id,
|
|
810
|
+
type: "spec",
|
|
811
|
+
title,
|
|
812
|
+
description: rawContent,
|
|
813
|
+
status: spec.status || undefined,
|
|
814
|
+
priority: this.statusToPriority(spec.status),
|
|
815
|
+
created_at: spec.createdAt?.toISOString(),
|
|
816
|
+
raw: {
|
|
817
|
+
rawTitle: spec.rawTitle,
|
|
818
|
+
featureBranch: spec.featureBranch,
|
|
819
|
+
metadata: Object.fromEntries(spec.metadata),
|
|
820
|
+
crossReferences: spec.crossReferences,
|
|
821
|
+
filePath: spec.filePath,
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Convert a parsed plan to ExternalEntity
|
|
827
|
+
* @param plan - Parsed plan data
|
|
828
|
+
* @param id - External entity ID
|
|
829
|
+
* @param featureDirName - Feature directory name (e.g., "001-test-feature")
|
|
830
|
+
*/
|
|
831
|
+
planToExternalEntity(plan, id, featureDirName, featureNumber) {
|
|
832
|
+
// Use directory-based title: "001-test-feature (plan)" instead of extracted title
|
|
833
|
+
const title = featureDirName
|
|
834
|
+
? `${featureDirName} (plan)`
|
|
835
|
+
: plan.title;
|
|
836
|
+
// Read raw file content (including frontmatter) instead of processed content
|
|
837
|
+
let rawContent = plan.content;
|
|
838
|
+
try {
|
|
839
|
+
rawContent = readFileSync(plan.filePath, "utf-8");
|
|
840
|
+
}
|
|
841
|
+
catch {
|
|
842
|
+
// Fall back to parsed content if file read fails
|
|
843
|
+
}
|
|
844
|
+
// Plan implements Spec relationship
|
|
845
|
+
const relationships = [];
|
|
846
|
+
if (featureNumber) {
|
|
847
|
+
const specId = getFeatureSpecId(featureNumber, this.options.spec_prefix || "sk");
|
|
848
|
+
relationships.push({
|
|
849
|
+
targetId: specId,
|
|
850
|
+
targetType: "spec",
|
|
851
|
+
relationshipType: "implements",
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
id,
|
|
856
|
+
type: "spec",
|
|
857
|
+
title,
|
|
858
|
+
description: rawContent,
|
|
859
|
+
status: plan.status || undefined,
|
|
860
|
+
priority: this.statusToPriority(plan.status),
|
|
861
|
+
created_at: plan.createdAt?.toISOString(),
|
|
862
|
+
relationships: relationships.length > 0 ? relationships : undefined,
|
|
863
|
+
raw: {
|
|
864
|
+
rawTitle: plan.rawTitle,
|
|
865
|
+
branch: plan.branch,
|
|
866
|
+
specReference: plan.specReference,
|
|
867
|
+
metadata: Object.fromEntries(plan.metadata),
|
|
868
|
+
crossReferences: plan.crossReferences,
|
|
869
|
+
filePath: plan.filePath,
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Convert a parsed task to ExternalEntity (as issue)
|
|
875
|
+
*/
|
|
876
|
+
taskToExternalEntity(task, featureNumber, id, tasksFilePath) {
|
|
877
|
+
const status = task.completed ? "closed" : "open";
|
|
878
|
+
// Task implements Plan relationship
|
|
879
|
+
const planId = getFeaturePlanId(featureNumber, this.options.spec_prefix || "sk");
|
|
880
|
+
const relationships = [
|
|
881
|
+
{
|
|
882
|
+
targetId: planId,
|
|
883
|
+
targetType: "spec",
|
|
884
|
+
relationshipType: "implements",
|
|
885
|
+
},
|
|
886
|
+
];
|
|
887
|
+
return {
|
|
888
|
+
id,
|
|
889
|
+
type: "issue",
|
|
890
|
+
title: `${task.taskId}: ${task.description}`,
|
|
891
|
+
description: task.description,
|
|
892
|
+
status,
|
|
893
|
+
priority: task.parallelizable ? 1 : 2, // Parallelizable tasks get higher priority
|
|
894
|
+
relationships,
|
|
895
|
+
raw: {
|
|
896
|
+
taskId: task.taskId,
|
|
897
|
+
completed: task.completed,
|
|
898
|
+
parallelizable: task.parallelizable,
|
|
899
|
+
userStory: task.userStory,
|
|
900
|
+
phase: task.phase,
|
|
901
|
+
phaseName: task.phaseName,
|
|
902
|
+
lineNumber: task.lineNumber,
|
|
903
|
+
indentLevel: task.indentLevel,
|
|
904
|
+
rawLine: task.rawLine,
|
|
905
|
+
featureNumber,
|
|
906
|
+
tasksFilePath,
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Convert a parsed supporting document to ExternalEntity
|
|
912
|
+
* @param doc - Parsed supporting document data
|
|
913
|
+
* @param id - External entity ID
|
|
914
|
+
* @param featureDirName - Feature directory name (e.g., "001-test-feature")
|
|
915
|
+
* @param featureNumber - Feature number (e.g., "001") for relationship creation
|
|
916
|
+
*/
|
|
917
|
+
supportingDocToExternalEntity(doc, id, featureDirName, featureNumber) {
|
|
918
|
+
// Use directory-based title: "001-test-feature (research)" instead of extracted title
|
|
919
|
+
const title = featureDirName
|
|
920
|
+
? `${featureDirName} (${doc.fileName})`
|
|
921
|
+
: doc.title;
|
|
922
|
+
// Read raw file content (including frontmatter) instead of processed content
|
|
923
|
+
let rawContent = doc.content;
|
|
924
|
+
try {
|
|
925
|
+
rawContent = readFileSync(doc.filePath, "utf-8");
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
// Fall back to parsed content if file read fails
|
|
929
|
+
}
|
|
930
|
+
// Supporting doc references Plan relationship
|
|
931
|
+
const relationships = [];
|
|
932
|
+
if (featureNumber) {
|
|
933
|
+
const planId = getFeaturePlanId(featureNumber, this.options.spec_prefix || "sk");
|
|
934
|
+
relationships.push({
|
|
935
|
+
targetId: planId,
|
|
936
|
+
targetType: "spec",
|
|
937
|
+
relationshipType: "references",
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
id,
|
|
942
|
+
type: "spec",
|
|
943
|
+
title,
|
|
944
|
+
description: rawContent,
|
|
945
|
+
relationships: relationships.length > 0 ? relationships : undefined,
|
|
946
|
+
raw: {
|
|
947
|
+
docType: doc.type,
|
|
948
|
+
metadata: Object.fromEntries(doc.metadata),
|
|
949
|
+
crossReferences: doc.crossReferences,
|
|
950
|
+
filePath: doc.filePath,
|
|
951
|
+
fileName: doc.fileName,
|
|
952
|
+
fileExtension: doc.fileExtension,
|
|
953
|
+
},
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Convert a parsed contract to ExternalEntity
|
|
958
|
+
* @param contract - Parsed contract data
|
|
959
|
+
* @param id - External entity ID
|
|
960
|
+
* @param featureDirName - Feature directory name (e.g., "001-test-feature")
|
|
961
|
+
* @param featureNumber - Feature number (e.g., "001") for relationship creation
|
|
962
|
+
*/
|
|
963
|
+
contractToExternalEntity(contract, id, featureDirName, featureNumber) {
|
|
964
|
+
// Use directory-based title: "001-test-feature (contract-api)" instead of contract name
|
|
965
|
+
const title = featureDirName
|
|
966
|
+
? `${featureDirName} (contract-${contract.name})`
|
|
967
|
+
: contract.name;
|
|
968
|
+
// Contract references Plan relationship
|
|
969
|
+
const relationships = [];
|
|
970
|
+
if (featureNumber) {
|
|
971
|
+
const planId = getFeaturePlanId(featureNumber, this.options.spec_prefix || "sk");
|
|
972
|
+
relationships.push({
|
|
973
|
+
targetId: planId,
|
|
974
|
+
targetType: "spec",
|
|
975
|
+
relationshipType: "references",
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
id,
|
|
980
|
+
type: "spec",
|
|
981
|
+
title,
|
|
982
|
+
description: JSON.stringify(contract.data, null, 2),
|
|
983
|
+
relationships: relationships.length > 0 ? relationships : undefined,
|
|
984
|
+
raw: {
|
|
985
|
+
contractName: contract.name,
|
|
986
|
+
format: contract.format,
|
|
987
|
+
data: contract.data,
|
|
988
|
+
filePath: contract.filePath,
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Map spec-kit status to sudocode priority
|
|
994
|
+
* Draft/Open -> lower priority, Complete -> normal
|
|
995
|
+
*/
|
|
996
|
+
statusToPriority(status) {
|
|
997
|
+
if (!status)
|
|
998
|
+
return 2;
|
|
999
|
+
const statusLower = status.toLowerCase();
|
|
1000
|
+
if (statusLower === "draft" || statusLower === "open") {
|
|
1001
|
+
return 3; // Lower priority for drafts
|
|
1002
|
+
}
|
|
1003
|
+
if (statusLower === "in progress" ||
|
|
1004
|
+
statusLower === "in_progress" ||
|
|
1005
|
+
statusLower === "active") {
|
|
1006
|
+
return 1; // Higher priority for in-progress
|
|
1007
|
+
}
|
|
1008
|
+
if (statusLower === "complete" ||
|
|
1009
|
+
statusLower === "completed" ||
|
|
1010
|
+
statusLower === "done") {
|
|
1011
|
+
return 2; // Normal priority for complete
|
|
1012
|
+
}
|
|
1013
|
+
return 2; // Default
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
export default specKitPlugin;
|
|
1017
|
+
//# sourceMappingURL=index.js.map
|