@chrisdudek/yg 2.2.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 +224 -20
- package/dist/bin.js.map +1 -1
- package/dist/templates/rules.ts +7 -5
- 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,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?
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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" |
|
|
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
|
|
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
|
|
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
|
|
917
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1129
|
-
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");
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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
|