@chrisdudek/yg 0.3.2 → 1.0.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 +1462 -1158
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +3 -14
- package/dist/templates/rules.ts +252 -181
- package/graph-schemas/aspect.yaml +9 -0
- package/graph-schemas/flow.yaml +10 -0
- package/graph-schemas/node.yaml +19 -0
- package/package.json +2 -2
- package/graph-templates/aspect.yaml +0 -2
- package/graph-templates/flow.yaml +0 -8
- package/graph-templates/knowledge.yaml +0 -2
- package/graph-templates/node.yaml +0 -17
package/dist/bin.js
CHANGED
|
@@ -17,12 +17,10 @@ stack:
|
|
|
17
17
|
|
|
18
18
|
standards: ""
|
|
19
19
|
|
|
20
|
-
tags: []
|
|
21
|
-
|
|
22
20
|
node_types:
|
|
23
|
-
- module
|
|
24
|
-
- service
|
|
25
|
-
- library
|
|
21
|
+
- name: module
|
|
22
|
+
- name: service
|
|
23
|
+
- name: library
|
|
26
24
|
|
|
27
25
|
artifacts:
|
|
28
26
|
responsibility.md:
|
|
@@ -56,21 +54,12 @@ artifacts:
|
|
|
56
54
|
required: never
|
|
57
55
|
description: "Local design decisions and rationale \u2014 choices specific to this node, not system-wide"
|
|
58
56
|
|
|
59
|
-
knowledge_categories:
|
|
60
|
-
- name: decisions
|
|
61
|
-
description: "Global semantic decisions and their rationale"
|
|
62
|
-
- name: patterns
|
|
63
|
-
description: "Implementation conventions with examples"
|
|
64
|
-
- name: invariants
|
|
65
|
-
description: "System truths that must never be violated"
|
|
66
|
-
|
|
67
57
|
quality:
|
|
68
58
|
min_artifact_length: 50
|
|
69
59
|
max_direct_relations: 10
|
|
70
60
|
context_budget:
|
|
71
61
|
warning: 10000
|
|
72
62
|
error: 20000
|
|
73
|
-
knowledge_staleness_days: 90
|
|
74
63
|
`;
|
|
75
64
|
|
|
76
65
|
// src/templates/platform.ts
|
|
@@ -78,242 +67,302 @@ import { readFile, writeFile, mkdir } from "fs/promises";
|
|
|
78
67
|
import path from "path";
|
|
79
68
|
|
|
80
69
|
// src/templates/rules.ts
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
## 1. CORE PRINCIPLES (NON-NEGOTIABLE)
|
|
103
|
-
|
|
104
|
-
1. **Graph First, Always:** Before answering a question, modifying code, or planning a feature, you MUST consult the graph.
|
|
105
|
-
2. **Context is Sufficient:** If you feel the need to randomly explore source files to understand what a node should do, the graph is incomplete. **Fix the graph** (add decisions, interface details, constraints). Do not bypass the graph by reading raw code.
|
|
106
|
-
3. **Graph is Intended Truth:** If the code and graph diverge, the graph is the truth. If a code change is deliberate, update the graph to match.
|
|
107
|
-
4. **Exhaustive Coverage:** Every source file MUST belong to exactly one graph node. No orphaned files.
|
|
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.
|
|
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.
|
|
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
|
|
70
|
+
var CORE_PROTOCOL = `## CORE PROTOCOL
|
|
71
|
+
|
|
72
|
+
Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the repository and provides deterministic implementation context for every node. This document is your complete operating manual. Follow it strictly.
|
|
73
|
+
|
|
74
|
+
### Quick Start Protocol
|
|
75
|
+
|
|
76
|
+
\`\`\`
|
|
77
|
+
BEFORE reading, researching, planning, OR modifying ANY mapped file:
|
|
78
|
+
1. yg owner --file <path>
|
|
79
|
+
2. yg build-context --node <owner>
|
|
80
|
+
The context package is your primary source of understanding.
|
|
81
|
+
Raw file reads are for implementation details WITHIN a node you
|
|
82
|
+
already understand from its context package.
|
|
83
|
+
If the context package seems insufficient \u2014 enrich the graph.
|
|
84
|
+
Do not bypass it.
|
|
85
|
+
|
|
86
|
+
AFTER modifying:
|
|
87
|
+
3. Update graph artifacts to reflect changes
|
|
88
|
+
4. yg validate \u2014 fix all errors
|
|
89
|
+
5. yg drift-sync --node <owner>
|
|
116
90
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
-
|
|
126
|
-
|
|
91
|
+
EVERY conversation start:
|
|
92
|
+
yg preflight \u2192 act on findings (see Operations)
|
|
93
|
+
|
|
94
|
+
NEVER: modify code without graph coverage.
|
|
95
|
+
NEVER: read mapped source files to understand a component without
|
|
96
|
+
running yg build-context first \u2014 the graph captures intent,
|
|
97
|
+
constraints, and relations that source files cannot.
|
|
98
|
+
NEVER: invent rationale, business rules, or decisions.
|
|
99
|
+
NEVER: auto-resolve drift without asking the user.
|
|
100
|
+
WHEN UNSURE: ask the user. Never guess. Never assume.
|
|
101
|
+
\`\`\`
|
|
127
102
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
## 2. CONVERSATION LIFECYCLE (YOUR HABITS)
|
|
131
|
-
|
|
132
|
-
You do not need explicit "session" commands. Follow these conversational triggers:
|
|
133
|
-
|
|
134
|
-
### A. Preflight (First message of the conversation)
|
|
135
|
-
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.
|
|
136
|
-
1. \`yg journal-read\` -> If entries exist, consolidate them into the graph, then \`yg journal-archive\`.
|
|
137
|
-
2. \`yg drift\` -> If **drift** (code changed, graph baseline stale): run \`yg drift-sync\` for each affected node. Do NOT ask the user \u2014 they do not need to know this step. If **missing** or **unmaterialized**: report and ask the user how to proceed.
|
|
138
|
-
3. \`yg status\` -> Report graph health.
|
|
139
|
-
4. \`yg validate\` -> If W008 stale-knowledge appears, update the knowledge artifacts to reflect current node state.
|
|
103
|
+
### Five Core Rules
|
|
140
104
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
105
|
+
1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and \`yg build-context\`. Always. The context package \u2014 not raw source \u2014 is your primary source of understanding.
|
|
106
|
+
2. **Code and graph are one.** Code changed \u2192 graph updated in the same response. Graph changed \u2192 source verified in the same response. No exceptions.
|
|
107
|
+
3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
|
|
108
|
+
4. **Always capture why.** When the user explains a reason, record it in the graph immediately. Conversation evaporates; graph persists.
|
|
109
|
+
5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
|
|
146
110
|
|
|
147
|
-
|
|
111
|
+
### Failure States
|
|
148
112
|
|
|
149
|
-
|
|
150
|
-
Triggered by phrases like: "we're done", "wrap up", "that's enough", "done", "ok".
|
|
151
|
-
**Note: The graph should ALREADY be up to date. If the graph requires massive updates at this stage, YOU HAVE FAILED.**
|
|
152
|
-
1. If iterative journal mode was used: consolidate notes to the graph, then \`yg journal-archive\`.
|
|
153
|
-
2. \`yg drift\` -> If drift detected, run \`yg drift-sync\` for each affected node. Do NOT ask \u2014 absorb automatically.
|
|
154
|
-
3. \`yg validate\` -> Fix any structural errors.
|
|
155
|
-
4. Report exactly what nodes and files were changed.
|
|
113
|
+
You have broken Yggdrasil if you do any of the following:
|
|
156
114
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
115
|
+
- \u274C Modified source code without running \`yg owner --file <path>\` first.
|
|
116
|
+
- \u274C Modified source code without updating graph artifacts in the same response.
|
|
117
|
+
- \u274C Modified graph files without verifying source code alignment in the same response.
|
|
118
|
+
- \u274C Resolved a code-graph inconsistency without asking the user first.
|
|
119
|
+
- \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
|
|
120
|
+
- \u274C Ran \`yg drift-sync\` before updating graph artifacts.
|
|
121
|
+
- \u274C Wrote a flow description that describes code sequences instead of a business process.
|
|
122
|
+
- \u274C Used an aspect identifier that has no corresponding \`aspects/\` directory.
|
|
123
|
+
- \u274C Placed a cross-cutting requirement in a local node artifact instead of an aspect.
|
|
124
|
+
- \u274C Invented a rationale, business rule, or architectural decision.
|
|
125
|
+
- \u274C Used blackbox coverage for greenfield (new) code.
|
|
126
|
+
- \u274C Answered a question about a mapped file without running \`yg build-context\` first.
|
|
127
|
+
- \u274C Read mapped source files to plan or research changes without running \`yg build-context\` first.
|
|
128
|
+
- \u274C Deferred \`yg drift-sync\` to the end of a multi-step task instead of running it incrementally after each logical group of changes.
|
|
160
129
|
|
|
161
|
-
|
|
130
|
+
### Escape Hatch
|
|
162
131
|
|
|
163
|
-
|
|
132
|
+
If the user explicitly requests a code-only change, comply but:
|
|
164
133
|
|
|
165
|
-
|
|
134
|
+
- Warn: "This creates drift. Run \`yg drift\` next session to reconcile."
|
|
135
|
+
- Do NOT run \`yg drift-sync\` \u2014 leave the drift visible.
|
|
166
136
|
|
|
167
|
-
|
|
168
|
-
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:
|
|
137
|
+
### Environment Check
|
|
169
138
|
|
|
170
|
-
|
|
171
|
-
- [ ] 2. Modify Source Code
|
|
172
|
-
- [ ] 3. Sync Graph Artifacts (manually edit the node's artifact files IMMEDIATELY to match new code behavior)
|
|
173
|
-
- [ ] 4. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER updating the graph)
|
|
139
|
+
Before preflight:
|
|
174
140
|
|
|
175
|
-
|
|
176
|
-
|
|
141
|
+
- Verify \`yg\` CLI is available. If not found, inform user and stop.
|
|
142
|
+
- If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
|
|
143
|
+
- If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.`;
|
|
144
|
+
var OPERATIONS = `## OPERATIONS
|
|
177
145
|
|
|
178
|
-
|
|
179
|
-
* **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.
|
|
180
|
-
* **If EXISTING CODE (legacy, third-party):** Present the user with 3 options and wait:
|
|
181
|
-
* **Option 1: Reverse Engineering:** Create/extend standard nodes to map the area fully before modifying.
|
|
182
|
-
* **Option 2: Blackbox Coverage:** Create a \`blackbox: true\` node to establish ownership without deep semantic exploration.
|
|
183
|
-
* **Option 3: Abort/Change Plan:** Do not touch the file.
|
|
184
|
-
|
|
185
|
-
---
|
|
146
|
+
### Conversation Lifecycle
|
|
186
147
|
|
|
187
|
-
|
|
148
|
+
\`\`\`
|
|
149
|
+
PREFLIGHT (every conversation, before any work):
|
|
150
|
+
- [ ] 1. yg preflight \u2192 read unified report
|
|
151
|
+
- [ ] 2. If journal entries: consolidate to graph, then yg journal-archive
|
|
152
|
+
- [ ] 3. If drift: resolve per Drift Resolution, then yg drift-sync per node
|
|
153
|
+
- [ ] 4. If validation errors: fix, re-run yg validate
|
|
154
|
+
Exception: read-only requests (explain, analyze) \u2014 skip preflight.
|
|
188
155
|
|
|
189
|
-
|
|
156
|
+
UNDERSTANDING mapped code (questions, research, OR planning):
|
|
157
|
+
- [ ] 1. yg owner --file <path>
|
|
158
|
+
- [ ] 2. Owner found \u2192 yg build-context --node <path>. Use context package as primary source.
|
|
159
|
+
- [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
|
|
160
|
+
Never use grep or raw file reads as primary understanding when graph coverage exists.
|
|
161
|
+
Raw reads supplement the context package \u2014 they do not replace it.
|
|
190
162
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* W005/W006: Context package too large. Consider splitting the node.
|
|
198
|
-
* W008: Stale semantic memory. Update knowledge artifacts.
|
|
163
|
+
WRAP-UP (user signals "done", "wrap up", "that's enough"):
|
|
164
|
+
- [ ] 1. Consolidate journal if used \u2192 yg journal-archive
|
|
165
|
+
- [ ] 2. yg drift --drifted-only \u2192 resolve
|
|
166
|
+
- [ ] 3. yg validate \u2192 fix errors
|
|
167
|
+
- [ ] 4. Report: which nodes and files were changed
|
|
168
|
+
\`\`\`
|
|
199
169
|
|
|
200
|
-
|
|
201
|
-
Whenever you change the graph structure or semantics, you MUST output and execute this exact checklist:
|
|
170
|
+
### Modify Source Code
|
|
202
171
|
|
|
203
|
-
|
|
204
|
-
- [ ] 2. Edit graph files (\`node.yaml\`, artifacts)
|
|
205
|
-
- [ ] 3. Verify corresponding source files exist and their behavior matches updated artifacts
|
|
206
|
-
- [ ] 4. Validate (ran \`yg validate\` \u2014 fix all Errors)
|
|
207
|
-
- [ ] 5. Baseline Hash (ran \`yg drift-sync\` ONLY AFTER steps 2-3 are confirmed)
|
|
172
|
+
You are not allowed to edit or create source code without establishing graph coverage first.
|
|
208
173
|
|
|
209
|
-
**
|
|
210
|
-
* **Default:** Write changes directly to graph files immediately. Do not defer.
|
|
211
|
-
* **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.
|
|
174
|
+
**Step 1** \u2014 Check coverage: \`yg owner --file <path>\`
|
|
212
175
|
|
|
213
|
-
|
|
176
|
+
**Step 2a** \u2014 Owner found: execute checklist:
|
|
214
177
|
|
|
215
|
-
|
|
178
|
+
- [ ] 1. Read specification: \`yg build-context --node <node_path>\`
|
|
179
|
+
- [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
|
|
180
|
+
- [ ] 3. Modify source code
|
|
181
|
+
- [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes
|
|
182
|
+
- [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
|
|
183
|
+
- [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
|
|
216
184
|
|
|
217
|
-
|
|
218
|
-
* **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
|
|
219
|
-
* **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
|
|
220
|
-
* **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
|
|
185
|
+
**Step 2b** \u2014 Owner not found: establish coverage first. Present options to the user:
|
|
221
186
|
|
|
222
|
-
|
|
187
|
+
*Partially mapped* (file unmapped but inside a mapped module): ask whether to add to existing node or create new one.
|
|
223
188
|
|
|
224
|
-
|
|
189
|
+
*Existing code:*
|
|
225
190
|
|
|
226
|
-
|
|
191
|
+
- Option A \u2014 Full node: create node(s), map files, write artifacts from code analysis
|
|
192
|
+
- Option B \u2014 Blackbox: create a blackbox node at agreed granularity
|
|
193
|
+
- Option C \u2014 Abort
|
|
227
194
|
|
|
228
|
-
*
|
|
229
|
-
* **\`.yggdrasil/templates/\`**: Schemas for each graph layer \u2014 \`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`.
|
|
230
|
-
* **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files.
|
|
231
|
-
* **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` and \`.md\` content.
|
|
232
|
-
* **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` and \`.md\` content.
|
|
233
|
-
* **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom. Directory contains \`knowledge.yaml\` and \`.md\` content.
|
|
195
|
+
*Greenfield (new code):* Only Option A. Blackbox is forbidden for new code. Create nodes with full artifacts, then materialize.
|
|
234
196
|
|
|
235
|
-
|
|
197
|
+
After the user chooses, return to Step 1 and follow Step 2a.
|
|
236
198
|
|
|
237
|
-
|
|
199
|
+
### Modify Graph
|
|
238
200
|
|
|
239
|
-
|
|
201
|
+
- [ ] 1. Read the relevant schema from \`schemas/\` before touching any YAML
|
|
202
|
+
- [ ] 2. Before changing an aspect or flow, check scope: \`yg impact --aspect <id>\` or \`yg impact --flow <name>\` \u2014 understand which nodes are affected before modifying shared rules or processes
|
|
203
|
+
- [ ] 3. Make changes
|
|
204
|
+
- [ ] 4. Run \`yg validate\` immediately \u2014 fix all errors
|
|
205
|
+
- [ ] 5. Verify affected source files are consistent \u2014 update if needed
|
|
206
|
+
- [ ] 6. Run \`yg drift-sync\` for affected nodes
|
|
240
207
|
|
|
241
|
-
|
|
208
|
+
### Reverse Engineering
|
|
242
209
|
|
|
243
|
-
|
|
244
|
-
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".
|
|
210
|
+
**Order:** aspects (cross-cutting patterns) \u2192 flows (business processes) \u2192 model nodes. Never create nodes before aspects and flows are understood.
|
|
245
211
|
|
|
246
|
-
|
|
247
|
-
2. **NEVER Invent the "Why":** Artifacts that imply human judgment (e.g. local decisions, \`knowledge/invariants\`) must reflect ACTUAL human choices.
|
|
248
|
-
3. **NO Hallucinations:** You MUST NEVER infer or hallucinate a rationale, an architectural decision, or a business rule.
|
|
249
|
-
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.
|
|
212
|
+
Per area checklist:
|
|
250
213
|
|
|
251
|
-
|
|
214
|
+
- [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
|
|
215
|
+
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
216
|
+
- [ ] 3. Create node directory, read \`schemas/node.yaml\`, create \`node.yaml\`
|
|
217
|
+
- [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
|
|
218
|
+
- [ ] 5. Identify relations \u2014 add to \`node.yaml\`
|
|
219
|
+
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
220
|
+
- [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
|
|
221
|
+
- [ ] 8. \`yg validate\` \u2014 fix errors
|
|
222
|
+
- [ ] 9. \`yg drift-sync --node <path>\`
|
|
252
223
|
|
|
253
|
-
|
|
254
|
-
* **What goes here:** Things exclusively true for this specific node.
|
|
255
|
-
* **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\`.
|
|
256
|
-
* For each artifact in \`config.artifacts\`, use its \`description\` to decide what content belongs there. Create optional artifacts (those with \`required: never\`) when the node has matching content. Extract from source; do not invent.
|
|
224
|
+
**When to ask:**
|
|
257
225
|
|
|
258
|
-
|
|
226
|
+
- Business process unclear: "This code appears to be part of a larger process. Can you describe what it means from a business perspective?"
|
|
227
|
+
- Constraint without rationale: "I see [constraint X]. Do you know why this exists? I want to record the reason, not just the rule."
|
|
228
|
+
- Unexplained architectural choice: "I see [approach X]. What was the reason for this choice?"
|
|
259
229
|
|
|
260
|
-
###
|
|
230
|
+
### Bootstrap Mode
|
|
261
231
|
|
|
262
|
-
|
|
232
|
+
Trigger: \`yg preflight\` shows 0 nodes, or no nodes cover the active work area.
|
|
263
233
|
|
|
264
|
-
|
|
234
|
+
- [ ] 1. Identify the active work area (files the user wants to modify)
|
|
235
|
+
- [ ] 2. Scan for cross-cutting patterns \u2192 create aspects
|
|
236
|
+
- [ ] 3. Ask user about business processes \u2192 create flows if applicable
|
|
237
|
+
- [ ] 4. Propose node structure for the area
|
|
238
|
+
- [ ] 5. Create node(s) with initial artifacts, map files
|
|
239
|
+
- [ ] 6. \`yg validate\`, \`yg drift-sync\`
|
|
240
|
+
- [ ] 7. Proceed with user's original request
|
|
265
241
|
|
|
266
|
-
|
|
242
|
+
Constraint: Do NOT map the entire repository. Focus on the active area. Expand incrementally.
|
|
267
243
|
|
|
268
|
-
|
|
244
|
+
### Drift Resolution
|
|
269
245
|
|
|
270
|
-
|
|
271
|
-
* **What goes here:** How this node interacts with others. You must not duplicate external interfaces locally.
|
|
272
|
-
* **Routing:**
|
|
273
|
-
* 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).
|
|
274
|
-
* 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.
|
|
275
|
-
* **Flows \u2014 writing flow content:** When creating or editing flow artifacts (e.g. \`description.md\` in \`flows/<name>/\`), write business-first: describe the process from user/business perspective. Technical details only as inserts when they clarify the flow. Not technical-first with business inserts.
|
|
246
|
+
Always ask the user before resolving drift. Never auto-resolve.
|
|
276
247
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
248
|
+
- **Source drift** (source files changed) \u2192 update graph artifacts to match source, then \`yg drift-sync\`
|
|
249
|
+
- **Graph drift** (graph artifacts changed) \u2192 review affected source, update if needed, then \`yg drift-sync\`
|
|
250
|
+
- **Full drift** (both changed) \u2192 present both sides to user, ask which direction wins
|
|
251
|
+
- **Missing** \u2192 ask: re-materialize or remove mapping?
|
|
252
|
+
- **Unmaterialized** \u2192 ask user how to proceed
|
|
280
253
|
|
|
281
|
-
|
|
282
|
-
* **What goes here:** Horizontal requirements like logging, auth, rate-limiting, or specific frameworks.
|
|
283
|
-
* **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.
|
|
254
|
+
Threshold: >10 drifted nodes \u2192 ask user which area to prioritize. Do not resolve all at once.
|
|
284
255
|
|
|
285
|
-
###
|
|
286
|
-
* **What goes here:** Global architectural decisions, design patterns, and systemic invariants.
|
|
287
|
-
* **Routing:** Read \`config.yaml\` (the \`knowledge_categories\` section) to know what categories exist.
|
|
288
|
-
* If the file implements a standard pattern: Do not describe the pattern locally. Add a \`knowledge\` reference in \`node.yaml\` to the existing pattern.
|
|
289
|
-
* 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.
|
|
256
|
+
### Error Recovery
|
|
290
257
|
|
|
291
|
-
**
|
|
292
|
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
258
|
+
- **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
|
|
259
|
+
- **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
|
|
260
|
+
- **Budget exceeded** \u2192 if \`yg build-context\` exits with error (context package exceeds budget), warn user: "This node should be split." Do not proceed with implementation.
|
|
261
|
+
- **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
|
|
262
|
+
- **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
|
|
263
|
+
var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
|
|
295
264
|
|
|
296
|
-
|
|
265
|
+
### Graph Structure
|
|
297
266
|
|
|
298
|
-
|
|
267
|
+
\`\`\`
|
|
268
|
+
.yggdrasil/
|
|
269
|
+
config.yaml \u2190 vocabulary, stack, node types, artifact rules, required aspects
|
|
270
|
+
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
271
|
+
aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
|
|
272
|
+
flows/ \u2190 why and in what process: business processes with node participation
|
|
273
|
+
schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
|
|
274
|
+
.drift-state \u2190 generated by CLI; never edit manually
|
|
275
|
+
.journal.yaml \u2190 generated by CLI; never edit manually
|
|
276
|
+
\`\`\`
|
|
299
277
|
|
|
300
|
-
|
|
278
|
+
Key facts:
|
|
279
|
+
|
|
280
|
+
- **Hierarchy:** nodes nest in \`model/\`. Children inherit parent context. Do not repeat parent content in children.
|
|
281
|
+
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
282
|
+
- **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
|
|
301
283
|
|
|
302
|
-
|
|
303
|
-
* \`yg build-context --node <node_path>\` -> Assemble strict specification.
|
|
304
|
-
* \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
|
|
305
|
-
* \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
|
|
306
|
-
* \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
|
|
307
|
-
* \`yg status\` -> Graph health metrics.
|
|
308
|
-
* \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
|
|
309
|
-
* \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
|
|
310
|
-
* \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
|
|
284
|
+
### Context Assembly
|
|
311
285
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
286
|
+
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. Trust the package \u2014 it assembles global config, hierarchy, own artifacts, aspects, and relational context. If the package is insufficient, enrich the graph. Do not bypass it with raw file exploration.
|
|
287
|
+
|
|
288
|
+
### Information Routing
|
|
289
|
+
|
|
290
|
+
When you encounter information, route it to the correct location:
|
|
291
|
+
|
|
292
|
+
- **Specific to this node** \u2192 local node artifact (check \`config.yaml artifacts\` for available types)
|
|
293
|
+
- **Rule for many nodes** \u2192 aspect (\`aspects/<id>/\` with \`aspect.yaml\` + content \`.md\` files). If applies to ALL nodes of a type \u2192 \`node_types[*].required_aspects\` in \`config.yaml\`
|
|
294
|
+
- **Business process** \u2192 flow (\`flows/<name>/\` with \`flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
295
|
+
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
296
|
+
- **Technology stack or standard** \u2192 \`config.yaml\` under \`stack\` or \`standards\` (+ \`rationale\` field)
|
|
297
|
+
- **Decision (why):** one node \u2192 local artifact; category of nodes \u2192 aspect content files; tech choice \u2192 \`config.yaml\` rationale field
|
|
298
|
+
|
|
299
|
+
### Creating Aspects
|
|
300
|
+
|
|
301
|
+
- [ ] 1. Read \`schemas/aspect.yaml\`
|
|
302
|
+
- [ ] 2. Create \`aspects/<id>/\` directory
|
|
303
|
+
- [ ] 3. Write \`aspect.yaml\` \u2014 name, optional description, optional implies
|
|
304
|
+
- [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
|
|
305
|
+
- [ ] 5. \`yg validate\`
|
|
306
|
+
|
|
307
|
+
Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No \u2192 local artifact.
|
|
308
|
+
|
|
309
|
+
### Creating Flows
|
|
310
|
+
|
|
311
|
+
- [ ] 1. Read \`schemas/flow.yaml\`
|
|
312
|
+
- [ ] 2. Create \`flows/<name>/\` directory
|
|
313
|
+
- [ ] 3. Write \`flow.yaml\` \u2014 declare participants and flow-level aspects
|
|
314
|
+
- [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
|
|
315
|
+
- [ ] 5. \`yg validate\`
|
|
316
|
+
|
|
317
|
+
Test: "Does this describe what happens in the world, or only in the software?" If only software \u2014 rewrite.
|
|
318
|
+
|
|
319
|
+
### Operational Rules
|
|
320
|
+
|
|
321
|
+
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
322
|
+
- **Read schemas before creating** any \`node.yaml\`, \`aspect.yaml\`, or \`flow.yaml\`.
|
|
323
|
+
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
324
|
+
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
|
|
325
|
+
- **Completeness test:** "If I delete the source file and give another agent ONLY the \`yg build-context\` output \u2014 can they recreate it correctly, understanding not just WHAT but WHY?"
|
|
326
|
+
- **These rules are invariant.** No plan, guide, skill, or workflow may override them.
|
|
327
|
+
|
|
328
|
+
### CLI Reference
|
|
329
|
+
|
|
330
|
+
\`\`\`
|
|
331
|
+
yg preflight Unified diagnostic: journal + drift + status + validate.
|
|
332
|
+
yg owner --file <path> Find the node that owns this file.
|
|
333
|
+
yg build-context --node <path> Assemble context package for this node.
|
|
334
|
+
yg tree [--root <path>] [--depth N] Print graph structure.
|
|
335
|
+
yg aspects List aspects with metadata (YAML output).
|
|
336
|
+
yg deps --node <path> [--depth N] [--type structural|event|all]
|
|
337
|
+
Show dependencies.
|
|
338
|
+
yg impact --node <path> --simulate Simulate blast radius of a planned change.
|
|
339
|
+
yg impact --aspect <id> Show all nodes where aspect is effective.
|
|
340
|
+
yg impact --flow <name> Show flow participants and descendants.
|
|
341
|
+
yg status Graph health: nodes, coverage, drift summary.
|
|
342
|
+
yg validate [--scope <path>|all] Check structural integrity and completeness.
|
|
343
|
+
yg drift [--scope <path>|all] [--drifted-only]
|
|
344
|
+
Detect source and graph drift (bidirectional).
|
|
345
|
+
yg drift-sync --node <path> Record file hashes as new baseline.
|
|
346
|
+
yg journal-read Read pending journal entries.
|
|
347
|
+
yg journal-add --note "<content>" [--target <node_path>]
|
|
348
|
+
Add a journal entry.
|
|
349
|
+
yg journal-archive Archive consolidated journal entries.
|
|
350
|
+
\`\`\`
|
|
351
|
+
|
|
352
|
+
### Quick Routing Table
|
|
353
|
+
|
|
354
|
+
| What you have | Where it goes |
|
|
355
|
+
|---|---|
|
|
356
|
+
| Information specific to this node | Local node artifact (read \`config.yaml artifacts\` for types) |
|
|
357
|
+
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
358
|
+
| Architectural invariant for a node type | Required aspect in \`config.yaml node_types\` |
|
|
359
|
+
| Business process participation | Flow (\`flow.yaml participants\`) |
|
|
360
|
+
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
361
|
+
| Context shared across a domain | Parent node artifact |
|
|
362
|
+
| Technology stack | \`config.yaml stack\` (+ \`rationale\` field) |
|
|
363
|
+
| Global coding standards | \`config.yaml standards\` |
|
|
316
364
|
`;
|
|
365
|
+
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n");
|
|
317
366
|
|
|
318
367
|
// src/templates/platform.ts
|
|
319
368
|
var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
|
|
@@ -556,10 +605,10 @@ function escapeRegex(s) {
|
|
|
556
605
|
}
|
|
557
606
|
|
|
558
607
|
// src/cli/init.ts
|
|
559
|
-
function
|
|
608
|
+
function getGraphSchemasDir() {
|
|
560
609
|
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
561
610
|
const packageRoot = path2.join(currentDir, "..");
|
|
562
|
-
return path2.join(packageRoot, "graph-
|
|
611
|
+
return path2.join(packageRoot, "graph-schemas");
|
|
563
612
|
}
|
|
564
613
|
var GITIGNORE_CONTENT = `.journal.yaml
|
|
565
614
|
journals-archive/
|
|
@@ -607,23 +656,20 @@ function registerInitCommand(program2) {
|
|
|
607
656
|
await mkdir2(path2.join(yggRoot, "model"), { recursive: true });
|
|
608
657
|
await mkdir2(path2.join(yggRoot, "aspects"), { recursive: true });
|
|
609
658
|
await mkdir2(path2.join(yggRoot, "flows"), { recursive: true });
|
|
610
|
-
|
|
611
|
-
await mkdir2(
|
|
612
|
-
|
|
613
|
-
const templatesDir = path2.join(yggRoot, "templates");
|
|
614
|
-
await mkdir2(templatesDir, { recursive: true });
|
|
615
|
-
const graphTemplatesDir = getGraphTemplatesDir();
|
|
659
|
+
const schemasDir = path2.join(yggRoot, "schemas");
|
|
660
|
+
await mkdir2(schemasDir, { recursive: true });
|
|
661
|
+
const graphSchemasDir = getGraphSchemasDir();
|
|
616
662
|
try {
|
|
617
|
-
const entries = await readdir(
|
|
618
|
-
const
|
|
619
|
-
for (const file of
|
|
620
|
-
const srcPath = path2.join(
|
|
663
|
+
const entries = await readdir(graphSchemasDir, { withFileTypes: true });
|
|
664
|
+
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
665
|
+
for (const file of schemaFiles) {
|
|
666
|
+
const srcPath = path2.join(graphSchemasDir, file);
|
|
621
667
|
const content = await readFile2(srcPath, "utf-8");
|
|
622
|
-
await writeFile2(path2.join(
|
|
668
|
+
await writeFile2(path2.join(schemasDir, file), content, "utf-8");
|
|
623
669
|
}
|
|
624
670
|
} catch (err) {
|
|
625
671
|
process.stderr.write(
|
|
626
|
-
`Warning: Could not copy graph
|
|
672
|
+
`Warning: Could not copy graph schemas from ${graphSchemasDir}: ${err.message}
|
|
627
673
|
`
|
|
628
674
|
);
|
|
629
675
|
}
|
|
@@ -637,10 +683,7 @@ function registerInitCommand(program2) {
|
|
|
637
683
|
process.stdout.write(" .yggdrasil/model/\n");
|
|
638
684
|
process.stdout.write(" .yggdrasil/aspects/\n");
|
|
639
685
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
640
|
-
process.stdout.write(" .yggdrasil/
|
|
641
|
-
process.stdout.write(
|
|
642
|
-
" .yggdrasil/templates/ (node, aspect, flow, knowledge)\n"
|
|
643
|
-
);
|
|
686
|
+
process.stdout.write(" .yggdrasil/schemas/ (node, aspect, flow)\n");
|
|
644
687
|
process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
|
|
645
688
|
|
|
646
689
|
`);
|
|
@@ -652,8 +695,8 @@ function registerInitCommand(program2) {
|
|
|
652
695
|
}
|
|
653
696
|
|
|
654
697
|
// src/core/graph-loader.ts
|
|
655
|
-
import { readdir as readdir3 } from "fs/promises";
|
|
656
|
-
import
|
|
698
|
+
import { readdir as readdir3, readFile as readFile9 } from "fs/promises";
|
|
699
|
+
import path7 from "path";
|
|
657
700
|
|
|
658
701
|
// src/io/config-parser.ts
|
|
659
702
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -661,8 +704,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
661
704
|
var DEFAULT_QUALITY = {
|
|
662
705
|
min_artifact_length: 50,
|
|
663
706
|
max_direct_relations: 10,
|
|
664
|
-
context_budget: { warning: 1e4, error: 2e4 }
|
|
665
|
-
knowledge_staleness_days: 90
|
|
707
|
+
context_budget: { warning: 1e4, error: 2e4 }
|
|
666
708
|
};
|
|
667
709
|
async function parseConfig(filePath) {
|
|
668
710
|
const content = await readFile3(filePath, "utf-8");
|
|
@@ -670,18 +712,34 @@ async function parseConfig(filePath) {
|
|
|
670
712
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
671
713
|
throw new Error(`config.yaml: missing or invalid 'name' field`);
|
|
672
714
|
}
|
|
673
|
-
const
|
|
674
|
-
if (!Array.isArray(
|
|
715
|
+
const nodeTypesRaw = raw.node_types;
|
|
716
|
+
if (!Array.isArray(nodeTypesRaw) || nodeTypesRaw.length === 0) {
|
|
675
717
|
throw new Error(`config.yaml: 'node_types' must be a non-empty array`);
|
|
676
718
|
}
|
|
719
|
+
const nodeTypes = nodeTypesRaw.map((item) => {
|
|
720
|
+
if (typeof item === "string") {
|
|
721
|
+
return { name: item };
|
|
722
|
+
}
|
|
723
|
+
if (typeof item === "object" && item !== null && "name" in item && typeof item.name === "string") {
|
|
724
|
+
const obj = item;
|
|
725
|
+
const requiredAspects = Array.isArray(obj.required_aspects) ? obj.required_aspects.filter((t) => typeof t === "string") : Array.isArray(obj.required_tags) ? obj.required_tags.filter((t) => typeof t === "string") : void 0;
|
|
726
|
+
return {
|
|
727
|
+
name: obj.name,
|
|
728
|
+
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
throw new Error(
|
|
732
|
+
`config.yaml: node_types entry must be string or { name, required_aspects? }`
|
|
733
|
+
);
|
|
734
|
+
});
|
|
677
735
|
const artifacts = raw.artifacts;
|
|
678
736
|
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
|
|
679
737
|
throw new Error(`config.yaml: 'artifacts' must be a non-empty object`);
|
|
680
738
|
}
|
|
681
739
|
const artifactsMap = {};
|
|
682
740
|
for (const [key, val] of Object.entries(artifacts)) {
|
|
683
|
-
if (key === "node") {
|
|
684
|
-
throw new Error(`config.yaml: artifact name 'node' is reserved`);
|
|
741
|
+
if (key === "node.yaml") {
|
|
742
|
+
throw new Error(`config.yaml: artifact name 'node.yaml' is reserved`);
|
|
685
743
|
}
|
|
686
744
|
const a = val;
|
|
687
745
|
const required = a.required;
|
|
@@ -690,10 +748,10 @@ async function parseConfig(filePath) {
|
|
|
690
748
|
}
|
|
691
749
|
if (typeof required === "object" && required && "when" in required) {
|
|
692
750
|
const when = required.when;
|
|
693
|
-
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && when.startsWith("has_tag:");
|
|
751
|
+
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
|
|
694
752
|
if (!validWhen) {
|
|
695
753
|
throw new Error(
|
|
696
|
-
`config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or
|
|
754
|
+
`config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
697
755
|
);
|
|
698
756
|
}
|
|
699
757
|
}
|
|
@@ -703,24 +761,6 @@ async function parseConfig(filePath) {
|
|
|
703
761
|
structural_context: a.structural_context ?? false
|
|
704
762
|
};
|
|
705
763
|
}
|
|
706
|
-
if (!("knowledge_categories" in raw)) {
|
|
707
|
-
throw new Error(
|
|
708
|
-
`config.yaml: missing 'knowledge_categories' field (required, may be empty list)`
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
const knowledgeCategoriesRaw = raw.knowledge_categories;
|
|
712
|
-
if (!Array.isArray(knowledgeCategoriesRaw)) {
|
|
713
|
-
throw new Error(`config.yaml: 'knowledge_categories' must be an array`);
|
|
714
|
-
}
|
|
715
|
-
const knowledgeCategories = knowledgeCategoriesRaw;
|
|
716
|
-
const categoryNames = /* @__PURE__ */ new Set();
|
|
717
|
-
for (const kc of knowledgeCategories) {
|
|
718
|
-
if (!kc?.name || typeof kc.name !== "string") continue;
|
|
719
|
-
if (categoryNames.has(kc.name)) {
|
|
720
|
-
throw new Error(`config.yaml: duplicate knowledge category '${kc.name}'`);
|
|
721
|
-
}
|
|
722
|
-
categoryNames.add(kc.name);
|
|
723
|
-
}
|
|
724
764
|
const qualityRaw = raw.quality;
|
|
725
765
|
const quality = qualityRaw ? {
|
|
726
766
|
min_artifact_length: qualityRaw.min_artifact_length ?? DEFAULT_QUALITY.min_artifact_length,
|
|
@@ -728,30 +768,19 @@ async function parseConfig(filePath) {
|
|
|
728
768
|
context_budget: {
|
|
729
769
|
warning: qualityRaw.context_budget?.warning ?? DEFAULT_QUALITY.context_budget.warning,
|
|
730
770
|
error: qualityRaw.context_budget?.error ?? DEFAULT_QUALITY.context_budget.error
|
|
731
|
-
}
|
|
732
|
-
knowledge_staleness_days: qualityRaw.knowledge_staleness_days ?? DEFAULT_QUALITY.knowledge_staleness_days
|
|
771
|
+
}
|
|
733
772
|
} : DEFAULT_QUALITY;
|
|
734
773
|
if (quality.context_budget.error < quality.context_budget.warning) {
|
|
735
774
|
throw new Error(
|
|
736
775
|
`config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
|
|
737
776
|
);
|
|
738
777
|
}
|
|
739
|
-
if (!("tags" in raw)) {
|
|
740
|
-
throw new Error(`config.yaml: missing 'tags' field (required, may be empty list)`);
|
|
741
|
-
}
|
|
742
|
-
const tags = raw.tags;
|
|
743
|
-
if (!Array.isArray(tags)) {
|
|
744
|
-
throw new Error(`config.yaml: 'tags' must be an array`);
|
|
745
|
-
}
|
|
746
|
-
const tagsList = tags.filter((t) => typeof t === "string");
|
|
747
778
|
return {
|
|
748
779
|
name: raw.name.trim(),
|
|
749
780
|
stack: raw.stack ?? {},
|
|
750
781
|
standards: typeof raw.standards === "string" ? raw.standards : "",
|
|
751
|
-
tags: tagsList,
|
|
752
782
|
node_types: nodeTypes,
|
|
753
783
|
artifacts: artifactsMap,
|
|
754
|
-
knowledge_categories: knowledgeCategories.filter((kc) => kc?.name),
|
|
755
784
|
quality
|
|
756
785
|
};
|
|
757
786
|
}
|
|
@@ -784,10 +813,9 @@ async function parseNodeYaml(filePath) {
|
|
|
784
813
|
return {
|
|
785
814
|
name: raw.name.trim(),
|
|
786
815
|
type: raw.type.trim(),
|
|
787
|
-
|
|
816
|
+
aspects: parseStringArray(raw.aspects) ?? parseStringArray(raw.tags),
|
|
788
817
|
blackbox: raw.blackbox ?? false,
|
|
789
818
|
relations: relations.length > 0 ? relations : void 0,
|
|
790
|
-
knowledge: parseStringArray(raw.knowledge),
|
|
791
819
|
mapping
|
|
792
820
|
};
|
|
793
821
|
}
|
|
@@ -887,25 +915,37 @@ async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles
|
|
|
887
915
|
}
|
|
888
916
|
|
|
889
917
|
// src/io/aspect-parser.ts
|
|
890
|
-
async function parseAspect(aspectDir, aspectYamlPath) {
|
|
918
|
+
async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
919
|
+
const idTrimmed = id?.trim() ?? "";
|
|
920
|
+
if (!idTrimmed) {
|
|
921
|
+
throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
|
|
922
|
+
}
|
|
891
923
|
const content = await readFile6(aspectYamlPath, "utf-8");
|
|
892
924
|
const raw = parseYaml3(content);
|
|
893
925
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
894
926
|
throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
|
|
895
927
|
}
|
|
896
|
-
|
|
897
|
-
throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'tag'`);
|
|
898
|
-
}
|
|
928
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
899
929
|
const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
|
|
930
|
+
let implies;
|
|
931
|
+
if (raw.implies !== void 0) {
|
|
932
|
+
if (!Array.isArray(raw.implies)) {
|
|
933
|
+
throw new Error(`Aspect file ${aspectYamlPath}: 'implies' must be an array of strings`);
|
|
934
|
+
}
|
|
935
|
+
implies = raw.implies.filter((t) => typeof t === "string");
|
|
936
|
+
}
|
|
900
937
|
return {
|
|
901
938
|
name: raw.name.trim(),
|
|
902
|
-
|
|
939
|
+
id: idTrimmed,
|
|
940
|
+
description,
|
|
941
|
+
implies,
|
|
903
942
|
artifacts
|
|
904
943
|
};
|
|
905
944
|
}
|
|
906
945
|
|
|
907
946
|
// src/io/flow-parser.ts
|
|
908
947
|
import { readFile as readFile7 } from "fs/promises";
|
|
948
|
+
import path4 from "path";
|
|
909
949
|
import { parse as parseYaml4 } from "yaml";
|
|
910
950
|
async function parseFlow(flowDir, flowYamlPath) {
|
|
911
951
|
const content = await readFile7(flowYamlPath, "utf-8");
|
|
@@ -921,79 +961,44 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
921
961
|
if (nodePaths.length === 0) {
|
|
922
962
|
throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
|
|
923
963
|
}
|
|
924
|
-
|
|
964
|
+
let aspects;
|
|
965
|
+
if (raw.aspects !== void 0) {
|
|
966
|
+
if (!Array.isArray(raw.aspects)) {
|
|
967
|
+
throw new Error(`flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
968
|
+
}
|
|
969
|
+
const aspectTags = raw.aspects.filter((a) => typeof a === "string");
|
|
970
|
+
aspects = aspectTags.length > 0 ? aspectTags : [];
|
|
971
|
+
}
|
|
925
972
|
const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
|
|
926
973
|
return {
|
|
974
|
+
path: path4.basename(flowDir),
|
|
927
975
|
name: raw.name.trim(),
|
|
928
976
|
nodes: nodePaths,
|
|
929
|
-
|
|
977
|
+
...aspects !== void 0 && { aspects },
|
|
930
978
|
artifacts
|
|
931
979
|
};
|
|
932
980
|
}
|
|
933
981
|
|
|
934
|
-
// src/io/
|
|
982
|
+
// src/io/schema-parser.ts
|
|
935
983
|
import { readFile as readFile8 } from "fs/promises";
|
|
984
|
+
import path5 from "path";
|
|
936
985
|
import { parse as parseYaml5 } from "yaml";
|
|
937
|
-
async function parseKnowledge(knowledgeDir, knowledgeYamlPath, category, relativePath) {
|
|
938
|
-
const content = await readFile8(knowledgeYamlPath, "utf-8");
|
|
939
|
-
const raw = parseYaml5(content);
|
|
940
|
-
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
941
|
-
throw new Error(`knowledge.yaml at ${knowledgeYamlPath}: missing or empty 'name'`);
|
|
942
|
-
}
|
|
943
|
-
const scope = parseScope(raw.scope, knowledgeYamlPath);
|
|
944
|
-
const artifacts = await readArtifacts(knowledgeDir, ["knowledge.yaml"]);
|
|
945
|
-
return {
|
|
946
|
-
name: raw.name.trim(),
|
|
947
|
-
scope,
|
|
948
|
-
category,
|
|
949
|
-
path: relativePath,
|
|
950
|
-
artifacts
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
function parseScope(raw, filePath) {
|
|
954
|
-
if (raw === "global") {
|
|
955
|
-
return "global";
|
|
956
|
-
}
|
|
957
|
-
if (raw && typeof raw === "object") {
|
|
958
|
-
const obj = raw;
|
|
959
|
-
if (Array.isArray(obj.tags)) {
|
|
960
|
-
const tags = obj.tags.filter((t) => typeof t === "string");
|
|
961
|
-
if (tags.length === 0) {
|
|
962
|
-
throw new Error(`knowledge.yaml at ${filePath}: scope.tags must be a non-empty array`);
|
|
963
|
-
}
|
|
964
|
-
return { tags };
|
|
965
|
-
}
|
|
966
|
-
if (Array.isArray(obj.nodes)) {
|
|
967
|
-
const nodes = obj.nodes.filter((n) => typeof n === "string");
|
|
968
|
-
if (nodes.length === 0) {
|
|
969
|
-
throw new Error(`knowledge.yaml at ${filePath}: scope.nodes must be a non-empty array`);
|
|
970
|
-
}
|
|
971
|
-
return { nodes };
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
throw new Error(`knowledge.yaml at ${filePath}: invalid 'scope' value`);
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// src/io/template-parser.ts
|
|
978
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
979
|
-
import path4 from "path";
|
|
980
|
-
import { parse as parseYaml6 } from "yaml";
|
|
981
986
|
async function parseSchema(filePath) {
|
|
982
|
-
const content = await
|
|
983
|
-
|
|
984
|
-
const schemaType =
|
|
987
|
+
const content = await readFile8(filePath, "utf-8");
|
|
988
|
+
parseYaml5(content);
|
|
989
|
+
const schemaType = path5.basename(filePath, path5.extname(filePath));
|
|
985
990
|
return { schemaType };
|
|
986
991
|
}
|
|
987
992
|
|
|
988
993
|
// src/utils/paths.ts
|
|
989
|
-
import
|
|
994
|
+
import path6 from "path";
|
|
990
995
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
991
996
|
import { stat as stat2 } from "fs/promises";
|
|
992
997
|
async function findYggRoot(projectRoot) {
|
|
993
|
-
let current =
|
|
994
|
-
const root =
|
|
998
|
+
let current = path6.resolve(projectRoot);
|
|
999
|
+
const root = path6.parse(current).root;
|
|
995
1000
|
while (true) {
|
|
996
|
-
const yggPath =
|
|
1001
|
+
const yggPath = path6.join(current, ".yggdrasil");
|
|
997
1002
|
try {
|
|
998
1003
|
const st = await stat2(yggPath);
|
|
999
1004
|
if (!st.isDirectory()) {
|
|
@@ -1007,7 +1012,7 @@ async function findYggRoot(projectRoot) {
|
|
|
1007
1012
|
if (current === root) {
|
|
1008
1013
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
1009
1014
|
}
|
|
1010
|
-
current =
|
|
1015
|
+
current = path6.dirname(current);
|
|
1011
1016
|
continue;
|
|
1012
1017
|
}
|
|
1013
1018
|
throw err;
|
|
@@ -1023,41 +1028,39 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
1023
1028
|
if (normalizedInput.length === 0) {
|
|
1024
1029
|
throw new Error("Path cannot be empty");
|
|
1025
1030
|
}
|
|
1026
|
-
const absolute =
|
|
1027
|
-
const relative =
|
|
1028
|
-
const isOutside = relative.startsWith("..") ||
|
|
1031
|
+
const absolute = path6.resolve(projectRoot, normalizedInput);
|
|
1032
|
+
const relative = path6.relative(projectRoot, absolute);
|
|
1033
|
+
const isOutside = relative.startsWith("..") || path6.isAbsolute(relative);
|
|
1029
1034
|
if (isOutside) {
|
|
1030
1035
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
1031
1036
|
}
|
|
1032
|
-
return relative.split(
|
|
1037
|
+
return relative.split(path6.sep).join("/");
|
|
1033
1038
|
}
|
|
1034
1039
|
|
|
1035
1040
|
// src/core/graph-loader.ts
|
|
1036
1041
|
function toModelPath(absolutePath, modelDir) {
|
|
1037
|
-
return
|
|
1042
|
+
return path7.relative(modelDir, absolutePath).split(path7.sep).join("/");
|
|
1038
1043
|
}
|
|
1039
1044
|
var FALLBACK_CONFIG = {
|
|
1040
1045
|
name: "",
|
|
1041
1046
|
stack: {},
|
|
1042
1047
|
standards: "",
|
|
1043
|
-
tags: [],
|
|
1044
1048
|
node_types: [],
|
|
1045
|
-
artifacts: {}
|
|
1046
|
-
knowledge_categories: []
|
|
1049
|
+
artifacts: {}
|
|
1047
1050
|
};
|
|
1048
1051
|
async function loadGraph(projectRoot, options = {}) {
|
|
1049
1052
|
const yggRoot = await findYggRoot(projectRoot);
|
|
1050
1053
|
let configError;
|
|
1051
1054
|
let config = FALLBACK_CONFIG;
|
|
1052
1055
|
try {
|
|
1053
|
-
config = await parseConfig(
|
|
1056
|
+
config = await parseConfig(path7.join(yggRoot, "config.yaml"));
|
|
1054
1057
|
} catch (error) {
|
|
1055
1058
|
if (!options.tolerateInvalidConfig) {
|
|
1056
1059
|
throw error;
|
|
1057
1060
|
}
|
|
1058
1061
|
configError = error.message;
|
|
1059
1062
|
}
|
|
1060
|
-
const modelDir =
|
|
1063
|
+
const modelDir = path7.join(yggRoot, "model");
|
|
1061
1064
|
const nodes = /* @__PURE__ */ new Map();
|
|
1062
1065
|
const nodeParseErrors = [];
|
|
1063
1066
|
const artifactFilenames = Object.keys(config.artifacts ?? {});
|
|
@@ -1071,13 +1074,9 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1071
1074
|
}
|
|
1072
1075
|
throw err;
|
|
1073
1076
|
}
|
|
1074
|
-
const aspects = await loadAspects(
|
|
1075
|
-
const flows = await loadFlows(
|
|
1076
|
-
const
|
|
1077
|
-
path6.join(yggRoot, "knowledge"),
|
|
1078
|
-
config.knowledge_categories
|
|
1079
|
-
);
|
|
1080
|
-
const schemas = await loadSchemas(path6.join(yggRoot, "templates"));
|
|
1077
|
+
const aspects = await loadAspects(path7.join(yggRoot, "aspects"));
|
|
1078
|
+
const flows = await loadFlows(path7.join(yggRoot, "flows"));
|
|
1079
|
+
const schemas = await loadSchemas(path7.join(yggRoot, "schemas"));
|
|
1081
1080
|
return {
|
|
1082
1081
|
config,
|
|
1083
1082
|
configError,
|
|
@@ -1085,7 +1084,6 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1085
1084
|
nodes,
|
|
1086
1085
|
aspects,
|
|
1087
1086
|
flows,
|
|
1088
|
-
knowledge,
|
|
1089
1087
|
schemas,
|
|
1090
1088
|
rootPath: yggRoot
|
|
1091
1089
|
};
|
|
@@ -1098,9 +1096,12 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1098
1096
|
}
|
|
1099
1097
|
if (hasNodeYaml) {
|
|
1100
1098
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1099
|
+
const nodeYamlPath = path7.join(dirPath, "node.yaml");
|
|
1101
1100
|
let meta;
|
|
1101
|
+
let nodeYamlRaw;
|
|
1102
1102
|
try {
|
|
1103
|
-
|
|
1103
|
+
nodeYamlRaw = await readFile9(nodeYamlPath, "utf-8");
|
|
1104
|
+
meta = await parseNodeYaml(nodeYamlPath);
|
|
1104
1105
|
} catch (err) {
|
|
1105
1106
|
nodeParseErrors.push({
|
|
1106
1107
|
nodePath: graphPath,
|
|
@@ -1112,6 +1113,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1112
1113
|
const node = {
|
|
1113
1114
|
path: graphPath,
|
|
1114
1115
|
meta,
|
|
1116
|
+
nodeYamlRaw,
|
|
1115
1117
|
artifacts,
|
|
1116
1118
|
children: [],
|
|
1117
1119
|
parent
|
|
@@ -1124,7 +1126,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1124
1126
|
if (!entry.isDirectory()) continue;
|
|
1125
1127
|
if (entry.name.startsWith(".")) continue;
|
|
1126
1128
|
await scanModelDirectory(
|
|
1127
|
-
|
|
1129
|
+
path7.join(dirPath, entry.name),
|
|
1128
1130
|
modelDir,
|
|
1129
1131
|
node,
|
|
1130
1132
|
nodes,
|
|
@@ -1137,7 +1139,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1137
1139
|
if (!entry.isDirectory()) continue;
|
|
1138
1140
|
if (entry.name.startsWith(".")) continue;
|
|
1139
1141
|
await scanModelDirectory(
|
|
1140
|
-
|
|
1142
|
+
path7.join(dirPath, entry.name),
|
|
1141
1143
|
modelDir,
|
|
1142
1144
|
null,
|
|
1143
1145
|
nodes,
|
|
@@ -1149,27 +1151,36 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1149
1151
|
}
|
|
1150
1152
|
async function loadAspects(aspectsDir) {
|
|
1151
1153
|
try {
|
|
1152
|
-
const entries = await readdir3(aspectsDir, { withFileTypes: true });
|
|
1153
1154
|
const aspects = [];
|
|
1154
|
-
|
|
1155
|
-
if (!entry.isDirectory()) continue;
|
|
1156
|
-
const aspectYamlPath = path6.join(aspectsDir, entry.name, "aspect.yaml");
|
|
1157
|
-
const aspect = await parseAspect(path6.join(aspectsDir, entry.name), aspectYamlPath);
|
|
1158
|
-
aspects.push(aspect);
|
|
1159
|
-
}
|
|
1155
|
+
await scanAspectsDirectory(aspectsDir, aspectsDir, aspects);
|
|
1160
1156
|
return aspects;
|
|
1161
1157
|
} catch {
|
|
1162
1158
|
return [];
|
|
1163
1159
|
}
|
|
1164
1160
|
}
|
|
1161
|
+
async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
1162
|
+
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1163
|
+
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "aspect.yaml");
|
|
1164
|
+
if (hasAspectYaml) {
|
|
1165
|
+
const id = path7.relative(aspectsRoot, dirPath).split(path7.sep).join("/");
|
|
1166
|
+
const aspectYamlPath = path7.join(dirPath, "aspect.yaml");
|
|
1167
|
+
const aspect = await parseAspect(dirPath, aspectYamlPath, id);
|
|
1168
|
+
aspects.push(aspect);
|
|
1169
|
+
}
|
|
1170
|
+
for (const entry of entries) {
|
|
1171
|
+
if (!entry.isDirectory()) continue;
|
|
1172
|
+
if (entry.name.startsWith(".")) continue;
|
|
1173
|
+
await scanAspectsDirectory(path7.join(dirPath, entry.name), aspectsRoot, aspects);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1165
1176
|
async function loadFlows(flowsDir) {
|
|
1166
1177
|
try {
|
|
1167
1178
|
const entries = await readdir3(flowsDir, { withFileTypes: true });
|
|
1168
1179
|
const flows = [];
|
|
1169
1180
|
for (const entry of entries) {
|
|
1170
1181
|
if (!entry.isDirectory()) continue;
|
|
1171
|
-
const flowYamlPath =
|
|
1172
|
-
const flow = await parseFlow(
|
|
1182
|
+
const flowYamlPath = path7.join(flowsDir, entry.name, "flow.yaml");
|
|
1183
|
+
const flow = await parseFlow(path7.join(flowsDir, entry.name), flowYamlPath);
|
|
1173
1184
|
flows.push(flow);
|
|
1174
1185
|
}
|
|
1175
1186
|
return flows;
|
|
@@ -1177,37 +1188,14 @@ async function loadFlows(flowsDir) {
|
|
|
1177
1188
|
return [];
|
|
1178
1189
|
}
|
|
1179
1190
|
}
|
|
1180
|
-
async function
|
|
1181
|
-
const items = [];
|
|
1182
|
-
const categorySet = new Set(categories.map((c) => c.name));
|
|
1183
|
-
try {
|
|
1184
|
-
const catEntries = await readdir3(knowledgeDir, { withFileTypes: true });
|
|
1185
|
-
for (const catEntry of catEntries) {
|
|
1186
|
-
if (!catEntry.isDirectory()) continue;
|
|
1187
|
-
if (!categorySet.has(catEntry.name)) continue;
|
|
1188
|
-
const catPath = path6.join(knowledgeDir, catEntry.name);
|
|
1189
|
-
const itemEntries = await readdir3(catPath, { withFileTypes: true });
|
|
1190
|
-
for (const itemEntry of itemEntries) {
|
|
1191
|
-
if (!itemEntry.isDirectory()) continue;
|
|
1192
|
-
const itemDir = path6.join(catPath, itemEntry.name);
|
|
1193
|
-
const knowledgeYamlPath = path6.join(itemDir, "knowledge.yaml");
|
|
1194
|
-
const relativePath = `${catEntry.name}/${itemEntry.name}`;
|
|
1195
|
-
const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
|
|
1196
|
-
items.push(item);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
} catch {
|
|
1200
|
-
}
|
|
1201
|
-
return items;
|
|
1202
|
-
}
|
|
1203
|
-
async function loadSchemas(templatesDir) {
|
|
1191
|
+
async function loadSchemas(schemasDir) {
|
|
1204
1192
|
try {
|
|
1205
|
-
const entries = await readdir3(
|
|
1193
|
+
const entries = await readdir3(schemasDir, { withFileTypes: true });
|
|
1206
1194
|
const schemas = [];
|
|
1207
1195
|
for (const entry of entries) {
|
|
1208
1196
|
if (!entry.isFile()) continue;
|
|
1209
1197
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1210
|
-
const s = await parseSchema(
|
|
1198
|
+
const s = await parseSchema(path7.join(schemasDir, entry.name));
|
|
1211
1199
|
schemas.push(s);
|
|
1212
1200
|
}
|
|
1213
1201
|
return schemas;
|
|
@@ -1218,7 +1206,7 @@ async function loadSchemas(templatesDir) {
|
|
|
1218
1206
|
|
|
1219
1207
|
// src/core/context-builder.ts
|
|
1220
1208
|
import { readFile as readFile10 } from "fs/promises";
|
|
1221
|
-
import
|
|
1209
|
+
import path8 from "path";
|
|
1222
1210
|
|
|
1223
1211
|
// src/utils/tokens.ts
|
|
1224
1212
|
function estimateTokens(text) {
|
|
@@ -1233,18 +1221,13 @@ async function buildContext(graph, nodePath) {
|
|
|
1233
1221
|
if (!node) {
|
|
1234
1222
|
throw new Error(`Node not found: ${nodePath}`);
|
|
1235
1223
|
}
|
|
1236
|
-
const nodeTags = new Set(node.meta.tags ?? []);
|
|
1237
|
-
const seenKnowledge = /* @__PURE__ */ new Set();
|
|
1238
1224
|
const layers = [];
|
|
1239
1225
|
layers.push(buildGlobalLayer(graph.config));
|
|
1240
|
-
for (const k of collectKnowledgeItems(graph, nodePath, nodeTags, seenKnowledge)) {
|
|
1241
|
-
layers.push(buildKnowledgeLayer(k));
|
|
1242
|
-
}
|
|
1243
1226
|
const ancestors = collectAncestors(node);
|
|
1244
1227
|
for (const ancestor of ancestors) {
|
|
1245
|
-
layers.push(buildHierarchyLayer(ancestor, graph.config));
|
|
1228
|
+
layers.push(buildHierarchyLayer(ancestor, graph.config, graph));
|
|
1246
1229
|
}
|
|
1247
|
-
layers.push(await buildOwnLayer(node, graph.config, graph.rootPath));
|
|
1230
|
+
layers.push(await buildOwnLayer(node, graph.config, graph.rootPath, graph));
|
|
1248
1231
|
for (const relation of node.meta.relations ?? []) {
|
|
1249
1232
|
const target = graph.nodes.get(relation.target);
|
|
1250
1233
|
if (!target) {
|
|
@@ -1256,24 +1239,22 @@ async function buildContext(graph, nodePath) {
|
|
|
1256
1239
|
layers.push(buildEventRelationLayer(target, relation));
|
|
1257
1240
|
}
|
|
1258
1241
|
}
|
|
1259
|
-
for (const tag of nodeTags) {
|
|
1260
|
-
for (const aspect of graph.aspects) {
|
|
1261
|
-
if (aspect.tag === tag) {
|
|
1262
|
-
layers.push(buildAspectLayer(aspect));
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
1242
|
for (const flow of collectParticipatingFlows(graph, node)) {
|
|
1267
|
-
layers.push(buildFlowLayer(flow));
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1243
|
+
layers.push(buildFlowLayer(flow, graph));
|
|
1244
|
+
}
|
|
1245
|
+
const allAspectIds = /* @__PURE__ */ new Set();
|
|
1246
|
+
for (const l of layers) {
|
|
1247
|
+
const aspects = l.attrs?.aspects;
|
|
1248
|
+
if (aspects) {
|
|
1249
|
+
for (const id of aspects.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
1250
|
+
allAspectIds.add(id);
|
|
1274
1251
|
}
|
|
1275
1252
|
}
|
|
1276
1253
|
}
|
|
1254
|
+
const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
|
|
1255
|
+
for (const aspect of aspectsToInclude) {
|
|
1256
|
+
layers.push(buildAspectLayer(aspect));
|
|
1257
|
+
}
|
|
1277
1258
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
1278
1259
|
const tokenCount = estimateTokens(fullText);
|
|
1279
1260
|
const mapping = normalizeMappingPaths(node.meta.mapping);
|
|
@@ -1287,47 +1268,46 @@ async function buildContext(graph, nodePath) {
|
|
|
1287
1268
|
tokenCount
|
|
1288
1269
|
};
|
|
1289
1270
|
}
|
|
1290
|
-
function
|
|
1271
|
+
function collectParticipatingFlows(graph, node) {
|
|
1272
|
+
const paths = /* @__PURE__ */ new Set([node.path, ...collectAncestors(node).map((a) => a.path)]);
|
|
1273
|
+
return graph.flows.filter((f) => f.nodes.some((n) => paths.has(n)));
|
|
1274
|
+
}
|
|
1275
|
+
function expandAspects(aspectIds, aspects) {
|
|
1276
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1277
|
+
for (const a of aspects) {
|
|
1278
|
+
idToAspect.set(a.id, a);
|
|
1279
|
+
}
|
|
1291
1280
|
const result = [];
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
}
|
|
1298
|
-
for (const k of graph.knowledge) {
|
|
1299
|
-
if (typeof k.scope === "object" && "tags" in k.scope) {
|
|
1300
|
-
const overlap = k.scope.tags.some((t) => nodeTags.has(t));
|
|
1301
|
-
if (overlap && !seenKnowledge.has(k.path)) {
|
|
1302
|
-
seenKnowledge.add(k.path);
|
|
1303
|
-
result.push(k);
|
|
1304
|
-
}
|
|
1281
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1282
|
+
const stack = /* @__PURE__ */ new Set();
|
|
1283
|
+
function collect(id) {
|
|
1284
|
+
if (stack.has(id)) {
|
|
1285
|
+
throw new Error(`Aspect implies cycle detected involving aspect '${id}'`);
|
|
1305
1286
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1287
|
+
if (visited.has(id)) return;
|
|
1288
|
+
stack.add(id);
|
|
1289
|
+
visited.add(id);
|
|
1290
|
+
result.push(id);
|
|
1291
|
+
const aspect = idToAspect.get(id);
|
|
1292
|
+
if (aspect) {
|
|
1293
|
+
for (const implied of aspect.implies ?? []) {
|
|
1294
|
+
collect(implied);
|
|
1312
1295
|
}
|
|
1313
1296
|
}
|
|
1297
|
+
stack.delete(id);
|
|
1314
1298
|
}
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
for (const kPath of node.meta.knowledge) {
|
|
1318
|
-
const norm = kPath.replace(/\/$/, "");
|
|
1319
|
-
const k = graph.knowledge.find((item) => item.path === norm || item.path === kPath);
|
|
1320
|
-
if (k && !seenKnowledge.has(k.path)) {
|
|
1321
|
-
seenKnowledge.add(k.path);
|
|
1322
|
-
result.push(k);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1299
|
+
for (const id of aspectIds) {
|
|
1300
|
+
collect(id);
|
|
1325
1301
|
}
|
|
1326
1302
|
return result;
|
|
1327
1303
|
}
|
|
1328
|
-
function
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1304
|
+
function resolveAspects(aspectIds, aspects) {
|
|
1305
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1306
|
+
for (const a of aspects) {
|
|
1307
|
+
idToAspect.set(a.id, a);
|
|
1308
|
+
}
|
|
1309
|
+
const expandedIds = expandAspects([...aspectIds], aspects);
|
|
1310
|
+
return expandedIds.map((id) => idToAspect.get(id)).filter((a) => a !== void 0);
|
|
1331
1311
|
}
|
|
1332
1312
|
function buildGlobalLayer(config) {
|
|
1333
1313
|
let content = `**Project:** ${config.name}
|
|
@@ -1345,41 +1325,39 @@ ${config.standards || "(none)"}
|
|
|
1345
1325
|
`;
|
|
1346
1326
|
return { type: "global", label: "Global Context", content };
|
|
1347
1327
|
}
|
|
1348
|
-
function buildKnowledgeLayer(k, fromFlow) {
|
|
1349
|
-
const categoryLabel = k.category.charAt(0).toUpperCase() + k.category.slice(1);
|
|
1350
|
-
const content = k.artifacts.map((a) => `### ${a.filename}
|
|
1351
|
-
${a.content}`).join("\n\n");
|
|
1352
|
-
const label = fromFlow ? `Long-term Memory (from flow): ${k.name}` : `${categoryLabel}: ${k.name}`;
|
|
1353
|
-
return {
|
|
1354
|
-
type: "knowledge",
|
|
1355
|
-
label,
|
|
1356
|
-
content
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
1328
|
function filterArtifactsByConfig(artifacts, config) {
|
|
1360
1329
|
const allowed = new Set(Object.keys(config.artifacts ?? {}));
|
|
1361
1330
|
return artifacts.filter((a) => allowed.has(a.filename));
|
|
1362
1331
|
}
|
|
1363
|
-
function buildHierarchyLayer(ancestor, config) {
|
|
1332
|
+
function buildHierarchyLayer(ancestor, config, graph) {
|
|
1364
1333
|
const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
|
|
1365
1334
|
const content = filtered.map((a) => `### ${a.filename}
|
|
1366
1335
|
${a.content}`).join("\n\n");
|
|
1336
|
+
const nodeAspects = ancestor.meta.aspects ?? [];
|
|
1337
|
+
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1338
|
+
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1367
1339
|
return {
|
|
1368
1340
|
type: "hierarchy",
|
|
1369
1341
|
label: `Module Context (${ancestor.path}/)`,
|
|
1370
|
-
content
|
|
1342
|
+
content,
|
|
1343
|
+
attrs
|
|
1371
1344
|
};
|
|
1372
1345
|
}
|
|
1373
|
-
async function buildOwnLayer(node, config, graphRootPath) {
|
|
1346
|
+
async function buildOwnLayer(node, config, graphRootPath, graph) {
|
|
1374
1347
|
const parts = [];
|
|
1375
|
-
|
|
1376
|
-
try {
|
|
1377
|
-
const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
|
|
1348
|
+
if (node.nodeYamlRaw) {
|
|
1378
1349
|
parts.push(`### node.yaml
|
|
1350
|
+
${node.nodeYamlRaw.trim()}`);
|
|
1351
|
+
} else {
|
|
1352
|
+
const nodeYamlPath = path8.join(graphRootPath, "model", node.path, "node.yaml");
|
|
1353
|
+
try {
|
|
1354
|
+
const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
|
|
1355
|
+
parts.push(`### node.yaml
|
|
1379
1356
|
${nodeYamlContent.trim()}`);
|
|
1380
|
-
|
|
1381
|
-
|
|
1357
|
+
} catch {
|
|
1358
|
+
parts.push(`### node.yaml
|
|
1382
1359
|
(not found)`);
|
|
1360
|
+
}
|
|
1383
1361
|
}
|
|
1384
1362
|
const filtered = filterArtifactsByConfig(node.artifacts, config);
|
|
1385
1363
|
for (const a of filtered) {
|
|
@@ -1387,10 +1365,14 @@ ${nodeYamlContent.trim()}`);
|
|
|
1387
1365
|
${a.content}`);
|
|
1388
1366
|
}
|
|
1389
1367
|
const content = parts.join("\n\n");
|
|
1368
|
+
const nodeAspects = node.meta.aspects ?? [];
|
|
1369
|
+
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1370
|
+
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1390
1371
|
return {
|
|
1391
1372
|
type: "own",
|
|
1392
1373
|
label: `Node: ${node.meta.name}`,
|
|
1393
|
-
content
|
|
1374
|
+
content,
|
|
1375
|
+
attrs
|
|
1394
1376
|
};
|
|
1395
1377
|
}
|
|
1396
1378
|
function buildStructuralRelationLayer(target, relation, config) {
|
|
@@ -1418,10 +1400,17 @@ ${a.content}`).join("\n\n");
|
|
|
1418
1400
|
content += filtered.map((a) => `### ${a.filename}
|
|
1419
1401
|
${a.content}`).join("\n\n");
|
|
1420
1402
|
}
|
|
1403
|
+
const attrs = {
|
|
1404
|
+
target: target.path,
|
|
1405
|
+
type: relation.type
|
|
1406
|
+
};
|
|
1407
|
+
if (relation.consumes?.length) attrs.consumes = relation.consumes.join(", ");
|
|
1408
|
+
if (relation.failure) attrs.failure = relation.failure;
|
|
1421
1409
|
return {
|
|
1422
1410
|
type: "relational",
|
|
1423
1411
|
label: `Dependency: ${target.meta.name} (${relation.type}) \u2014 ${target.path}`,
|
|
1424
|
-
content: content.trim()
|
|
1412
|
+
content: content.trim(),
|
|
1413
|
+
attrs
|
|
1425
1414
|
};
|
|
1426
1415
|
}
|
|
1427
1416
|
function buildEventRelationLayer(target, relation) {
|
|
@@ -1434,10 +1423,17 @@ You listen for ${eventName}.`;
|
|
|
1434
1423
|
content += `
|
|
1435
1424
|
Consumes: ${relation.consumes.join(", ")}`;
|
|
1436
1425
|
}
|
|
1426
|
+
const attrs = {
|
|
1427
|
+
target: target.path,
|
|
1428
|
+
type: relation.type,
|
|
1429
|
+
"event-name": eventName
|
|
1430
|
+
};
|
|
1431
|
+
if (relation.consumes?.length) attrs.consumes = relation.consumes.join(", ");
|
|
1437
1432
|
return {
|
|
1438
1433
|
type: "relational",
|
|
1439
1434
|
label: `Event: ${eventName} [${relation.type}]`,
|
|
1440
|
-
content
|
|
1435
|
+
content,
|
|
1436
|
+
attrs
|
|
1441
1437
|
};
|
|
1442
1438
|
}
|
|
1443
1439
|
function buildAspectLayer(aspect) {
|
|
@@ -1445,17 +1441,21 @@ function buildAspectLayer(aspect) {
|
|
|
1445
1441
|
${a.content}`).join("\n\n");
|
|
1446
1442
|
return {
|
|
1447
1443
|
type: "aspects",
|
|
1448
|
-
label: `${aspect.name} (
|
|
1444
|
+
label: `${aspect.name} (aspect: ${aspect.id})`,
|
|
1449
1445
|
content
|
|
1450
1446
|
};
|
|
1451
1447
|
}
|
|
1452
|
-
function buildFlowLayer(flow) {
|
|
1448
|
+
function buildFlowLayer(flow, graph) {
|
|
1453
1449
|
const content = flow.artifacts.map((a) => `### ${a.filename}
|
|
1454
1450
|
${a.content}`).join("\n\n");
|
|
1451
|
+
const flowAspects = flow.aspects ?? [];
|
|
1452
|
+
const expanded = expandAspects(flowAspects, graph.aspects);
|
|
1453
|
+
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1455
1454
|
return {
|
|
1456
1455
|
type: "flows",
|
|
1457
1456
|
label: `Flow: ${flow.name}`,
|
|
1458
|
-
content: content || "(no artifacts)"
|
|
1457
|
+
content: content || "(no artifacts)",
|
|
1458
|
+
attrs
|
|
1459
1459
|
};
|
|
1460
1460
|
}
|
|
1461
1461
|
function buildSections(layers, mapping) {
|
|
@@ -1469,12 +1469,16 @@ function buildSections(layers, mapping) {
|
|
|
1469
1469
|
}
|
|
1470
1470
|
return [
|
|
1471
1471
|
{ key: "Global", layers: layers.filter((l) => l.type === "global") },
|
|
1472
|
-
{ key: "Knowledge", layers: layers.filter((l) => l.type === "knowledge") },
|
|
1473
1472
|
{ key: "Hierarchy", layers: layers.filter((l) => l.type === "hierarchy") },
|
|
1474
1473
|
{ key: "OwnArtifacts", layers: ownLayers },
|
|
1475
|
-
{ key: "Dependencies", layers: layers.filter((l) => l.type === "relational") },
|
|
1476
1474
|
{ key: "Aspects", layers: layers.filter((l) => l.type === "aspects") },
|
|
1477
|
-
{
|
|
1475
|
+
{
|
|
1476
|
+
key: "Relational",
|
|
1477
|
+
layers: [
|
|
1478
|
+
...layers.filter((l) => l.type === "relational"),
|
|
1479
|
+
...layers.filter((l) => l.type === "flows")
|
|
1480
|
+
]
|
|
1481
|
+
}
|
|
1478
1482
|
];
|
|
1479
1483
|
}
|
|
1480
1484
|
function collectAncestors(node) {
|
|
@@ -1486,30 +1490,27 @@ function collectAncestors(node) {
|
|
|
1486
1490
|
}
|
|
1487
1491
|
return ancestors;
|
|
1488
1492
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1504
|
-
});
|
|
1505
|
-
const ts = parseInt(out.trim(), 10);
|
|
1506
|
-
return Number.isNaN(ts) ? null : ts;
|
|
1507
|
-
} catch {
|
|
1508
|
-
return null;
|
|
1493
|
+
function collectEffectiveAspectIds(graph, nodePath) {
|
|
1494
|
+
const node = graph.nodes.get(nodePath);
|
|
1495
|
+
if (!node) return /* @__PURE__ */ new Set();
|
|
1496
|
+
const raw = new Set(node.meta.aspects ?? []);
|
|
1497
|
+
let ancestor = node.parent;
|
|
1498
|
+
while (ancestor) {
|
|
1499
|
+
for (const id of ancestor.meta.aspects ?? []) raw.add(id);
|
|
1500
|
+
ancestor = ancestor.parent;
|
|
1501
|
+
}
|
|
1502
|
+
const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
|
|
1503
|
+
for (const flow of graph.flows) {
|
|
1504
|
+
if (flow.nodes.some((n) => ancestorPaths.has(n))) {
|
|
1505
|
+
for (const id of flow.aspects ?? []) raw.add(id);
|
|
1506
|
+
}
|
|
1509
1507
|
}
|
|
1508
|
+
return new Set(expandAspects([...raw], graph.aspects));
|
|
1510
1509
|
}
|
|
1511
1510
|
|
|
1512
1511
|
// src/core/validator.ts
|
|
1512
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1513
|
+
import path9 from "path";
|
|
1513
1514
|
var RESERVED_DIRS = /* @__PURE__ */ new Set();
|
|
1514
1515
|
async function validate(graph, scope = "all") {
|
|
1515
1516
|
const issues = [];
|
|
@@ -1532,28 +1533,25 @@ async function validate(graph, scope = "all") {
|
|
|
1532
1533
|
}
|
|
1533
1534
|
if (!graph.configError) {
|
|
1534
1535
|
issues.push(...checkNodeTypes(graph));
|
|
1535
|
-
issues.push(...
|
|
1536
|
-
issues.push(...
|
|
1537
|
-
issues.push(...
|
|
1536
|
+
issues.push(...checkAspectsDefined(graph));
|
|
1537
|
+
issues.push(...checkAspectIds(graph));
|
|
1538
|
+
issues.push(...checkAspectIdUniqueness(graph));
|
|
1539
|
+
issues.push(...checkImpliedAspectsExist(graph));
|
|
1540
|
+
issues.push(...checkImpliesNoCycles(graph));
|
|
1541
|
+
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
1538
1542
|
issues.push(...checkRequiredArtifacts(graph));
|
|
1539
|
-
issues.push(...await checkUnknownKnowledgeCategories(graph));
|
|
1540
1543
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
1541
|
-
issues.push(...checkScopeTagsDefined(graph));
|
|
1542
|
-
issues.push(...await checkMissingPatternExamples(graph));
|
|
1543
1544
|
issues.push(...await checkContextBudget(graph));
|
|
1544
1545
|
issues.push(...checkHighFanOut(graph));
|
|
1545
|
-
issues.push(...await checkStaleKnowledge(graph));
|
|
1546
1546
|
}
|
|
1547
1547
|
issues.push(...checkSchemas(graph));
|
|
1548
1548
|
issues.push(...checkRelationTargets(graph));
|
|
1549
1549
|
issues.push(...checkNoCycles(graph));
|
|
1550
1550
|
issues.push(...checkMappingOverlap(graph));
|
|
1551
|
-
issues.push(...checkBrokenKnowledgeRefs(graph));
|
|
1552
1551
|
issues.push(...checkBrokenFlowRefs(graph));
|
|
1553
|
-
issues.push(...
|
|
1552
|
+
issues.push(...checkFlowAspectIds(graph));
|
|
1554
1553
|
issues.push(...await checkDirectoriesHaveNodeYaml(graph));
|
|
1555
1554
|
issues.push(...await checkShallowArtifacts(graph));
|
|
1556
|
-
issues.push(...await checkUnreachableKnowledge(graph));
|
|
1557
1555
|
issues.push(...checkUnpairedEvents(graph));
|
|
1558
1556
|
let filtered = issues;
|
|
1559
1557
|
let nodesScanned = graph.nodes.size;
|
|
@@ -1571,7 +1569,7 @@ async function validate(graph, scope = "all") {
|
|
|
1571
1569
|
}
|
|
1572
1570
|
function checkNodeTypes(graph) {
|
|
1573
1571
|
const issues = [];
|
|
1574
|
-
const allowedTypes = new Set(graph.config.node_types ?? []);
|
|
1572
|
+
const allowedTypes = new Set((graph.config.node_types ?? []).map((t) => t.name));
|
|
1575
1573
|
for (const [nodePath, node] of graph.nodes) {
|
|
1576
1574
|
if (!allowedTypes.has(node.meta.type)) {
|
|
1577
1575
|
issues.push({
|
|
@@ -1634,17 +1632,17 @@ function checkRelationTargets(graph) {
|
|
|
1634
1632
|
}
|
|
1635
1633
|
return issues;
|
|
1636
1634
|
}
|
|
1637
|
-
function
|
|
1635
|
+
function checkAspectsDefined(graph) {
|
|
1638
1636
|
const issues = [];
|
|
1639
|
-
const
|
|
1637
|
+
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
1640
1638
|
for (const [nodePath, node] of graph.nodes) {
|
|
1641
|
-
for (const
|
|
1642
|
-
if (!
|
|
1639
|
+
for (const aspectId of node.meta.aspects ?? []) {
|
|
1640
|
+
if (!validAspectIds.has(aspectId)) {
|
|
1643
1641
|
issues.push({
|
|
1644
1642
|
severity: "error",
|
|
1645
1643
|
code: "E003",
|
|
1646
|
-
rule: "unknown-
|
|
1647
|
-
message: `
|
|
1644
|
+
rule: "unknown-aspect",
|
|
1645
|
+
message: `Aspect '${aspectId}' has no corresponding directory in aspects/`,
|
|
1648
1646
|
nodePath
|
|
1649
1647
|
});
|
|
1650
1648
|
}
|
|
@@ -1652,40 +1650,124 @@ function checkTagsDefined(graph) {
|
|
|
1652
1650
|
}
|
|
1653
1651
|
return issues;
|
|
1654
1652
|
}
|
|
1655
|
-
function
|
|
1656
|
-
|
|
1657
|
-
const definedTags = new Set(graph.config.tags ?? []);
|
|
1658
|
-
for (const aspect of graph.aspects) {
|
|
1659
|
-
if (!definedTags.has(aspect.tag)) {
|
|
1660
|
-
issues.push({
|
|
1661
|
-
severity: "error",
|
|
1662
|
-
code: "E007",
|
|
1663
|
-
rule: "broken-aspect-tag",
|
|
1664
|
-
message: `Aspect '${aspect.name}' references undefined tag '${aspect.tag}'`
|
|
1665
|
-
});
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
return issues;
|
|
1653
|
+
function checkAspectIds(_graph) {
|
|
1654
|
+
return [];
|
|
1669
1655
|
}
|
|
1670
|
-
function
|
|
1656
|
+
function checkAspectIdUniqueness(graph) {
|
|
1671
1657
|
const issues = [];
|
|
1672
|
-
const
|
|
1658
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1673
1659
|
for (const aspect of graph.aspects) {
|
|
1674
|
-
const names =
|
|
1660
|
+
const names = byId.get(aspect.id) ?? [];
|
|
1675
1661
|
names.push(aspect.name);
|
|
1676
|
-
|
|
1662
|
+
byId.set(aspect.id, names);
|
|
1677
1663
|
}
|
|
1678
|
-
for (const [
|
|
1664
|
+
for (const [id, names] of byId) {
|
|
1679
1665
|
if (names.length <= 1) continue;
|
|
1680
1666
|
issues.push({
|
|
1681
1667
|
severity: "error",
|
|
1682
1668
|
code: "E014",
|
|
1683
1669
|
rule: "duplicate-aspect-binding",
|
|
1684
|
-
message: `
|
|
1670
|
+
message: `Aspect '${id}' is bound to multiple aspects (${names.join(", ")})`
|
|
1685
1671
|
});
|
|
1686
1672
|
}
|
|
1687
1673
|
return issues;
|
|
1688
1674
|
}
|
|
1675
|
+
function checkImpliedAspectsExist(graph) {
|
|
1676
|
+
const issues = [];
|
|
1677
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1678
|
+
for (const a of graph.aspects) {
|
|
1679
|
+
idToAspect.set(a.id, { name: a.name });
|
|
1680
|
+
}
|
|
1681
|
+
for (const aspect of graph.aspects) {
|
|
1682
|
+
for (const impliedId of aspect.implies ?? []) {
|
|
1683
|
+
if (!idToAspect.has(impliedId)) {
|
|
1684
|
+
issues.push({
|
|
1685
|
+
severity: "error",
|
|
1686
|
+
code: "E016",
|
|
1687
|
+
rule: "implied-aspect-missing",
|
|
1688
|
+
message: `Aspect '${aspect.name}' implies '${impliedId}' but no aspect with that id exists in aspects/`
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return issues;
|
|
1694
|
+
}
|
|
1695
|
+
function checkImpliesNoCycles(graph) {
|
|
1696
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1697
|
+
for (const a of graph.aspects) {
|
|
1698
|
+
idToAspect.set(a.id, { implies: a.implies });
|
|
1699
|
+
}
|
|
1700
|
+
const WHITE = 0;
|
|
1701
|
+
const GRAY = 1;
|
|
1702
|
+
const BLACK = 2;
|
|
1703
|
+
const color = /* @__PURE__ */ new Map();
|
|
1704
|
+
for (const id of idToAspect.keys()) color.set(id, WHITE);
|
|
1705
|
+
const issues = [];
|
|
1706
|
+
function dfs(id, pathArr) {
|
|
1707
|
+
color.set(id, GRAY);
|
|
1708
|
+
pathArr.push(id);
|
|
1709
|
+
const aspect = idToAspect.get(id);
|
|
1710
|
+
for (const implied of aspect?.implies ?? []) {
|
|
1711
|
+
if (color.get(implied) === GRAY) {
|
|
1712
|
+
const cycle = pathArr.slice(pathArr.indexOf(implied)).concat(implied);
|
|
1713
|
+
issues.push({
|
|
1714
|
+
severity: "error",
|
|
1715
|
+
code: "E017",
|
|
1716
|
+
rule: "aspect-implies-cycle",
|
|
1717
|
+
message: `Aspect implies cycle: ${cycle.join(" \u2192 ")}`
|
|
1718
|
+
});
|
|
1719
|
+
pathArr.pop();
|
|
1720
|
+
color.set(id, BLACK);
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
1723
|
+
if (color.get(implied) === WHITE && dfs(implied, pathArr)) {
|
|
1724
|
+
pathArr.pop();
|
|
1725
|
+
color.set(id, BLACK);
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
pathArr.pop();
|
|
1730
|
+
color.set(id, BLACK);
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
for (const id of idToAspect.keys()) {
|
|
1734
|
+
if (color.get(id) === WHITE) {
|
|
1735
|
+
dfs(id, []);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return issues;
|
|
1739
|
+
}
|
|
1740
|
+
function checkRequiredAspectsCoverage(graph) {
|
|
1741
|
+
const issues = [];
|
|
1742
|
+
const typeConfig = new Map(
|
|
1743
|
+
(graph.config.node_types ?? []).map((t) => [t.name, t.required_aspects ?? []])
|
|
1744
|
+
);
|
|
1745
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
1746
|
+
if (node.meta.blackbox) continue;
|
|
1747
|
+
const requiredAspects = typeConfig.get(node.meta.type);
|
|
1748
|
+
if (!requiredAspects || requiredAspects.length === 0) continue;
|
|
1749
|
+
const nodeAspects = node.meta.aspects ?? [];
|
|
1750
|
+
let effectiveAspects;
|
|
1751
|
+
try {
|
|
1752
|
+
effectiveAspects = resolveAspects(nodeAspects, graph.aspects);
|
|
1753
|
+
} catch {
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
const effectiveAspectIds = new Set(effectiveAspects.map((a) => a.id));
|
|
1757
|
+
for (const required of requiredAspects) {
|
|
1758
|
+
if (!effectiveAspectIds.has(required)) {
|
|
1759
|
+
issues.push({
|
|
1760
|
+
severity: "warning",
|
|
1761
|
+
code: "W011",
|
|
1762
|
+
rule: "missing-required-aspect-coverage",
|
|
1763
|
+
message: `Node '${nodePath}' (type: ${node.meta.type}) missing required aspect coverage for '${required}'`,
|
|
1764
|
+
nodePath
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return issues;
|
|
1770
|
+
}
|
|
1689
1771
|
function checkNoCycles(graph) {
|
|
1690
1772
|
const WHITE = 0;
|
|
1691
1773
|
const GRAY = 1;
|
|
@@ -1787,9 +1869,10 @@ function artifactRequiredReason(graph, nodePath, node, required) {
|
|
|
1787
1869
|
const count = node.meta.relations?.length ?? 0;
|
|
1788
1870
|
return count > 0 ? `${count} outgoing relation(s)` : null;
|
|
1789
1871
|
}
|
|
1790
|
-
if (when.startsWith("has_tag:")) {
|
|
1791
|
-
const
|
|
1792
|
-
|
|
1872
|
+
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
1873
|
+
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
1874
|
+
const aspectId = when.slice(prefix.length);
|
|
1875
|
+
return (node.meta.aspects ?? []).includes(aspectId) ? `node has aspect '${aspectId}'` : null;
|
|
1793
1876
|
}
|
|
1794
1877
|
return null;
|
|
1795
1878
|
}
|
|
@@ -1829,29 +1912,9 @@ function checkRequiredArtifacts(graph) {
|
|
|
1829
1912
|
}
|
|
1830
1913
|
return issues;
|
|
1831
1914
|
}
|
|
1832
|
-
function checkBrokenKnowledgeRefs(graph) {
|
|
1833
|
-
const issues = [];
|
|
1834
|
-
const knowledgePaths = new Set(graph.knowledge.map((k) => k.path));
|
|
1835
|
-
for (const [nodePath, node] of graph.nodes) {
|
|
1836
|
-
for (const kPath of node.meta.knowledge ?? []) {
|
|
1837
|
-
const norm = kPath.replace(/\/$/, "");
|
|
1838
|
-
if (!knowledgePaths.has(norm) && !knowledgePaths.has(kPath)) {
|
|
1839
|
-
issues.push({
|
|
1840
|
-
severity: "error",
|
|
1841
|
-
code: "E005",
|
|
1842
|
-
rule: "broken-knowledge-ref",
|
|
1843
|
-
message: `Knowledge ref '${kPath}' does not resolve to existing knowledge item`,
|
|
1844
|
-
nodePath
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
return issues;
|
|
1850
|
-
}
|
|
1851
1915
|
function checkBrokenFlowRefs(graph) {
|
|
1852
1916
|
const issues = [];
|
|
1853
1917
|
const nodePaths = new Set(graph.nodes.keys());
|
|
1854
|
-
const knowledgePaths = new Set(graph.knowledge.map((k) => k.path));
|
|
1855
1918
|
for (const flow of graph.flows) {
|
|
1856
1919
|
for (const n of flow.nodes) {
|
|
1857
1920
|
if (!nodePaths.has(n)) {
|
|
@@ -1863,107 +1926,43 @@ function checkBrokenFlowRefs(graph) {
|
|
|
1863
1926
|
});
|
|
1864
1927
|
}
|
|
1865
1928
|
}
|
|
1866
|
-
for (const kPath of flow.knowledge ?? []) {
|
|
1867
|
-
const norm = kPath.replace(/\/$/, "");
|
|
1868
|
-
if (!knowledgePaths.has(norm) && !knowledgePaths.has(kPath)) {
|
|
1869
|
-
issues.push({
|
|
1870
|
-
severity: "error",
|
|
1871
|
-
code: "E005",
|
|
1872
|
-
rule: "broken-knowledge-ref",
|
|
1873
|
-
message: `Flow '${flow.name}' references non-existent knowledge '${kPath}'`,
|
|
1874
|
-
nodePath: `flows/${flow.name}`
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
return issues;
|
|
1880
|
-
}
|
|
1881
|
-
function checkBrokenScopeRefs(graph) {
|
|
1882
|
-
const issues = [];
|
|
1883
|
-
const nodePaths = new Set(graph.nodes.keys());
|
|
1884
|
-
for (const k of graph.knowledge) {
|
|
1885
|
-
if (typeof k.scope === "object" && "nodes" in k.scope) {
|
|
1886
|
-
for (const n of k.scope.nodes) {
|
|
1887
|
-
if (!nodePaths.has(n)) {
|
|
1888
|
-
issues.push({
|
|
1889
|
-
severity: "error",
|
|
1890
|
-
code: "E008",
|
|
1891
|
-
rule: "broken-scope-ref",
|
|
1892
|
-
message: `Knowledge '${k.path}' scope references non-existent node '${n}'`
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
return issues;
|
|
1899
|
-
}
|
|
1900
|
-
function checkScopeTagsDefined(graph) {
|
|
1901
|
-
const issues = [];
|
|
1902
|
-
const definedTags = new Set(graph.config.tags ?? []);
|
|
1903
|
-
for (const k of graph.knowledge) {
|
|
1904
|
-
if (typeof k.scope !== "object" || !("tags" in k.scope)) continue;
|
|
1905
|
-
for (const tag of k.scope.tags) {
|
|
1906
|
-
if (definedTags.has(tag)) continue;
|
|
1907
|
-
issues.push({
|
|
1908
|
-
severity: "error",
|
|
1909
|
-
code: "E008",
|
|
1910
|
-
rule: "broken-scope-ref",
|
|
1911
|
-
message: `Knowledge '${k.path}' scope references undefined tag '${tag}'`
|
|
1912
|
-
});
|
|
1913
|
-
}
|
|
1914
1929
|
}
|
|
1915
1930
|
return issues;
|
|
1916
1931
|
}
|
|
1917
|
-
|
|
1932
|
+
function checkFlowAspectIds(graph) {
|
|
1918
1933
|
const issues = [];
|
|
1919
|
-
const
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const entries = await readdir4(knowledgeDir, { withFileTypes: true });
|
|
1924
|
-
for (const e of entries) {
|
|
1925
|
-
if (!e.isDirectory()) continue;
|
|
1926
|
-
if (e.name.startsWith(".")) continue;
|
|
1927
|
-
existingDirs.add(e.name);
|
|
1928
|
-
if (!categorySet.has(e.name)) {
|
|
1934
|
+
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
1935
|
+
for (const flow of graph.flows) {
|
|
1936
|
+
for (const aspectId of flow.aspects ?? []) {
|
|
1937
|
+
if (!validAspectIds.has(aspectId)) {
|
|
1929
1938
|
issues.push({
|
|
1930
1939
|
severity: "error",
|
|
1931
|
-
code: "
|
|
1932
|
-
rule: "
|
|
1933
|
-
message: `
|
|
1940
|
+
code: "E007",
|
|
1941
|
+
rule: "broken-aspect-ref",
|
|
1942
|
+
message: `Flow '${flow.name}' references aspect '${aspectId}' but no aspect with that id exists in aspects/`
|
|
1934
1943
|
});
|
|
1935
1944
|
}
|
|
1936
1945
|
}
|
|
1937
|
-
} catch {
|
|
1938
|
-
}
|
|
1939
|
-
for (const cat of graph.config.knowledge_categories ?? []) {
|
|
1940
|
-
if (!existingDirs.has(cat.name)) {
|
|
1941
|
-
issues.push({
|
|
1942
|
-
severity: "error",
|
|
1943
|
-
code: "E017",
|
|
1944
|
-
rule: "missing-knowledge-category-dir",
|
|
1945
|
-
message: `Category '${cat.name}' in config has no knowledge/${cat.name}/ directory`
|
|
1946
|
-
});
|
|
1947
|
-
}
|
|
1948
1946
|
}
|
|
1949
1947
|
return issues;
|
|
1950
1948
|
}
|
|
1951
1949
|
function checkInvalidArtifactConditions(graph) {
|
|
1952
1950
|
const issues = [];
|
|
1953
|
-
const
|
|
1951
|
+
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
1954
1952
|
const artifacts = graph.config.artifacts ?? {};
|
|
1955
1953
|
for (const [artifactName, config] of Object.entries(artifacts)) {
|
|
1956
1954
|
const required = config.required;
|
|
1957
1955
|
if (typeof required === "object" && required && "when" in required) {
|
|
1958
1956
|
const when = required.when;
|
|
1959
|
-
if (when.startsWith("has_tag:")) {
|
|
1960
|
-
const
|
|
1961
|
-
|
|
1957
|
+
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
1958
|
+
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
1959
|
+
const aspectId = when.slice(prefix.length);
|
|
1960
|
+
if (!validAspectIds.has(aspectId)) {
|
|
1962
1961
|
issues.push({
|
|
1963
1962
|
severity: "error",
|
|
1964
1963
|
code: "E013",
|
|
1965
1964
|
rule: "invalid-artifact-condition",
|
|
1966
|
-
message: `Artifact '${artifactName}' condition
|
|
1965
|
+
message: `Artifact '${artifactName}' condition has_aspect:${aspectId} has no corresponding aspect in aspects/`
|
|
1967
1966
|
});
|
|
1968
1967
|
}
|
|
1969
1968
|
}
|
|
@@ -1981,7 +1980,7 @@ async function checkShallowArtifacts(graph) {
|
|
|
1981
1980
|
severity: "warning",
|
|
1982
1981
|
code: "W002",
|
|
1983
1982
|
rule: "shallow-artifact",
|
|
1984
|
-
message: `Artifact '${art.filename}' is below minimum length (${art.content.length} < ${minLen})`,
|
|
1983
|
+
message: `Artifact '${art.filename}' is below minimum length (${art.content.trim().length} < ${minLen})`,
|
|
1985
1984
|
nodePath
|
|
1986
1985
|
});
|
|
1987
1986
|
}
|
|
@@ -1989,100 +1988,7 @@ async function checkShallowArtifacts(graph) {
|
|
|
1989
1988
|
}
|
|
1990
1989
|
return issues;
|
|
1991
1990
|
}
|
|
1992
|
-
|
|
1993
|
-
const issues = [];
|
|
1994
|
-
const nodePaths = new Set(graph.nodes.keys());
|
|
1995
|
-
const nodeTags = /* @__PURE__ */ new Map();
|
|
1996
|
-
for (const [p, n] of graph.nodes) {
|
|
1997
|
-
nodeTags.set(p, new Set(n.meta.tags ?? []));
|
|
1998
|
-
}
|
|
1999
|
-
const knowledgeReachable = /* @__PURE__ */ new Set();
|
|
2000
|
-
for (const k of graph.knowledge) {
|
|
2001
|
-
if (k.scope === "global") {
|
|
2002
|
-
knowledgeReachable.add(k.path);
|
|
2003
|
-
continue;
|
|
2004
|
-
}
|
|
2005
|
-
if (typeof k.scope === "object" && "tags" in k.scope) {
|
|
2006
|
-
for (const [, tags] of nodeTags) {
|
|
2007
|
-
if (k.scope.tags.some((t) => tags.has(t))) {
|
|
2008
|
-
knowledgeReachable.add(k.path);
|
|
2009
|
-
break;
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
if (typeof k.scope === "object" && "nodes" in k.scope) {
|
|
2014
|
-
if (k.scope.nodes.some((n) => nodePaths.has(n))) {
|
|
2015
|
-
knowledgeReachable.add(k.path);
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
for (const [, node] of graph.nodes) {
|
|
2020
|
-
for (const kPath of node.meta.knowledge ?? []) {
|
|
2021
|
-
const k = graph.knowledge.find(
|
|
2022
|
-
(i) => i.path === kPath || i.path === kPath.replace(/\/$/, "")
|
|
2023
|
-
);
|
|
2024
|
-
if (k) knowledgeReachable.add(k.path);
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
for (const flow of graph.flows) {
|
|
2028
|
-
for (const kPath of flow.knowledge ?? []) {
|
|
2029
|
-
const k = graph.knowledge.find(
|
|
2030
|
-
(i) => i.path === kPath || i.path === kPath.replace(/\/$/, "")
|
|
2031
|
-
);
|
|
2032
|
-
if (k) knowledgeReachable.add(k.path);
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
for (const k of graph.knowledge) {
|
|
2036
|
-
if (!knowledgeReachable.has(k.path)) {
|
|
2037
|
-
issues.push({
|
|
2038
|
-
severity: "warning",
|
|
2039
|
-
code: "W003",
|
|
2040
|
-
rule: "unreachable-knowledge",
|
|
2041
|
-
message: `Knowledge '${k.path}' does not reach any context package`
|
|
2042
|
-
});
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
return issues;
|
|
2046
|
-
}
|
|
2047
|
-
async function checkMissingPatternExamples(graph) {
|
|
2048
|
-
const issues = [];
|
|
2049
|
-
const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
|
|
2050
|
-
if (!hasPatterns) return issues;
|
|
2051
|
-
const patternsDir = path9.join(graph.rootPath, "knowledge", "patterns");
|
|
2052
|
-
try {
|
|
2053
|
-
const entries = await readdir4(patternsDir, { withFileTypes: true });
|
|
2054
|
-
const exampleExtensions = /* @__PURE__ */ new Set([
|
|
2055
|
-
".ts",
|
|
2056
|
-
".js",
|
|
2057
|
-
".tsx",
|
|
2058
|
-
".jsx",
|
|
2059
|
-
".py",
|
|
2060
|
-
".go",
|
|
2061
|
-
".rs",
|
|
2062
|
-
".java",
|
|
2063
|
-
".kt"
|
|
2064
|
-
]);
|
|
2065
|
-
for (const e of entries) {
|
|
2066
|
-
if (!e.isDirectory()) continue;
|
|
2067
|
-
const itemDir = path9.join(patternsDir, e.name);
|
|
2068
|
-
const itemEntries = await readdir4(itemDir, { withFileTypes: true });
|
|
2069
|
-
const hasExample = itemEntries.some(
|
|
2070
|
-
(f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path9.extname(f.name).toLowerCase()))
|
|
2071
|
-
);
|
|
2072
|
-
if (!hasExample) {
|
|
2073
|
-
issues.push({
|
|
2074
|
-
severity: "warning",
|
|
2075
|
-
code: "W004",
|
|
2076
|
-
rule: "missing-example",
|
|
2077
|
-
message: `Pattern 'patterns/${e.name}' has no example file`
|
|
2078
|
-
});
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
} catch {
|
|
2082
|
-
}
|
|
2083
|
-
return issues;
|
|
2084
|
-
}
|
|
2085
|
-
function checkHighFanOut(graph) {
|
|
1991
|
+
function checkHighFanOut(graph) {
|
|
2086
1992
|
const issues = [];
|
|
2087
1993
|
const maxRel = graph.config.quality?.max_direct_relations ?? 10;
|
|
2088
1994
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -2099,57 +2005,6 @@ function checkHighFanOut(graph) {
|
|
|
2099
2005
|
}
|
|
2100
2006
|
return issues;
|
|
2101
2007
|
}
|
|
2102
|
-
function getNodesInScope(k, graph) {
|
|
2103
|
-
if (k.scope === "global") {
|
|
2104
|
-
return [...graph.nodes.keys()];
|
|
2105
|
-
}
|
|
2106
|
-
if (typeof k.scope === "object" && "nodes" in k.scope && k.scope.nodes) {
|
|
2107
|
-
return k.scope.nodes.filter((p) => graph.nodes.has(p));
|
|
2108
|
-
}
|
|
2109
|
-
if (typeof k.scope === "object" && "tags" in k.scope && k.scope.tags) {
|
|
2110
|
-
const tagSet = new Set(k.scope.tags);
|
|
2111
|
-
return [...graph.nodes.keys()].filter((p) => {
|
|
2112
|
-
const node = graph.nodes.get(p);
|
|
2113
|
-
return (node.meta.tags ?? []).some((t) => tagSet.has(t));
|
|
2114
|
-
});
|
|
2115
|
-
}
|
|
2116
|
-
return [];
|
|
2117
|
-
}
|
|
2118
|
-
async function checkStaleKnowledge(graph) {
|
|
2119
|
-
const issues = [];
|
|
2120
|
-
const stalenessDays = graph.config.quality?.knowledge_staleness_days ?? 90;
|
|
2121
|
-
const projectRoot = path9.dirname(graph.rootPath);
|
|
2122
|
-
const yggRel = path9.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
|
|
2123
|
-
for (const k of graph.knowledge) {
|
|
2124
|
-
const scopeNodes = getNodesInScope(k, graph);
|
|
2125
|
-
if (scopeNodes.length === 0) continue;
|
|
2126
|
-
const kPath = `${yggRel}/knowledge/${k.path}`;
|
|
2127
|
-
const tK = getLastCommitTimestamp(projectRoot, kPath);
|
|
2128
|
-
if (tK === null) continue;
|
|
2129
|
-
let maxTp = 0;
|
|
2130
|
-
let latestNode = "";
|
|
2131
|
-
for (const nodePath of scopeNodes) {
|
|
2132
|
-
const nodePathRel = `${yggRel}/model/${nodePath}`;
|
|
2133
|
-
const tP = getLastCommitTimestamp(projectRoot, nodePathRel);
|
|
2134
|
-
if (tP !== null && tP > maxTp) {
|
|
2135
|
-
maxTp = tP;
|
|
2136
|
-
latestNode = nodePath;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
if (maxTp === 0) continue;
|
|
2140
|
-
const diffDays = (maxTp - tK) / (60 * 60 * 24);
|
|
2141
|
-
if (diffDays > stalenessDays) {
|
|
2142
|
-
issues.push({
|
|
2143
|
-
severity: "warning",
|
|
2144
|
-
code: "W008",
|
|
2145
|
-
rule: "stale-knowledge",
|
|
2146
|
-
message: `Knowledge '${k.path}' may be stale: node '${latestNode}' modified ${Math.floor(diffDays)} days later (Git commits)`,
|
|
2147
|
-
nodePath: latestNode
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
return issues;
|
|
2152
|
-
}
|
|
2153
2008
|
function checkUnpairedEvents(graph) {
|
|
2154
2009
|
const issues = [];
|
|
2155
2010
|
const emitsTo = /* @__PURE__ */ new Map();
|
|
@@ -2198,7 +2053,7 @@ function checkUnpairedEvents(graph) {
|
|
|
2198
2053
|
}
|
|
2199
2054
|
return issues;
|
|
2200
2055
|
}
|
|
2201
|
-
var REQUIRED_SCHEMAS = ["node", "aspect", "flow"
|
|
2056
|
+
var REQUIRED_SCHEMAS = ["node", "aspect", "flow"];
|
|
2202
2057
|
function checkSchemas(graph) {
|
|
2203
2058
|
const issues = [];
|
|
2204
2059
|
const present = new Set(graph.schemas.map((s) => s.schemaType));
|
|
@@ -2208,7 +2063,7 @@ function checkSchemas(graph) {
|
|
|
2208
2063
|
severity: "warning",
|
|
2209
2064
|
code: "W010",
|
|
2210
2065
|
rule: "missing-schema",
|
|
2211
|
-
message: `Schema '${required}.yaml' missing from .yggdrasil/
|
|
2066
|
+
message: `Schema '${required}.yaml' missing from .yggdrasil/schemas/`
|
|
2212
2067
|
});
|
|
2213
2068
|
}
|
|
2214
2069
|
}
|
|
@@ -2253,26 +2108,26 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2253
2108
|
}
|
|
2254
2109
|
async function checkContextBudget(graph) {
|
|
2255
2110
|
const issues = [];
|
|
2256
|
-
const warningThreshold = graph.config.quality?.context_budget.warning ??
|
|
2257
|
-
const errorThreshold = graph.config.quality?.context_budget.error ??
|
|
2111
|
+
const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
|
|
2112
|
+
const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
|
|
2258
2113
|
for (const [nodePath, node] of graph.nodes) {
|
|
2259
2114
|
if (node.meta.blackbox) continue;
|
|
2260
2115
|
try {
|
|
2261
|
-
const
|
|
2262
|
-
if (
|
|
2116
|
+
const pkg2 = await buildContext(graph, nodePath);
|
|
2117
|
+
if (pkg2.tokenCount >= errorThreshold) {
|
|
2263
2118
|
issues.push({
|
|
2264
2119
|
severity: "warning",
|
|
2265
2120
|
code: "W006",
|
|
2266
2121
|
rule: "budget-error",
|
|
2267
|
-
message: `Context is ${
|
|
2122
|
+
message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}) \u2014 blocks materialization, node must be split`,
|
|
2268
2123
|
nodePath
|
|
2269
2124
|
});
|
|
2270
|
-
} else if (
|
|
2125
|
+
} else if (pkg2.tokenCount >= warningThreshold) {
|
|
2271
2126
|
issues.push({
|
|
2272
2127
|
severity: "warning",
|
|
2273
2128
|
code: "W005",
|
|
2274
2129
|
rule: "budget-warning",
|
|
2275
|
-
message: `Context is ${
|
|
2130
|
+
message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Consider splitting the node or reducing dependencies.`,
|
|
2276
2131
|
nodePath
|
|
2277
2132
|
});
|
|
2278
2133
|
}
|
|
@@ -2282,42 +2137,76 @@ async function checkContextBudget(graph) {
|
|
|
2282
2137
|
return issues;
|
|
2283
2138
|
}
|
|
2284
2139
|
|
|
2285
|
-
// src/formatters/
|
|
2286
|
-
function
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2140
|
+
// src/formatters/context-text.ts
|
|
2141
|
+
function escapeAttr(val) {
|
|
2142
|
+
return val.replace(/"/g, """);
|
|
2143
|
+
}
|
|
2144
|
+
function formatLayer(layer) {
|
|
2145
|
+
switch (layer.type) {
|
|
2146
|
+
case "global":
|
|
2147
|
+
return `<global>
|
|
2148
|
+
${layer.content}
|
|
2149
|
+
</global>`;
|
|
2150
|
+
case "hierarchy": {
|
|
2151
|
+
const pathMatch = layer.label.match(/\((.+)\/\)/);
|
|
2152
|
+
const pathAttr = pathMatch ? ` path="${escapeAttr(pathMatch[1])}"` : "";
|
|
2153
|
+
const aspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
|
|
2154
|
+
return `<hierarchy${pathAttr}${aspectsAttr}>
|
|
2155
|
+
${layer.content}
|
|
2156
|
+
</hierarchy>`;
|
|
2157
|
+
}
|
|
2158
|
+
case "own": {
|
|
2159
|
+
if (layer.label === "Materialization Target") {
|
|
2160
|
+
return `<materialization-target paths="${escapeAttr(layer.content)}" />`;
|
|
2161
|
+
}
|
|
2162
|
+
const ownAspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
|
|
2163
|
+
return `<own-artifacts${ownAspectsAttr}>
|
|
2164
|
+
${layer.content}
|
|
2165
|
+
</own-artifacts>`;
|
|
2166
|
+
}
|
|
2167
|
+
case "aspects": {
|
|
2168
|
+
const nameMatch = layer.label.match(/^(.+?) \(aspect: (.+)\)$/);
|
|
2169
|
+
const name = nameMatch ? escapeAttr(nameMatch[1]) : "";
|
|
2170
|
+
const id = nameMatch ? escapeAttr(nameMatch[2]) : "";
|
|
2171
|
+
return `<aspect name="${name}" id="${id}">
|
|
2172
|
+
${layer.content}
|
|
2173
|
+
</aspect>`;
|
|
2174
|
+
}
|
|
2175
|
+
case "relational": {
|
|
2176
|
+
const attrs = layer.attrs ?? {};
|
|
2177
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
|
|
2178
|
+
const tagName = attrs.type && ["emits", "listens"].includes(attrs.type) ? "event" : "dependency";
|
|
2179
|
+
return `<${tagName}${attrStr}>
|
|
2180
|
+
${layer.content}
|
|
2181
|
+
</${tagName}>`;
|
|
2182
|
+
}
|
|
2183
|
+
case "flows": {
|
|
2184
|
+
const flowName = layer.label.replace(/^Flow: /, "").trim();
|
|
2185
|
+
const flowAspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
|
|
2186
|
+
return `<flow name="${escapeAttr(flowName)}"${flowAspectsAttr}>
|
|
2187
|
+
${layer.content}
|
|
2188
|
+
</flow>`;
|
|
2189
|
+
}
|
|
2190
|
+
default:
|
|
2191
|
+
return layer.content;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
function formatContextText(pkg2) {
|
|
2195
|
+
const attrs = [
|
|
2196
|
+
`node-path="${escapeAttr(pkg2.nodePath)}"`,
|
|
2197
|
+
`node-name="${escapeAttr(pkg2.nodeName)}"`,
|
|
2198
|
+
`token-count="${pkg2.tokenCount}"`
|
|
2199
|
+
].join(" ");
|
|
2200
|
+
let out = `<context-package ${attrs}>
|
|
2301
2201
|
|
|
2302
2202
|
`;
|
|
2203
|
+
for (const section of pkg2.sections) {
|
|
2303
2204
|
for (const layer of section.layers) {
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
`;
|
|
2307
|
-
md += layer.content;
|
|
2308
|
-
md += `
|
|
2309
|
-
|
|
2310
|
-
`;
|
|
2205
|
+
out += formatLayer(layer) + "\n\n";
|
|
2311
2206
|
}
|
|
2312
|
-
md += `---
|
|
2313
|
-
|
|
2314
|
-
`;
|
|
2315
2207
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
md += `Layers: ${pkg.layers.map((l) => l.type).join(", ")}
|
|
2319
|
-
`;
|
|
2320
|
-
return md;
|
|
2208
|
+
out += "</context-package>";
|
|
2209
|
+
return out;
|
|
2321
2210
|
}
|
|
2322
2211
|
|
|
2323
2212
|
// src/cli/build-context.ts
|
|
@@ -2337,20 +2226,19 @@ function registerBuildCommand(program2) {
|
|
|
2337
2226
|
process.exit(1);
|
|
2338
2227
|
}
|
|
2339
2228
|
const nodePath = options.node.trim().replace(/\/$/, "");
|
|
2340
|
-
const
|
|
2341
|
-
const warningThreshold = graph.config.quality?.context_budget.warning ??
|
|
2342
|
-
const errorThreshold = graph.config.quality?.context_budget.error ??
|
|
2343
|
-
const budgetStatus =
|
|
2344
|
-
let output =
|
|
2229
|
+
const pkg2 = await buildContext(graph, nodePath);
|
|
2230
|
+
const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
|
|
2231
|
+
const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
|
|
2232
|
+
const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
|
|
2233
|
+
let output = formatContextText(pkg2);
|
|
2345
2234
|
output += `Budget status: ${budgetStatus}
|
|
2346
2235
|
`;
|
|
2347
2236
|
process.stdout.write(output);
|
|
2348
2237
|
if (budgetStatus === "error") {
|
|
2349
2238
|
process.stderr.write(
|
|
2350
|
-
`
|
|
2239
|
+
`Warning: context package exceeds error budget (${pkg2.tokenCount} >= ${errorThreshold}). Consider splitting the node.
|
|
2351
2240
|
`
|
|
2352
2241
|
);
|
|
2353
|
-
process.exit(1);
|
|
2354
2242
|
}
|
|
2355
2243
|
} catch (error) {
|
|
2356
2244
|
process.stderr.write(`Error: ${error.message}
|
|
@@ -2409,40 +2297,28 @@ import chalk2 from "chalk";
|
|
|
2409
2297
|
|
|
2410
2298
|
// src/io/drift-state-store.ts
|
|
2411
2299
|
import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
2412
|
-
import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
|
|
2413
2300
|
import path10 from "path";
|
|
2301
|
+
import { stringify, parse } from "yaml";
|
|
2414
2302
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2415
|
-
function getCanonicalHash(entry) {
|
|
2416
|
-
return typeof entry === "string" ? entry : entry.hash;
|
|
2417
|
-
}
|
|
2418
|
-
function getFileHashes(entry) {
|
|
2419
|
-
return typeof entry === "object" ? entry.files : void 0;
|
|
2420
|
-
}
|
|
2421
2303
|
async function readDriftState(yggRoot) {
|
|
2422
|
-
const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
|
|
2423
2304
|
try {
|
|
2424
|
-
const content = await readFile11(
|
|
2425
|
-
const raw =
|
|
2426
|
-
if (raw
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
} else if (typeof k === "string" && typeof v === "object" && v !== null && "hash" in v) {
|
|
2432
|
-
result[k] = v;
|
|
2433
|
-
}
|
|
2305
|
+
const content = await readFile11(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
|
|
2306
|
+
const raw = parse(content);
|
|
2307
|
+
if (!raw || typeof raw !== "object") return {};
|
|
2308
|
+
const state = {};
|
|
2309
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2310
|
+
if (typeof value === "object" && value !== null && "hash" in value) {
|
|
2311
|
+
state[key] = value;
|
|
2434
2312
|
}
|
|
2435
|
-
return result;
|
|
2436
2313
|
}
|
|
2437
|
-
return
|
|
2314
|
+
return state;
|
|
2438
2315
|
} catch {
|
|
2439
2316
|
return {};
|
|
2440
2317
|
}
|
|
2441
2318
|
}
|
|
2442
2319
|
async function writeDriftState(yggRoot, state) {
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2445
|
-
await writeFile3(filePath, content, "utf-8");
|
|
2320
|
+
const content = stringify(state, { lineWidth: 0 });
|
|
2321
|
+
await writeFile3(path10.join(yggRoot, DRIFT_STATE_FILE), content, "utf-8");
|
|
2446
2322
|
}
|
|
2447
2323
|
|
|
2448
2324
|
// src/utils/hash.ts
|
|
@@ -2456,46 +2332,29 @@ async function hashFile(filePath) {
|
|
|
2456
2332
|
const content = await readFile12(filePath);
|
|
2457
2333
|
return createHash("sha256").update(content).digest("hex");
|
|
2458
2334
|
}
|
|
2459
|
-
async function hashPath(targetPath, options = {}) {
|
|
2460
|
-
const projectRoot = options.projectRoot ? path11.resolve(options.projectRoot) : void 0;
|
|
2461
|
-
const gitignoreMatcher = await loadGitignoreMatcher(projectRoot);
|
|
2462
|
-
const targetStat = await stat3(targetPath);
|
|
2463
|
-
if (targetStat.isFile()) {
|
|
2464
|
-
if (isIgnoredPath(targetPath, projectRoot, gitignoreMatcher)) {
|
|
2465
|
-
return hashString("");
|
|
2466
|
-
}
|
|
2467
|
-
return hashFile(targetPath);
|
|
2468
|
-
}
|
|
2469
|
-
if (targetStat.isDirectory()) {
|
|
2470
|
-
const fileHashes = await collectDirectoryFileHashes(targetPath, targetPath, {
|
|
2471
|
-
projectRoot,
|
|
2472
|
-
gitignoreMatcher
|
|
2473
|
-
});
|
|
2474
|
-
const digestInput = fileHashes.sort((a, b) => a.path.localeCompare(b.path)).map((entry) => `${entry.path}:${entry.hash}`).join("\n");
|
|
2475
|
-
return hashString(digestInput);
|
|
2476
|
-
}
|
|
2477
|
-
throw new Error(`Unsupported mapping path type: ${targetPath}`);
|
|
2478
|
-
}
|
|
2479
2335
|
async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, options) {
|
|
2336
|
+
let stack = options.gitignoreStack ?? [];
|
|
2337
|
+
try {
|
|
2338
|
+
const localContent = await readFile12(path11.join(directoryPath, ".gitignore"), "utf-8");
|
|
2339
|
+
const localMatcher = ignoreFactory();
|
|
2340
|
+
localMatcher.add(localContent);
|
|
2341
|
+
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
2342
|
+
} catch {
|
|
2343
|
+
}
|
|
2480
2344
|
const entries = await readdir5(directoryPath, { withFileTypes: true });
|
|
2481
2345
|
const result = [];
|
|
2482
2346
|
for (const entry of entries) {
|
|
2483
2347
|
const absoluteChildPath = path11.join(directoryPath, entry.name);
|
|
2484
|
-
if (
|
|
2348
|
+
if (isIgnoredByStack(absoluteChildPath, stack)) {
|
|
2485
2349
|
continue;
|
|
2486
2350
|
}
|
|
2487
2351
|
if (entry.isDirectory()) {
|
|
2488
2352
|
const nested = await collectDirectoryFileHashes(
|
|
2489
2353
|
absoluteChildPath,
|
|
2490
2354
|
rootDirectoryPath,
|
|
2491
|
-
options
|
|
2355
|
+
{ projectRoot: options.projectRoot, gitignoreStack: stack }
|
|
2492
2356
|
);
|
|
2493
|
-
|
|
2494
|
-
result.push({
|
|
2495
|
-
path: path11.relative(rootDirectoryPath, path11.join(absoluteChildPath, nestedEntry.path)),
|
|
2496
|
-
hash: nestedEntry.hash
|
|
2497
|
-
});
|
|
2498
|
-
}
|
|
2357
|
+
result.push(...nested);
|
|
2499
2358
|
continue;
|
|
2500
2359
|
}
|
|
2501
2360
|
if (!entry.isFile()) {
|
|
@@ -2508,83 +2367,152 @@ async function collectDirectoryFileHashes(directoryPath, rootDirectoryPath, opti
|
|
|
2508
2367
|
}
|
|
2509
2368
|
return result;
|
|
2510
2369
|
}
|
|
2511
|
-
async function
|
|
2512
|
-
if (!projectRoot)
|
|
2513
|
-
return void 0;
|
|
2514
|
-
}
|
|
2370
|
+
async function loadRootGitignoreStack(projectRoot) {
|
|
2371
|
+
if (!projectRoot) return [];
|
|
2515
2372
|
try {
|
|
2516
|
-
const
|
|
2517
|
-
const gitignoreContent = await readFile12(gitignorePath, "utf-8");
|
|
2373
|
+
const content = await readFile12(path11.join(projectRoot, ".gitignore"), "utf-8");
|
|
2518
2374
|
const matcher = ignoreFactory();
|
|
2519
|
-
matcher.add(
|
|
2520
|
-
return matcher;
|
|
2375
|
+
matcher.add(content);
|
|
2376
|
+
return [{ basePath: projectRoot, matcher }];
|
|
2521
2377
|
} catch {
|
|
2522
|
-
return
|
|
2378
|
+
return [];
|
|
2523
2379
|
}
|
|
2524
2380
|
}
|
|
2525
|
-
function
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
if (relativePath === "" || relativePath.startsWith("..")) {
|
|
2531
|
-
return false;
|
|
2381
|
+
function isIgnoredByStack(candidatePath, stack) {
|
|
2382
|
+
for (const { basePath, matcher } of stack) {
|
|
2383
|
+
const relativePath = path11.relative(basePath, candidatePath);
|
|
2384
|
+
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
2385
|
+
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
2532
2386
|
}
|
|
2533
|
-
return
|
|
2387
|
+
return false;
|
|
2534
2388
|
}
|
|
2535
2389
|
function hashString(content) {
|
|
2536
2390
|
return createHash("sha256").update(content).digest("hex");
|
|
2537
2391
|
}
|
|
2538
|
-
async function
|
|
2539
|
-
const
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
} else if (st.isDirectory()) {
|
|
2550
|
-
const hashes = await collectDirectoryFileHashes(absPath, absPath, {
|
|
2551
|
-
projectRoot: root,
|
|
2552
|
-
gitignoreMatcher
|
|
2553
|
-
});
|
|
2554
|
-
for (const h of hashes) {
|
|
2555
|
-
result.push({
|
|
2556
|
-
path: path11.join(p, h.path).split(path11.sep).join("/"),
|
|
2557
|
-
hash: h.hash
|
|
2392
|
+
async function hashTrackedFiles(projectRoot, trackedFiles) {
|
|
2393
|
+
const fileHashes = {};
|
|
2394
|
+
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2395
|
+
for (const tf of trackedFiles) {
|
|
2396
|
+
const absPath = path11.join(projectRoot, tf.path);
|
|
2397
|
+
try {
|
|
2398
|
+
const st = await stat3(absPath);
|
|
2399
|
+
if (st.isDirectory()) {
|
|
2400
|
+
const dirHashes = await collectDirectoryFileHashes(absPath, absPath, {
|
|
2401
|
+
projectRoot,
|
|
2402
|
+
gitignoreStack
|
|
2558
2403
|
});
|
|
2404
|
+
for (const entry of dirHashes) {
|
|
2405
|
+
const fullRelPath = path11.join(tf.path, entry.path).replace(/\\/g, "/");
|
|
2406
|
+
fileHashes[fullRelPath] = entry.hash;
|
|
2407
|
+
}
|
|
2408
|
+
} else {
|
|
2409
|
+
fileHashes[tf.path] = await hashFile(absPath);
|
|
2559
2410
|
}
|
|
2411
|
+
} catch {
|
|
2412
|
+
continue;
|
|
2560
2413
|
}
|
|
2561
2414
|
}
|
|
2562
|
-
|
|
2415
|
+
const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
|
|
2416
|
+
const digest = sorted.map(([p, h]) => `${p}:${h}`).join("\n");
|
|
2417
|
+
const canonicalHash = hashString(digest);
|
|
2418
|
+
return { canonicalHash, fileHashes };
|
|
2563
2419
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2420
|
+
|
|
2421
|
+
// src/core/context-files.ts
|
|
2422
|
+
import path12 from "path";
|
|
2423
|
+
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2424
|
+
function collectTrackedFiles(node, graph) {
|
|
2425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2426
|
+
const result = [];
|
|
2427
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2428
|
+
const yggPrefix = path12.relative(projectRoot, graph.rootPath);
|
|
2429
|
+
const yggPrefixNormalized = yggPrefix.split(path12.sep).join("/");
|
|
2430
|
+
const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
|
|
2431
|
+
function addFile(filePath, category) {
|
|
2432
|
+
if (seen.has(filePath)) return;
|
|
2433
|
+
seen.add(filePath);
|
|
2434
|
+
result.push({ path: filePath, category });
|
|
2435
|
+
}
|
|
2436
|
+
function graphPath(...segments) {
|
|
2437
|
+
return [yggPrefixNormalized, ...segments].join("/");
|
|
2438
|
+
}
|
|
2439
|
+
function addNodeFiles(n) {
|
|
2440
|
+
addFile(graphPath("model", n.path, "node.yaml"), "graph");
|
|
2441
|
+
for (const art of n.artifacts) {
|
|
2442
|
+
if (configArtifactKeys.has(art.filename)) {
|
|
2443
|
+
addFile(graphPath("model", n.path, art.filename), "graph");
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
addNodeFiles(node);
|
|
2448
|
+
const ancestors = collectAncestors(node);
|
|
2449
|
+
for (const ancestor of ancestors) {
|
|
2450
|
+
addNodeFiles(ancestor);
|
|
2451
|
+
}
|
|
2452
|
+
const allAspectIds = /* @__PURE__ */ new Set();
|
|
2453
|
+
for (const id of node.meta.aspects ?? []) {
|
|
2454
|
+
allAspectIds.add(id);
|
|
2455
|
+
}
|
|
2456
|
+
for (const ancestor of ancestors) {
|
|
2457
|
+
for (const id of ancestor.meta.aspects ?? []) {
|
|
2458
|
+
allAspectIds.add(id);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
const participatingFlows = collectParticipatingFlows2(graph, node, ancestors);
|
|
2462
|
+
for (const flow of participatingFlows) {
|
|
2463
|
+
for (const id of flow.aspects ?? []) {
|
|
2464
|
+
allAspectIds.add(id);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
|
|
2468
|
+
for (const aspect of resolvedAspects) {
|
|
2469
|
+
addFile(graphPath("aspects", aspect.id, "aspect.yaml"), "graph");
|
|
2470
|
+
for (const art of aspect.artifacts) {
|
|
2471
|
+
addFile(graphPath("aspects", aspect.id, art.filename), "graph");
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
for (const relation of node.meta.relations ?? []) {
|
|
2475
|
+
if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
|
|
2476
|
+
const target = graph.nodes.get(relation.target);
|
|
2477
|
+
if (!target) continue;
|
|
2478
|
+
const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
|
|
2479
|
+
const structuralArts = structuralFilenames.filter(
|
|
2480
|
+
(filename) => target.artifacts.some((a) => a.filename === filename)
|
|
2481
|
+
);
|
|
2482
|
+
if (structuralArts.length > 0) {
|
|
2483
|
+
for (const filename of structuralArts) {
|
|
2484
|
+
addFile(graphPath("model", target.path, filename), "graph");
|
|
2485
|
+
}
|
|
2486
|
+
} else {
|
|
2487
|
+
for (const art of target.artifacts) {
|
|
2488
|
+
if (configArtifactKeys.has(art.filename)) {
|
|
2489
|
+
addFile(graphPath("model", target.path, art.filename), "graph");
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
for (const flow of participatingFlows) {
|
|
2495
|
+
addFile(graphPath("flows", flow.path, "flow.yaml"), "graph");
|
|
2496
|
+
for (const art of flow.artifacts) {
|
|
2497
|
+
addFile(graphPath("flows", flow.path, art.filename), "graph");
|
|
2577
2498
|
}
|
|
2578
2499
|
}
|
|
2579
|
-
const
|
|
2580
|
-
|
|
2500
|
+
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2501
|
+
for (const p of mappingPaths) {
|
|
2502
|
+
addFile(p, "source");
|
|
2503
|
+
}
|
|
2504
|
+
return result;
|
|
2505
|
+
}
|
|
2506
|
+
function collectParticipatingFlows2(graph, node, ancestors) {
|
|
2507
|
+
const paths = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
|
|
2508
|
+
return graph.flows.filter((f) => f.nodes.some((n) => paths.has(n)));
|
|
2581
2509
|
}
|
|
2582
2510
|
|
|
2583
2511
|
// src/core/drift-detector.ts
|
|
2584
2512
|
import { access } from "fs/promises";
|
|
2585
|
-
import
|
|
2513
|
+
import path13 from "path";
|
|
2586
2514
|
async function detectDrift(graph, filterNodePath) {
|
|
2587
|
-
const projectRoot =
|
|
2515
|
+
const projectRoot = path13.dirname(graph.rootPath);
|
|
2588
2516
|
const driftState = await readDriftState(graph.rootPath);
|
|
2589
2517
|
const entries = [];
|
|
2590
2518
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -2598,67 +2526,80 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
2598
2526
|
const allMissing = await allPathsMissing(projectRoot, mappingPaths);
|
|
2599
2527
|
entries.push({
|
|
2600
2528
|
nodePath,
|
|
2601
|
-
|
|
2602
|
-
status: allMissing ? "unmaterialized" : "drift",
|
|
2529
|
+
status: allMissing ? "unmaterialized" : "source-drift",
|
|
2603
2530
|
details: allMissing ? "No drift state recorded, files do not exist" : "No drift state recorded, files exist (run drift-sync after materialization)"
|
|
2604
2531
|
});
|
|
2605
2532
|
continue;
|
|
2606
2533
|
}
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2534
|
+
const sourceFilesMissing = await allPathsMissing(projectRoot, mappingPaths);
|
|
2535
|
+
if (sourceFilesMissing) {
|
|
2536
|
+
entries.push({
|
|
2537
|
+
nodePath,
|
|
2538
|
+
status: "missing",
|
|
2539
|
+
details: "All source mapping paths are missing"
|
|
2540
|
+
});
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
const trackedFiles = collectTrackedFiles(node, graph);
|
|
2544
|
+
const { canonicalHash, fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles);
|
|
2545
|
+
if (canonicalHash === storedEntry.hash) {
|
|
2546
|
+
entries.push({ nodePath, status: "ok" });
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
const changedFiles = [];
|
|
2550
|
+
const storedFiles = storedEntry.files;
|
|
2551
|
+
for (const [filePath, hash] of Object.entries(fileHashes)) {
|
|
2552
|
+
const storedHash = storedFiles[filePath];
|
|
2553
|
+
if (!storedHash || storedHash !== hash) {
|
|
2554
|
+
changedFiles.push({
|
|
2555
|
+
filePath,
|
|
2556
|
+
category: categorizeFile(filePath, graph.rootPath, projectRoot)
|
|
2557
|
+
});
|
|
2620
2558
|
}
|
|
2621
|
-
} catch {
|
|
2622
|
-
status = "missing";
|
|
2623
|
-
details = "Mapped path(s) do not exist";
|
|
2624
2559
|
}
|
|
2625
|
-
|
|
2560
|
+
for (const storedPath of Object.keys(storedFiles)) {
|
|
2561
|
+
if (!(storedPath in fileHashes)) {
|
|
2562
|
+
changedFiles.push({
|
|
2563
|
+
filePath: `${storedPath} (deleted)`,
|
|
2564
|
+
category: categorizeFile(storedPath, graph.rootPath, projectRoot)
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
const hasSourceChanges = changedFiles.some((f) => f.category === "source");
|
|
2569
|
+
const hasGraphChanges = changedFiles.some((f) => f.category === "graph");
|
|
2570
|
+
let status;
|
|
2571
|
+
if (hasSourceChanges && hasGraphChanges) {
|
|
2572
|
+
status = "full-drift";
|
|
2573
|
+
} else if (hasGraphChanges) {
|
|
2574
|
+
status = "graph-drift";
|
|
2575
|
+
} else if (hasSourceChanges) {
|
|
2576
|
+
status = "source-drift";
|
|
2577
|
+
} else {
|
|
2578
|
+
status = "source-drift";
|
|
2579
|
+
}
|
|
2580
|
+
const details = changedFiles.length > 0 ? `Changed files: ${changedFiles.map((f) => f.filePath).join(", ")}` : "File(s) modified since last sync";
|
|
2581
|
+
entries.push({ nodePath, status, details, changedFiles });
|
|
2626
2582
|
}
|
|
2627
2583
|
return {
|
|
2628
2584
|
entries,
|
|
2629
2585
|
totalChecked: entries.length,
|
|
2630
2586
|
okCount: entries.filter((e) => e.status === "ok").length,
|
|
2631
|
-
|
|
2587
|
+
sourceDriftCount: entries.filter((e) => e.status === "source-drift").length,
|
|
2588
|
+
graphDriftCount: entries.filter((e) => e.status === "graph-drift").length,
|
|
2589
|
+
fullDriftCount: entries.filter((e) => e.status === "full-drift").length,
|
|
2632
2590
|
missingCount: entries.filter((e) => e.status === "missing").length,
|
|
2633
2591
|
unmaterializedCount: entries.filter((e) => e.status === "unmaterialized").length
|
|
2634
2592
|
};
|
|
2635
2593
|
}
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
}
|
|
2642
|
-
const changed = [];
|
|
2643
|
-
const storedPaths = new Set(Object.keys(storedFileHashes));
|
|
2644
|
-
for (const { path: filePath, hash } of currentHashes) {
|
|
2645
|
-
const stored = storedFileHashes[filePath];
|
|
2646
|
-
if (!stored || stored !== hash) {
|
|
2647
|
-
changed.push(filePath);
|
|
2648
|
-
}
|
|
2649
|
-
storedPaths.delete(filePath);
|
|
2650
|
-
}
|
|
2651
|
-
for (const removed of storedPaths) {
|
|
2652
|
-
changed.push(`${removed} (deleted)`);
|
|
2653
|
-
}
|
|
2654
|
-
return changed.sort();
|
|
2655
|
-
} catch {
|
|
2656
|
-
return [];
|
|
2657
|
-
}
|
|
2594
|
+
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
2595
|
+
const yggPrefix = path13.relative(projectRoot, _rootPath);
|
|
2596
|
+
const normalizedPrefix = yggPrefix.split(path13.sep).join("/");
|
|
2597
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
2598
|
+
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
2658
2599
|
}
|
|
2659
2600
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
2660
2601
|
for (const mp of mappingPaths) {
|
|
2661
|
-
const absPath =
|
|
2602
|
+
const absPath = path13.join(projectRoot, mp);
|
|
2662
2603
|
try {
|
|
2663
2604
|
await access(absPath);
|
|
2664
2605
|
return false;
|
|
@@ -2668,82 +2609,43 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
2668
2609
|
return true;
|
|
2669
2610
|
}
|
|
2670
2611
|
async function syncDriftState(graph, nodePath) {
|
|
2671
|
-
const projectRoot =
|
|
2612
|
+
const projectRoot = path13.dirname(graph.rootPath);
|
|
2672
2613
|
const node = graph.nodes.get(nodePath);
|
|
2673
2614
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
const
|
|
2677
|
-
const
|
|
2678
|
-
const
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
for (const fh of fileHashes) {
|
|
2683
|
-
files[fh.path] = fh.hash;
|
|
2684
|
-
}
|
|
2685
|
-
const newEntry = { hash: currentHash, files };
|
|
2686
|
-
driftState[nodePath] = newEntry;
|
|
2687
|
-
await writeDriftState(graph.rootPath, driftState);
|
|
2688
|
-
return { previousHash, currentHash };
|
|
2615
|
+
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
2616
|
+
const trackedFiles = collectTrackedFiles(node, graph);
|
|
2617
|
+
const { canonicalHash, fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles);
|
|
2618
|
+
const state = await readDriftState(graph.rootPath);
|
|
2619
|
+
const previousHash = state[nodePath]?.hash;
|
|
2620
|
+
state[nodePath] = { hash: canonicalHash, files: fileHashes };
|
|
2621
|
+
await writeDriftState(graph.rootPath, state);
|
|
2622
|
+
return { previousHash, currentHash: canonicalHash };
|
|
2689
2623
|
}
|
|
2690
2624
|
|
|
2691
2625
|
// src/cli/drift.ts
|
|
2692
2626
|
function registerDriftCommand(program2) {
|
|
2693
|
-
program2.command("drift").description("Detect
|
|
2627
|
+
program2.command("drift").description("Detect divergences between graph and mapped files").option("--scope <scope>", 'Scope: "all" or node path', "all").option("--drifted-only", "Show only nodes with drift (hide ok entries)").action(async (opts) => {
|
|
2694
2628
|
try {
|
|
2695
2629
|
const graph = await loadGraph(process.cwd());
|
|
2696
|
-
const scope = (
|
|
2697
|
-
if (scope
|
|
2698
|
-
|
|
2630
|
+
const scope = (opts.scope ?? "all").trim() || "all";
|
|
2631
|
+
if (scope !== "all") {
|
|
2632
|
+
const node = graph.nodes.get(scope);
|
|
2633
|
+
if (!node) {
|
|
2634
|
+
process.stderr.write(`Error: Node not found: ${scope}
|
|
2699
2635
|
`);
|
|
2700
|
-
process.exit(1);
|
|
2701
|
-
}
|
|
2702
|
-
if (scope && scope !== "all") {
|
|
2703
|
-
const scopedNode = graph.nodes.get(scope);
|
|
2704
|
-
if (!scopedNode.meta.mapping) {
|
|
2705
|
-
process.stderr.write(
|
|
2706
|
-
`Error: Node has no mapping (does not participate in drift detection): ${options.scope}
|
|
2707
|
-
`
|
|
2708
|
-
);
|
|
2709
2636
|
process.exit(1);
|
|
2710
2637
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
const report = await detectDrift(graph, scopeNode);
|
|
2714
|
-
process.stdout.write("Drift:\n");
|
|
2715
|
-
for (const entry of report.entries) {
|
|
2716
|
-
const paths = entry.mappingPaths.join(", ");
|
|
2717
|
-
switch (entry.status) {
|
|
2718
|
-
case "ok":
|
|
2719
|
-
process.stdout.write(chalk2.green(` ok ${entry.nodePath} -> ${paths}
|
|
2720
|
-
`));
|
|
2721
|
-
break;
|
|
2722
|
-
case "drift":
|
|
2723
|
-
process.stdout.write(chalk2.red(` drift ${entry.nodePath} -> ${paths}
|
|
2724
|
-
`));
|
|
2725
|
-
if (entry.details) process.stdout.write(` ${entry.details}
|
|
2638
|
+
if (!node.meta.mapping) {
|
|
2639
|
+
process.stderr.write(`Error: Node has no mapping: ${scope}
|
|
2726
2640
|
`);
|
|
2727
|
-
|
|
2728
|
-
case "missing":
|
|
2729
|
-
process.stdout.write(chalk2.yellow(` missing ${entry.nodePath} -> ${paths}
|
|
2730
|
-
`));
|
|
2731
|
-
break;
|
|
2732
|
-
case "unmaterialized":
|
|
2733
|
-
process.stdout.write(chalk2.dim(` unmat. ${entry.nodePath} -> ${paths}
|
|
2734
|
-
`));
|
|
2735
|
-
break;
|
|
2641
|
+
process.exit(1);
|
|
2736
2642
|
}
|
|
2737
2643
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
);
|
|
2743
|
-
if (report.driftCount > 0 || report.missingCount > 0 || report.unmaterializedCount > 0) {
|
|
2744
|
-
process.exit(1);
|
|
2745
|
-
}
|
|
2746
|
-
process.exit(0);
|
|
2644
|
+
const scopeNode = scope === "all" ? void 0 : scope;
|
|
2645
|
+
const report = await detectDrift(graph, scopeNode);
|
|
2646
|
+
printReport(report, opts.driftedOnly ?? false);
|
|
2647
|
+
const hasIssues = report.sourceDriftCount > 0 || report.graphDriftCount > 0 || report.fullDriftCount > 0 || report.missingCount > 0 || report.unmaterializedCount > 0;
|
|
2648
|
+
process.exit(hasIssues ? 1 : 0);
|
|
2747
2649
|
} catch (error) {
|
|
2748
2650
|
process.stderr.write(`Error: ${error.message}
|
|
2749
2651
|
`);
|
|
@@ -2751,6 +2653,94 @@ Summary: ${report.driftCount} drift, ${report.missingCount} missing, ${report.un
|
|
|
2751
2653
|
}
|
|
2752
2654
|
});
|
|
2753
2655
|
}
|
|
2656
|
+
function printReport(report, driftedOnly) {
|
|
2657
|
+
const sourceEntries = classifyForSection(report.entries, "source", driftedOnly);
|
|
2658
|
+
const graphEntries = classifyForSection(report.entries, "graph", driftedOnly);
|
|
2659
|
+
process.stdout.write("Source drift:\n");
|
|
2660
|
+
printSectionEntries(sourceEntries, "source");
|
|
2661
|
+
process.stdout.write("\nGraph drift:\n");
|
|
2662
|
+
printSectionEntries(graphEntries, "graph");
|
|
2663
|
+
const parts = [
|
|
2664
|
+
`${report.sourceDriftCount} source-drift`,
|
|
2665
|
+
`${report.graphDriftCount} graph-drift`,
|
|
2666
|
+
`${report.fullDriftCount} full-drift`,
|
|
2667
|
+
`${report.missingCount} missing`,
|
|
2668
|
+
`${report.unmaterializedCount} unmaterialized`
|
|
2669
|
+
];
|
|
2670
|
+
let summary = `
|
|
2671
|
+
Summary: ${parts.join(", ")}`;
|
|
2672
|
+
if (driftedOnly && report.okCount > 0) {
|
|
2673
|
+
summary += ` (${report.okCount} ok hidden)`;
|
|
2674
|
+
} else {
|
|
2675
|
+
summary += `, ${report.okCount} ok`;
|
|
2676
|
+
}
|
|
2677
|
+
process.stdout.write(summary + "\n");
|
|
2678
|
+
}
|
|
2679
|
+
function classifyForSection(entries, section, driftedOnly) {
|
|
2680
|
+
return entries.filter((entry) => {
|
|
2681
|
+
if (section === "source") {
|
|
2682
|
+
if (entry.status === "graph-drift") return false;
|
|
2683
|
+
if (entry.status === "ok" && driftedOnly) return false;
|
|
2684
|
+
return true;
|
|
2685
|
+
} else {
|
|
2686
|
+
if (entry.status === "source-drift" || entry.status === "missing" || entry.status === "unmaterialized")
|
|
2687
|
+
return false;
|
|
2688
|
+
if (entry.status === "ok" && driftedOnly) return false;
|
|
2689
|
+
return true;
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
function printSectionEntries(entries, section) {
|
|
2694
|
+
if (entries.length === 0) {
|
|
2695
|
+
process.stdout.write(chalk2.dim(" (none)\n"));
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
for (const entry of entries) {
|
|
2699
|
+
printEntryLine(entry);
|
|
2700
|
+
printChangedFiles(entry, section);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
function printEntryLine(entry) {
|
|
2704
|
+
const pad = 13;
|
|
2705
|
+
switch (entry.status) {
|
|
2706
|
+
case "ok":
|
|
2707
|
+
process.stdout.write(chalk2.green(` ${"[ok]".padEnd(pad)}${entry.nodePath}
|
|
2708
|
+
`));
|
|
2709
|
+
break;
|
|
2710
|
+
case "source-drift":
|
|
2711
|
+
process.stdout.write(chalk2.red(` ${"[drift]".padEnd(pad)}${entry.nodePath}
|
|
2712
|
+
`));
|
|
2713
|
+
break;
|
|
2714
|
+
case "graph-drift":
|
|
2715
|
+
process.stdout.write(chalk2.magenta(` ${"[drift]".padEnd(pad)}${entry.nodePath}
|
|
2716
|
+
`));
|
|
2717
|
+
break;
|
|
2718
|
+
case "full-drift":
|
|
2719
|
+
process.stdout.write(chalk2.red(` ${"[drift]".padEnd(pad)}${entry.nodePath}
|
|
2720
|
+
`));
|
|
2721
|
+
break;
|
|
2722
|
+
case "missing":
|
|
2723
|
+
process.stdout.write(chalk2.yellow(` ${"[missing]".padEnd(pad)}${entry.nodePath}
|
|
2724
|
+
`));
|
|
2725
|
+
break;
|
|
2726
|
+
case "unmaterialized":
|
|
2727
|
+
process.stdout.write(chalk2.dim(` ${"[unmat.]".padEnd(pad)}${entry.nodePath}
|
|
2728
|
+
`));
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function printChangedFiles(entry, section) {
|
|
2733
|
+
if (!entry.changedFiles || entry.changedFiles.length === 0) return;
|
|
2734
|
+
const indent = " ".repeat(15);
|
|
2735
|
+
const relevantFiles = entry.changedFiles.filter((f) => {
|
|
2736
|
+
if (section === "source") return f.category === "source";
|
|
2737
|
+
return f.category === "graph";
|
|
2738
|
+
});
|
|
2739
|
+
for (const file of relevantFiles) {
|
|
2740
|
+
process.stdout.write(chalk2.dim(`${indent}${file.filePath} (changed)
|
|
2741
|
+
`));
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2754
2744
|
|
|
2755
2745
|
// src/cli/drift-sync.ts
|
|
2756
2746
|
import chalk3 from "chalk";
|
|
@@ -2788,20 +2778,40 @@ function registerStatusCommand(program2) {
|
|
|
2788
2778
|
let structuralRelations = 0;
|
|
2789
2779
|
let eventRelations = 0;
|
|
2790
2780
|
const structuralTypes = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2781
|
+
let maxRelCount = 0;
|
|
2782
|
+
let maxRelNode = "";
|
|
2791
2783
|
for (const node of graph.nodes.values()) {
|
|
2784
|
+
const relCount = (node.meta.relations ?? []).length;
|
|
2785
|
+
if (relCount > maxRelCount) {
|
|
2786
|
+
maxRelCount = relCount;
|
|
2787
|
+
maxRelNode = node.path;
|
|
2788
|
+
}
|
|
2792
2789
|
for (const rel of node.meta.relations ?? []) {
|
|
2793
2790
|
if (structuralTypes.has(rel.type)) structuralRelations += 1;
|
|
2794
2791
|
else eventRelations += 1;
|
|
2795
2792
|
}
|
|
2796
2793
|
}
|
|
2797
2794
|
const flowCount = graph.flows.length;
|
|
2798
|
-
const knowledgeCount = graph.knowledge.length;
|
|
2799
2795
|
const drift = await detectDrift(graph);
|
|
2800
2796
|
const validation = await validate(graph, "all");
|
|
2801
2797
|
const errorCount = validation.issues.filter((issue) => issue.severity === "error").length;
|
|
2802
2798
|
const warningCount = validation.issues.filter(
|
|
2803
2799
|
(issue) => issue.severity === "warning"
|
|
2804
2800
|
).length;
|
|
2801
|
+
const configuredArtifactTypes = Object.keys(graph.config.artifacts ?? {});
|
|
2802
|
+
const totalSlots = graph.nodes.size * configuredArtifactTypes.length;
|
|
2803
|
+
let filledSlots = 0;
|
|
2804
|
+
let mappedNodeCount = 0;
|
|
2805
|
+
for (const node of graph.nodes.values()) {
|
|
2806
|
+
const allowed = new Set(configuredArtifactTypes);
|
|
2807
|
+
filledSlots += node.artifacts.filter((a) => allowed.has(a.filename)).length;
|
|
2808
|
+
if (normalizeMappingPaths(node.meta.mapping).length > 0) mappedNodeCount++;
|
|
2809
|
+
}
|
|
2810
|
+
let aspectCoveredNodes = 0;
|
|
2811
|
+
for (const node of graph.nodes.values()) {
|
|
2812
|
+
const effective = collectEffectiveAspectIds(graph, node.path);
|
|
2813
|
+
if (effective.size > 0) aspectCoveredNodes++;
|
|
2814
|
+
}
|
|
2805
2815
|
process.stdout.write(`Graph: ${graph.config.name}
|
|
2806
2816
|
`);
|
|
2807
2817
|
const pluralize = (word, count) => count === 1 ? word : word.endsWith("y") ? word.slice(0, -1) + "ies" : word + "s";
|
|
@@ -2815,15 +2825,37 @@ function registerStatusCommand(program2) {
|
|
|
2815
2825
|
`
|
|
2816
2826
|
);
|
|
2817
2827
|
process.stdout.write(
|
|
2818
|
-
`Aspects: ${graph.aspects.length} Flows: ${flowCount}
|
|
2828
|
+
`Aspects: ${graph.aspects.length} Flows: ${flowCount}
|
|
2819
2829
|
`
|
|
2820
2830
|
);
|
|
2821
2831
|
process.stdout.write(
|
|
2822
|
-
`Drift: ${drift.
|
|
2832
|
+
`Drift: ${drift.sourceDriftCount} source-drift, ${drift.graphDriftCount} graph-drift, ${drift.fullDriftCount} full-drift, ${drift.missingCount} missing, ${drift.unmaterializedCount} unmaterialized, ${drift.okCount} ok
|
|
2823
2833
|
`
|
|
2824
2834
|
);
|
|
2825
2835
|
process.stdout.write(`Validation: ${errorCount} errors, ${warningCount} warnings
|
|
2826
2836
|
`);
|
|
2837
|
+
const fillPct = totalSlots > 0 ? Math.round(filledSlots / totalSlots * 100) : 0;
|
|
2838
|
+
const totalRelations = structuralRelations + eventRelations;
|
|
2839
|
+
const avgRel = graph.nodes.size > 0 ? (totalRelations / graph.nodes.size).toFixed(1) : "0";
|
|
2840
|
+
process.stdout.write(`
|
|
2841
|
+
Quality:
|
|
2842
|
+
`);
|
|
2843
|
+
process.stdout.write(
|
|
2844
|
+
` Artifacts: ${filledSlots}/${totalSlots} slots filled (${fillPct}%) \u2014 ${configuredArtifactTypes.length} types \xD7 ${graph.nodes.size} nodes
|
|
2845
|
+
`
|
|
2846
|
+
);
|
|
2847
|
+
process.stdout.write(
|
|
2848
|
+
` Relations: avg ${avgRel}/node, max ${maxRelCount}${maxRelNode ? ` (${maxRelNode})` : ""}
|
|
2849
|
+
`
|
|
2850
|
+
);
|
|
2851
|
+
process.stdout.write(
|
|
2852
|
+
` Mapping: ${mappedNodeCount}/${graph.nodes.size} nodes mapped to source
|
|
2853
|
+
`
|
|
2854
|
+
);
|
|
2855
|
+
process.stdout.write(
|
|
2856
|
+
` Aspects: ${aspectCoveredNodes}/${graph.nodes.size} nodes have aspect coverage
|
|
2857
|
+
`
|
|
2858
|
+
);
|
|
2827
2859
|
} catch (error) {
|
|
2828
2860
|
process.stderr.write(`Error: ${error.message}
|
|
2829
2861
|
`);
|
|
@@ -2840,10 +2872,10 @@ function registerTreeCommand(program2) {
|
|
|
2840
2872
|
let roots;
|
|
2841
2873
|
let showProjectName;
|
|
2842
2874
|
if (options.root?.trim()) {
|
|
2843
|
-
const
|
|
2844
|
-
const node = graph.nodes.get(
|
|
2875
|
+
const path17 = options.root.trim().replace(/\/$/, "");
|
|
2876
|
+
const node = graph.nodes.get(path17);
|
|
2845
2877
|
if (!node) {
|
|
2846
|
-
process.stderr.write(`Error: path '${
|
|
2878
|
+
process.stderr.write(`Error: path '${path17}' not found
|
|
2847
2879
|
`);
|
|
2848
2880
|
process.exit(1);
|
|
2849
2881
|
}
|
|
@@ -2871,7 +2903,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
2871
2903
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
2872
2904
|
const name = node.path.split("/").pop() ?? node.path;
|
|
2873
2905
|
const type = `[${node.meta.type}]`;
|
|
2874
|
-
const tags = node.meta.
|
|
2906
|
+
const tags = node.meta.aspects?.length ? ` aspects:${node.meta.aspects.join(",")}` : "";
|
|
2875
2907
|
const blackbox = node.meta.blackbox ? " \u25A0 blackbox" : "";
|
|
2876
2908
|
const relationCount = node.meta.relations?.length ?? 0;
|
|
2877
2909
|
process.stdout.write(
|
|
@@ -2930,13 +2962,13 @@ function registerOwnerCommand(program2) {
|
|
|
2930
2962
|
}
|
|
2931
2963
|
|
|
2932
2964
|
// src/core/dependency-resolver.ts
|
|
2933
|
-
import { execSync
|
|
2934
|
-
import
|
|
2935
|
-
var
|
|
2965
|
+
import { execSync } from "child_process";
|
|
2966
|
+
import path14 from "path";
|
|
2967
|
+
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2936
2968
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
2937
2969
|
function filterRelationType(relType, filter) {
|
|
2938
2970
|
if (filter === "all") return true;
|
|
2939
|
-
if (filter === "structural") return
|
|
2971
|
+
if (filter === "structural") return STRUCTURAL_RELATION_TYPES3.has(relType);
|
|
2940
2972
|
if (filter === "event") return EVENT_RELATION_TYPES2.has(relType);
|
|
2941
2973
|
return false;
|
|
2942
2974
|
}
|
|
@@ -3008,24 +3040,24 @@ function registerDepsCommand(program2) {
|
|
|
3008
3040
|
// src/core/graph-from-git.ts
|
|
3009
3041
|
import { mkdtemp, rm } from "fs/promises";
|
|
3010
3042
|
import { tmpdir } from "os";
|
|
3011
|
-
import
|
|
3012
|
-
import { execSync as
|
|
3043
|
+
import path15 from "path";
|
|
3044
|
+
import { execSync as execSync2 } from "child_process";
|
|
3013
3045
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
3014
3046
|
const yggPath = ".yggdrasil";
|
|
3015
3047
|
let tmpDir = null;
|
|
3016
3048
|
try {
|
|
3017
|
-
|
|
3049
|
+
execSync2(`git rev-parse ${ref}`, { cwd: projectRoot, stdio: "pipe" });
|
|
3018
3050
|
} catch {
|
|
3019
3051
|
return null;
|
|
3020
3052
|
}
|
|
3021
3053
|
try {
|
|
3022
|
-
tmpDir = await mkdtemp(
|
|
3023
|
-
const archivePath =
|
|
3024
|
-
|
|
3054
|
+
tmpDir = await mkdtemp(path15.join(tmpdir(), "ygg-git-"));
|
|
3055
|
+
const archivePath = path15.join(tmpDir, "archive.tar");
|
|
3056
|
+
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
3025
3057
|
cwd: projectRoot,
|
|
3026
3058
|
stdio: "pipe"
|
|
3027
3059
|
});
|
|
3028
|
-
|
|
3060
|
+
execSync2(`tar -xf "${archivePath}" -C "${tmpDir}"`, { stdio: "pipe" });
|
|
3029
3061
|
const graph = await loadGraph(tmpDir);
|
|
3030
3062
|
return graph;
|
|
3031
3063
|
} catch {
|
|
@@ -3067,14 +3099,14 @@ function collectReverseDependents(graph, targetNode) {
|
|
|
3067
3099
|
}
|
|
3068
3100
|
return {
|
|
3069
3101
|
direct,
|
|
3070
|
-
|
|
3102
|
+
allDependents: [...seen].sort(),
|
|
3071
3103
|
reverse,
|
|
3072
3104
|
relationFrom
|
|
3073
3105
|
};
|
|
3074
3106
|
}
|
|
3075
|
-
function buildTransitiveChains(targetNode, direct,
|
|
3107
|
+
function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
3076
3108
|
const directSet = new Set(direct);
|
|
3077
|
-
const transitiveOnly =
|
|
3109
|
+
const transitiveOnly = allDependents.filter((t) => !directSet.has(t));
|
|
3078
3110
|
if (transitiveOnly.length === 0) return [];
|
|
3079
3111
|
const parent = /* @__PURE__ */ new Map();
|
|
3080
3112
|
const queue = [targetNode];
|
|
@@ -3090,148 +3122,342 @@ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
|
|
|
3090
3122
|
}
|
|
3091
3123
|
const chains = [];
|
|
3092
3124
|
for (const node of transitiveOnly) {
|
|
3093
|
-
const
|
|
3125
|
+
const path17 = [];
|
|
3094
3126
|
let current = node;
|
|
3095
3127
|
while (current) {
|
|
3096
|
-
|
|
3128
|
+
path17.unshift(current);
|
|
3097
3129
|
current = parent.get(current);
|
|
3098
3130
|
}
|
|
3099
|
-
if (
|
|
3100
|
-
chains.push(
|
|
3131
|
+
if (path17.length >= 3) {
|
|
3132
|
+
chains.push(path17.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
3101
3133
|
}
|
|
3102
3134
|
}
|
|
3103
3135
|
return chains.sort();
|
|
3104
3136
|
}
|
|
3105
|
-
function
|
|
3106
|
-
|
|
3137
|
+
function collectDescendants(graph, nodePath) {
|
|
3138
|
+
const node = graph.nodes.get(nodePath);
|
|
3139
|
+
if (!node) return [];
|
|
3140
|
+
const result = [];
|
|
3141
|
+
const stack = [...node.children];
|
|
3142
|
+
while (stack.length > 0) {
|
|
3143
|
+
const child = stack.pop();
|
|
3144
|
+
result.push(child.path);
|
|
3145
|
+
stack.push(...child.children);
|
|
3146
|
+
}
|
|
3147
|
+
return result.sort();
|
|
3148
|
+
}
|
|
3149
|
+
async function runSimulation(graph, nodePaths, targetNodePath) {
|
|
3150
|
+
const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
|
|
3151
|
+
process.stdout.write("\nChanges in context packages:\n\n");
|
|
3152
|
+
const baselineGraph = await loadGraphFromRef(process.cwd(), "HEAD");
|
|
3153
|
+
const driftReport = await detectDrift(graph);
|
|
3154
|
+
const driftByNode = new Map(driftReport.entries.map((e) => [e.nodePath, e]));
|
|
3155
|
+
for (const dep of nodePaths) {
|
|
3107
3156
|
try {
|
|
3108
|
-
const
|
|
3109
|
-
const
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3157
|
+
const pkg2 = await buildContext(graph, dep);
|
|
3158
|
+
const status = pkg2.tokenCount >= budget.error ? "error" : pkg2.tokenCount >= budget.warning ? "warning" : "ok";
|
|
3159
|
+
let baselineTokens = null;
|
|
3160
|
+
if (baselineGraph?.nodes.has(dep)) {
|
|
3161
|
+
try {
|
|
3162
|
+
const baselinePkg = await buildContext(baselineGraph, dep);
|
|
3163
|
+
baselineTokens = baselinePkg.tokenCount;
|
|
3164
|
+
} catch {
|
|
3165
|
+
}
|
|
3114
3166
|
}
|
|
3115
|
-
const
|
|
3116
|
-
|
|
3117
|
-
nodePath
|
|
3167
|
+
const hasDepOnTarget = targetNodePath && graph.nodes.get(dep)?.meta.relations?.some(
|
|
3168
|
+
(r) => r.target === targetNodePath && STRUCTURAL_TYPES.has(r.type)
|
|
3118
3169
|
);
|
|
3119
|
-
const
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3170
|
+
const changedLine = hasDepOnTarget ? ` + Changed dependency interface: ${targetNodePath}
|
|
3171
|
+
` : "";
|
|
3172
|
+
const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${pkg2.tokenCount} tokens (${status})
|
|
3173
|
+
` : ` Budget: ${pkg2.tokenCount} tokens (${status})
|
|
3174
|
+
`;
|
|
3175
|
+
const driftEntry = driftByNode.get(dep);
|
|
3176
|
+
const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}
|
|
3177
|
+
` : driftEntry ? ` Mapped files (on-disk): ok
|
|
3178
|
+
` : "";
|
|
3179
|
+
process.stdout.write(`${dep}:
|
|
3180
|
+
${changedLine}${budgetLine}${driftLine}
|
|
3181
|
+
`);
|
|
3182
|
+
} catch {
|
|
3183
|
+
process.stdout.write(`${dep}:
|
|
3184
|
+
failed to build context
|
|
3185
|
+
|
|
3186
|
+
`);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
async function handleAspectImpact(graph, aspectId, simulate) {
|
|
3191
|
+
const aspect = graph.aspects.find((a) => a.id === aspectId);
|
|
3192
|
+
if (!aspect) {
|
|
3193
|
+
process.stderr.write(`Aspect not found: ${aspectId}
|
|
3194
|
+
`);
|
|
3195
|
+
process.exit(1);
|
|
3196
|
+
}
|
|
3197
|
+
const affected = [];
|
|
3198
|
+
for (const [nodePath] of graph.nodes) {
|
|
3199
|
+
const effective = collectEffectiveAspectIds(graph, nodePath);
|
|
3200
|
+
if (effective.has(aspectId)) {
|
|
3201
|
+
const node = graph.nodes.get(nodePath);
|
|
3202
|
+
const ownAspects = new Set(node.meta.aspects ?? []);
|
|
3203
|
+
if (ownAspects.has(aspectId)) {
|
|
3204
|
+
affected.push({ path: nodePath, source: "own" });
|
|
3205
|
+
} else {
|
|
3206
|
+
let fromHierarchy = false;
|
|
3207
|
+
let anc = node.parent;
|
|
3208
|
+
while (anc) {
|
|
3209
|
+
if ((anc.meta.aspects ?? []).includes(aspectId)) {
|
|
3210
|
+
fromHierarchy = true;
|
|
3211
|
+
break;
|
|
3212
|
+
}
|
|
3213
|
+
anc = anc.parent;
|
|
3124
3214
|
}
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3215
|
+
if (fromHierarchy) {
|
|
3216
|
+
affected.push({ path: nodePath, source: `hierarchy from ${anc.path}` });
|
|
3217
|
+
} else {
|
|
3218
|
+
const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
|
|
3219
|
+
const flow = graph.flows.find(
|
|
3220
|
+
(f) => (f.aspects ?? []).includes(aspectId) && f.nodes.some((n) => ancestorPaths.has(n))
|
|
3221
|
+
);
|
|
3222
|
+
affected.push({ path: nodePath, source: flow ? `flow: ${flow.name}` : "implied" });
|
|
3132
3223
|
}
|
|
3133
3224
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
affected.sort((a, b) => a.path.localeCompare(b.path));
|
|
3228
|
+
const propagatingFlows = graph.flows.filter((f) => (f.aspects ?? []).includes(aspectId)).map((f) => f.name);
|
|
3229
|
+
const impliedBy = graph.aspects.filter((a) => (a.implies ?? []).includes(aspectId)).map((a) => a.id);
|
|
3230
|
+
const implies = aspect.implies ?? [];
|
|
3231
|
+
process.stdout.write(`Impact of changes in aspect ${aspectId}:
|
|
3232
|
+
|
|
3233
|
+
`);
|
|
3234
|
+
process.stdout.write(`Affected nodes (${affected.length}):
|
|
3235
|
+
`);
|
|
3236
|
+
if (affected.length === 0) {
|
|
3237
|
+
process.stdout.write(" (none)\n");
|
|
3238
|
+
} else {
|
|
3239
|
+
for (const { path: p, source } of affected) {
|
|
3240
|
+
process.stdout.write(` ${p} (${source})
|
|
3241
|
+
`);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
process.stdout.write(
|
|
3245
|
+
`
|
|
3246
|
+
Flows propagating this aspect: ${propagatingFlows.length > 0 ? propagatingFlows.join(", ") : "(none)"}
|
|
3247
|
+
`
|
|
3248
|
+
);
|
|
3249
|
+
process.stdout.write(`Implied by: ${impliedBy.length > 0 ? impliedBy.join(", ") : "(none)"}
|
|
3250
|
+
`);
|
|
3251
|
+
process.stdout.write(`Implies: ${implies.length > 0 ? implies.join(", ") : "(none)"}
|
|
3252
|
+
`);
|
|
3253
|
+
process.stdout.write(`
|
|
3254
|
+
Total scope: ${affected.length} nodes, ${propagatingFlows.length} flows
|
|
3255
|
+
`);
|
|
3256
|
+
if (simulate && affected.length > 0) {
|
|
3257
|
+
await runSimulation(
|
|
3258
|
+
graph,
|
|
3259
|
+
affected.map((a) => a.path),
|
|
3260
|
+
null
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
async function handleFlowImpact(graph, flowName, simulate) {
|
|
3265
|
+
const flow = graph.flows.find((f) => f.name === flowName || f.path === flowName);
|
|
3266
|
+
if (!flow) {
|
|
3267
|
+
process.stderr.write(`Flow not found: ${flowName}
|
|
3268
|
+
`);
|
|
3269
|
+
process.exit(1);
|
|
3270
|
+
}
|
|
3271
|
+
const participants = /* @__PURE__ */ new Set();
|
|
3272
|
+
for (const nodePath of flow.nodes) {
|
|
3273
|
+
if (graph.nodes.has(nodePath)) {
|
|
3274
|
+
participants.add(nodePath);
|
|
3275
|
+
for (const desc of collectDescendants(graph, nodePath)) {
|
|
3276
|
+
participants.add(desc);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
const sorted = [...participants].sort();
|
|
3281
|
+
const flowAspects = flow.aspects ?? [];
|
|
3282
|
+
process.stdout.write(`Impact of changes in flow ${flow.name}:
|
|
3283
|
+
|
|
3284
|
+
`);
|
|
3285
|
+
process.stdout.write("Participants:\n");
|
|
3286
|
+
if (sorted.length === 0) {
|
|
3287
|
+
process.stdout.write(" (none)\n");
|
|
3288
|
+
} else {
|
|
3289
|
+
for (const p of sorted) {
|
|
3290
|
+
const isDeclared = flow.nodes.includes(p);
|
|
3291
|
+
const suffix = isDeclared ? "" : " (descendant)";
|
|
3292
|
+
process.stdout.write(` ${p}${suffix}
|
|
3293
|
+
`);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
process.stdout.write(
|
|
3297
|
+
`
|
|
3298
|
+
Flow aspects: ${flowAspects.length > 0 ? flowAspects.join(", ") : "(none)"}
|
|
3299
|
+
`
|
|
3300
|
+
);
|
|
3301
|
+
process.stdout.write(`
|
|
3302
|
+
Total scope: ${sorted.length} nodes
|
|
3303
|
+
`);
|
|
3304
|
+
if (simulate && sorted.length > 0) {
|
|
3305
|
+
await runSimulation(graph, sorted, null);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
function registerImpactCommand(program2) {
|
|
3309
|
+
program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
|
|
3310
|
+
async (options) => {
|
|
3311
|
+
try {
|
|
3312
|
+
const modeCount = [options.node, options.aspect, options.flow].filter(Boolean).length;
|
|
3313
|
+
if (modeCount === 0) {
|
|
3314
|
+
process.stderr.write(
|
|
3315
|
+
"Error: one of --node, --aspect, or --flow is required\n"
|
|
3316
|
+
);
|
|
3317
|
+
process.exit(1);
|
|
3318
|
+
}
|
|
3319
|
+
if (modeCount > 1) {
|
|
3320
|
+
process.stderr.write(
|
|
3321
|
+
"Error: --node, --aspect, and --flow are mutually exclusive\n"
|
|
3322
|
+
);
|
|
3323
|
+
process.exit(1);
|
|
3324
|
+
}
|
|
3325
|
+
const graph = await loadGraph(process.cwd());
|
|
3326
|
+
if (options.aspect) {
|
|
3327
|
+
await handleAspectImpact(graph, options.aspect.trim(), options.simulate);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
if (options.flow) {
|
|
3331
|
+
await handleFlowImpact(graph, options.flow.trim(), options.simulate);
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
const nodePath = options.node.trim().replace(/\/$/, "");
|
|
3335
|
+
if (!graph.nodes.has(nodePath)) {
|
|
3336
|
+
process.stderr.write(`Node not found: ${nodePath}
|
|
3337
|
+
`);
|
|
3338
|
+
process.exit(1);
|
|
3139
3339
|
}
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3340
|
+
const { direct, allDependents, reverse, relationFrom } = collectReverseDependents(
|
|
3341
|
+
graph,
|
|
3342
|
+
nodePath
|
|
3343
|
+
);
|
|
3344
|
+
const chains = buildTransitiveChains(nodePath, direct, allDependents, reverse);
|
|
3345
|
+
const flows = [];
|
|
3346
|
+
for (const flow of graph.flows) {
|
|
3347
|
+
if (flow.nodes.includes(nodePath)) {
|
|
3348
|
+
flows.push(flow.name);
|
|
3143
3349
|
}
|
|
3144
|
-
continue;
|
|
3145
3350
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3351
|
+
const targetEffective = collectEffectiveAspectIds(graph, nodePath);
|
|
3352
|
+
const aspectsInScope = [];
|
|
3353
|
+
for (const aspect of graph.aspects) {
|
|
3354
|
+
if (targetEffective.has(aspect.id)) {
|
|
3355
|
+
aspectsInScope.push(aspect.name);
|
|
3149
3356
|
}
|
|
3150
3357
|
}
|
|
3151
|
-
|
|
3152
|
-
const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
|
|
3153
|
-
process.stdout.write(`Impact of changes in ${nodePath}:
|
|
3358
|
+
process.stdout.write(`Impact of changes in ${nodePath}:
|
|
3154
3359
|
|
|
3155
3360
|
`);
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3361
|
+
process.stdout.write("Directly dependent:\n");
|
|
3362
|
+
if (direct.length === 0) {
|
|
3363
|
+
process.stdout.write(" (none)\n");
|
|
3364
|
+
} else {
|
|
3365
|
+
for (const dep of direct) {
|
|
3366
|
+
const rel = relationFrom.get(`${dep}->${nodePath}`);
|
|
3367
|
+
const annot = rel?.consumes?.length ? ` (${rel.type}, you consume: ${rel.consumes.join(", ")})` : rel ? ` (${rel.type})` : "";
|
|
3368
|
+
process.stdout.write(` <- ${dep}${annot}
|
|
3164
3369
|
`);
|
|
3370
|
+
}
|
|
3165
3371
|
}
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
process.stdout.write(` ${chain}
|
|
3372
|
+
process.stdout.write("\nTransitively dependent:\n");
|
|
3373
|
+
if (chains.length === 0) {
|
|
3374
|
+
process.stdout.write(" (none)\n");
|
|
3375
|
+
} else {
|
|
3376
|
+
for (const chain of chains) {
|
|
3377
|
+
process.stdout.write(` ${chain}
|
|
3173
3378
|
`);
|
|
3379
|
+
}
|
|
3174
3380
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3381
|
+
const descendants = collectDescendants(graph, nodePath);
|
|
3382
|
+
if (descendants.length > 0) {
|
|
3383
|
+
process.stdout.write("\nDescendants (hierarchy impact):\n");
|
|
3384
|
+
for (const desc of descendants) {
|
|
3385
|
+
process.stdout.write(` ${desc}
|
|
3178
3386
|
`);
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
`Knowledge (scope covers node): ${knowledgeInScope.length > 0 ? knowledgeInScope.join(", ") : "(none)"}
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
process.stdout.write(
|
|
3390
|
+
`
|
|
3391
|
+
Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
|
|
3185
3392
|
`
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
Total scope: ${transitive.length} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects, ${knowledgeInScope.length} knowledge
|
|
3393
|
+
);
|
|
3394
|
+
process.stdout.write(
|
|
3395
|
+
`Aspects (scope covers node): ${aspectsInScope.length > 0 ? aspectsInScope.join(", ") : "(none)"}
|
|
3190
3396
|
`
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
const status = pkg.tokenCount >= budget.error ? "error" : pkg.tokenCount >= budget.warning ? "warning" : "ok";
|
|
3201
|
-
let baselineTokens = null;
|
|
3202
|
-
if (baselineGraph?.nodes.has(dep)) {
|
|
3203
|
-
try {
|
|
3204
|
-
const baselinePkg = await buildContext(baselineGraph, dep);
|
|
3205
|
-
baselineTokens = baselinePkg.tokenCount;
|
|
3206
|
-
} catch {
|
|
3207
|
-
}
|
|
3397
|
+
);
|
|
3398
|
+
const coAspectNodes = [];
|
|
3399
|
+
if (targetEffective.size > 0) {
|
|
3400
|
+
for (const [p] of graph.nodes) {
|
|
3401
|
+
if (p === nodePath) continue;
|
|
3402
|
+
const nodeEffective = collectEffectiveAspectIds(graph, p);
|
|
3403
|
+
const shared = [...targetEffective].filter((id) => nodeEffective.has(id));
|
|
3404
|
+
if (shared.length > 0) {
|
|
3405
|
+
coAspectNodes.push({ path: p, shared });
|
|
3208
3406
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
const driftEntry = driftByNode.get(dep);
|
|
3218
|
-
const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}
|
|
3219
|
-
` : driftEntry ? ` Mapped files (on-disk): ok
|
|
3220
|
-
` : "";
|
|
3221
|
-
process.stdout.write(`${dep}:
|
|
3222
|
-
${changedLine}${budgetLine}${driftLine}
|
|
3223
|
-
`);
|
|
3224
|
-
} catch {
|
|
3225
|
-
process.stdout.write(`${dep}:
|
|
3226
|
-
failed to build context
|
|
3227
|
-
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
if (coAspectNodes.length > 0) {
|
|
3410
|
+
process.stdout.write("Nodes sharing aspects:\n");
|
|
3411
|
+
for (const { path: p, shared } of coAspectNodes.sort(
|
|
3412
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
3413
|
+
)) {
|
|
3414
|
+
process.stdout.write(` ${p} (${shared.join(", ")})
|
|
3228
3415
|
`);
|
|
3229
3416
|
}
|
|
3230
3417
|
}
|
|
3418
|
+
const allAffected = /* @__PURE__ */ new Set([...allDependents, ...descendants]);
|
|
3419
|
+
process.stdout.write(
|
|
3420
|
+
`
|
|
3421
|
+
Total scope: ${allAffected.size} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects
|
|
3422
|
+
`
|
|
3423
|
+
);
|
|
3424
|
+
if (options.simulate && allAffected.size > 0) {
|
|
3425
|
+
await runSimulation(graph, allAffected, nodePath);
|
|
3426
|
+
}
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
process.stderr.write(`Error: ${error.message}
|
|
3429
|
+
`);
|
|
3430
|
+
process.exit(1);
|
|
3231
3431
|
}
|
|
3432
|
+
}
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// src/cli/aspects.ts
|
|
3437
|
+
import { stringify as yamlStringify } from "yaml";
|
|
3438
|
+
function registerAspectsCommand(program2) {
|
|
3439
|
+
program2.command("aspects").description("List aspects with metadata (YAML output)").action(async () => {
|
|
3440
|
+
try {
|
|
3441
|
+
const yggRoot = await findYggRoot(process.cwd());
|
|
3442
|
+
const graph = await loadGraph(yggRoot);
|
|
3443
|
+
const output = graph.aspects.sort((a, b) => a.id.localeCompare(b.id)).map((aspect) => {
|
|
3444
|
+
const entry = { id: aspect.id, name: aspect.name };
|
|
3445
|
+
if (aspect.description) entry.description = aspect.description;
|
|
3446
|
+
if (aspect.implies && aspect.implies.length > 0) entry.implies = aspect.implies;
|
|
3447
|
+
return entry;
|
|
3448
|
+
});
|
|
3449
|
+
process.stdout.write(yamlStringify(output));
|
|
3232
3450
|
} catch (error) {
|
|
3233
|
-
|
|
3451
|
+
const err = error;
|
|
3452
|
+
if (err.code === "ENOENT") {
|
|
3453
|
+
process.stderr.write(
|
|
3454
|
+
`Error: No .yggdrasil/ directory found. Run 'yg init' first.
|
|
3455
|
+
`
|
|
3456
|
+
);
|
|
3457
|
+
} else {
|
|
3458
|
+
process.stderr.write(`Error: ${error.message}
|
|
3234
3459
|
`);
|
|
3460
|
+
}
|
|
3235
3461
|
process.exit(1);
|
|
3236
3462
|
}
|
|
3237
3463
|
});
|
|
@@ -3239,15 +3465,15 @@ ${changedLine}${budgetLine}${driftLine}
|
|
|
3239
3465
|
|
|
3240
3466
|
// src/io/journal-store.ts
|
|
3241
3467
|
import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access2 } from "fs/promises";
|
|
3242
|
-
import { parse as
|
|
3243
|
-
import
|
|
3468
|
+
import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
|
|
3469
|
+
import path16 from "path";
|
|
3244
3470
|
var JOURNAL_FILE = ".journal.yaml";
|
|
3245
3471
|
var ARCHIVE_DIR = "journals-archive";
|
|
3246
3472
|
async function readJournal(yggRoot) {
|
|
3247
|
-
const filePath =
|
|
3473
|
+
const filePath = path16.join(yggRoot, JOURNAL_FILE);
|
|
3248
3474
|
try {
|
|
3249
3475
|
const content = await readFile13(filePath, "utf-8");
|
|
3250
|
-
const raw =
|
|
3476
|
+
const raw = parseYaml6(content);
|
|
3251
3477
|
const entries = raw.entries ?? [];
|
|
3252
3478
|
return Array.isArray(entries) ? entries : [];
|
|
3253
3479
|
} catch {
|
|
@@ -3259,13 +3485,13 @@ async function appendJournalEntry(yggRoot, note, target) {
|
|
|
3259
3485
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3260
3486
|
const entry = target ? { at, target, note } : { at, note };
|
|
3261
3487
|
entries.push(entry);
|
|
3262
|
-
const filePath =
|
|
3263
|
-
const content =
|
|
3488
|
+
const filePath = path16.join(yggRoot, JOURNAL_FILE);
|
|
3489
|
+
const content = stringifyYaml({ entries });
|
|
3264
3490
|
await writeFile4(filePath, content, "utf-8");
|
|
3265
3491
|
return entry;
|
|
3266
3492
|
}
|
|
3267
3493
|
async function archiveJournal(yggRoot) {
|
|
3268
|
-
const journalPath =
|
|
3494
|
+
const journalPath = path16.join(yggRoot, JOURNAL_FILE);
|
|
3269
3495
|
try {
|
|
3270
3496
|
await access2(journalPath);
|
|
3271
3497
|
} catch {
|
|
@@ -3273,12 +3499,12 @@ async function archiveJournal(yggRoot) {
|
|
|
3273
3499
|
}
|
|
3274
3500
|
const entries = await readJournal(yggRoot);
|
|
3275
3501
|
if (entries.length === 0) return null;
|
|
3276
|
-
const archiveDir =
|
|
3502
|
+
const archiveDir = path16.join(yggRoot, ARCHIVE_DIR);
|
|
3277
3503
|
await mkdir3(archiveDir, { recursive: true });
|
|
3278
3504
|
const now = /* @__PURE__ */ new Date();
|
|
3279
3505
|
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")}`;
|
|
3280
3506
|
const archiveName = `.journal.${timestamp}.yaml`;
|
|
3281
|
-
const archivePath =
|
|
3507
|
+
const archivePath = path16.join(archiveDir, archiveName);
|
|
3282
3508
|
await rename(journalPath, archivePath);
|
|
3283
3509
|
return { archiveName, entryCount: entries.length };
|
|
3284
3510
|
}
|
|
@@ -3354,9 +3580,85 @@ function registerJournalArchiveCommand(program2) {
|
|
|
3354
3580
|
});
|
|
3355
3581
|
}
|
|
3356
3582
|
|
|
3583
|
+
// src/cli/preflight.ts
|
|
3584
|
+
function registerPreflightCommand(program2) {
|
|
3585
|
+
program2.command("preflight").description("Unified diagnostic report: journal, drift, status, validation").action(async () => {
|
|
3586
|
+
try {
|
|
3587
|
+
const cwd = process.cwd();
|
|
3588
|
+
const graph = await loadGraph(cwd);
|
|
3589
|
+
const yggRoot = await findYggRoot(cwd);
|
|
3590
|
+
const journalEntries = await readJournal(yggRoot);
|
|
3591
|
+
const drift = await detectDrift(graph);
|
|
3592
|
+
const driftedEntries = drift.entries.filter((e) => e.status !== "ok");
|
|
3593
|
+
const nodeCount = graph.nodes.size;
|
|
3594
|
+
const aspectCount = graph.aspects.length;
|
|
3595
|
+
const flowCount = graph.flows.length;
|
|
3596
|
+
let mappedPathCount = 0;
|
|
3597
|
+
for (const node of graph.nodes.values()) {
|
|
3598
|
+
mappedPathCount += normalizeMappingPaths(node.meta.mapping).length;
|
|
3599
|
+
}
|
|
3600
|
+
const validation = await validate(graph, "all");
|
|
3601
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
3602
|
+
const warnings = validation.issues.filter((i) => i.severity === "warning");
|
|
3603
|
+
const lines = [];
|
|
3604
|
+
lines.push("=== Preflight Report ===");
|
|
3605
|
+
lines.push("");
|
|
3606
|
+
if (journalEntries.length === 0) {
|
|
3607
|
+
lines.push("Journal: clean");
|
|
3608
|
+
} else {
|
|
3609
|
+
lines.push(`Journal: ${journalEntries.length} pending entries`);
|
|
3610
|
+
for (const entry of journalEntries) {
|
|
3611
|
+
const target = entry.target ? ` [${entry.target}]` : "";
|
|
3612
|
+
lines.push(` - ${entry.note}${target}`);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
lines.push("");
|
|
3616
|
+
if (driftedEntries.length === 0) {
|
|
3617
|
+
lines.push("Drift: clean");
|
|
3618
|
+
} else {
|
|
3619
|
+
lines.push(`Drift: ${driftedEntries.length} nodes need attention`);
|
|
3620
|
+
for (const entry of driftedEntries) {
|
|
3621
|
+
lines.push(` - ${entry.nodePath}: ${entry.status}`);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
lines.push("");
|
|
3625
|
+
lines.push(
|
|
3626
|
+
`Status: ${nodeCount} nodes, ${aspectCount} aspects, ${flowCount} flows, ${mappedPathCount} mapped paths`
|
|
3627
|
+
);
|
|
3628
|
+
lines.push("");
|
|
3629
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
3630
|
+
lines.push("Validation: clean");
|
|
3631
|
+
} else {
|
|
3632
|
+
const parts = [];
|
|
3633
|
+
if (errors.length > 0) parts.push(`${errors.length} errors`);
|
|
3634
|
+
if (warnings.length > 0) parts.push(`${warnings.length} warnings`);
|
|
3635
|
+
lines.push(`Validation: ${parts.join(", ")}`);
|
|
3636
|
+
for (const issue of [...errors, ...warnings]) {
|
|
3637
|
+
const code = issue.code ? `[${issue.code}] ` : "";
|
|
3638
|
+
lines.push(` - ${code}${issue.message}`);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
lines.push("");
|
|
3642
|
+
process.stdout.write(lines.join("\n"));
|
|
3643
|
+
const hasIssues = journalEntries.length > 0 || driftedEntries.length > 0 || errors.length > 0;
|
|
3644
|
+
process.exit(hasIssues ? 1 : 0);
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
process.stderr.write(`Error: ${error.message}
|
|
3647
|
+
`);
|
|
3648
|
+
process.exit(1);
|
|
3649
|
+
}
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3357
3653
|
// src/bin.ts
|
|
3654
|
+
import { readFileSync } from "fs";
|
|
3655
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3656
|
+
import { dirname, join } from "path";
|
|
3657
|
+
var __filename = fileURLToPath3(import.meta.url);
|
|
3658
|
+
var __dirname = dirname(__filename);
|
|
3659
|
+
var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
3358
3660
|
var program = new Command();
|
|
3359
|
-
program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(
|
|
3661
|
+
program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
|
|
3360
3662
|
registerInitCommand(program);
|
|
3361
3663
|
registerBuildCommand(program);
|
|
3362
3664
|
registerValidateCommand(program);
|
|
@@ -3367,8 +3669,10 @@ registerTreeCommand(program);
|
|
|
3367
3669
|
registerOwnerCommand(program);
|
|
3368
3670
|
registerDepsCommand(program);
|
|
3369
3671
|
registerImpactCommand(program);
|
|
3672
|
+
registerAspectsCommand(program);
|
|
3370
3673
|
registerJournalAddCommand(program);
|
|
3371
3674
|
registerJournalReadCommand(program);
|
|
3372
3675
|
registerJournalArchiveCommand(program);
|
|
3676
|
+
registerPreflightCommand(program);
|
|
3373
3677
|
program.parse();
|
|
3374
3678
|
//# sourceMappingURL=bin.js.map
|