lean-spec 0.2.6-dev.20251126062137 → 0.2.6-dev.20251126063304

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.
@@ -648,10 +648,144 @@ function parseCustomFieldOptions(fieldOptions) {
648
648
  }
649
649
  return customFields;
650
650
  }
651
+ function linkCommand() {
652
+ return new Command("link").description("Add relationships between specs (depends_on, related)").argument("<spec>", "Spec to update").option("--depends-on <specs>", "Add dependencies (comma-separated spec numbers or names)").option("--related <specs>", "Add related specs (comma-separated spec numbers or names)").action(async (specPath, options) => {
653
+ if (!options.dependsOn && !options.related) {
654
+ console.error("Error: At least one relationship type required (--depends-on or --related)");
655
+ process.exit(1);
656
+ }
657
+ await linkSpec(specPath, options);
658
+ });
659
+ }
660
+ async function linkSpec(specPath, options) {
661
+ await autoCheckIfEnabled();
662
+ const config = await loadConfig();
663
+ const cwd = process.cwd();
664
+ const specsDir = path15.join(cwd, config.specsDir);
665
+ const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
666
+ if (!resolvedPath) {
667
+ throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
668
+ }
669
+ const specFile = await getSpecFile(resolvedPath, config.structure.defaultFile);
670
+ if (!specFile) {
671
+ throw new Error(`No spec file found in: ${sanitizeUserInput(specPath)}`);
672
+ }
673
+ const allSpecs = await loadAllSpecs({ includeArchived: true });
674
+ const specMap = new Map(allSpecs.map((s) => [s.path, s]));
675
+ const dependsOnSpecs = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
676
+ const relatedSpecs = options.related ? options.related.split(",").map((s) => s.trim()) : [];
677
+ const targetSpecName = path15.basename(resolvedPath);
678
+ const allRelationshipSpecs = [...dependsOnSpecs, ...relatedSpecs];
679
+ const resolvedRelationships = /* @__PURE__ */ new Map();
680
+ for (const relSpec of allRelationshipSpecs) {
681
+ if (relSpec === targetSpecName || relSpec === specPath) {
682
+ throw new Error(`Cannot link spec to itself: ${sanitizeUserInput(relSpec)}`);
683
+ }
684
+ const relResolvedPath = await resolveSpecPath(relSpec, cwd, specsDir);
685
+ if (!relResolvedPath) {
686
+ throw new Error(`Spec not found: ${sanitizeUserInput(relSpec)}`);
687
+ }
688
+ if (relResolvedPath === resolvedPath) {
689
+ throw new Error(`Cannot link spec to itself: ${sanitizeUserInput(relSpec)}`);
690
+ }
691
+ const relSpecName = path15.basename(relResolvedPath);
692
+ resolvedRelationships.set(relSpec, relSpecName);
693
+ }
694
+ const { parseFrontmatter: parseFrontmatter2 } = await import('./frontmatter-R2DANL5X.js');
695
+ const currentFrontmatter = await parseFrontmatter2(specFile);
696
+ const currentDependsOn = currentFrontmatter?.depends_on || [];
697
+ const currentRelated = currentFrontmatter?.related || [];
698
+ const updates = {};
699
+ if (dependsOnSpecs.length > 0) {
700
+ const newDependsOn = [...currentDependsOn];
701
+ let added = 0;
702
+ for (const spec of dependsOnSpecs) {
703
+ const resolvedName = resolvedRelationships.get(spec);
704
+ if (resolvedName && !newDependsOn.includes(resolvedName)) {
705
+ newDependsOn.push(resolvedName);
706
+ added++;
707
+ }
708
+ }
709
+ updates.depends_on = newDependsOn;
710
+ if (added === 0) {
711
+ console.log(chalk19.gray(`\u2139 Dependencies already exist, no changes made`));
712
+ }
713
+ }
714
+ if (relatedSpecs.length > 0) {
715
+ const newRelated = [...currentRelated];
716
+ let added = 0;
717
+ const bidirectionalUpdates = [];
718
+ for (const spec of relatedSpecs) {
719
+ const resolvedName = resolvedRelationships.get(spec);
720
+ if (resolvedName && !newRelated.includes(resolvedName)) {
721
+ newRelated.push(resolvedName);
722
+ added++;
723
+ bidirectionalUpdates.push(resolvedName);
724
+ }
725
+ }
726
+ updates.related = newRelated;
727
+ for (const relSpecName of bidirectionalUpdates) {
728
+ const relSpecPath = await resolveSpecPath(relSpecName, cwd, specsDir);
729
+ if (relSpecPath) {
730
+ const relSpecFile = await getSpecFile(relSpecPath, config.structure.defaultFile);
731
+ if (relSpecFile) {
732
+ const relFrontmatter = await parseFrontmatter2(relSpecFile);
733
+ const relCurrentRelated = relFrontmatter?.related || [];
734
+ if (!relCurrentRelated.includes(targetSpecName)) {
735
+ await updateFrontmatter(relSpecFile, {
736
+ related: [...relCurrentRelated, targetSpecName]
737
+ });
738
+ console.log(chalk19.gray(` Updated: ${sanitizeUserInput(relSpecName)} (bidirectional)`));
739
+ }
740
+ }
741
+ }
742
+ }
743
+ if (added === 0) {
744
+ console.log(chalk19.gray(`\u2139 Related specs already exist, no changes made`));
745
+ }
746
+ }
747
+ if (updates.depends_on && updates.depends_on.length > 0) {
748
+ const cycles = detectCycles(targetSpecName, updates.depends_on, specMap);
749
+ if (cycles.length > 0) {
750
+ console.log(chalk19.yellow(`\u26A0\uFE0F Dependency cycle detected: ${cycles.join(" \u2192 ")}`));
751
+ }
752
+ }
753
+ await updateFrontmatter(specFile, updates);
754
+ const updatedFields = [];
755
+ if (dependsOnSpecs.length > 0) {
756
+ updatedFields.push(`depends_on: ${dependsOnSpecs.join(", ")}`);
757
+ }
758
+ if (relatedSpecs.length > 0) {
759
+ updatedFields.push(`related: ${relatedSpecs.join(", ")}`);
760
+ }
761
+ console.log(chalk19.green(`\u2713 Added relationships: ${updatedFields.join(", ")}`));
762
+ console.log(chalk19.gray(` Updated: ${sanitizeUserInput(path15.relative(cwd, resolvedPath))}`));
763
+ }
764
+ function detectCycles(startSpec, dependsOn, specMap, visited = /* @__PURE__ */ new Set(), path31 = []) {
765
+ if (visited.has(startSpec)) {
766
+ const cycleStart = path31.indexOf(startSpec);
767
+ if (cycleStart !== -1) {
768
+ return [...path31.slice(cycleStart), startSpec];
769
+ }
770
+ return [];
771
+ }
772
+ visited.add(startSpec);
773
+ path31.push(startSpec);
774
+ for (const dep of dependsOn) {
775
+ const depSpec = specMap.get(dep);
776
+ if (depSpec && depSpec.frontmatter.depends_on) {
777
+ const cycle = detectCycles(dep, depSpec.frontmatter.depends_on, specMap, new Set(visited), [...path31]);
778
+ if (cycle.length > 0) {
779
+ return cycle;
780
+ }
781
+ }
782
+ }
783
+ return [];
784
+ }
651
785
 
652
786
  // src/commands/create.ts
653
787
  function createCommand() {
654
- return new Command("create").description("Create new spec in folder structure").argument("<name>", "Name of the spec").option("--title <title>", "Set custom title").option("--description <desc>", "Set initial description").option("--tags <tags>", "Set tags (comma-separated)").option("--priority <priority>", "Set priority (low, medium, high, critical)").option("--assignee <name>", "Set assignee").option("--template <template>", "Use a specific template").option("--field <name=value...>", "Set custom field (can specify multiple)").option("--no-prefix", "Skip date prefix even if configured").action(async (name, options) => {
788
+ return new Command("create").description("Create new spec in folder structure").argument("<name>", "Name of the spec").option("--title <title>", "Set custom title").option("--description <desc>", "Set initial description").option("--tags <tags>", "Set tags (comma-separated)").option("--priority <priority>", "Set priority (low, medium, high, critical)").option("--assignee <name>", "Set assignee").option("--template <template>", "Use a specific template").option("--field <name=value...>", "Set custom field (can specify multiple)").option("--no-prefix", "Skip date prefix even if configured").option("--depends-on <specs>", "Add dependencies (comma-separated spec numbers or names)").option("--related <specs>", "Add related specs (comma-separated spec numbers or names)").action(async (name, options) => {
655
789
  const customFields = parseCustomFieldOptions(options.field);
656
790
  const createOptions = {
657
791
  title: options.title,
@@ -661,7 +795,9 @@ function createCommand() {
661
795
  assignee: options.assignee,
662
796
  template: options.template,
663
797
  customFields: Object.keys(customFields).length > 0 ? customFields : void 0,
664
- noPrefix: options.prefix === false
798
+ noPrefix: options.prefix === false,
799
+ dependsOn: options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : void 0,
800
+ related: options.related ? options.related.split(",").map((s) => s.trim()) : void 0
665
801
  };
666
802
  await createSpec(name, createOptions);
667
803
  });
@@ -804,6 +940,18 @@ ${options.description}`
804
940
  console.log(chalk19.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
805
941
  console.log(chalk19.gray(` Edit: ${sanitizeUserInput(specFile)}`));
806
942
  }
943
+ const hasRelationships = options.dependsOn && options.dependsOn.length > 0 || options.related && options.related.length > 0;
944
+ if (hasRelationships) {
945
+ const newSpecName = path15.basename(specDir);
946
+ try {
947
+ await linkSpec(newSpecName, {
948
+ dependsOn: options.dependsOn?.join(","),
949
+ related: options.related?.join(",")
950
+ });
951
+ } catch (error) {
952
+ console.log(chalk19.yellow(`\u26A0\uFE0F Warning: Failed to add relationships: ${error.message}`));
953
+ }
954
+ }
807
955
  await autoCheckIfEnabled();
808
956
  }
809
957
  function archiveCommand() {
@@ -1162,140 +1310,6 @@ async function updateSpec(specPath, updates, options = {}) {
1162
1310
  }
1163
1311
  console.log(chalk19.gray(` Fields: ${updatedFields.join(", ")}`));
1164
1312
  }
1165
- function linkCommand() {
1166
- return new Command("link").description("Add relationships between specs (depends_on, related)").argument("<spec>", "Spec to update").option("--depends-on <specs>", "Add dependencies (comma-separated spec numbers or names)").option("--related <specs>", "Add related specs (comma-separated spec numbers or names)").action(async (specPath, options) => {
1167
- if (!options.dependsOn && !options.related) {
1168
- console.error("Error: At least one relationship type required (--depends-on or --related)");
1169
- process.exit(1);
1170
- }
1171
- await linkSpec(specPath, options);
1172
- });
1173
- }
1174
- async function linkSpec(specPath, options) {
1175
- await autoCheckIfEnabled();
1176
- const config = await loadConfig();
1177
- const cwd = process.cwd();
1178
- const specsDir = path15.join(cwd, config.specsDir);
1179
- const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
1180
- if (!resolvedPath) {
1181
- throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
1182
- }
1183
- const specFile = await getSpecFile(resolvedPath, config.structure.defaultFile);
1184
- if (!specFile) {
1185
- throw new Error(`No spec file found in: ${sanitizeUserInput(specPath)}`);
1186
- }
1187
- const allSpecs = await loadAllSpecs({ includeArchived: true });
1188
- const specMap = new Map(allSpecs.map((s) => [s.path, s]));
1189
- const dependsOnSpecs = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
1190
- const relatedSpecs = options.related ? options.related.split(",").map((s) => s.trim()) : [];
1191
- const targetSpecName = path15.basename(resolvedPath);
1192
- const allRelationshipSpecs = [...dependsOnSpecs, ...relatedSpecs];
1193
- const resolvedRelationships = /* @__PURE__ */ new Map();
1194
- for (const relSpec of allRelationshipSpecs) {
1195
- if (relSpec === targetSpecName || relSpec === specPath) {
1196
- throw new Error(`Cannot link spec to itself: ${sanitizeUserInput(relSpec)}`);
1197
- }
1198
- const relResolvedPath = await resolveSpecPath(relSpec, cwd, specsDir);
1199
- if (!relResolvedPath) {
1200
- throw new Error(`Spec not found: ${sanitizeUserInput(relSpec)}`);
1201
- }
1202
- if (relResolvedPath === resolvedPath) {
1203
- throw new Error(`Cannot link spec to itself: ${sanitizeUserInput(relSpec)}`);
1204
- }
1205
- const relSpecName = path15.basename(relResolvedPath);
1206
- resolvedRelationships.set(relSpec, relSpecName);
1207
- }
1208
- const { parseFrontmatter: parseFrontmatter2 } = await import('./frontmatter-R2DANL5X.js');
1209
- const currentFrontmatter = await parseFrontmatter2(specFile);
1210
- const currentDependsOn = currentFrontmatter?.depends_on || [];
1211
- const currentRelated = currentFrontmatter?.related || [];
1212
- const updates = {};
1213
- if (dependsOnSpecs.length > 0) {
1214
- const newDependsOn = [...currentDependsOn];
1215
- let added = 0;
1216
- for (const spec of dependsOnSpecs) {
1217
- const resolvedName = resolvedRelationships.get(spec);
1218
- if (resolvedName && !newDependsOn.includes(resolvedName)) {
1219
- newDependsOn.push(resolvedName);
1220
- added++;
1221
- }
1222
- }
1223
- updates.depends_on = newDependsOn;
1224
- if (added === 0) {
1225
- console.log(chalk19.gray(`\u2139 Dependencies already exist, no changes made`));
1226
- }
1227
- }
1228
- if (relatedSpecs.length > 0) {
1229
- const newRelated = [...currentRelated];
1230
- let added = 0;
1231
- const bidirectionalUpdates = [];
1232
- for (const spec of relatedSpecs) {
1233
- const resolvedName = resolvedRelationships.get(spec);
1234
- if (resolvedName && !newRelated.includes(resolvedName)) {
1235
- newRelated.push(resolvedName);
1236
- added++;
1237
- bidirectionalUpdates.push(resolvedName);
1238
- }
1239
- }
1240
- updates.related = newRelated;
1241
- for (const relSpecName of bidirectionalUpdates) {
1242
- const relSpecPath = await resolveSpecPath(relSpecName, cwd, specsDir);
1243
- if (relSpecPath) {
1244
- const relSpecFile = await getSpecFile(relSpecPath, config.structure.defaultFile);
1245
- if (relSpecFile) {
1246
- const relFrontmatter = await parseFrontmatter2(relSpecFile);
1247
- const relCurrentRelated = relFrontmatter?.related || [];
1248
- if (!relCurrentRelated.includes(targetSpecName)) {
1249
- await updateFrontmatter(relSpecFile, {
1250
- related: [...relCurrentRelated, targetSpecName]
1251
- });
1252
- console.log(chalk19.gray(` Updated: ${sanitizeUserInput(relSpecName)} (bidirectional)`));
1253
- }
1254
- }
1255
- }
1256
- }
1257
- if (added === 0) {
1258
- console.log(chalk19.gray(`\u2139 Related specs already exist, no changes made`));
1259
- }
1260
- }
1261
- if (updates.depends_on && updates.depends_on.length > 0) {
1262
- const cycles = detectCycles(targetSpecName, updates.depends_on, specMap);
1263
- if (cycles.length > 0) {
1264
- console.log(chalk19.yellow(`\u26A0\uFE0F Dependency cycle detected: ${cycles.join(" \u2192 ")}`));
1265
- }
1266
- }
1267
- await updateFrontmatter(specFile, updates);
1268
- const updatedFields = [];
1269
- if (dependsOnSpecs.length > 0) {
1270
- updatedFields.push(`depends_on: ${dependsOnSpecs.join(", ")}`);
1271
- }
1272
- if (relatedSpecs.length > 0) {
1273
- updatedFields.push(`related: ${relatedSpecs.join(", ")}`);
1274
- }
1275
- console.log(chalk19.green(`\u2713 Added relationships: ${updatedFields.join(", ")}`));
1276
- console.log(chalk19.gray(` Updated: ${sanitizeUserInput(path15.relative(cwd, resolvedPath))}`));
1277
- }
1278
- function detectCycles(startSpec, dependsOn, specMap, visited = /* @__PURE__ */ new Set(), path31 = []) {
1279
- if (visited.has(startSpec)) {
1280
- const cycleStart = path31.indexOf(startSpec);
1281
- if (cycleStart !== -1) {
1282
- return [...path31.slice(cycleStart), startSpec];
1283
- }
1284
- return [];
1285
- }
1286
- visited.add(startSpec);
1287
- path31.push(startSpec);
1288
- for (const dep of dependsOn) {
1289
- const depSpec = specMap.get(dep);
1290
- if (depSpec && depSpec.frontmatter.depends_on) {
1291
- const cycle = detectCycles(dep, depSpec.frontmatter.depends_on, specMap, new Set(visited), [...path31]);
1292
- if (cycle.length > 0) {
1293
- return cycle;
1294
- }
1295
- }
1296
- }
1297
- return [];
1298
- }
1299
1313
  function unlinkCommand() {
1300
1314
  return new Command("unlink").description("Remove relationships between specs (depends_on, related)").argument("<spec>", "Spec to update").option("--depends-on [specs]", "Remove dependencies (comma-separated spec numbers or names, or use with --all)").option("--related [specs]", "Remove related specs (comma-separated spec numbers or names, or use with --all)").option("--all", "Remove all relationships of the specified type(s)").action(async (specPath, options) => {
1301
1315
  if (!options.dependsOn && !options.related) {
@@ -8323,7 +8337,9 @@ function createTool() {
8323
8337
  tags: z.array(z.string()).optional().describe('Tags to categorize the spec (e.g., ["api", "frontend", "v2.0"]).'),
8324
8338
  priority: z.enum(["low", "medium", "high", "critical"]).optional().describe('Priority level for the spec. Defaults to "medium" if not specified.'),
8325
8339
  assignee: z.string().optional().describe("Person responsible for this spec."),
8326
- template: z.string().optional().describe('Template name to use (e.g., "minimal", "enterprise"). Uses default template if omitted.')
8340
+ template: z.string().optional().describe('Template name to use (e.g., "minimal", "enterprise"). Uses default template if omitted.'),
8341
+ dependsOn: z.array(z.string()).optional().describe('Specs this depends on (e.g., ["045-api-design", "046-database"]). Creates upstream dependencies.'),
8342
+ related: z.array(z.string()).optional().describe('Related specs (e.g., ["047-frontend"]). Creates bidirectional relationships.')
8327
8343
  },
8328
8344
  outputSchema: {
8329
8345
  success: z.boolean(),
@@ -8345,7 +8361,9 @@ function createTool() {
8345
8361
  tags: input.tags,
8346
8362
  priority: input.priority,
8347
8363
  assignee: input.assignee,
8348
- template: input.template
8364
+ template: input.template,
8365
+ dependsOn: input.dependsOn,
8366
+ related: input.related
8349
8367
  });
8350
8368
  const output = {
8351
8369
  success: true,
@@ -8704,11 +8722,24 @@ function searchTool() {
8704
8722
  "search",
8705
8723
  {
8706
8724
  title: "Search Specs",
8707
- description: "Intelligent relevance-ranked search across all specification content. Uses field-weighted scoring (title > tags > description > content) to return the most relevant specs. Returns matching specs with relevance scores, highlighted excerpts, and metadata.",
8725
+ description: `Intelligent relevance-ranked search across all specification content. Uses field-weighted scoring (title > tags > description > content) to return the most relevant specs.
8726
+
8727
+ **Query Formulation Tips:**
8728
+ - Use 2-4 specific terms for best results (e.g., "search ranking" not "AI agent integration coding agent orchestration")
8729
+ - All terms must appear in the SAME field/line to match - keep queries focused
8730
+ - Prefer nouns and technical terms over common words
8731
+ - Use filters (status, tags, priority) to narrow scope instead of adding more search terms
8732
+
8733
+ **Examples:**
8734
+ - Good: "search ranking" or "token validation"
8735
+ - Good: "api" with tags filter ["integration"]
8736
+ - Poor: "AI agent integration coding agent orchestration" (too many terms, unlikely all in one line)
8737
+
8738
+ Returns matching specs with relevance scores, highlighted excerpts, and metadata.`,
8708
8739
  inputSchema: {
8709
- query: z.string().describe("Search term or phrase to find in spec content. Multiple terms are combined with AND logic. Searches across titles, tags, descriptions, and body text with intelligent relevance ranking."),
8740
+ query: z.string().describe("Search term or phrase. Use 2-4 specific terms. All terms must appear in the same field/line to match. For broad concepts, use fewer terms + filters instead of long queries."),
8710
8741
  status: z.enum(["planned", "in-progress", "complete", "archived"]).optional().describe("Limit search to specs with this status."),
8711
- tags: z.array(z.string()).optional().describe("Limit search to specs with these tags."),
8742
+ tags: z.array(z.string()).optional().describe("Limit search to specs with these tags. Use this to narrow scope instead of adding more search terms."),
8712
8743
  priority: z.enum(["low", "medium", "high", "critical"]).optional().describe("Limit search to specs with this priority.")
8713
8744
  },
8714
8745
  outputSchema: {
@@ -9557,5 +9588,5 @@ if (import.meta.url === `file://${process.argv[1]}`) {
9557
9588
  }
9558
9589
 
9559
9590
  export { analyzeCommand, archiveCommand, backfillCommand, boardCommand, checkCommand, compactCommand, createCommand, createMcpServer, depsCommand, examplesCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, validateCommand, viewCommand };
9560
- //# sourceMappingURL=chunk-YPVK4J5Z.js.map
9561
- //# sourceMappingURL=chunk-YPVK4J5Z.js.map
9591
+ //# sourceMappingURL=chunk-KJ2FT3JZ.js.map
9592
+ //# sourceMappingURL=chunk-KJ2FT3JZ.js.map