@chrisdudek/yg 0.1.0 → 0.2.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/dist/bin.js +247 -192
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +12 -4
- package/dist/templates/rules.ts +133 -58
- package/package.json +1 -1
- package/graph-templates/knowledge-scope-nodes.yaml +0 -5
- package/graph-templates/knowledge-scope-tags.yaml +0 -3
- package/graph-templates/library.yaml +0 -22
- package/graph-templates/module.yaml +0 -18
- package/graph-templates/service.yaml +0 -30
package/dist/bin.js
CHANGED
|
@@ -28,25 +28,33 @@ artifacts:
|
|
|
28
28
|
responsibility.md:
|
|
29
29
|
required: always
|
|
30
30
|
description: "What this node is responsible for, and what it is not"
|
|
31
|
+
structural_context: true
|
|
31
32
|
interface.md:
|
|
32
33
|
required:
|
|
33
34
|
when: has_incoming_relations
|
|
34
35
|
description: "Public API \u2014 methods, parameters, return types, contracts"
|
|
35
36
|
structural_context: true
|
|
37
|
+
logic.md:
|
|
38
|
+
required: never
|
|
39
|
+
description: "Algorithmic flow, control flow, branching logic, decision trees \u2014 the 'how' of execution"
|
|
36
40
|
constraints.md:
|
|
37
41
|
required: never
|
|
38
42
|
description: "Validation rules, business rules, invariants"
|
|
43
|
+
structural_context: true
|
|
39
44
|
errors.md:
|
|
40
45
|
required:
|
|
41
46
|
when: has_incoming_relations
|
|
42
|
-
description: "
|
|
47
|
+
description: "Failure modes, edge cases, error conditions, recovery behavior"
|
|
43
48
|
structural_context: true
|
|
49
|
+
model.md:
|
|
50
|
+
required: never
|
|
51
|
+
description: "Data structures, schemas, entities, type definitions \u2014 the shape of data this node owns or manages"
|
|
44
52
|
state.md:
|
|
45
53
|
required: never
|
|
46
54
|
description: "State machines, lifecycle, transitions"
|
|
47
55
|
decisions.md:
|
|
48
56
|
required: never
|
|
49
|
-
description: "Local design decisions and rationale"
|
|
57
|
+
description: "Local design decisions and rationale \u2014 choices specific to this node, not system-wide"
|
|
50
58
|
|
|
51
59
|
knowledge_categories:
|
|
52
60
|
- name: decisions
|
|
@@ -60,8 +68,8 @@ quality:
|
|
|
60
68
|
min_artifact_length: 50
|
|
61
69
|
max_direct_relations: 10
|
|
62
70
|
context_budget:
|
|
63
|
-
warning:
|
|
64
|
-
error:
|
|
71
|
+
warning: 10000
|
|
72
|
+
error: 20000
|
|
65
73
|
knowledge_staleness_days: 90
|
|
66
74
|
`;
|
|
67
75
|
|
|
@@ -79,6 +87,16 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
|
|
|
79
87
|
|
|
80
88
|
---
|
|
81
89
|
|
|
90
|
+
## 0. ATOMIC INVARIANT \u2014 OVERRIDES EVERYTHING
|
|
91
|
+
|
|
92
|
+
**CODE \u2194 GRAPH ARE ONE.**
|
|
93
|
+
- Code changed \u2192 graph artifacts MUST be updated IN THE SAME RESPONSE. No exceptions.
|
|
94
|
+
- Graph changed \u2192 source files MUST be verified/updated IN THE SAME RESPONSE. No exceptions.
|
|
95
|
+
- There is NO "code-only" response. There is NO "graph-only" response (unless explicitly a dry-run plan).
|
|
96
|
+
- If you complete a response and one side is changed but the other is not, you have VIOLATED the core contract of Yggdrasil.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
82
100
|
## 1. CORE PRINCIPLES (NON-NEGOTIABLE)
|
|
83
101
|
|
|
84
102
|
1. **Graph First, Always:** Before answering a question, modifying code, or planning a feature, you MUST consult the graph.
|
|
@@ -88,6 +106,20 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
|
|
|
88
106
|
5. **Tools Read, You Write:** The \`yg\` CLI tools only read, validate, and manage metadata. YOU must create and edit graph directories, \`.yaml\` files, and \`.md\` artifacts manually.
|
|
89
107
|
6. **English Only for Artifacts:** All graph artifact files (filenames from \`config.artifacts\`, in the same directory as \`node.yaml\`) MUST be written in English. Conversation can be in the user's language.
|
|
90
108
|
7. **Never Touch Operational Metadata:** NEVER manually edit \`.yggdrasil/.drift-state\` or \`.yggdrasil/.journal.yaml\`.
|
|
109
|
+
8. **Ask, Never Infer:** If graph and code diverge in a way with multiple valid resolutions, or if a required decision is ambiguous \u2014 STOP. State the ambiguity. List interpretations. Ask the user to decide. Never silently choose. Never patch without confirmation.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 1.5 FAILURE STATES
|
|
114
|
+
|
|
115
|
+
You have fundamentally broken Yggdrasil if you do any of the following:
|
|
116
|
+
- \u274C You modified source code without updating graph artifacts in the SAME response.
|
|
117
|
+
- \u274C You modified graph files without verifying/updating source code alignment in the SAME response.
|
|
118
|
+
- \u274C You resolved a code\u2194graph inconsistency without asking the user first.
|
|
119
|
+
- \u274C You created or edited a graph element without reading its schema in \`.yggdrasil/templates/\`.
|
|
120
|
+
- \u274C You ran \`yg drift-sync\` before updating graph artifacts.
|
|
121
|
+
- \u274C You ran \`yg drift-sync\` after a graph-only change without verifying source files.
|
|
122
|
+
- \u274C You used Blackbox coverage for greenfield/new code.
|
|
91
123
|
|
|
92
124
|
---
|
|
93
125
|
|
|
@@ -96,15 +128,15 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
|
|
|
96
128
|
You do not need explicit "session" commands. Follow these conversational triggers:
|
|
97
129
|
|
|
98
130
|
### A. Preflight (First message of the conversation)
|
|
99
|
-
Always execute these commands before doing anything else:
|
|
131
|
+
Always execute these commands before doing anything else. *(Exception: If the user's request is clearly read-only, like "explain this", run ONLY step 1).*
|
|
100
132
|
1. \`yg journal-read\` -> If entries exist, consolidate them into the graph, then \`yg journal-archive\`.
|
|
101
133
|
2. \`yg drift\` -> If divergence is detected, present states (\`ok\`, \`drift\`, \`missing\`, \`unmaterialized\`). Ask the user: Absorb (update graph) or Reject (re-materialize code from graph)?
|
|
102
134
|
3. \`yg status\` -> Report graph health.
|
|
103
135
|
4. \`yg validate\` -> If W008 stale-knowledge appears, update the knowledge artifacts to reflect current node state.
|
|
104
136
|
|
|
105
|
-
### B. Wrap-up
|
|
106
|
-
Triggered by phrases like: "
|
|
107
|
-
**Note: The graph should ALREADY be up to date.
|
|
137
|
+
### B. Session Verification (Wrap-up)
|
|
138
|
+
Triggered by phrases like: "we're done", "wrap up", "that's enough", "done".
|
|
139
|
+
**Note: The graph should ALREADY be up to date. If the graph requires massive updates at this stage, YOU HAVE FAILED.**
|
|
108
140
|
1. If iterative journal mode was used: consolidate notes to the graph, then \`yg journal-archive\`.
|
|
109
141
|
2. \`yg drift\` -> Check if files changed manually during the conversation.
|
|
110
142
|
3. \`yg validate\` -> Fix any structural errors.
|
|
@@ -112,64 +144,66 @@ Triggered by phrases like: "ko\u0144czymy", "wrap up", "to tyle", "gotowe".
|
|
|
112
144
|
|
|
113
145
|
---
|
|
114
146
|
|
|
115
|
-
## 3. WORKFLOW: MODIFYING OR CREATING FILES
|
|
147
|
+
## 3. WORKFLOW: MODIFYING OR CREATING FILES (Code-First)
|
|
116
148
|
|
|
117
149
|
You are NOT ALLOWED to edit or create source code without establishing graph coverage first.
|
|
118
150
|
|
|
119
151
|
**Step 1: Check coverage** -> Run \`yg owner --file <path>\`
|
|
120
152
|
|
|
121
153
|
**Step 2: If Owner FOUND (The Execution Checklist)**
|
|
122
|
-
Whenever you write or edit source code, you MUST output this exact checklist in your response to the user, and execute each step BEFORE finishing your turn
|
|
123
|
-
|
|
124
|
-
- [x] 1. Read Specification (ran \`yg build-context\`)
|
|
125
|
-
- [x] 2. Modify Source Code
|
|
126
|
-
- [x] 3. Sync Graph Artifacts (manually edit the node's artifact files \u2014 filenames from \`config.artifacts\` \u2014 IMMEDIATELY to match new code behavior)
|
|
127
|
-
- [x] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
|
|
154
|
+
Whenever you write or edit source code, you MUST output this exact checklist in your response to the user, and execute each step BEFORE finishing your turn:
|
|
128
155
|
|
|
129
|
-
|
|
156
|
+
- [ ] 1. Read Specification (ran \`yg build-context\`)
|
|
157
|
+
- [ ] 2. Modify Source Code
|
|
158
|
+
- [ ] 3. Sync Graph Artifacts (manually edit the node's artifact files IMMEDIATELY to match new code behavior)
|
|
159
|
+
- [ ] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
|
|
130
160
|
|
|
131
161
|
**Step 3: If Owner NOT FOUND (Uncovered Area)**
|
|
132
|
-
STOP. Do not modify the code. First determine: **Is this greenfield
|
|
133
|
-
|
|
134
|
-
* **If GREENFIELD (empty directory, new project, code not yet written):** Do NOT offer blackbox. Use Option 1 only \u2014 create proper nodes (reverse engineering or upfront design) before implementing. Blackbox is forbidden for new code.
|
|
135
|
-
* **If EXISTING CODE (legacy, third-party, shipped-but-unmapped):** Present the user with 3 options and wait for their decision:
|
|
136
|
-
* **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
|
|
137
|
-
* **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node at a user-chosen granularity to establish ownership without deep semantic exploration.
|
|
138
|
-
* **Option 3: Abort/Change Plan:** Do not touch the file.
|
|
162
|
+
STOP. Do not modify the code. First determine: **Is this greenfield or existing code?**
|
|
139
163
|
|
|
164
|
+
* **If GREENFIELD (empty directory, new project):** Do NOT offer blackbox. Create proper nodes (reverse engineering or upfront design) before implementing.
|
|
165
|
+
* **If PARTIALLY MAPPED (file is unmapped, but lives inside a mapped module):** Stop and ask the user if this file should be added to the existing node or if a new node is required.
|
|
166
|
+
* **If EXISTING CODE (legacy, third-party):** Present the user with 3 options and wait:
|
|
167
|
+
* **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
|
|
168
|
+
* **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node to establish ownership without deep semantic exploration.
|
|
169
|
+
* **Option 3: Abort/Change Plan:** Do not touch the file.
|
|
140
170
|
|
|
141
171
|
---
|
|
142
172
|
|
|
143
|
-
## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS
|
|
144
|
-
|
|
145
|
-
When adding features or changing architecture, update the graph FIRST.
|
|
173
|
+
## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS (Graph-First)
|
|
146
174
|
|
|
147
|
-
|
|
148
|
-
* **DO NOT wait for the user to confirm if a change is "final".** The graph must evolve continuously with your code edits.
|
|
149
|
-
* **Default Behavior:** If iterative journal mode is OFF, you MUST write structural and semantic changes directly to the graph files (\`node.yaml\`, artifacts or other files like aspects or flows, etc.) IMMEDIATELY. Suppress your innate safety bias to wait for permission.
|
|
175
|
+
When adding features, changing architecture, or doing graph-first design:
|
|
150
176
|
|
|
151
177
|
1. **Check Blast Radius:** Before modifying a node that others depend on, run \`yg impact --node <node_path> --simulate\`. Report the impact to the user.
|
|
152
178
|
2. **Read Config & Templates:**
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
* Check \`.yggdrasil/config.yaml\` for allowed \`node_types\` and \`tags\`.
|
|
180
|
+
* **CRITICAL:** ALWAYS read the schema in \`.yggdrasil/templates/\` for the element type (node.yaml, aspect.yaml, flow.yaml, knowledge.yaml) before creating or editing it.
|
|
155
181
|
3. **Validate & Fix:** Run \`yg validate\`. You must fix all E-codes (Errors).
|
|
156
|
-
4. **Token Economy & W-codes
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
* **Smallest Viable Scope:** Prefer \`scope: nodes\` over \`scope: tags\`. Prefer tags over \`scope: global\`. Global scope costs token budget in EVERY context package.
|
|
182
|
+
4. **Token Economy & W-codes:**
|
|
183
|
+
* W005/W006: Context package too large. Consider splitting the node.
|
|
184
|
+
* W008: Stale semantic memory. Update knowledge artifacts.
|
|
160
185
|
|
|
161
|
-
**
|
|
162
|
-
|
|
163
|
-
|
|
186
|
+
**Graph Modification Checklist**
|
|
187
|
+
Whenever you change the graph structure or semantics, you MUST output and execute this exact checklist:
|
|
188
|
+
|
|
189
|
+
- [ ] 1. Read schema from \`.yggdrasil/templates/\` (node.yaml, aspect.yaml, flow.yaml, or knowledge.yaml for the element type)
|
|
190
|
+
- [ ] 2. Edit graph files (\`node.yaml\`, artifacts)
|
|
191
|
+
- [ ] 3. Verify corresponding source files exist and their behavior matches updated artifacts
|
|
192
|
+
- [ ] 4. Validate (ran \`yg validate\` \u2014 fix all Errors)
|
|
193
|
+
- [ ] 5. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER steps 2-3 are confirmed)
|
|
194
|
+
|
|
195
|
+
**Journaling (Iterative Mode Scope):**
|
|
196
|
+
* **Default:** Write changes directly to graph files immediately. Do not defer.
|
|
197
|
+
* **Opt-in:** ONLY if the user says "use iterative mode" or "use journal". Once activated, it remains active for the ENTIRE conversation until wrap-up. Use \`yg journal-add --note "..."\` to buffer intent.
|
|
164
198
|
|
|
165
199
|
---
|
|
166
200
|
|
|
167
201
|
## 5. PATH CONVENTIONS (CRITICAL)
|
|
168
202
|
|
|
169
203
|
To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
204
|
+
* **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
|
|
205
|
+
* **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
|
|
206
|
+
* **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
|
|
173
207
|
|
|
174
208
|
---
|
|
175
209
|
|
|
@@ -177,35 +211,84 @@ To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
|
|
|
177
211
|
|
|
178
212
|
The graph lives entirely under \`.yggdrasil/\`. You NEVER guess structure. You MUST ALWAYS read the corresponding schema reference in \`.yggdrasil/templates/\` before creating or editing any graph file.
|
|
179
213
|
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` (lists \`nodes: [paths]\` and \`knowledge: [paths]\`) and \`.md\` content.
|
|
187
|
-
* **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom (\`decisions/\`, \`patterns/\`, \`invariants/\`). Directory contains \`knowledge.yaml\` and \`.md\` content.
|
|
214
|
+
* **\`.yggdrasil/config.yaml\`**: Defines \`node_types\`, \`tags\`, \`artifacts\`, \`knowledge_categories\`.
|
|
215
|
+
* **\`.yggdrasil/templates/\`**: Schemas for each graph layer \u2014 \`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`.
|
|
216
|
+
* **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files.
|
|
217
|
+
* **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` and \`.md\` content.
|
|
218
|
+
* **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` and \`.md\` content.
|
|
219
|
+
* **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom. Directory contains \`knowledge.yaml\` and \`.md\` content.
|
|
188
220
|
|
|
189
221
|
---
|
|
190
222
|
|
|
191
|
-
## 7.
|
|
223
|
+
## 7. CONTEXT ASSEMBLY & KNOWLEDGE DECONSTRUCTION (HOW TO MAP FILES)
|
|
224
|
+
|
|
225
|
+
Your ultimate goal when describing a file or node is **Context Reproducibility**. A future agent reading ONLY the output of \`yg build-context\` for this node must be able to perfectly reconstruct the source code's behavior, constraints, environment, and purpose.
|
|
226
|
+
|
|
227
|
+
However, you must NOT dump all knowledge into a single file. Yggdrasil's context package is **multi-layered** and hierarchically assembled. When you map existing code or design new code, you must deconstruct the knowledge and place it at the correct abstraction layer so the engine can mechanically reassemble it.
|
|
228
|
+
|
|
229
|
+
### CRITICAL RULE: CAPTURE INTENT, BUT NEVER INVENT IT
|
|
230
|
+
The graph is not just a structural map; it is the semantic meaning of the system. Code explains "what" and "how". The graph MUST explain "WHY".
|
|
231
|
+
|
|
232
|
+
1. **ALWAYS Capture the User's "Why":** If the user explains the business reason, intent, or rationale behind a request (e.g., "We need to do X because Y"), you MUST permanently record this reasoning in the relevant graph artifacts (e.g., \`responsibility.md\`, \`constraints.md\`, \`decisions.md\`). Do not let the conversation context evaporate.
|
|
233
|
+
2. **NEVER Invent the "Why":** Files that imply human judgment (like \`decisions.md\` or \`knowledge/invariants\`) must reflect ACTUAL human choices.
|
|
234
|
+
3. **NO Hallucinations:** You MUST NEVER infer or hallucinate a rationale, an architectural decision, or a business rule.
|
|
235
|
+
4. **Ask if Missing:** If the user requests a significant architectural or business logic change but does not provide the rationale, you MUST ask them "Why are we making this change?" before documenting the decision in the graph.
|
|
236
|
+
|
|
237
|
+
When mapping a file, execute this mental routing:
|
|
238
|
+
|
|
239
|
+
### Layer 1: Unit Identity (Local Node Artifacts)
|
|
240
|
+
* **What goes here:** Things exclusively true for this specific node.
|
|
241
|
+
* **Routing:** **DO NOT ASSUME FILE NAMES.** You MUST read \`.yggdrasil/config.yaml\` (the \`artifacts\` section) to see the exact allowed filenames for the current project and their requirement conditions (e.g., \`required: always\` vs \`when: has_incoming_relations\`). Write local node knowledge ONLY into these configured files next to \`node.yaml\`.
|
|
242
|
+
* **logic.md:** Algorithmic flow, control flow, branching logic, decision trees \u2014 the "how" of execution. Create when the node has non-trivial logic that cannot be inferred from interface alone. Extract from source; do not invent.
|
|
243
|
+
* **model.md:** Data structures, schemas, entities, type definitions \u2014 the shape of data this node owns. Create when the node manages non-trivial data structures. Extract from source types/interfaces; do not invent fields.
|
|
244
|
+
|
|
245
|
+
**Subagents:** When mapping a node (reverse engineering or graph-first), if the source code contains non-trivial logic (branching, control flow, algorithms, workflows), create \`logic.md\` for that node. Document: control flow, branching conditions, decision trees, sequencing. Follow Layer 1 routing. Do not invent \u2014 extract only what is implemented.
|
|
246
|
+
|
|
247
|
+
### Layer 2: Surroundings (Relations & Flows)
|
|
248
|
+
* **What goes here:** How this node interacts with others. You must not duplicate external interfaces locally.
|
|
249
|
+
* **Routing:**
|
|
250
|
+
* If it calls another module: Add an outgoing structural \`relation\` in \`node.yaml\`. (The engine will automatically fetch the target's structural-context artifacts: responsibility, interface, constraints, errors).
|
|
251
|
+
* If it participates in an end-to-end process: Do not explain the whole process locally. Ensure the node is listed in \`.yggdrasil/flows/<flow_name>/flow.yaml\`. The engine will attach the flow knowledge automatically.
|
|
252
|
+
|
|
253
|
+
### Layer 3: Domain Context (Hierarchy)
|
|
254
|
+
* **What goes here:** Business rules shared by a family of nodes.
|
|
255
|
+
* **Routing:** Do not repeat module-wide rules in every child node. Place the child node directory *inside* a parent Module Node directory. Write the shared rules in the parent's configured artifacts. The engine inherently passes parent context to children.
|
|
256
|
+
|
|
257
|
+
### Layer 4: Cross-Cutting Rules (Aspects)
|
|
258
|
+
* **What goes here:** Horizontal requirements like logging, auth, rate-limiting, or specific frameworks.
|
|
259
|
+
* **Routing:** Do NOT write generic rules like "This node must log all errors" in local artifacts. Instead, read \`config.yaml\` for available \`tags\`. Add the relevant tag (e.g., \`requires-audit\`) to \`node.yaml\`. The engine will automatically attach the aspect knowledge.
|
|
260
|
+
|
|
261
|
+
### Layer 5: Long-Term Memory (Knowledge Elements)
|
|
262
|
+
* **What goes here:** Global architectural decisions, design patterns, and systemic invariants.
|
|
263
|
+
* **Routing:** Read \`config.yaml\` (the \`knowledge_categories\` section) to know what categories exist.
|
|
264
|
+
* If the file implements a standard pattern: Do not describe the pattern locally. Add a \`knowledge\` reference in \`node.yaml\` to the existing pattern.
|
|
265
|
+
* If the file reveals an undocumented global invariant or decision: Ask the user to confirm it. If confirmed, create it under \`.yggdrasil/knowledge/<category>/\` so all future nodes inherit it.
|
|
266
|
+
|
|
267
|
+
**THE COMPLETENESS CHECK:**
|
|
268
|
+
Before finishing a mapping, ask yourself: *"If I delete the source file and give another agent ONLY the output of \`yg build-context\`, can they recreate it perfectly based on the configured artifacts, AND will they understand EXACTLY WHY this code exists and why it was designed this way?"*
|
|
269
|
+
- If no -> You missed a local constraint, a relation, or you failed to capture the user's provided rationale.
|
|
270
|
+
- If yes, but the local files are bloated -> You failed to deconstruct knowledge into Tags, Aspects, Flows, and Hierarchy. Fix the routing.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 8. CLI TOOLS REFERENCE (\`yg\`)
|
|
192
275
|
|
|
193
276
|
Always use these exact commands.
|
|
194
277
|
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
278
|
+
* \`yg owner --file <file_path>\` -> Find owning node.
|
|
279
|
+
* \`yg build-context --node <node_path>\` -> Assemble strict specification.
|
|
280
|
+
* \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
|
|
281
|
+
* \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
|
|
282
|
+
* \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
|
|
283
|
+
* \`yg status\` -> Graph health metrics.
|
|
284
|
+
* \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
|
|
285
|
+
* \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
|
|
286
|
+
* \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
|
|
204
287
|
|
|
205
288
|
*(Iterative mode only)*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
289
|
+
* \`yg journal-read\`
|
|
290
|
+
* \`yg journal-add --note "<content>" [--target <node_path>]\`
|
|
291
|
+
* \`yg journal-archive\`
|
|
209
292
|
`;
|
|
210
293
|
|
|
211
294
|
// src/templates/platform.ts
|
|
@@ -532,7 +615,7 @@ function registerInitCommand(program2) {
|
|
|
532
615
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
533
616
|
process.stdout.write(" .yggdrasil/knowledge/ (decisions, patterns, invariants)\n");
|
|
534
617
|
process.stdout.write(
|
|
535
|
-
" .yggdrasil/templates/ (
|
|
618
|
+
" .yggdrasil/templates/ (node, aspect, flow, knowledge)\n"
|
|
536
619
|
);
|
|
537
620
|
process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
|
|
538
621
|
|
|
@@ -546,7 +629,7 @@ function registerInitCommand(program2) {
|
|
|
546
629
|
|
|
547
630
|
// src/core/graph-loader.ts
|
|
548
631
|
import { readdir as readdir3 } from "fs/promises";
|
|
549
|
-
import
|
|
632
|
+
import path6 from "path";
|
|
550
633
|
|
|
551
634
|
// src/io/config-parser.ts
|
|
552
635
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -554,7 +637,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
554
637
|
var DEFAULT_QUALITY = {
|
|
555
638
|
min_artifact_length: 50,
|
|
556
639
|
max_direct_relations: 10,
|
|
557
|
-
context_budget: { warning:
|
|
640
|
+
context_budget: { warning: 1e4, error: 2e4 },
|
|
558
641
|
knowledge_staleness_days: 90
|
|
559
642
|
};
|
|
560
643
|
async function parseConfig(filePath) {
|
|
@@ -869,30 +952,24 @@ function parseScope(raw, filePath) {
|
|
|
869
952
|
|
|
870
953
|
// src/io/template-parser.ts
|
|
871
954
|
import { readFile as readFile9 } from "fs/promises";
|
|
955
|
+
import path4 from "path";
|
|
872
956
|
import { parse as parseYaml6 } from "yaml";
|
|
873
|
-
async function
|
|
957
|
+
async function parseSchema(filePath) {
|
|
874
958
|
const content = await readFile9(filePath, "utf-8");
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
const suggestedArtifacts = Array.isArray(raw.suggested_artifacts) ? raw.suggested_artifacts.filter((a) => typeof a === "string") : void 0;
|
|
880
|
-
return {
|
|
881
|
-
nodeType: raw.node_type.trim(),
|
|
882
|
-
suggestedArtifacts: suggestedArtifacts && suggestedArtifacts.length > 0 ? suggestedArtifacts : void 0,
|
|
883
|
-
guidance: typeof raw.guidance === "string" ? raw.guidance : void 0
|
|
884
|
-
};
|
|
959
|
+
parseYaml6(content);
|
|
960
|
+
const schemaType = path4.basename(filePath, path4.extname(filePath));
|
|
961
|
+
return { schemaType };
|
|
885
962
|
}
|
|
886
963
|
|
|
887
964
|
// src/utils/paths.ts
|
|
888
|
-
import
|
|
965
|
+
import path5 from "path";
|
|
889
966
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
890
967
|
import { stat as stat2 } from "fs/promises";
|
|
891
968
|
async function findYggRoot(projectRoot) {
|
|
892
|
-
let current =
|
|
893
|
-
const root =
|
|
969
|
+
let current = path5.resolve(projectRoot);
|
|
970
|
+
const root = path5.parse(current).root;
|
|
894
971
|
while (true) {
|
|
895
|
-
const yggPath =
|
|
972
|
+
const yggPath = path5.join(current, ".yggdrasil");
|
|
896
973
|
try {
|
|
897
974
|
const st = await stat2(yggPath);
|
|
898
975
|
if (!st.isDirectory()) {
|
|
@@ -906,7 +983,7 @@ async function findYggRoot(projectRoot) {
|
|
|
906
983
|
if (current === root) {
|
|
907
984
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
908
985
|
}
|
|
909
|
-
current =
|
|
986
|
+
current = path5.dirname(current);
|
|
910
987
|
continue;
|
|
911
988
|
}
|
|
912
989
|
throw err;
|
|
@@ -922,18 +999,18 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
922
999
|
if (normalizedInput.length === 0) {
|
|
923
1000
|
throw new Error("Path cannot be empty");
|
|
924
1001
|
}
|
|
925
|
-
const absolute =
|
|
926
|
-
const relative =
|
|
927
|
-
const isOutside = relative.startsWith("..") ||
|
|
1002
|
+
const absolute = path5.resolve(projectRoot, normalizedInput);
|
|
1003
|
+
const relative = path5.relative(projectRoot, absolute);
|
|
1004
|
+
const isOutside = relative.startsWith("..") || path5.isAbsolute(relative);
|
|
928
1005
|
if (isOutside) {
|
|
929
1006
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
930
1007
|
}
|
|
931
|
-
return relative.split(
|
|
1008
|
+
return relative.split(path5.sep).join("/");
|
|
932
1009
|
}
|
|
933
1010
|
|
|
934
1011
|
// src/core/graph-loader.ts
|
|
935
1012
|
function toModelPath(absolutePath, modelDir) {
|
|
936
|
-
return
|
|
1013
|
+
return path6.relative(modelDir, absolutePath).split(path6.sep).join("/");
|
|
937
1014
|
}
|
|
938
1015
|
var FALLBACK_CONFIG = {
|
|
939
1016
|
name: "",
|
|
@@ -949,14 +1026,14 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
949
1026
|
let configError;
|
|
950
1027
|
let config = FALLBACK_CONFIG;
|
|
951
1028
|
try {
|
|
952
|
-
config = await parseConfig(
|
|
1029
|
+
config = await parseConfig(path6.join(yggRoot, "config.yaml"));
|
|
953
1030
|
} catch (error) {
|
|
954
1031
|
if (!options.tolerateInvalidConfig) {
|
|
955
1032
|
throw error;
|
|
956
1033
|
}
|
|
957
1034
|
configError = error.message;
|
|
958
1035
|
}
|
|
959
|
-
const modelDir =
|
|
1036
|
+
const modelDir = path6.join(yggRoot, "model");
|
|
960
1037
|
const nodes = /* @__PURE__ */ new Map();
|
|
961
1038
|
const nodeParseErrors = [];
|
|
962
1039
|
const artifactFilenames = Object.keys(config.artifacts ?? {});
|
|
@@ -970,13 +1047,13 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
970
1047
|
}
|
|
971
1048
|
throw err;
|
|
972
1049
|
}
|
|
973
|
-
const aspects = await loadAspects(
|
|
974
|
-
const flows = await loadFlows(
|
|
1050
|
+
const aspects = await loadAspects(path6.join(yggRoot, "aspects"));
|
|
1051
|
+
const flows = await loadFlows(path6.join(yggRoot, "flows"));
|
|
975
1052
|
const knowledge = await loadKnowledge(
|
|
976
|
-
|
|
1053
|
+
path6.join(yggRoot, "knowledge"),
|
|
977
1054
|
config.knowledge_categories
|
|
978
1055
|
);
|
|
979
|
-
const
|
|
1056
|
+
const schemas = await loadSchemas(path6.join(yggRoot, "templates"));
|
|
980
1057
|
return {
|
|
981
1058
|
config,
|
|
982
1059
|
configError,
|
|
@@ -985,7 +1062,7 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
985
1062
|
aspects,
|
|
986
1063
|
flows,
|
|
987
1064
|
knowledge,
|
|
988
|
-
|
|
1065
|
+
schemas,
|
|
989
1066
|
rootPath: yggRoot
|
|
990
1067
|
};
|
|
991
1068
|
}
|
|
@@ -999,7 +1076,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
999
1076
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1000
1077
|
let meta;
|
|
1001
1078
|
try {
|
|
1002
|
-
meta = await parseNodeYaml(
|
|
1079
|
+
meta = await parseNodeYaml(path6.join(dirPath, "node.yaml"));
|
|
1003
1080
|
} catch (err) {
|
|
1004
1081
|
nodeParseErrors.push({
|
|
1005
1082
|
nodePath: graphPath,
|
|
@@ -1023,7 +1100,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1023
1100
|
if (!entry.isDirectory()) continue;
|
|
1024
1101
|
if (entry.name.startsWith(".")) continue;
|
|
1025
1102
|
await scanModelDirectory(
|
|
1026
|
-
|
|
1103
|
+
path6.join(dirPath, entry.name),
|
|
1027
1104
|
modelDir,
|
|
1028
1105
|
node,
|
|
1029
1106
|
nodes,
|
|
@@ -1036,7 +1113,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1036
1113
|
if (!entry.isDirectory()) continue;
|
|
1037
1114
|
if (entry.name.startsWith(".")) continue;
|
|
1038
1115
|
await scanModelDirectory(
|
|
1039
|
-
|
|
1116
|
+
path6.join(dirPath, entry.name),
|
|
1040
1117
|
modelDir,
|
|
1041
1118
|
null,
|
|
1042
1119
|
nodes,
|
|
@@ -1052,8 +1129,8 @@ async function loadAspects(aspectsDir) {
|
|
|
1052
1129
|
const aspects = [];
|
|
1053
1130
|
for (const entry of entries) {
|
|
1054
1131
|
if (!entry.isDirectory()) continue;
|
|
1055
|
-
const aspectYamlPath =
|
|
1056
|
-
const aspect = await parseAspect(
|
|
1132
|
+
const aspectYamlPath = path6.join(aspectsDir, entry.name, "aspect.yaml");
|
|
1133
|
+
const aspect = await parseAspect(path6.join(aspectsDir, entry.name), aspectYamlPath);
|
|
1057
1134
|
aspects.push(aspect);
|
|
1058
1135
|
}
|
|
1059
1136
|
return aspects;
|
|
@@ -1067,8 +1144,8 @@ async function loadFlows(flowsDir) {
|
|
|
1067
1144
|
const flows = [];
|
|
1068
1145
|
for (const entry of entries) {
|
|
1069
1146
|
if (!entry.isDirectory()) continue;
|
|
1070
|
-
const flowYamlPath =
|
|
1071
|
-
const flow = await parseFlow(
|
|
1147
|
+
const flowYamlPath = path6.join(flowsDir, entry.name, "flow.yaml");
|
|
1148
|
+
const flow = await parseFlow(path6.join(flowsDir, entry.name), flowYamlPath);
|
|
1072
1149
|
flows.push(flow);
|
|
1073
1150
|
}
|
|
1074
1151
|
return flows;
|
|
@@ -1084,12 +1161,12 @@ async function loadKnowledge(knowledgeDir, categories) {
|
|
|
1084
1161
|
for (const catEntry of catEntries) {
|
|
1085
1162
|
if (!catEntry.isDirectory()) continue;
|
|
1086
1163
|
if (!categorySet.has(catEntry.name)) continue;
|
|
1087
|
-
const catPath =
|
|
1164
|
+
const catPath = path6.join(knowledgeDir, catEntry.name);
|
|
1088
1165
|
const itemEntries = await readdir3(catPath, { withFileTypes: true });
|
|
1089
1166
|
for (const itemEntry of itemEntries) {
|
|
1090
1167
|
if (!itemEntry.isDirectory()) continue;
|
|
1091
|
-
const itemDir =
|
|
1092
|
-
const knowledgeYamlPath =
|
|
1168
|
+
const itemDir = path6.join(catPath, itemEntry.name);
|
|
1169
|
+
const knowledgeYamlPath = path6.join(itemDir, "knowledge.yaml");
|
|
1093
1170
|
const relativePath = `${catEntry.name}/${itemEntry.name}`;
|
|
1094
1171
|
const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
|
|
1095
1172
|
items.push(item);
|
|
@@ -1099,17 +1176,17 @@ async function loadKnowledge(knowledgeDir, categories) {
|
|
|
1099
1176
|
}
|
|
1100
1177
|
return items;
|
|
1101
1178
|
}
|
|
1102
|
-
async function
|
|
1179
|
+
async function loadSchemas(templatesDir) {
|
|
1103
1180
|
try {
|
|
1104
1181
|
const entries = await readdir3(templatesDir, { withFileTypes: true });
|
|
1105
|
-
const
|
|
1182
|
+
const schemas = [];
|
|
1106
1183
|
for (const entry of entries) {
|
|
1107
1184
|
if (!entry.isFile()) continue;
|
|
1108
1185
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1186
|
+
const s = await parseSchema(path6.join(templatesDir, entry.name));
|
|
1187
|
+
schemas.push(s);
|
|
1111
1188
|
}
|
|
1112
|
-
return
|
|
1189
|
+
return schemas;
|
|
1113
1190
|
} catch {
|
|
1114
1191
|
return [];
|
|
1115
1192
|
}
|
|
@@ -1117,7 +1194,7 @@ async function loadTemplates(templatesDir) {
|
|
|
1117
1194
|
|
|
1118
1195
|
// src/core/context-builder.ts
|
|
1119
1196
|
import { readFile as readFile10 } from "fs/promises";
|
|
1120
|
-
import
|
|
1197
|
+
import path7 from "path";
|
|
1121
1198
|
|
|
1122
1199
|
// src/utils/tokens.ts
|
|
1123
1200
|
function estimateTokens(text) {
|
|
@@ -1270,7 +1347,7 @@ ${a.content}`).join("\n\n");
|
|
|
1270
1347
|
}
|
|
1271
1348
|
async function buildOwnLayer(node, config, graphRootPath) {
|
|
1272
1349
|
const parts = [];
|
|
1273
|
-
const nodeYamlPath =
|
|
1350
|
+
const nodeYamlPath = path7.join(graphRootPath, "model", node.path, "node.yaml");
|
|
1274
1351
|
try {
|
|
1275
1352
|
const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
|
|
1276
1353
|
parts.push(`### node.yaml
|
|
@@ -1387,13 +1464,13 @@ function collectAncestors(node) {
|
|
|
1387
1464
|
|
|
1388
1465
|
// src/core/validator.ts
|
|
1389
1466
|
import { readdir as readdir4 } from "fs/promises";
|
|
1390
|
-
import
|
|
1467
|
+
import path9 from "path";
|
|
1391
1468
|
|
|
1392
1469
|
// src/utils/git.ts
|
|
1393
1470
|
import { execSync } from "child_process";
|
|
1394
|
-
import
|
|
1471
|
+
import path8 from "path";
|
|
1395
1472
|
function getLastCommitTimestamp(projectRoot, relativePath) {
|
|
1396
|
-
const normalized =
|
|
1473
|
+
const normalized = path8.normalize(relativePath).replace(/\\/g, "/");
|
|
1397
1474
|
try {
|
|
1398
1475
|
const out = execSync(`git log -1 --format=%ct -- "${normalized}"`, {
|
|
1399
1476
|
cwd: projectRoot,
|
|
@@ -1441,8 +1518,8 @@ async function validate(graph, scope = "all") {
|
|
|
1441
1518
|
issues.push(...await checkContextBudget(graph));
|
|
1442
1519
|
issues.push(...checkHighFanOut(graph));
|
|
1443
1520
|
issues.push(...await checkStaleKnowledge(graph));
|
|
1444
|
-
issues.push(...checkTemplates(graph));
|
|
1445
1521
|
}
|
|
1522
|
+
issues.push(...checkSchemas(graph));
|
|
1446
1523
|
issues.push(...checkRelationTargets(graph));
|
|
1447
1524
|
issues.push(...checkNoCycles(graph));
|
|
1448
1525
|
issues.push(...checkMappingOverlap(graph));
|
|
@@ -1815,7 +1892,7 @@ function checkScopeTagsDefined(graph) {
|
|
|
1815
1892
|
async function checkUnknownKnowledgeCategories(graph) {
|
|
1816
1893
|
const issues = [];
|
|
1817
1894
|
const categorySet = new Set((graph.config.knowledge_categories ?? []).map((c) => c.name));
|
|
1818
|
-
const knowledgeDir =
|
|
1895
|
+
const knowledgeDir = path9.join(graph.rootPath, "knowledge");
|
|
1819
1896
|
const existingDirs = /* @__PURE__ */ new Set();
|
|
1820
1897
|
try {
|
|
1821
1898
|
const entries = await readdir4(knowledgeDir, { withFileTypes: true });
|
|
@@ -1946,7 +2023,7 @@ async function checkMissingPatternExamples(graph) {
|
|
|
1946
2023
|
const issues = [];
|
|
1947
2024
|
const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
|
|
1948
2025
|
if (!hasPatterns) return issues;
|
|
1949
|
-
const patternsDir =
|
|
2026
|
+
const patternsDir = path9.join(graph.rootPath, "knowledge", "patterns");
|
|
1950
2027
|
try {
|
|
1951
2028
|
const entries = await readdir4(patternsDir, { withFileTypes: true });
|
|
1952
2029
|
const exampleExtensions = /* @__PURE__ */ new Set([
|
|
@@ -1962,10 +2039,10 @@ async function checkMissingPatternExamples(graph) {
|
|
|
1962
2039
|
]);
|
|
1963
2040
|
for (const e of entries) {
|
|
1964
2041
|
if (!e.isDirectory()) continue;
|
|
1965
|
-
const itemDir =
|
|
2042
|
+
const itemDir = path9.join(patternsDir, e.name);
|
|
1966
2043
|
const itemEntries = await readdir4(itemDir, { withFileTypes: true });
|
|
1967
2044
|
const hasExample = itemEntries.some(
|
|
1968
|
-
(f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(
|
|
2045
|
+
(f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path9.extname(f.name).toLowerCase()))
|
|
1969
2046
|
);
|
|
1970
2047
|
if (!hasExample) {
|
|
1971
2048
|
issues.push({
|
|
@@ -2016,8 +2093,8 @@ function getNodesInScope(k, graph) {
|
|
|
2016
2093
|
async function checkStaleKnowledge(graph) {
|
|
2017
2094
|
const issues = [];
|
|
2018
2095
|
const stalenessDays = graph.config.quality?.knowledge_staleness_days ?? 90;
|
|
2019
|
-
const projectRoot =
|
|
2020
|
-
const yggRel =
|
|
2096
|
+
const projectRoot = path9.dirname(graph.rootPath);
|
|
2097
|
+
const yggRel = path9.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
|
|
2021
2098
|
for (const k of graph.knowledge) {
|
|
2022
2099
|
const scopeNodes = getNodesInScope(k, graph);
|
|
2023
2100
|
if (scopeNodes.length === 0) continue;
|
|
@@ -2096,51 +2173,29 @@ function checkUnpairedEvents(graph) {
|
|
|
2096
2173
|
}
|
|
2097
2174
|
return issues;
|
|
2098
2175
|
}
|
|
2099
|
-
|
|
2176
|
+
var REQUIRED_SCHEMAS = ["node", "aspect", "flow", "knowledge"];
|
|
2177
|
+
function checkSchemas(graph) {
|
|
2100
2178
|
const issues = [];
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
for (const template of graph.templates) {
|
|
2105
|
-
if (!allowedTypes.has(template.nodeType)) {
|
|
2106
|
-
issues.push({
|
|
2107
|
-
severity: "error",
|
|
2108
|
-
code: "E002",
|
|
2109
|
-
rule: "unknown-node-type",
|
|
2110
|
-
message: `Template for '${template.nodeType}' references node_type not in config.node_types (${[...allowedTypes].join(", ")})`
|
|
2111
|
-
});
|
|
2112
|
-
}
|
|
2113
|
-
for (const artifact of template.suggestedArtifacts ?? []) {
|
|
2114
|
-
if (!allowedArtifacts.has(artifact)) {
|
|
2115
|
-
issues.push({
|
|
2116
|
-
severity: "warning",
|
|
2117
|
-
code: "W001",
|
|
2118
|
-
rule: "missing-artifact",
|
|
2119
|
-
message: `Template for '${template.nodeType}' suggests artifact '${artifact}' not defined in config.artifacts`
|
|
2120
|
-
});
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
const existing = typeToTemplate.get(template.nodeType);
|
|
2124
|
-
if (existing) {
|
|
2179
|
+
const present = new Set(graph.schemas.map((s) => s.schemaType));
|
|
2180
|
+
for (const required of REQUIRED_SCHEMAS) {
|
|
2181
|
+
if (!present.has(required)) {
|
|
2125
2182
|
issues.push({
|
|
2126
|
-
severity: "
|
|
2127
|
-
code: "
|
|
2128
|
-
rule: "
|
|
2129
|
-
message: `
|
|
2183
|
+
severity: "warning",
|
|
2184
|
+
code: "W010",
|
|
2185
|
+
rule: "missing-schema",
|
|
2186
|
+
message: `Schema '${required}.yaml' missing from .yggdrasil/templates/`
|
|
2130
2187
|
});
|
|
2131
|
-
} else {
|
|
2132
|
-
typeToTemplate.set(template.nodeType, template.nodeType);
|
|
2133
2188
|
}
|
|
2134
2189
|
}
|
|
2135
2190
|
return issues;
|
|
2136
2191
|
}
|
|
2137
2192
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
2138
2193
|
const issues = [];
|
|
2139
|
-
const modelDir =
|
|
2194
|
+
const modelDir = path9.join(graph.rootPath, "model");
|
|
2140
2195
|
async function scanDir(dirPath, segments) {
|
|
2141
2196
|
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
2142
2197
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
2143
|
-
const dirName =
|
|
2198
|
+
const dirName = path9.basename(dirPath);
|
|
2144
2199
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
2145
2200
|
const hasContent = entries.some((e) => e.isFile()) || entries.some((e) => e.isDirectory());
|
|
2146
2201
|
const graphPath = segments.join("/");
|
|
@@ -2157,7 +2212,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2157
2212
|
if (!entry.isDirectory()) continue;
|
|
2158
2213
|
if (RESERVED_DIRS.has(entry.name)) continue;
|
|
2159
2214
|
if (entry.name.startsWith(".")) continue;
|
|
2160
|
-
await scanDir(
|
|
2215
|
+
await scanDir(path9.join(dirPath, entry.name), [...segments, entry.name]);
|
|
2161
2216
|
}
|
|
2162
2217
|
}
|
|
2163
2218
|
try {
|
|
@@ -2165,7 +2220,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2165
2220
|
for (const entry of rootEntries) {
|
|
2166
2221
|
if (!entry.isDirectory()) continue;
|
|
2167
2222
|
if (entry.name.startsWith(".")) continue;
|
|
2168
|
-
await scanDir(
|
|
2223
|
+
await scanDir(path9.join(modelDir, entry.name), [entry.name]);
|
|
2169
2224
|
}
|
|
2170
2225
|
} catch {
|
|
2171
2226
|
}
|
|
@@ -2330,7 +2385,7 @@ import chalk2 from "chalk";
|
|
|
2330
2385
|
// src/io/drift-state-store.ts
|
|
2331
2386
|
import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
2332
2387
|
import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
|
|
2333
|
-
import
|
|
2388
|
+
import path10 from "path";
|
|
2334
2389
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2335
2390
|
function getCanonicalHash(entry) {
|
|
2336
2391
|
return typeof entry === "string" ? entry : entry.hash;
|
|
@@ -2339,7 +2394,7 @@ function getFileHashes(entry) {
|
|
|
2339
2394
|
return typeof entry === "object" ? entry.files : void 0;
|
|
2340
2395
|
}
|
|
2341
2396
|
async function readDriftState(yggRoot) {
|
|
2342
|
-
const filePath =
|
|
2397
|
+
const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
|
|
2343
2398
|
try {
|
|
2344
2399
|
const content = await readFile11(filePath, "utf-8");
|
|
2345
2400
|
const raw = parseYaml7(content);
|
|
@@ -2360,14 +2415,14 @@ async function readDriftState(yggRoot) {
|
|
|
2360
2415
|
}
|
|
2361
2416
|
}
|
|
2362
2417
|
async function writeDriftState(yggRoot, state) {
|
|
2363
|
-
const filePath =
|
|
2418
|
+
const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
|
|
2364
2419
|
const content = stringifyYaml(state);
|
|
2365
2420
|
await writeFile3(filePath, content, "utf-8");
|
|
2366
2421
|
}
|
|
2367
2422
|
|
|
2368
2423
|
// src/utils/hash.ts
|
|
2369
2424
|
import { readFile as readFile12, readdir as readdir5, stat as stat3 } from "fs/promises";
|
|
2370
|
-
import
|
|
2425
|
+
import path11 from "path";
|
|
2371
2426
|
import { createHash } from "crypto";
|
|
2372
2427
|
import { createRequire } from "module";
|
|
2373
2428
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2377,7 +2432,7 @@ async function hashFile(filePath) {
|
|
|
2377
2432
|
return createHash("sha256").update(content).digest("hex");
|
|
2378
2433
|
}
|
|
2379
2434
|
async function hashPath(targetPath, options = {}) {
|
|
2380
|
-
const projectRoot = options.projectRoot ?
|
|
2435
|
+
const projectRoot = options.projectRoot ? path11.resolve(options.projectRoot) : void 0;
|
|
2381
2436
|
const gitignoreMatcher = await loadGitignoreMatcher(projectRoot);
|
|
2382
2437
|
const targetStat = await stat3(targetPath);
|
|
2383
2438
|
if (targetStat.isFile()) {
|
|
@@ -2400,7 +2455,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2400
2455
|
const entries = await readdir5(directoryPath, { withFileTypes: true });
|
|
2401
2456
|
const result = [];
|
|
2402
2457
|
for (const entry of entries) {
|
|
2403
|
-
const absoluteChildPath =
|
|
2458
|
+
const absoluteChildPath = path11.join(directoryPath, entry.name);
|
|
2404
2459
|
if (isIgnoredPath(absoluteChildPath, options.projectRoot, options.gitignoreMatcher)) {
|
|
2405
2460
|
continue;
|
|
2406
2461
|
}
|
|
@@ -2412,7 +2467,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2412
2467
|
);
|
|
2413
2468
|
for (const nestedEntry of nested) {
|
|
2414
2469
|
result.push({
|
|
2415
|
-
path:
|
|
2470
|
+
path: path11.relative(rootDirectoryPath, path11.join(absoluteChildPath, nestedEntry.path)),
|
|
2416
2471
|
hash: nestedEntry.hash
|
|
2417
2472
|
});
|
|
2418
2473
|
}
|
|
@@ -2422,7 +2477,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2422
2477
|
continue;
|
|
2423
2478
|
}
|
|
2424
2479
|
result.push({
|
|
2425
|
-
path:
|
|
2480
|
+
path: path11.relative(rootDirectoryPath, absoluteChildPath),
|
|
2426
2481
|
hash: await hashFile(absoluteChildPath)
|
|
2427
2482
|
});
|
|
2428
2483
|
}
|
|
@@ -2433,7 +2488,7 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
2433
2488
|
return void 0;
|
|
2434
2489
|
}
|
|
2435
2490
|
try {
|
|
2436
|
-
const gitignorePath =
|
|
2491
|
+
const gitignorePath = path11.join(projectRoot, ".gitignore");
|
|
2437
2492
|
const gitignoreContent = await readFile12(gitignorePath, "utf-8");
|
|
2438
2493
|
const matcher = ignoreFactory();
|
|
2439
2494
|
matcher.add(gitignoreContent);
|
|
@@ -2446,7 +2501,7 @@ function isIgnoredPath(candidatePath, projectRoot, matcher) {
|
|
|
2446
2501
|
if (!projectRoot || !matcher) {
|
|
2447
2502
|
return false;
|
|
2448
2503
|
}
|
|
2449
|
-
const relativePath =
|
|
2504
|
+
const relativePath = path11.relative(projectRoot, candidatePath);
|
|
2450
2505
|
if (relativePath === "" || relativePath.startsWith("..")) {
|
|
2451
2506
|
return false;
|
|
2452
2507
|
}
|
|
@@ -2456,13 +2511,13 @@ function hashString(content) {
|
|
|
2456
2511
|
return createHash("sha256").update(content).digest("hex");
|
|
2457
2512
|
}
|
|
2458
2513
|
async function perFileHashes(projectRoot, mapping) {
|
|
2459
|
-
const root =
|
|
2514
|
+
const root = path11.resolve(projectRoot);
|
|
2460
2515
|
const paths = mapping.paths ?? [];
|
|
2461
2516
|
if (paths.length === 0) return [];
|
|
2462
2517
|
const result = [];
|
|
2463
2518
|
const gitignoreMatcher = await loadGitignoreMatcher(root);
|
|
2464
2519
|
for (const p of paths) {
|
|
2465
|
-
const absPath =
|
|
2520
|
+
const absPath = path11.join(root, p);
|
|
2466
2521
|
const st = await stat3(absPath);
|
|
2467
2522
|
if (st.isFile()) {
|
|
2468
2523
|
result.push({ path: p, hash: await hashFile(absPath) });
|
|
@@ -2473,7 +2528,7 @@ async function perFileHashes(projectRoot, mapping) {
|
|
|
2473
2528
|
});
|
|
2474
2529
|
for (const h of hashes) {
|
|
2475
2530
|
result.push({
|
|
2476
|
-
path:
|
|
2531
|
+
path: path11.join(p, h.path).split(path11.sep).join("/"),
|
|
2477
2532
|
hash: h.hash
|
|
2478
2533
|
});
|
|
2479
2534
|
}
|
|
@@ -2482,12 +2537,12 @@ async function perFileHashes(projectRoot, mapping) {
|
|
|
2482
2537
|
return result;
|
|
2483
2538
|
}
|
|
2484
2539
|
async function hashForMapping(projectRoot, mapping) {
|
|
2485
|
-
const root =
|
|
2540
|
+
const root = path11.resolve(projectRoot);
|
|
2486
2541
|
const paths = mapping.paths ?? [];
|
|
2487
2542
|
if (paths.length === 0) throw new Error("Invalid mapping for hash: no paths");
|
|
2488
2543
|
const pairs = [];
|
|
2489
2544
|
for (const p of paths) {
|
|
2490
|
-
const absPath =
|
|
2545
|
+
const absPath = path11.join(root, p);
|
|
2491
2546
|
const st = await stat3(absPath);
|
|
2492
2547
|
if (st.isFile()) {
|
|
2493
2548
|
pairs.push({ path: p, hash: await hashFile(absPath) });
|
|
@@ -2502,9 +2557,9 @@ async function hashForMapping(projectRoot, mapping) {
|
|
|
2502
2557
|
|
|
2503
2558
|
// src/core/drift-detector.ts
|
|
2504
2559
|
import { access } from "fs/promises";
|
|
2505
|
-
import
|
|
2560
|
+
import path12 from "path";
|
|
2506
2561
|
async function detectDrift(graph, filterNodePath) {
|
|
2507
|
-
const projectRoot =
|
|
2562
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2508
2563
|
const driftState = await readDriftState(graph.rootPath);
|
|
2509
2564
|
const entries = [];
|
|
2510
2565
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -2578,7 +2633,7 @@ async function diagnoseChangedFiles(projectRoot, mapping, storedFileHashes) {
|
|
|
2578
2633
|
}
|
|
2579
2634
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
2580
2635
|
for (const mp of mappingPaths) {
|
|
2581
|
-
const absPath =
|
|
2636
|
+
const absPath = path12.join(projectRoot, mp);
|
|
2582
2637
|
try {
|
|
2583
2638
|
await access(absPath);
|
|
2584
2639
|
return false;
|
|
@@ -2588,7 +2643,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
2588
2643
|
return true;
|
|
2589
2644
|
}
|
|
2590
2645
|
async function syncDriftState(graph, nodePath) {
|
|
2591
|
-
const projectRoot =
|
|
2646
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2592
2647
|
const node = graph.nodes.get(nodePath);
|
|
2593
2648
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
2594
2649
|
const mapping = node.meta.mapping;
|
|
@@ -2760,10 +2815,10 @@ function registerTreeCommand(program2) {
|
|
|
2760
2815
|
let roots;
|
|
2761
2816
|
let showProjectName;
|
|
2762
2817
|
if (options.root?.trim()) {
|
|
2763
|
-
const
|
|
2764
|
-
const node = graph.nodes.get(
|
|
2818
|
+
const path16 = options.root.trim().replace(/\/$/, "");
|
|
2819
|
+
const node = graph.nodes.get(path16);
|
|
2765
2820
|
if (!node) {
|
|
2766
|
-
process.stderr.write(`Error: path '${
|
|
2821
|
+
process.stderr.write(`Error: path '${path16}' not found
|
|
2767
2822
|
`);
|
|
2768
2823
|
process.exit(1);
|
|
2769
2824
|
}
|
|
@@ -2851,7 +2906,7 @@ function registerOwnerCommand(program2) {
|
|
|
2851
2906
|
|
|
2852
2907
|
// src/core/dependency-resolver.ts
|
|
2853
2908
|
import { execSync as execSync2 } from "child_process";
|
|
2854
|
-
import
|
|
2909
|
+
import path13 from "path";
|
|
2855
2910
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2856
2911
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
2857
2912
|
function filterRelationType(relType, filter) {
|
|
@@ -2928,7 +2983,7 @@ function registerDepsCommand(program2) {
|
|
|
2928
2983
|
// src/core/graph-from-git.ts
|
|
2929
2984
|
import { mkdtemp, rm } from "fs/promises";
|
|
2930
2985
|
import { tmpdir } from "os";
|
|
2931
|
-
import
|
|
2986
|
+
import path14 from "path";
|
|
2932
2987
|
import { execSync as execSync3 } from "child_process";
|
|
2933
2988
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
2934
2989
|
const yggPath = ".yggdrasil";
|
|
@@ -2939,8 +2994,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
2939
2994
|
return null;
|
|
2940
2995
|
}
|
|
2941
2996
|
try {
|
|
2942
|
-
tmpDir = await mkdtemp(
|
|
2943
|
-
const archivePath =
|
|
2997
|
+
tmpDir = await mkdtemp(path14.join(tmpdir(), "ygg-git-"));
|
|
2998
|
+
const archivePath = path14.join(tmpDir, "archive.tar");
|
|
2944
2999
|
execSync3(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
2945
3000
|
cwd: projectRoot,
|
|
2946
3001
|
stdio: "pipe"
|
|
@@ -3010,14 +3065,14 @@ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
|
|
|
3010
3065
|
}
|
|
3011
3066
|
const chains = [];
|
|
3012
3067
|
for (const node of transitiveOnly) {
|
|
3013
|
-
const
|
|
3068
|
+
const path16 = [];
|
|
3014
3069
|
let current = node;
|
|
3015
3070
|
while (current) {
|
|
3016
|
-
|
|
3071
|
+
path16.unshift(current);
|
|
3017
3072
|
current = parent.get(current);
|
|
3018
3073
|
}
|
|
3019
|
-
if (
|
|
3020
|
-
chains.push(
|
|
3074
|
+
if (path16.length >= 2) {
|
|
3075
|
+
chains.push(path16.map((p) => `<- ${p}`).join(" "));
|
|
3021
3076
|
}
|
|
3022
3077
|
}
|
|
3023
3078
|
return chains.sort();
|
|
@@ -3069,7 +3124,7 @@ function registerImpactCommand(program2) {
|
|
|
3069
3124
|
}
|
|
3070
3125
|
}
|
|
3071
3126
|
}
|
|
3072
|
-
const budget = graph.config.quality?.context_budget ?? { warning:
|
|
3127
|
+
const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
|
|
3073
3128
|
process.stdout.write(`Impact of changes in ${nodePath}:
|
|
3074
3129
|
|
|
3075
3130
|
`);
|
|
@@ -3160,11 +3215,11 @@ ${changedLine}${budgetLine}${driftLine}
|
|
|
3160
3215
|
// src/io/journal-store.ts
|
|
3161
3216
|
import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access2 } from "fs/promises";
|
|
3162
3217
|
import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
|
|
3163
|
-
import
|
|
3218
|
+
import path15 from "path";
|
|
3164
3219
|
var JOURNAL_FILE = ".journal.yaml";
|
|
3165
3220
|
var ARCHIVE_DIR = "journals-archive";
|
|
3166
3221
|
async function readJournal(yggRoot) {
|
|
3167
|
-
const filePath =
|
|
3222
|
+
const filePath = path15.join(yggRoot, JOURNAL_FILE);
|
|
3168
3223
|
try {
|
|
3169
3224
|
const content = await readFile13(filePath, "utf-8");
|
|
3170
3225
|
const raw = parseYaml8(content);
|
|
@@ -3179,13 +3234,13 @@ async function appendJournalEntry(yggRoot, note, target) {
|
|
|
3179
3234
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3180
3235
|
const entry = target ? { at, target, note } : { at, note };
|
|
3181
3236
|
entries.push(entry);
|
|
3182
|
-
const filePath =
|
|
3237
|
+
const filePath = path15.join(yggRoot, JOURNAL_FILE);
|
|
3183
3238
|
const content = stringifyYaml2({ entries });
|
|
3184
3239
|
await writeFile4(filePath, content, "utf-8");
|
|
3185
3240
|
return entry;
|
|
3186
3241
|
}
|
|
3187
3242
|
async function archiveJournal(yggRoot) {
|
|
3188
|
-
const journalPath =
|
|
3243
|
+
const journalPath = path15.join(yggRoot, JOURNAL_FILE);
|
|
3189
3244
|
try {
|
|
3190
3245
|
await access2(journalPath);
|
|
3191
3246
|
} catch {
|
|
@@ -3193,12 +3248,12 @@ async function archiveJournal(yggRoot) {
|
|
|
3193
3248
|
}
|
|
3194
3249
|
const entries = await readJournal(yggRoot);
|
|
3195
3250
|
if (entries.length === 0) return null;
|
|
3196
|
-
const archiveDir =
|
|
3251
|
+
const archiveDir = path15.join(yggRoot, ARCHIVE_DIR);
|
|
3197
3252
|
await mkdir3(archiveDir, { recursive: true });
|
|
3198
3253
|
const now = /* @__PURE__ */ new Date();
|
|
3199
3254
|
const timestamp = `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}-${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`;
|
|
3200
3255
|
const archiveName = `.journal.${timestamp}.yaml`;
|
|
3201
|
-
const archivePath =
|
|
3256
|
+
const archivePath = path15.join(archiveDir, archiveName);
|
|
3202
3257
|
await rename(journalPath, archivePath);
|
|
3203
3258
|
return { archiveName, entryCount: entries.length };
|
|
3204
3259
|
}
|