@chrisdudek/yg 0.1.0 → 0.2.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/dist/bin.js +252 -192
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +12 -4
- package/dist/templates/rules.ts +138 -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
|
|
|
@@ -75,10 +83,22 @@ var AGENT_RULES_CONTENT = `# Yggdrasil - System Semantic Memory (Operating Manua
|
|
|
75
83
|
You are working in a repository managed by Yggdrasil.
|
|
76
84
|
Yggdrasil is a persistent, structured semantic memory graph stored in \`.yggdrasil/\`. It maps the repository, dictates system rules, and assembles implementation contexts.
|
|
77
85
|
|
|
86
|
+
**Why:** AI agents degrade with project size \u2014 too little context breaks contracts, too much context drowns in noise. The graph is persistent semantic memory: agents need exactly the right tokens for the task, not the whole repo. Small, precise context beats massive noisy. A repository with Yggdrasil is self-aware.
|
|
87
|
+
|
|
78
88
|
THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strictly.
|
|
79
89
|
|
|
80
90
|
---
|
|
81
91
|
|
|
92
|
+
## 0. ATOMIC INVARIANT \u2014 OVERRIDES EVERYTHING
|
|
93
|
+
|
|
94
|
+
**CODE \u2194 GRAPH ARE ONE.**
|
|
95
|
+
- Code changed \u2192 graph artifacts MUST be updated IN THE SAME RESPONSE. No exceptions.
|
|
96
|
+
- Graph changed \u2192 source files MUST be verified/updated IN THE SAME RESPONSE. No exceptions.
|
|
97
|
+
- There is NO "code-only" response. There is NO "graph-only" response (unless explicitly a dry-run plan).
|
|
98
|
+
- If you complete a response and one side is changed but the other is not, you have VIOLATED the core contract of Yggdrasil.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
82
102
|
## 1. CORE PRINCIPLES (NON-NEGOTIABLE)
|
|
83
103
|
|
|
84
104
|
1. **Graph First, Always:** Before answering a question, modifying code, or planning a feature, you MUST consult the graph.
|
|
@@ -88,6 +108,21 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
|
|
|
88
108
|
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
109
|
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
110
|
7. **Never Touch Operational Metadata:** NEVER manually edit \`.yggdrasil/.drift-state\` or \`.yggdrasil/.journal.yaml\`.
|
|
111
|
+
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. When you stop, always explain the context and available options clearly so the user can make an informed choice.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 1.5 FAILURE STATES
|
|
116
|
+
|
|
117
|
+
You have fundamentally broken Yggdrasil if you do any of the following:
|
|
118
|
+
- \u274C You modified source code without having run \`yg owner --file <path>\` for each modified file in the SAME response.
|
|
119
|
+
- \u274C You modified source code without updating graph artifacts in the SAME response.
|
|
120
|
+
- \u274C You modified graph files without verifying/updating source code alignment in the SAME response.
|
|
121
|
+
- \u274C You resolved a code\u2194graph inconsistency without asking the user first.
|
|
122
|
+
- \u274C You created or edited a graph element without reading its schema in \`.yggdrasil/templates/\`.
|
|
123
|
+
- \u274C You ran \`yg drift-sync\` before updating graph artifacts.
|
|
124
|
+
- \u274C You ran \`yg drift-sync\` after a graph-only change without verifying source files.
|
|
125
|
+
- \u274C You used Blackbox coverage for greenfield/new code.
|
|
91
126
|
|
|
92
127
|
---
|
|
93
128
|
|
|
@@ -96,15 +131,15 @@ THIS PROMPT IS YOUR ENTIRE OPERATING MANUAL. Read it carefully. Follow it strict
|
|
|
96
131
|
You do not need explicit "session" commands. Follow these conversational triggers:
|
|
97
132
|
|
|
98
133
|
### A. Preflight (First message of the conversation)
|
|
99
|
-
Always execute these commands before doing anything else:
|
|
134
|
+
Always execute these commands before doing anything else. *(Exception: If the user's request is clearly read-only, run ONLY step 1).* **Read-only** means the user asks only for explanation, clarification, analysis, or evaluation \u2014 no code or graph modification will occur. Examples: "explain this", "evaluate X", "what does Y do?", "analyze Z". If unsure, run full preflight.
|
|
100
135
|
1. \`yg journal-read\` -> If entries exist, consolidate them into the graph, then \`yg journal-archive\`.
|
|
101
136
|
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
137
|
3. \`yg status\` -> Report graph health.
|
|
103
138
|
4. \`yg validate\` -> If W008 stale-knowledge appears, update the knowledge artifacts to reflect current node state.
|
|
104
139
|
|
|
105
|
-
### B. Wrap-up
|
|
106
|
-
Triggered by phrases like: "
|
|
107
|
-
**Note: The graph should ALREADY be up to date.
|
|
140
|
+
### B. Session Verification (Wrap-up)
|
|
141
|
+
Triggered by phrases like: "we're done", "wrap up", "that's enough", "done".
|
|
142
|
+
**Note: The graph should ALREADY be up to date. If the graph requires massive updates at this stage, YOU HAVE FAILED.**
|
|
108
143
|
1. If iterative journal mode was used: consolidate notes to the graph, then \`yg journal-archive\`.
|
|
109
144
|
2. \`yg drift\` -> Check if files changed manually during the conversation.
|
|
110
145
|
3. \`yg validate\` -> Fix any structural errors.
|
|
@@ -112,64 +147,68 @@ Triggered by phrases like: "ko\u0144czymy", "wrap up", "to tyle", "gotowe".
|
|
|
112
147
|
|
|
113
148
|
---
|
|
114
149
|
|
|
115
|
-
## 3. WORKFLOW: MODIFYING OR CREATING FILES
|
|
150
|
+
## 3. WORKFLOW: MODIFYING OR CREATING FILES (Code-First)
|
|
116
151
|
|
|
117
152
|
You are NOT ALLOWED to edit or create source code without establishing graph coverage first.
|
|
118
153
|
|
|
154
|
+
**Gate:** Before using any tool that modifies files, you MUST have run \`yg owner --file <path>\` for each file you intend to modify. If you have not \u2014 run it first, then proceed. No exceptions. Gate applies to **source files** (files outside \`.yggdrasil/\`). For graph files (\`.yggdrasil/model/\`, \`.yggdrasil/aspects/\`, etc.), follow the Graph Modification Checklist in section 4 instead.
|
|
155
|
+
|
|
119
156
|
**Step 1: Check coverage** -> Run \`yg owner --file <path>\`
|
|
120
157
|
|
|
121
158
|
**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)
|
|
159
|
+
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
160
|
|
|
129
|
-
|
|
161
|
+
- [ ] 1. Read Specification (ran \`yg build-context\`)
|
|
162
|
+
- [ ] 2. Modify Source Code
|
|
163
|
+
- [ ] 3. Sync Graph Artifacts (manually edit the node's artifact files IMMEDIATELY to match new code behavior)
|
|
164
|
+
- [ ] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
|
|
130
165
|
|
|
131
166
|
**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.
|
|
167
|
+
STOP. Do not modify the code. First determine: **Is this greenfield or existing code?**
|
|
139
168
|
|
|
169
|
+
* **If GREENFIELD (empty directory, new project):** Do NOT offer blackbox. Create proper nodes (reverse engineering or upfront design) before implementing.
|
|
170
|
+
* **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.
|
|
171
|
+
* **If EXISTING CODE (legacy, third-party):** Present the user with 3 options and wait:
|
|
172
|
+
* **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
|
|
173
|
+
* **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node to establish ownership without deep semantic exploration.
|
|
174
|
+
* **Option 3: Abort/Change Plan:** Do not touch the file.
|
|
140
175
|
|
|
141
176
|
---
|
|
142
177
|
|
|
143
|
-
## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS
|
|
144
|
-
|
|
145
|
-
When adding features or changing architecture, update the graph FIRST.
|
|
178
|
+
## 4. WORKFLOW: MODIFYING THE GRAPH & BLAST RADIUS (Graph-First)
|
|
146
179
|
|
|
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.
|
|
180
|
+
When adding features, changing architecture, or doing graph-first design:
|
|
150
181
|
|
|
151
182
|
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
183
|
2. **Read Config & Templates:**
|
|
153
|
-
|
|
154
|
-
|
|
184
|
+
* Check \`.yggdrasil/config.yaml\` for allowed \`node_types\` and \`tags\`.
|
|
185
|
+
* **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
186
|
3. **Validate & Fix:** Run \`yg validate\`. You must fix all E-codes (Errors).
|
|
156
|
-
4. **Token Economy & W-codes
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
187
|
+
4. **Token Economy & W-codes:**
|
|
188
|
+
* W005/W006: Context package too large. Consider splitting the node.
|
|
189
|
+
* W008: Stale semantic memory. Update knowledge artifacts.
|
|
190
|
+
|
|
191
|
+
**Graph Modification Checklist**
|
|
192
|
+
Whenever you change the graph structure or semantics, you MUST output and execute this exact checklist:
|
|
193
|
+
|
|
194
|
+
- [ ] 1. Read schema from \`.yggdrasil/templates/\` (node.yaml, aspect.yaml, flow.yaml, or knowledge.yaml for the element type)
|
|
195
|
+
- [ ] 2. Edit graph files (\`node.yaml\`, artifacts)
|
|
196
|
+
- [ ] 3. Verify corresponding source files exist and their behavior matches updated artifacts
|
|
197
|
+
- [ ] 4. Validate (ran \`yg validate\` \u2014 fix all Errors)
|
|
198
|
+
- [ ] 5. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER steps 2-3 are confirmed)
|
|
160
199
|
|
|
161
|
-
**Journaling (Iterative Mode):**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
200
|
+
**Journaling (Iterative Mode Scope):**
|
|
201
|
+
* **Default:** Write changes directly to graph files immediately. Do not defer.
|
|
202
|
+
* **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
203
|
|
|
165
204
|
---
|
|
166
205
|
|
|
167
206
|
## 5. PATH CONVENTIONS (CRITICAL)
|
|
168
207
|
|
|
169
208
|
To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
209
|
+
* **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
|
|
210
|
+
* **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
|
|
211
|
+
* **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
|
|
173
212
|
|
|
174
213
|
---
|
|
175
214
|
|
|
@@ -177,35 +216,84 @@ To avoid broken references (\`E004\`, \`E005\`), use correct relative paths:
|
|
|
177
216
|
|
|
178
217
|
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
218
|
|
|
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.
|
|
219
|
+
* **\`.yggdrasil/config.yaml\`**: Defines \`node_types\`, \`tags\`, \`artifacts\`, \`knowledge_categories\`.
|
|
220
|
+
* **\`.yggdrasil/templates/\`**: Schemas for each graph layer \u2014 \`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`.
|
|
221
|
+
* **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files.
|
|
222
|
+
* **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` and \`.md\` content.
|
|
223
|
+
* **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` and \`.md\` content.
|
|
224
|
+
* **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom. Directory contains \`knowledge.yaml\` and \`.md\` content.
|
|
188
225
|
|
|
189
226
|
---
|
|
190
227
|
|
|
191
|
-
## 7.
|
|
228
|
+
## 7. CONTEXT ASSEMBLY & KNOWLEDGE DECONSTRUCTION (HOW TO MAP FILES)
|
|
229
|
+
|
|
230
|
+
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.
|
|
231
|
+
|
|
232
|
+
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.
|
|
233
|
+
|
|
234
|
+
### CRITICAL RULE: CAPTURE INTENT, BUT NEVER INVENT IT
|
|
235
|
+
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".
|
|
236
|
+
|
|
237
|
+
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.
|
|
238
|
+
2. **NEVER Invent the "Why":** Files that imply human judgment (like \`decisions.md\` or \`knowledge/invariants\`) must reflect ACTUAL human choices.
|
|
239
|
+
3. **NO Hallucinations:** You MUST NEVER infer or hallucinate a rationale, an architectural decision, or a business rule.
|
|
240
|
+
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.
|
|
241
|
+
|
|
242
|
+
When mapping a file, execute this mental routing:
|
|
243
|
+
|
|
244
|
+
### Layer 1: Unit Identity (Local Node Artifacts)
|
|
245
|
+
* **What goes here:** Things exclusively true for this specific node.
|
|
246
|
+
* **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\`.
|
|
247
|
+
* **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.
|
|
248
|
+
* **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.
|
|
249
|
+
|
|
250
|
+
**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.
|
|
251
|
+
|
|
252
|
+
### Layer 2: Surroundings (Relations & Flows)
|
|
253
|
+
* **What goes here:** How this node interacts with others. You must not duplicate external interfaces locally.
|
|
254
|
+
* **Routing:**
|
|
255
|
+
* 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).
|
|
256
|
+
* 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.
|
|
257
|
+
|
|
258
|
+
### Layer 3: Domain Context (Hierarchy)
|
|
259
|
+
* **What goes here:** Business rules shared by a family of nodes.
|
|
260
|
+
* **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.
|
|
261
|
+
|
|
262
|
+
### Layer 4: Cross-Cutting Rules (Aspects)
|
|
263
|
+
* **What goes here:** Horizontal requirements like logging, auth, rate-limiting, or specific frameworks.
|
|
264
|
+
* **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.
|
|
265
|
+
|
|
266
|
+
### Layer 5: Long-Term Memory (Knowledge Elements)
|
|
267
|
+
* **What goes here:** Global architectural decisions, design patterns, and systemic invariants.
|
|
268
|
+
* **Routing:** Read \`config.yaml\` (the \`knowledge_categories\` section) to know what categories exist.
|
|
269
|
+
* If the file implements a standard pattern: Do not describe the pattern locally. Add a \`knowledge\` reference in \`node.yaml\` to the existing pattern.
|
|
270
|
+
* 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.
|
|
271
|
+
|
|
272
|
+
**THE COMPLETENESS CHECK:**
|
|
273
|
+
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?"*
|
|
274
|
+
- If no -> You missed a local constraint, a relation, or you failed to capture the user's provided rationale.
|
|
275
|
+
- If yes, but the local files are bloated -> You failed to deconstruct knowledge into Tags, Aspects, Flows, and Hierarchy. Fix the routing.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 8. CLI TOOLS REFERENCE (\`yg\`)
|
|
192
280
|
|
|
193
281
|
Always use these exact commands.
|
|
194
282
|
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
283
|
+
* \`yg owner --file <file_path>\` -> Find owning node.
|
|
284
|
+
* \`yg build-context --node <node_path>\` -> Assemble strict specification.
|
|
285
|
+
* \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
|
|
286
|
+
* \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
|
|
287
|
+
* \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
|
|
288
|
+
* \`yg status\` -> Graph health metrics.
|
|
289
|
+
* \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
|
|
290
|
+
* \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
|
|
291
|
+
* \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
|
|
204
292
|
|
|
205
293
|
*(Iterative mode only)*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
294
|
+
* \`yg journal-read\`
|
|
295
|
+
* \`yg journal-add --note "<content>" [--target <node_path>]\`
|
|
296
|
+
* \`yg journal-archive\`
|
|
209
297
|
`;
|
|
210
298
|
|
|
211
299
|
// src/templates/platform.ts
|
|
@@ -532,7 +620,7 @@ function registerInitCommand(program2) {
|
|
|
532
620
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
533
621
|
process.stdout.write(" .yggdrasil/knowledge/ (decisions, patterns, invariants)\n");
|
|
534
622
|
process.stdout.write(
|
|
535
|
-
" .yggdrasil/templates/ (
|
|
623
|
+
" .yggdrasil/templates/ (node, aspect, flow, knowledge)\n"
|
|
536
624
|
);
|
|
537
625
|
process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
|
|
538
626
|
|
|
@@ -546,7 +634,7 @@ function registerInitCommand(program2) {
|
|
|
546
634
|
|
|
547
635
|
// src/core/graph-loader.ts
|
|
548
636
|
import { readdir as readdir3 } from "fs/promises";
|
|
549
|
-
import
|
|
637
|
+
import path6 from "path";
|
|
550
638
|
|
|
551
639
|
// src/io/config-parser.ts
|
|
552
640
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -554,7 +642,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
554
642
|
var DEFAULT_QUALITY = {
|
|
555
643
|
min_artifact_length: 50,
|
|
556
644
|
max_direct_relations: 10,
|
|
557
|
-
context_budget: { warning:
|
|
645
|
+
context_budget: { warning: 1e4, error: 2e4 },
|
|
558
646
|
knowledge_staleness_days: 90
|
|
559
647
|
};
|
|
560
648
|
async function parseConfig(filePath) {
|
|
@@ -869,30 +957,24 @@ function parseScope(raw, filePath) {
|
|
|
869
957
|
|
|
870
958
|
// src/io/template-parser.ts
|
|
871
959
|
import { readFile as readFile9 } from "fs/promises";
|
|
960
|
+
import path4 from "path";
|
|
872
961
|
import { parse as parseYaml6 } from "yaml";
|
|
873
|
-
async function
|
|
962
|
+
async function parseSchema(filePath) {
|
|
874
963
|
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
|
-
};
|
|
964
|
+
parseYaml6(content);
|
|
965
|
+
const schemaType = path4.basename(filePath, path4.extname(filePath));
|
|
966
|
+
return { schemaType };
|
|
885
967
|
}
|
|
886
968
|
|
|
887
969
|
// src/utils/paths.ts
|
|
888
|
-
import
|
|
970
|
+
import path5 from "path";
|
|
889
971
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
890
972
|
import { stat as stat2 } from "fs/promises";
|
|
891
973
|
async function findYggRoot(projectRoot) {
|
|
892
|
-
let current =
|
|
893
|
-
const root =
|
|
974
|
+
let current = path5.resolve(projectRoot);
|
|
975
|
+
const root = path5.parse(current).root;
|
|
894
976
|
while (true) {
|
|
895
|
-
const yggPath =
|
|
977
|
+
const yggPath = path5.join(current, ".yggdrasil");
|
|
896
978
|
try {
|
|
897
979
|
const st = await stat2(yggPath);
|
|
898
980
|
if (!st.isDirectory()) {
|
|
@@ -906,7 +988,7 @@ async function findYggRoot(projectRoot) {
|
|
|
906
988
|
if (current === root) {
|
|
907
989
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
908
990
|
}
|
|
909
|
-
current =
|
|
991
|
+
current = path5.dirname(current);
|
|
910
992
|
continue;
|
|
911
993
|
}
|
|
912
994
|
throw err;
|
|
@@ -922,18 +1004,18 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
922
1004
|
if (normalizedInput.length === 0) {
|
|
923
1005
|
throw new Error("Path cannot be empty");
|
|
924
1006
|
}
|
|
925
|
-
const absolute =
|
|
926
|
-
const relative =
|
|
927
|
-
const isOutside = relative.startsWith("..") ||
|
|
1007
|
+
const absolute = path5.resolve(projectRoot, normalizedInput);
|
|
1008
|
+
const relative = path5.relative(projectRoot, absolute);
|
|
1009
|
+
const isOutside = relative.startsWith("..") || path5.isAbsolute(relative);
|
|
928
1010
|
if (isOutside) {
|
|
929
1011
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
930
1012
|
}
|
|
931
|
-
return relative.split(
|
|
1013
|
+
return relative.split(path5.sep).join("/");
|
|
932
1014
|
}
|
|
933
1015
|
|
|
934
1016
|
// src/core/graph-loader.ts
|
|
935
1017
|
function toModelPath(absolutePath, modelDir) {
|
|
936
|
-
return
|
|
1018
|
+
return path6.relative(modelDir, absolutePath).split(path6.sep).join("/");
|
|
937
1019
|
}
|
|
938
1020
|
var FALLBACK_CONFIG = {
|
|
939
1021
|
name: "",
|
|
@@ -949,14 +1031,14 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
949
1031
|
let configError;
|
|
950
1032
|
let config = FALLBACK_CONFIG;
|
|
951
1033
|
try {
|
|
952
|
-
config = await parseConfig(
|
|
1034
|
+
config = await parseConfig(path6.join(yggRoot, "config.yaml"));
|
|
953
1035
|
} catch (error) {
|
|
954
1036
|
if (!options.tolerateInvalidConfig) {
|
|
955
1037
|
throw error;
|
|
956
1038
|
}
|
|
957
1039
|
configError = error.message;
|
|
958
1040
|
}
|
|
959
|
-
const modelDir =
|
|
1041
|
+
const modelDir = path6.join(yggRoot, "model");
|
|
960
1042
|
const nodes = /* @__PURE__ */ new Map();
|
|
961
1043
|
const nodeParseErrors = [];
|
|
962
1044
|
const artifactFilenames = Object.keys(config.artifacts ?? {});
|
|
@@ -970,13 +1052,13 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
970
1052
|
}
|
|
971
1053
|
throw err;
|
|
972
1054
|
}
|
|
973
|
-
const aspects = await loadAspects(
|
|
974
|
-
const flows = await loadFlows(
|
|
1055
|
+
const aspects = await loadAspects(path6.join(yggRoot, "aspects"));
|
|
1056
|
+
const flows = await loadFlows(path6.join(yggRoot, "flows"));
|
|
975
1057
|
const knowledge = await loadKnowledge(
|
|
976
|
-
|
|
1058
|
+
path6.join(yggRoot, "knowledge"),
|
|
977
1059
|
config.knowledge_categories
|
|
978
1060
|
);
|
|
979
|
-
const
|
|
1061
|
+
const schemas = await loadSchemas(path6.join(yggRoot, "templates"));
|
|
980
1062
|
return {
|
|
981
1063
|
config,
|
|
982
1064
|
configError,
|
|
@@ -985,7 +1067,7 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
985
1067
|
aspects,
|
|
986
1068
|
flows,
|
|
987
1069
|
knowledge,
|
|
988
|
-
|
|
1070
|
+
schemas,
|
|
989
1071
|
rootPath: yggRoot
|
|
990
1072
|
};
|
|
991
1073
|
}
|
|
@@ -999,7 +1081,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
999
1081
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1000
1082
|
let meta;
|
|
1001
1083
|
try {
|
|
1002
|
-
meta = await parseNodeYaml(
|
|
1084
|
+
meta = await parseNodeYaml(path6.join(dirPath, "node.yaml"));
|
|
1003
1085
|
} catch (err) {
|
|
1004
1086
|
nodeParseErrors.push({
|
|
1005
1087
|
nodePath: graphPath,
|
|
@@ -1023,7 +1105,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1023
1105
|
if (!entry.isDirectory()) continue;
|
|
1024
1106
|
if (entry.name.startsWith(".")) continue;
|
|
1025
1107
|
await scanModelDirectory(
|
|
1026
|
-
|
|
1108
|
+
path6.join(dirPath, entry.name),
|
|
1027
1109
|
modelDir,
|
|
1028
1110
|
node,
|
|
1029
1111
|
nodes,
|
|
@@ -1036,7 +1118,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1036
1118
|
if (!entry.isDirectory()) continue;
|
|
1037
1119
|
if (entry.name.startsWith(".")) continue;
|
|
1038
1120
|
await scanModelDirectory(
|
|
1039
|
-
|
|
1121
|
+
path6.join(dirPath, entry.name),
|
|
1040
1122
|
modelDir,
|
|
1041
1123
|
null,
|
|
1042
1124
|
nodes,
|
|
@@ -1052,8 +1134,8 @@ async function loadAspects(aspectsDir) {
|
|
|
1052
1134
|
const aspects = [];
|
|
1053
1135
|
for (const entry of entries) {
|
|
1054
1136
|
if (!entry.isDirectory()) continue;
|
|
1055
|
-
const aspectYamlPath =
|
|
1056
|
-
const aspect = await parseAspect(
|
|
1137
|
+
const aspectYamlPath = path6.join(aspectsDir, entry.name, "aspect.yaml");
|
|
1138
|
+
const aspect = await parseAspect(path6.join(aspectsDir, entry.name), aspectYamlPath);
|
|
1057
1139
|
aspects.push(aspect);
|
|
1058
1140
|
}
|
|
1059
1141
|
return aspects;
|
|
@@ -1067,8 +1149,8 @@ async function loadFlows(flowsDir) {
|
|
|
1067
1149
|
const flows = [];
|
|
1068
1150
|
for (const entry of entries) {
|
|
1069
1151
|
if (!entry.isDirectory()) continue;
|
|
1070
|
-
const flowYamlPath =
|
|
1071
|
-
const flow = await parseFlow(
|
|
1152
|
+
const flowYamlPath = path6.join(flowsDir, entry.name, "flow.yaml");
|
|
1153
|
+
const flow = await parseFlow(path6.join(flowsDir, entry.name), flowYamlPath);
|
|
1072
1154
|
flows.push(flow);
|
|
1073
1155
|
}
|
|
1074
1156
|
return flows;
|
|
@@ -1084,12 +1166,12 @@ async function loadKnowledge(knowledgeDir, categories) {
|
|
|
1084
1166
|
for (const catEntry of catEntries) {
|
|
1085
1167
|
if (!catEntry.isDirectory()) continue;
|
|
1086
1168
|
if (!categorySet.has(catEntry.name)) continue;
|
|
1087
|
-
const catPath =
|
|
1169
|
+
const catPath = path6.join(knowledgeDir, catEntry.name);
|
|
1088
1170
|
const itemEntries = await readdir3(catPath, { withFileTypes: true });
|
|
1089
1171
|
for (const itemEntry of itemEntries) {
|
|
1090
1172
|
if (!itemEntry.isDirectory()) continue;
|
|
1091
|
-
const itemDir =
|
|
1092
|
-
const knowledgeYamlPath =
|
|
1173
|
+
const itemDir = path6.join(catPath, itemEntry.name);
|
|
1174
|
+
const knowledgeYamlPath = path6.join(itemDir, "knowledge.yaml");
|
|
1093
1175
|
const relativePath = `${catEntry.name}/${itemEntry.name}`;
|
|
1094
1176
|
const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
|
|
1095
1177
|
items.push(item);
|
|
@@ -1099,17 +1181,17 @@ async function loadKnowledge(knowledgeDir, categories) {
|
|
|
1099
1181
|
}
|
|
1100
1182
|
return items;
|
|
1101
1183
|
}
|
|
1102
|
-
async function
|
|
1184
|
+
async function loadSchemas(templatesDir) {
|
|
1103
1185
|
try {
|
|
1104
1186
|
const entries = await readdir3(templatesDir, { withFileTypes: true });
|
|
1105
|
-
const
|
|
1187
|
+
const schemas = [];
|
|
1106
1188
|
for (const entry of entries) {
|
|
1107
1189
|
if (!entry.isFile()) continue;
|
|
1108
1190
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1191
|
+
const s = await parseSchema(path6.join(templatesDir, entry.name));
|
|
1192
|
+
schemas.push(s);
|
|
1111
1193
|
}
|
|
1112
|
-
return
|
|
1194
|
+
return schemas;
|
|
1113
1195
|
} catch {
|
|
1114
1196
|
return [];
|
|
1115
1197
|
}
|
|
@@ -1117,7 +1199,7 @@ async function loadTemplates(templatesDir) {
|
|
|
1117
1199
|
|
|
1118
1200
|
// src/core/context-builder.ts
|
|
1119
1201
|
import { readFile as readFile10 } from "fs/promises";
|
|
1120
|
-
import
|
|
1202
|
+
import path7 from "path";
|
|
1121
1203
|
|
|
1122
1204
|
// src/utils/tokens.ts
|
|
1123
1205
|
function estimateTokens(text) {
|
|
@@ -1270,7 +1352,7 @@ ${a.content}`).join("\n\n");
|
|
|
1270
1352
|
}
|
|
1271
1353
|
async function buildOwnLayer(node, config, graphRootPath) {
|
|
1272
1354
|
const parts = [];
|
|
1273
|
-
const nodeYamlPath =
|
|
1355
|
+
const nodeYamlPath = path7.join(graphRootPath, "model", node.path, "node.yaml");
|
|
1274
1356
|
try {
|
|
1275
1357
|
const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
|
|
1276
1358
|
parts.push(`### node.yaml
|
|
@@ -1387,13 +1469,13 @@ function collectAncestors(node) {
|
|
|
1387
1469
|
|
|
1388
1470
|
// src/core/validator.ts
|
|
1389
1471
|
import { readdir as readdir4 } from "fs/promises";
|
|
1390
|
-
import
|
|
1472
|
+
import path9 from "path";
|
|
1391
1473
|
|
|
1392
1474
|
// src/utils/git.ts
|
|
1393
1475
|
import { execSync } from "child_process";
|
|
1394
|
-
import
|
|
1476
|
+
import path8 from "path";
|
|
1395
1477
|
function getLastCommitTimestamp(projectRoot, relativePath) {
|
|
1396
|
-
const normalized =
|
|
1478
|
+
const normalized = path8.normalize(relativePath).replace(/\\/g, "/");
|
|
1397
1479
|
try {
|
|
1398
1480
|
const out = execSync(`git log -1 --format=%ct -- "${normalized}"`, {
|
|
1399
1481
|
cwd: projectRoot,
|
|
@@ -1441,8 +1523,8 @@ async function validate(graph, scope = "all") {
|
|
|
1441
1523
|
issues.push(...await checkContextBudget(graph));
|
|
1442
1524
|
issues.push(...checkHighFanOut(graph));
|
|
1443
1525
|
issues.push(...await checkStaleKnowledge(graph));
|
|
1444
|
-
issues.push(...checkTemplates(graph));
|
|
1445
1526
|
}
|
|
1527
|
+
issues.push(...checkSchemas(graph));
|
|
1446
1528
|
issues.push(...checkRelationTargets(graph));
|
|
1447
1529
|
issues.push(...checkNoCycles(graph));
|
|
1448
1530
|
issues.push(...checkMappingOverlap(graph));
|
|
@@ -1815,7 +1897,7 @@ function checkScopeTagsDefined(graph) {
|
|
|
1815
1897
|
async function checkUnknownKnowledgeCategories(graph) {
|
|
1816
1898
|
const issues = [];
|
|
1817
1899
|
const categorySet = new Set((graph.config.knowledge_categories ?? []).map((c) => c.name));
|
|
1818
|
-
const knowledgeDir =
|
|
1900
|
+
const knowledgeDir = path9.join(graph.rootPath, "knowledge");
|
|
1819
1901
|
const existingDirs = /* @__PURE__ */ new Set();
|
|
1820
1902
|
try {
|
|
1821
1903
|
const entries = await readdir4(knowledgeDir, { withFileTypes: true });
|
|
@@ -1946,7 +2028,7 @@ async function checkMissingPatternExamples(graph) {
|
|
|
1946
2028
|
const issues = [];
|
|
1947
2029
|
const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
|
|
1948
2030
|
if (!hasPatterns) return issues;
|
|
1949
|
-
const patternsDir =
|
|
2031
|
+
const patternsDir = path9.join(graph.rootPath, "knowledge", "patterns");
|
|
1950
2032
|
try {
|
|
1951
2033
|
const entries = await readdir4(patternsDir, { withFileTypes: true });
|
|
1952
2034
|
const exampleExtensions = /* @__PURE__ */ new Set([
|
|
@@ -1962,10 +2044,10 @@ async function checkMissingPatternExamples(graph) {
|
|
|
1962
2044
|
]);
|
|
1963
2045
|
for (const e of entries) {
|
|
1964
2046
|
if (!e.isDirectory()) continue;
|
|
1965
|
-
const itemDir =
|
|
2047
|
+
const itemDir = path9.join(patternsDir, e.name);
|
|
1966
2048
|
const itemEntries = await readdir4(itemDir, { withFileTypes: true });
|
|
1967
2049
|
const hasExample = itemEntries.some(
|
|
1968
|
-
(f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(
|
|
2050
|
+
(f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path9.extname(f.name).toLowerCase()))
|
|
1969
2051
|
);
|
|
1970
2052
|
if (!hasExample) {
|
|
1971
2053
|
issues.push({
|
|
@@ -2016,8 +2098,8 @@ function getNodesInScope(k, graph) {
|
|
|
2016
2098
|
async function checkStaleKnowledge(graph) {
|
|
2017
2099
|
const issues = [];
|
|
2018
2100
|
const stalenessDays = graph.config.quality?.knowledge_staleness_days ?? 90;
|
|
2019
|
-
const projectRoot =
|
|
2020
|
-
const yggRel =
|
|
2101
|
+
const projectRoot = path9.dirname(graph.rootPath);
|
|
2102
|
+
const yggRel = path9.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
|
|
2021
2103
|
for (const k of graph.knowledge) {
|
|
2022
2104
|
const scopeNodes = getNodesInScope(k, graph);
|
|
2023
2105
|
if (scopeNodes.length === 0) continue;
|
|
@@ -2096,51 +2178,29 @@ function checkUnpairedEvents(graph) {
|
|
|
2096
2178
|
}
|
|
2097
2179
|
return issues;
|
|
2098
2180
|
}
|
|
2099
|
-
|
|
2181
|
+
var REQUIRED_SCHEMAS = ["node", "aspect", "flow", "knowledge"];
|
|
2182
|
+
function checkSchemas(graph) {
|
|
2100
2183
|
const issues = [];
|
|
2101
|
-
const
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
for (const template of graph.templates) {
|
|
2105
|
-
if (!allowedTypes.has(template.nodeType)) {
|
|
2184
|
+
const present = new Set(graph.schemas.map((s) => s.schemaType));
|
|
2185
|
+
for (const required of REQUIRED_SCHEMAS) {
|
|
2186
|
+
if (!present.has(required)) {
|
|
2106
2187
|
issues.push({
|
|
2107
|
-
severity: "
|
|
2108
|
-
code: "
|
|
2109
|
-
rule: "
|
|
2110
|
-
message: `
|
|
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) {
|
|
2125
|
-
issues.push({
|
|
2126
|
-
severity: "error",
|
|
2127
|
-
code: "E016",
|
|
2128
|
-
rule: "duplicate-template",
|
|
2129
|
-
message: `Multiple templates for node_type '${template.nodeType}'`
|
|
2188
|
+
severity: "warning",
|
|
2189
|
+
code: "W010",
|
|
2190
|
+
rule: "missing-schema",
|
|
2191
|
+
message: `Schema '${required}.yaml' missing from .yggdrasil/templates/`
|
|
2130
2192
|
});
|
|
2131
|
-
} else {
|
|
2132
|
-
typeToTemplate.set(template.nodeType, template.nodeType);
|
|
2133
2193
|
}
|
|
2134
2194
|
}
|
|
2135
2195
|
return issues;
|
|
2136
2196
|
}
|
|
2137
2197
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
2138
2198
|
const issues = [];
|
|
2139
|
-
const modelDir =
|
|
2199
|
+
const modelDir = path9.join(graph.rootPath, "model");
|
|
2140
2200
|
async function scanDir(dirPath, segments) {
|
|
2141
2201
|
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
2142
2202
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
2143
|
-
const dirName =
|
|
2203
|
+
const dirName = path9.basename(dirPath);
|
|
2144
2204
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
2145
2205
|
const hasContent = entries.some((e) => e.isFile()) || entries.some((e) => e.isDirectory());
|
|
2146
2206
|
const graphPath = segments.join("/");
|
|
@@ -2157,7 +2217,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2157
2217
|
if (!entry.isDirectory()) continue;
|
|
2158
2218
|
if (RESERVED_DIRS.has(entry.name)) continue;
|
|
2159
2219
|
if (entry.name.startsWith(".")) continue;
|
|
2160
|
-
await scanDir(
|
|
2220
|
+
await scanDir(path9.join(dirPath, entry.name), [...segments, entry.name]);
|
|
2161
2221
|
}
|
|
2162
2222
|
}
|
|
2163
2223
|
try {
|
|
@@ -2165,7 +2225,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2165
2225
|
for (const entry of rootEntries) {
|
|
2166
2226
|
if (!entry.isDirectory()) continue;
|
|
2167
2227
|
if (entry.name.startsWith(".")) continue;
|
|
2168
|
-
await scanDir(
|
|
2228
|
+
await scanDir(path9.join(modelDir, entry.name), [entry.name]);
|
|
2169
2229
|
}
|
|
2170
2230
|
} catch {
|
|
2171
2231
|
}
|
|
@@ -2330,7 +2390,7 @@ import chalk2 from "chalk";
|
|
|
2330
2390
|
// src/io/drift-state-store.ts
|
|
2331
2391
|
import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
2332
2392
|
import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
|
|
2333
|
-
import
|
|
2393
|
+
import path10 from "path";
|
|
2334
2394
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2335
2395
|
function getCanonicalHash(entry) {
|
|
2336
2396
|
return typeof entry === "string" ? entry : entry.hash;
|
|
@@ -2339,7 +2399,7 @@ function getFileHashes(entry) {
|
|
|
2339
2399
|
return typeof entry === "object" ? entry.files : void 0;
|
|
2340
2400
|
}
|
|
2341
2401
|
async function readDriftState(yggRoot) {
|
|
2342
|
-
const filePath =
|
|
2402
|
+
const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
|
|
2343
2403
|
try {
|
|
2344
2404
|
const content = await readFile11(filePath, "utf-8");
|
|
2345
2405
|
const raw = parseYaml7(content);
|
|
@@ -2360,14 +2420,14 @@ async function readDriftState(yggRoot) {
|
|
|
2360
2420
|
}
|
|
2361
2421
|
}
|
|
2362
2422
|
async function writeDriftState(yggRoot, state) {
|
|
2363
|
-
const filePath =
|
|
2423
|
+
const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
|
|
2364
2424
|
const content = stringifyYaml(state);
|
|
2365
2425
|
await writeFile3(filePath, content, "utf-8");
|
|
2366
2426
|
}
|
|
2367
2427
|
|
|
2368
2428
|
// src/utils/hash.ts
|
|
2369
2429
|
import { readFile as readFile12, readdir as readdir5, stat as stat3 } from "fs/promises";
|
|
2370
|
-
import
|
|
2430
|
+
import path11 from "path";
|
|
2371
2431
|
import { createHash } from "crypto";
|
|
2372
2432
|
import { createRequire } from "module";
|
|
2373
2433
|
var require2 = createRequire(import.meta.url);
|
|
@@ -2377,7 +2437,7 @@ async function hashFile(filePath) {
|
|
|
2377
2437
|
return createHash("sha256").update(content).digest("hex");
|
|
2378
2438
|
}
|
|
2379
2439
|
async function hashPath(targetPath, options = {}) {
|
|
2380
|
-
const projectRoot = options.projectRoot ?
|
|
2440
|
+
const projectRoot = options.projectRoot ? path11.resolve(options.projectRoot) : void 0;
|
|
2381
2441
|
const gitignoreMatcher = await loadGitignoreMatcher(projectRoot);
|
|
2382
2442
|
const targetStat = await stat3(targetPath);
|
|
2383
2443
|
if (targetStat.isFile()) {
|
|
@@ -2400,7 +2460,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2400
2460
|
const entries = await readdir5(directoryPath, { withFileTypes: true });
|
|
2401
2461
|
const result = [];
|
|
2402
2462
|
for (const entry of entries) {
|
|
2403
|
-
const absoluteChildPath =
|
|
2463
|
+
const absoluteChildPath = path11.join(directoryPath, entry.name);
|
|
2404
2464
|
if (isIgnoredPath(absoluteChildPath, options.projectRoot, options.gitignoreMatcher)) {
|
|
2405
2465
|
continue;
|
|
2406
2466
|
}
|
|
@@ -2412,7 +2472,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2412
2472
|
);
|
|
2413
2473
|
for (const nestedEntry of nested) {
|
|
2414
2474
|
result.push({
|
|
2415
|
-
path:
|
|
2475
|
+
path: path11.relative(rootDirectoryPath, path11.join(absoluteChildPath, nestedEntry.path)),
|
|
2416
2476
|
hash: nestedEntry.hash
|
|
2417
2477
|
});
|
|
2418
2478
|
}
|
|
@@ -2422,7 +2482,7 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2422
2482
|
continue;
|
|
2423
2483
|
}
|
|
2424
2484
|
result.push({
|
|
2425
|
-
path:
|
|
2485
|
+
path: path11.relative(rootDirectoryPath, absoluteChildPath),
|
|
2426
2486
|
hash: await hashFile(absoluteChildPath)
|
|
2427
2487
|
});
|
|
2428
2488
|
}
|
|
@@ -2433,7 +2493,7 @@ async function loadGitignoreMatcher(projectRoot) {
|
|
|
2433
2493
|
return void 0;
|
|
2434
2494
|
}
|
|
2435
2495
|
try {
|
|
2436
|
-
const gitignorePath =
|
|
2496
|
+
const gitignorePath = path11.join(projectRoot, ".gitignore");
|
|
2437
2497
|
const gitignoreContent = await readFile12(gitignorePath, "utf-8");
|
|
2438
2498
|
const matcher = ignoreFactory();
|
|
2439
2499
|
matcher.add(gitignoreContent);
|
|
@@ -2446,7 +2506,7 @@ function isIgnoredPath(candidatePath, projectRoot, matcher) {
|
|
|
2446
2506
|
if (!projectRoot || !matcher) {
|
|
2447
2507
|
return false;
|
|
2448
2508
|
}
|
|
2449
|
-
const relativePath =
|
|
2509
|
+
const relativePath = path11.relative(projectRoot, candidatePath);
|
|
2450
2510
|
if (relativePath === "" || relativePath.startsWith("..")) {
|
|
2451
2511
|
return false;
|
|
2452
2512
|
}
|
|
@@ -2456,13 +2516,13 @@ function hashString(content) {
|
|
|
2456
2516
|
return createHash("sha256").update(content).digest("hex");
|
|
2457
2517
|
}
|
|
2458
2518
|
async function perFileHashes(projectRoot, mapping) {
|
|
2459
|
-
const root =
|
|
2519
|
+
const root = path11.resolve(projectRoot);
|
|
2460
2520
|
const paths = mapping.paths ?? [];
|
|
2461
2521
|
if (paths.length === 0) return [];
|
|
2462
2522
|
const result = [];
|
|
2463
2523
|
const gitignoreMatcher = await loadGitignoreMatcher(root);
|
|
2464
2524
|
for (const p of paths) {
|
|
2465
|
-
const absPath =
|
|
2525
|
+
const absPath = path11.join(root, p);
|
|
2466
2526
|
const st = await stat3(absPath);
|
|
2467
2527
|
if (st.isFile()) {
|
|
2468
2528
|
result.push({ path: p, hash: await hashFile(absPath) });
|
|
@@ -2473,7 +2533,7 @@ async function perFileHashes(projectRoot, mapping) {
|
|
|
2473
2533
|
});
|
|
2474
2534
|
for (const h of hashes) {
|
|
2475
2535
|
result.push({
|
|
2476
|
-
path:
|
|
2536
|
+
path: path11.join(p, h.path).split(path11.sep).join("/"),
|
|
2477
2537
|
hash: h.hash
|
|
2478
2538
|
});
|
|
2479
2539
|
}
|
|
@@ -2482,12 +2542,12 @@ async function perFileHashes(projectRoot, mapping) {
|
|
|
2482
2542
|
return result;
|
|
2483
2543
|
}
|
|
2484
2544
|
async function hashForMapping(projectRoot, mapping) {
|
|
2485
|
-
const root =
|
|
2545
|
+
const root = path11.resolve(projectRoot);
|
|
2486
2546
|
const paths = mapping.paths ?? [];
|
|
2487
2547
|
if (paths.length === 0) throw new Error("Invalid mapping for hash: no paths");
|
|
2488
2548
|
const pairs = [];
|
|
2489
2549
|
for (const p of paths) {
|
|
2490
|
-
const absPath =
|
|
2550
|
+
const absPath = path11.join(root, p);
|
|
2491
2551
|
const st = await stat3(absPath);
|
|
2492
2552
|
if (st.isFile()) {
|
|
2493
2553
|
pairs.push({ path: p, hash: await hashFile(absPath) });
|
|
@@ -2502,9 +2562,9 @@ async function hashForMapping(projectRoot, mapping) {
|
|
|
2502
2562
|
|
|
2503
2563
|
// src/core/drift-detector.ts
|
|
2504
2564
|
import { access } from "fs/promises";
|
|
2505
|
-
import
|
|
2565
|
+
import path12 from "path";
|
|
2506
2566
|
async function detectDrift(graph, filterNodePath) {
|
|
2507
|
-
const projectRoot =
|
|
2567
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2508
2568
|
const driftState = await readDriftState(graph.rootPath);
|
|
2509
2569
|
const entries = [];
|
|
2510
2570
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -2578,7 +2638,7 @@ async function diagnoseChangedFiles(projectRoot, mapping, storedFileHashes) {
|
|
|
2578
2638
|
}
|
|
2579
2639
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
2580
2640
|
for (const mp of mappingPaths) {
|
|
2581
|
-
const absPath =
|
|
2641
|
+
const absPath = path12.join(projectRoot, mp);
|
|
2582
2642
|
try {
|
|
2583
2643
|
await access(absPath);
|
|
2584
2644
|
return false;
|
|
@@ -2588,7 +2648,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
2588
2648
|
return true;
|
|
2589
2649
|
}
|
|
2590
2650
|
async function syncDriftState(graph, nodePath) {
|
|
2591
|
-
const projectRoot =
|
|
2651
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2592
2652
|
const node = graph.nodes.get(nodePath);
|
|
2593
2653
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
2594
2654
|
const mapping = node.meta.mapping;
|
|
@@ -2760,10 +2820,10 @@ function registerTreeCommand(program2) {
|
|
|
2760
2820
|
let roots;
|
|
2761
2821
|
let showProjectName;
|
|
2762
2822
|
if (options.root?.trim()) {
|
|
2763
|
-
const
|
|
2764
|
-
const node = graph.nodes.get(
|
|
2823
|
+
const path16 = options.root.trim().replace(/\/$/, "");
|
|
2824
|
+
const node = graph.nodes.get(path16);
|
|
2765
2825
|
if (!node) {
|
|
2766
|
-
process.stderr.write(`Error: path '${
|
|
2826
|
+
process.stderr.write(`Error: path '${path16}' not found
|
|
2767
2827
|
`);
|
|
2768
2828
|
process.exit(1);
|
|
2769
2829
|
}
|
|
@@ -2851,7 +2911,7 @@ function registerOwnerCommand(program2) {
|
|
|
2851
2911
|
|
|
2852
2912
|
// src/core/dependency-resolver.ts
|
|
2853
2913
|
import { execSync as execSync2 } from "child_process";
|
|
2854
|
-
import
|
|
2914
|
+
import path13 from "path";
|
|
2855
2915
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2856
2916
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
2857
2917
|
function filterRelationType(relType, filter) {
|
|
@@ -2928,7 +2988,7 @@ function registerDepsCommand(program2) {
|
|
|
2928
2988
|
// src/core/graph-from-git.ts
|
|
2929
2989
|
import { mkdtemp, rm } from "fs/promises";
|
|
2930
2990
|
import { tmpdir } from "os";
|
|
2931
|
-
import
|
|
2991
|
+
import path14 from "path";
|
|
2932
2992
|
import { execSync as execSync3 } from "child_process";
|
|
2933
2993
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
2934
2994
|
const yggPath = ".yggdrasil";
|
|
@@ -2939,8 +2999,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
2939
2999
|
return null;
|
|
2940
3000
|
}
|
|
2941
3001
|
try {
|
|
2942
|
-
tmpDir = await mkdtemp(
|
|
2943
|
-
const archivePath =
|
|
3002
|
+
tmpDir = await mkdtemp(path14.join(tmpdir(), "ygg-git-"));
|
|
3003
|
+
const archivePath = path14.join(tmpDir, "archive.tar");
|
|
2944
3004
|
execSync3(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
2945
3005
|
cwd: projectRoot,
|
|
2946
3006
|
stdio: "pipe"
|
|
@@ -3010,14 +3070,14 @@ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
|
|
|
3010
3070
|
}
|
|
3011
3071
|
const chains = [];
|
|
3012
3072
|
for (const node of transitiveOnly) {
|
|
3013
|
-
const
|
|
3073
|
+
const path16 = [];
|
|
3014
3074
|
let current = node;
|
|
3015
3075
|
while (current) {
|
|
3016
|
-
|
|
3076
|
+
path16.unshift(current);
|
|
3017
3077
|
current = parent.get(current);
|
|
3018
3078
|
}
|
|
3019
|
-
if (
|
|
3020
|
-
chains.push(
|
|
3079
|
+
if (path16.length >= 2) {
|
|
3080
|
+
chains.push(path16.map((p) => `<- ${p}`).join(" "));
|
|
3021
3081
|
}
|
|
3022
3082
|
}
|
|
3023
3083
|
return chains.sort();
|
|
@@ -3069,7 +3129,7 @@ function registerImpactCommand(program2) {
|
|
|
3069
3129
|
}
|
|
3070
3130
|
}
|
|
3071
3131
|
}
|
|
3072
|
-
const budget = graph.config.quality?.context_budget ?? { warning:
|
|
3132
|
+
const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
|
|
3073
3133
|
process.stdout.write(`Impact of changes in ${nodePath}:
|
|
3074
3134
|
|
|
3075
3135
|
`);
|
|
@@ -3160,11 +3220,11 @@ ${changedLine}${budgetLine}${driftLine}
|
|
|
3160
3220
|
// src/io/journal-store.ts
|
|
3161
3221
|
import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access2 } from "fs/promises";
|
|
3162
3222
|
import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
|
|
3163
|
-
import
|
|
3223
|
+
import path15 from "path";
|
|
3164
3224
|
var JOURNAL_FILE = ".journal.yaml";
|
|
3165
3225
|
var ARCHIVE_DIR = "journals-archive";
|
|
3166
3226
|
async function readJournal(yggRoot) {
|
|
3167
|
-
const filePath =
|
|
3227
|
+
const filePath = path15.join(yggRoot, JOURNAL_FILE);
|
|
3168
3228
|
try {
|
|
3169
3229
|
const content = await readFile13(filePath, "utf-8");
|
|
3170
3230
|
const raw = parseYaml8(content);
|
|
@@ -3179,13 +3239,13 @@ async function appendJournalEntry(yggRoot, note, target) {
|
|
|
3179
3239
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3180
3240
|
const entry = target ? { at, target, note } : { at, note };
|
|
3181
3241
|
entries.push(entry);
|
|
3182
|
-
const filePath =
|
|
3242
|
+
const filePath = path15.join(yggRoot, JOURNAL_FILE);
|
|
3183
3243
|
const content = stringifyYaml2({ entries });
|
|
3184
3244
|
await writeFile4(filePath, content, "utf-8");
|
|
3185
3245
|
return entry;
|
|
3186
3246
|
}
|
|
3187
3247
|
async function archiveJournal(yggRoot) {
|
|
3188
|
-
const journalPath =
|
|
3248
|
+
const journalPath = path15.join(yggRoot, JOURNAL_FILE);
|
|
3189
3249
|
try {
|
|
3190
3250
|
await access2(journalPath);
|
|
3191
3251
|
} catch {
|
|
@@ -3193,12 +3253,12 @@ async function archiveJournal(yggRoot) {
|
|
|
3193
3253
|
}
|
|
3194
3254
|
const entries = await readJournal(yggRoot);
|
|
3195
3255
|
if (entries.length === 0) return null;
|
|
3196
|
-
const archiveDir =
|
|
3256
|
+
const archiveDir = path15.join(yggRoot, ARCHIVE_DIR);
|
|
3197
3257
|
await mkdir3(archiveDir, { recursive: true });
|
|
3198
3258
|
const now = /* @__PURE__ */ new Date();
|
|
3199
3259
|
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
3260
|
const archiveName = `.journal.${timestamp}.yaml`;
|
|
3201
|
-
const archivePath =
|
|
3261
|
+
const archivePath = path15.join(archiveDir, archiveName);
|
|
3202
3262
|
await rename(journalPath, archivePath);
|
|
3203
3263
|
return { archiveName, entryCount: entries.length };
|
|
3204
3264
|
}
|