@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 +1 -0
- package/dist/bin.js +225 -15
- package/dist/bin.js.map +1 -1
- package/dist/templates/rules.ts +8 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
911
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1123
|
-
await
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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
|