@chrisdudek/yg 2.2.0 → 2.3.1

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,10 +68,10 @@ 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? If a semantic search
72
- tool is available, search for your intent \u2014 the graph contains
73
- responsibility, flow, and aspect files with rich natural-language
74
- descriptions that match goal-oriented queries. Use the results
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
75
  to identify relevant nodes, then proceed to step 1.
76
76
  1. yg owner --file <path>
77
77
  2. Choose the right graph tool for your task:
@@ -147,7 +147,7 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
147
147
  | "I'm brainstorming, not implementing" | Brainstorming about mapped code needs graph context |
148
148
  | "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
149
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" | If semantic search is available, search by intent first \u2014 graph files are designed to match natural-language goals. Then \`yg owner\` on results. |
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. |
151
151
 
152
152
  ### Failure States
153
153
 
@@ -454,6 +454,8 @@ yg build-context --node <path> Assemble context package for this node.
454
454
  yg tree [--root <path>] [--depth N] Print graph structure.
455
455
  yg aspects List aspects with metadata (YAML output).
456
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.
457
459
  yg deps --node <path> [--depth N] [--type structural|event|all]
458
460
  Show dependencies.
459
461
  yg impact --node <path> --simulate Simulate blast radius of a planned change.
@@ -723,7 +725,7 @@ function escapeRegex(s) {
723
725
  }
724
726
 
725
727
  // src/core/migrator.ts
726
- import { readFile as readFile2, access } from "fs/promises";
728
+ import { readFile as readFile2, writeFile as writeFile2, access } from "fs/promises";
727
729
  import path2 from "path";
728
730
  import { parse as parseYaml } from "yaml";
729
731
  import { gt, valid, compare } from "semver";
@@ -761,9 +763,16 @@ async function runMigrations(currentVersion, migrations, yggRoot) {
761
763
  }
762
764
  return results;
763
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
+ }
764
773
 
765
774
  // src/migrations/to-2.0.0.ts
766
- 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";
767
776
  import path3 from "path";
768
777
  import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
769
778
  var KNOWN_TYPE_DESCRIPTIONS = {
@@ -849,7 +858,7 @@ async function migrateTo2(yggRoot) {
849
858
  if (raw.quality) {
850
859
  newConfig.quality = raw.quality;
851
860
  }
852
- await writeFile2(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
861
+ await writeFile3(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
853
862
  actions.push("Updated config: version, artifacts, removed stack/standards");
854
863
  const modelDir = path3.join(yggRoot, "model");
855
864
  if (await fileExists(modelDir)) {
@@ -913,8 +922,8 @@ async function migrateStackStandards(yggRoot, stack, standards, actions) {
913
922
  const rootNodeOldPath = path3.join(modelDir, "node.yaml");
914
923
  const hasRootNode = await fileExists(rootNodeYgPath) || await fileExists(rootNodeOldPath);
915
924
  if (!hasRootNode) {
916
- await writeFile2(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
917
- 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");
918
927
  actions.push("Created root node in model/ for stack/standards migration");
919
928
  }
920
929
  const internalsPath = path3.join(modelDir, "internals.md");
@@ -925,7 +934,7 @@ async function migrateStackStandards(yggRoot, stack, standards, actions) {
925
934
  }
926
935
  const markerLine = MIGRATION_MARKER + "\n";
927
936
  const newContent = existingInternals ? existingInternals.trimEnd() + "\n\n" + markerLine + lines.join("\n") : markerLine + lines.join("\n");
928
- await writeFile2(internalsPath, newContent, "utf-8");
937
+ await writeFile3(internalsPath, newContent, "utf-8");
929
938
  actions.push("Migrated stack/standards to model/internals.md");
930
939
  }
931
940
  async function renameFilesRecursively(dir, oldName, newName, actions) {
@@ -990,7 +999,7 @@ async function transformSingleNode(filePath, actions, warnings) {
990
999
  changed = true;
991
1000
  }
992
1001
  if (changed) {
993
- await writeFile2(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
1002
+ await writeFile3(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
994
1003
  actions.push(`Transformed ${path3.basename(path3.dirname(filePath))}/yg-node.yaml`);
995
1004
  }
996
1005
  }
@@ -1026,7 +1035,7 @@ async function refreshSchemas(yggRoot) {
1026
1035
  for (const file of schemaFiles) {
1027
1036
  const srcPath = path4.join(graphSchemasDir, file);
1028
1037
  const content = await readFile4(srcPath, "utf-8");
1029
- await writeFile3(path4.join(schemasDir, file), content, "utf-8");
1038
+ await writeFile4(path4.join(schemasDir, file), content, "utf-8");
1030
1039
  }
1031
1040
  } catch {
1032
1041
  }
@@ -1097,6 +1106,7 @@ function registerInitCommand(program2) {
1097
1106
  if (results.length > 0) {
1098
1107
  process.stdout.write("\n");
1099
1108
  }
1109
+ await updateConfigVersion(yggRoot, cliVersion);
1100
1110
  }
1101
1111
  await refreshSchemas(yggRoot);
1102
1112
  const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
@@ -1117,7 +1127,7 @@ function registerInitCommand(program2) {
1117
1127
  for (const file of schemaFiles) {
1118
1128
  const srcPath = path4.join(graphSchemasDir, file);
1119
1129
  const content = await readFile4(srcPath, "utf-8");
1120
- await writeFile3(path4.join(schemasDir, file), content, "utf-8");
1130
+ await writeFile4(path4.join(schemasDir, file), content, "utf-8");
1121
1131
  }
1122
1132
  } catch (err) {
1123
1133
  process.stderr.write(
@@ -1125,8 +1135,8 @@ function registerInitCommand(program2) {
1125
1135
  `
1126
1136
  );
1127
1137
  }
1128
- await writeFile3(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
1129
- 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");
1130
1140
  const rulesPath = await installRulesForPlatform(projectRoot, platform);
1131
1141
  process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
1132
1142
  process.stdout.write("Created:\n");
@@ -2971,7 +2981,7 @@ ${errors.length} errors, ${warnings.length} warnings.
2971
2981
  import chalk2 from "chalk";
2972
2982
 
2973
2983
  // src/io/drift-state-store.ts
2974
- 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";
2975
2985
  import path12 from "path";
2976
2986
  import { parse as yamlParse } from "yaml";
2977
2987
  var DRIFT_STATE_DIR = ".drift-state";
@@ -3029,7 +3039,7 @@ async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
3029
3039
  const filePath = nodeStatePath(yggRoot, nodePath);
3030
3040
  await mkdir3(path12.dirname(filePath), { recursive: true });
3031
3041
  const content = JSON.stringify(nodeState, null, 2) + "\n";
3032
- await writeFile4(filePath, content, "utf-8");
3042
+ await writeFile5(filePath, content, "utf-8");
3033
3043
  }
3034
3044
  async function garbageCollectDriftState(yggRoot, validNodePaths) {
3035
3045
  const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
@@ -3842,7 +3852,7 @@ function registerOwnerCommand(program2) {
3842
3852
  `);
3843
3853
  if (result.direct === false && result.mappingPath) {
3844
3854
  process.stdout.write(
3845
- ` 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}
3846
3856
  `
3847
3857
  );
3848
3858
  }
@@ -4510,6 +4520,199 @@ function registerPreflightCommand(program2) {
4510
4520
  });
4511
4521
  }
4512
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
+
4513
4716
  // src/bin.ts
4514
4717
  import { readFileSync as readFileSync2 } from "fs";
4515
4718
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -4532,5 +4735,6 @@ registerImpactCommand(program);
4532
4735
  registerAspectsCommand(program);
4533
4736
  registerFlowsCommand(program);
4534
4737
  registerPreflightCommand(program);
4738
+ registerSelectCommand(program);
4535
4739
  program.parse();
4536
4740
  //# sourceMappingURL=bin.js.map