@chrisdudek/yg 2.1.0 → 2.3.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.
package/README.md CHANGED
@@ -51,6 +51,7 @@ yg build-context --node orders/order-service
51
51
  - `yg owner --file <path>` — Find which graph node owns a source file
52
52
  - `yg deps --node <path>` — Forward dependency tree and materialization order
53
53
  - `yg impact --node <path> [--simulate]` — Reverse dependencies and context impact
54
+ - `yg select --task <description> [--limit <n>]` — Find graph nodes relevant to a task
54
55
  - `yg aspects` — List aspects with metadata (YAML output)
55
56
  - `yg flows` — List flows with metadata (YAML output)
56
57
 
package/dist/bin.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/cli/init.ts
7
- import { mkdir as mkdir2, writeFile as writeFile3, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
7
+ import { mkdir as mkdir2, writeFile as writeFile4, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
8
8
  import path4 from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import { readFileSync } from "fs";
@@ -68,6 +68,11 @@ Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the r
68
68
 
69
69
  \`\`\`
70
70
  BEFORE reading, researching, planning, OR modifying ANY mapped file:
71
+ 0. Don't know which file or node to start from? Run
72
+ yg select --task "<your goal>" to find relevant nodes via keyword
73
+ matching against graph artifacts. If a semantic search tool is also
74
+ available, use it for richer intent matching. Use the results
75
+ to identify relevant nodes, then proceed to step 1.
71
76
  1. yg owner --file <path>
72
77
  2. Choose the right graph tool for your task:
73
78
  - Understanding how/why it works \u2192 yg build-context --node <owner>
@@ -142,6 +147,7 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
142
147
  | "I'm brainstorming, not implementing" | Brainstorming about mapped code needs graph context |
143
148
  | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
144
149
  | "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
150
+ | "I'll grep the codebase to find where to start" | Run \`yg select --task\` first \u2014 it matches your intent against graph artifacts. Then \`yg owner\` on results. |
145
151
 
146
152
  ### Failure States
147
153
 
@@ -448,6 +454,8 @@ yg build-context --node <path> Assemble context package for this node.
448
454
  yg tree [--root <path>] [--depth N] Print graph structure.
449
455
  yg aspects List aspects with metadata (YAML output).
450
456
  yg flows List flows with metadata (YAML output).
457
+ yg select --task <description> [--limit <n>]
458
+ Find graph nodes relevant to a task description.
451
459
  yg deps --node <path> [--depth N] [--type structural|event|all]
452
460
  Show dependencies.
453
461
  yg impact --node <path> --simulate Simulate blast radius of a planned change.
@@ -717,7 +725,7 @@ function escapeRegex(s) {
717
725
  }
718
726
 
719
727
  // src/core/migrator.ts
720
- import { readFile as readFile2, access } from "fs/promises";
728
+ import { readFile as readFile2, writeFile as writeFile2, access } from "fs/promises";
721
729
  import path2 from "path";
722
730
  import { parse as parseYaml } from "yaml";
723
731
  import { gt, valid, compare } from "semver";
@@ -755,9 +763,16 @@ async function runMigrations(currentVersion, migrations, yggRoot) {
755
763
  }
756
764
  return results;
757
765
  }
766
+ async function updateConfigVersion(yggRoot, version) {
767
+ const configPath = path2.join(yggRoot, "yg-config.yaml");
768
+ const content = await readFile2(configPath, "utf-8");
769
+ const updated = content.match(/^version:\s/m) ? content.replace(/^version:\s.*$/m, `version: "${version}"`) : `version: "${version}"
770
+ ` + content;
771
+ await writeFile2(configPath, updated, "utf-8");
772
+ }
758
773
 
759
774
  // src/migrations/to-2.0.0.ts
760
- import { readFile as readFile3, writeFile as writeFile2, rename, readdir, rm, stat } from "fs/promises";
775
+ import { readFile as readFile3, writeFile as writeFile3, rename, readdir, rm, stat } from "fs/promises";
761
776
  import path3 from "path";
762
777
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
763
778
  var KNOWN_TYPE_DESCRIPTIONS = {
@@ -843,7 +858,7 @@ async function migrateTo2(yggRoot) {
843
858
  if (raw.quality) {
844
859
  newConfig.quality = raw.quality;
845
860
  }
846
- await writeFile2(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
861
+ await writeFile3(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
847
862
  actions.push("Updated config: version, artifacts, removed stack/standards");
848
863
  const modelDir = path3.join(yggRoot, "model");
849
864
  if (await fileExists(modelDir)) {
@@ -907,8 +922,8 @@ async function migrateStackStandards(yggRoot, stack, standards, actions) {
907
922
  const rootNodeOldPath = path3.join(modelDir, "node.yaml");
908
923
  const hasRootNode = await fileExists(rootNodeYgPath) || await fileExists(rootNodeOldPath);
909
924
  if (!hasRootNode) {
910
- await writeFile2(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
911
- await writeFile2(path3.join(modelDir, "responsibility.md"), "TBD\n", "utf-8");
925
+ await writeFile3(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
926
+ await writeFile3(path3.join(modelDir, "responsibility.md"), "TBD\n", "utf-8");
912
927
  actions.push("Created root node in model/ for stack/standards migration");
913
928
  }
914
929
  const internalsPath = path3.join(modelDir, "internals.md");
@@ -919,7 +934,7 @@ async function migrateStackStandards(yggRoot, stack, standards, actions) {
919
934
  }
920
935
  const markerLine = MIGRATION_MARKER + "\n";
921
936
  const newContent = existingInternals ? existingInternals.trimEnd() + "\n\n" + markerLine + lines.join("\n") : markerLine + lines.join("\n");
922
- await writeFile2(internalsPath, newContent, "utf-8");
937
+ await writeFile3(internalsPath, newContent, "utf-8");
923
938
  actions.push("Migrated stack/standards to model/internals.md");
924
939
  }
925
940
  async function renameFilesRecursively(dir, oldName, newName, actions) {
@@ -984,7 +999,7 @@ async function transformSingleNode(filePath, actions, warnings) {
984
999
  changed = true;
985
1000
  }
986
1001
  if (changed) {
987
- await writeFile2(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
1002
+ await writeFile3(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
988
1003
  actions.push(`Transformed ${path3.basename(path3.dirname(filePath))}/yg-node.yaml`);
989
1004
  }
990
1005
  }
@@ -1020,7 +1035,7 @@ async function refreshSchemas(yggRoot) {
1020
1035
  for (const file of schemaFiles) {
1021
1036
  const srcPath = path4.join(graphSchemasDir, file);
1022
1037
  const content = await readFile4(srcPath, "utf-8");
1023
- await writeFile3(path4.join(schemasDir, file), content, "utf-8");
1038
+ await writeFile4(path4.join(schemasDir, file), content, "utf-8");
1024
1039
  }
1025
1040
  } catch {
1026
1041
  }
@@ -1091,6 +1106,7 @@ function registerInitCommand(program2) {
1091
1106
  if (results.length > 0) {
1092
1107
  process.stdout.write("\n");
1093
1108
  }
1109
+ await updateConfigVersion(yggRoot, cliVersion);
1094
1110
  }
1095
1111
  await refreshSchemas(yggRoot);
1096
1112
  const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
@@ -1111,7 +1127,7 @@ function registerInitCommand(program2) {
1111
1127
  for (const file of schemaFiles) {
1112
1128
  const srcPath = path4.join(graphSchemasDir, file);
1113
1129
  const content = await readFile4(srcPath, "utf-8");
1114
- await writeFile3(path4.join(schemasDir, file), content, "utf-8");
1130
+ await writeFile4(path4.join(schemasDir, file), content, "utf-8");
1115
1131
  }
1116
1132
  } catch (err) {
1117
1133
  process.stderr.write(
@@ -1119,8 +1135,8 @@ function registerInitCommand(program2) {
1119
1135
  `
1120
1136
  );
1121
1137
  }
1122
- await writeFile3(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
1123
- await writeFile3(path4.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
1138
+ await writeFile4(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
1139
+ await writeFile4(path4.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
1124
1140
  const rulesPath = await installRulesForPlatform(projectRoot, platform);
1125
1141
  process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
1126
1142
  process.stdout.write("Created:\n");
@@ -2965,7 +2981,7 @@ ${errors.length} errors, ${warnings.length} warnings.
2965
2981
  import chalk2 from "chalk";
2966
2982
 
2967
2983
  // src/io/drift-state-store.ts
2968
- import { readFile as readFile14, writeFile as writeFile4, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
2984
+ import { readFile as readFile14, writeFile as writeFile5, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
2969
2985
  import path12 from "path";
2970
2986
  import { parse as yamlParse } from "yaml";
2971
2987
  var DRIFT_STATE_DIR = ".drift-state";
@@ -3023,7 +3039,7 @@ async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3023
3039
  const filePath = nodeStatePath(yggRoot, nodePath);
3024
3040
  await mkdir3(path12.dirname(filePath), { recursive: true });
3025
3041
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3026
- await writeFile4(filePath, content, "utf-8");
3042
+ await writeFile5(filePath, content, "utf-8");
3027
3043
  }
3028
3044
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3029
3045
  const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
@@ -3836,7 +3852,7 @@ function registerOwnerCommand(program2) {
3836
3852
  `);
3837
3853
  if (result.direct === false && result.mappingPath) {
3838
3854
  process.stdout.write(
3839
- ` Plik nie ma w\u0142asnego mapowania; kontekst pochodzi z nadrz\u0119dnego katalogu ${result.mappingPath}. U\u017Cyj: yg build-context --node ${result.nodePath}
3855
+ ` File has no direct mapping; context comes from ancestor directory ${result.mappingPath}. Use: yg build-context --node ${result.nodePath}
3840
3856
  `
3841
3857
  );
3842
3858
  }
@@ -4504,6 +4520,199 @@ function registerPreflightCommand(program2) {
4504
4520
  });
4505
4521
  }
4506
4522
 
4523
+ // src/cli/select.ts
4524
+ import { stringify as yamlStringify3 } from "yaml";
4525
+
4526
+ // src/utils/tokenizer.ts
4527
+ var STOP_WORDS = /* @__PURE__ */ new Set([
4528
+ "a",
4529
+ "an",
4530
+ "the",
4531
+ "is",
4532
+ "are",
4533
+ "was",
4534
+ "were",
4535
+ "be",
4536
+ "been",
4537
+ "being",
4538
+ "have",
4539
+ "has",
4540
+ "had",
4541
+ "do",
4542
+ "does",
4543
+ "did",
4544
+ "will",
4545
+ "would",
4546
+ "shall",
4547
+ "should",
4548
+ "may",
4549
+ "might",
4550
+ "must",
4551
+ "can",
4552
+ "could",
4553
+ "to",
4554
+ "of",
4555
+ "in",
4556
+ "for",
4557
+ "on",
4558
+ "with",
4559
+ "at",
4560
+ "by",
4561
+ "from",
4562
+ "as",
4563
+ "into",
4564
+ "through",
4565
+ "during",
4566
+ "this",
4567
+ "that",
4568
+ "it",
4569
+ "its",
4570
+ "or",
4571
+ "and",
4572
+ "but",
4573
+ "if",
4574
+ "not",
4575
+ "no",
4576
+ "so",
4577
+ "up",
4578
+ "out",
4579
+ "about",
4580
+ "which",
4581
+ "what",
4582
+ "when",
4583
+ "where",
4584
+ "who",
4585
+ "how",
4586
+ "all",
4587
+ "each",
4588
+ "every",
4589
+ "both",
4590
+ "few",
4591
+ "more",
4592
+ "some",
4593
+ "any",
4594
+ "other",
4595
+ "than",
4596
+ "too",
4597
+ "very",
4598
+ "just",
4599
+ "also"
4600
+ ]);
4601
+ function tokenize(text) {
4602
+ const tokens = text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2 && !STOP_WORDS.has(t));
4603
+ return [...new Set(tokens)];
4604
+ }
4605
+
4606
+ // src/core/node-selector.ts
4607
+ function countHits(tokens, text) {
4608
+ const lower = text.toLowerCase();
4609
+ return tokens.filter((t) => lower.includes(t)).length;
4610
+ }
4611
+ function collectAspectContent(graphNode, aspects) {
4612
+ const aspectIds = (graphNode.meta.aspects ?? []).map((a) => a.aspect);
4613
+ if (aspectIds.length === 0) return "";
4614
+ const aspectMap = new Map(aspects.map((a) => [a.id, a]));
4615
+ const parts = [];
4616
+ for (const id of aspectIds) {
4617
+ const aspect = aspectMap.get(id);
4618
+ if (aspect) {
4619
+ for (const artifact of aspect.artifacts) {
4620
+ parts.push(artifact.content);
4621
+ }
4622
+ }
4623
+ }
4624
+ return parts.join(" ");
4625
+ }
4626
+ function scoreNodeS1(graphNode, tokens, aspects) {
4627
+ let score = 0;
4628
+ for (const artifact of graphNode.artifacts) {
4629
+ const hits = countHits(tokens, artifact.content);
4630
+ if (artifact.filename === "responsibility.md") {
4631
+ score += hits * 3;
4632
+ } else if (artifact.filename === "interface.md") {
4633
+ score += hits * 2;
4634
+ } else {
4635
+ score += hits * 1;
4636
+ }
4637
+ }
4638
+ const aspectText = collectAspectContent(graphNode, aspects);
4639
+ if (aspectText) {
4640
+ score += countHits(tokens, aspectText) * 2;
4641
+ }
4642
+ return score;
4643
+ }
4644
+ function pathDepth(nodePath) {
4645
+ return nodePath.split("/").length;
4646
+ }
4647
+ function selectNodes(graph, task, limit) {
4648
+ const tokens = tokenize(task);
4649
+ if (tokens.length === 0) return [];
4650
+ const scored = [];
4651
+ for (const [nodePath, node] of graph.nodes) {
4652
+ const score = scoreNodeS1(node, tokens, graph.aspects);
4653
+ if (score > 0) {
4654
+ scored.push({ node: nodePath, score, name: node.meta.name });
4655
+ }
4656
+ }
4657
+ if (scored.length > 0) {
4658
+ scored.sort((a, b) => {
4659
+ if (b.score !== a.score) return b.score - a.score;
4660
+ return pathDepth(b.node) - pathDepth(a.node);
4661
+ });
4662
+ return scored.slice(0, limit);
4663
+ }
4664
+ return selectFromFlows(graph, tokens, limit);
4665
+ }
4666
+ function selectFromFlows(graph, tokens, limit) {
4667
+ const flowScores = [];
4668
+ for (const flow of graph.flows) {
4669
+ let score = 0;
4670
+ for (const artifact of flow.artifacts) {
4671
+ score += countHits(tokens, artifact.content);
4672
+ }
4673
+ score += countHits(tokens, flow.name);
4674
+ if (score > 0) {
4675
+ flowScores.push({ flow: flow.name, score, participants: flow.nodes });
4676
+ }
4677
+ }
4678
+ if (flowScores.length === 0) return [];
4679
+ flowScores.sort((a, b) => b.score - a.score);
4680
+ const seen = /* @__PURE__ */ new Set();
4681
+ const results = [];
4682
+ for (const fs of flowScores) {
4683
+ for (const participant of fs.participants) {
4684
+ if (seen.has(participant)) continue;
4685
+ seen.add(participant);
4686
+ const node = graph.nodes.get(participant);
4687
+ if (node) {
4688
+ results.push({ node: participant, score: fs.score, name: node.meta.name });
4689
+ }
4690
+ }
4691
+ }
4692
+ return results.slice(0, limit);
4693
+ }
4694
+
4695
+ // src/cli/select.ts
4696
+ function registerSelectCommand(program2) {
4697
+ program2.command("select").description("Find graph nodes relevant to a task description").requiredOption("--task <description>", "Natural-language task description").option("--limit <n>", "Maximum nodes to return", "5").action(async (options) => {
4698
+ try {
4699
+ const yggRoot = await findYggRoot(process.cwd());
4700
+ const graph = await loadGraph(yggRoot);
4701
+ const limit = parseInt(options.limit, 10);
4702
+ if (isNaN(limit) || limit < 1) {
4703
+ process.stderr.write("Error: --limit must be a positive integer\n");
4704
+ process.exit(1);
4705
+ }
4706
+ const results = selectNodes(graph, options.task, limit);
4707
+ process.stdout.write(yamlStringify3(results));
4708
+ } catch (error) {
4709
+ process.stderr.write(`Error: ${error.message}
4710
+ `);
4711
+ process.exit(1);
4712
+ }
4713
+ });
4714
+ }
4715
+
4507
4716
  // src/bin.ts
4508
4717
  import { readFileSync as readFileSync2 } from "fs";
4509
4718
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -4526,5 +4735,6 @@ registerImpactCommand(program);
4526
4735
  registerAspectsCommand(program);
4527
4736
  registerFlowsCommand(program);
4528
4737
  registerPreflightCommand(program);
4738
+ registerSelectCommand(program);
4529
4739
  program.parse();
4530
4740
  //# sourceMappingURL=bin.js.map