@chrisdudek/yg 0.3.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -2
- package/dist/bin.js +1818 -1232
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +3 -14
- package/dist/templates/rules.ts +272 -186
- package/graph-schemas/aspect.yaml +9 -0
- package/graph-schemas/config.yaml +39 -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,244 +67,319 @@ 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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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>
|
|
90
|
+
|
|
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
|
+
\`\`\`
|
|
102
|
+
|
|
103
|
+
### Five Core Rules
|
|
116
104
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- \u274C You created or edited a graph element without reading its schema in \`.yggdrasil/templates/\`.
|
|
123
|
-
- \u274C You ran \`yg drift-sync\` before updating graph artifacts.
|
|
124
|
-
- \u274C You ran \`yg drift-sync\` after a graph-only change without verifying source files.
|
|
125
|
-
- \u274C You used Blackbox coverage for greenfield/new code.
|
|
126
|
-
- \u274C You answered a question about a mapped file/area without running \`yg build-context\` when the path was known.
|
|
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 \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
|
|
109
|
+
5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
|
|
127
110
|
|
|
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.
|
|
140
|
-
|
|
141
|
-
### B. Answering Questions (When a specific file or area is known)
|
|
142
|
-
When the user asks a question and you know (or can infer) which file or area of the codebase it concerns:
|
|
143
|
-
1. Run \`yg owner --file <path>\` for the relevant file(s).
|
|
144
|
-
2. **If owner FOUND:** Run \`yg build-context --node <node_path>\` and base your answer on that context. Do NOT answer from grep/search alone \u2014 the graph provides intent, constraints, and relations that yield better answers.
|
|
145
|
-
3. **If owner NOT FOUND:** The file is outside the graph (e.g. third-party code, user's theme/plugin, unmapped area). You may answer from grep/search, but state that the answer is not graph-based.
|
|
146
|
-
|
|
147
|
-
This applies even when you are **not modifying files** \u2014 e.g. when providing code snippets to paste elsewhere, explaining behavior, or suggesting hooks. If the question touches mapped code, build-context first.
|
|
148
|
-
|
|
149
|
-
### C. Session Verification (Wrap-up)
|
|
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.
|
|
156
|
-
|
|
157
|
-
---
|
|
158
|
-
|
|
159
|
-
## 3. WORKFLOW: MODIFYING OR CREATING FILES (Code-First)
|
|
160
|
-
|
|
161
|
-
You are NOT ALLOWED to edit or create source code without establishing graph coverage first.
|
|
162
|
-
|
|
163
|
-
**Gate:** Before using any tool that modifies files, you MUST have run \`yg owner --file <path>\` for each file you intend to modify. If you have not \u2014 run it first, then proceed. No exceptions. Gate applies to **source files** (files outside \`.yggdrasil/\`). For graph files (\`.yggdrasil/model/\`, \`.yggdrasil/aspects/\`, etc.), follow the Graph Modification Checklist in section 4 instead.
|
|
111
|
+
### Failure States
|
|
164
112
|
|
|
165
|
-
|
|
113
|
+
You have broken Yggdrasil if you do any of the following:
|
|
114
|
+
|
|
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.
|
|
129
|
+
- \u274C Recorded a design decision without documenting which alternatives were rejected and why.
|
|
130
|
+
|
|
131
|
+
### Escape Hatch
|
|
166
132
|
|
|
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:
|
|
133
|
+
If the user explicitly requests a code-only change, comply but:
|
|
169
134
|
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
|
|
173
|
-
|
|
135
|
+
- Warn: "This creates drift. Run \`yg drift\` next session to reconcile."
|
|
136
|
+
- Do NOT run \`yg drift-sync\` \u2014 leave the drift visible.
|
|
137
|
+
|
|
138
|
+
### Environment Check
|
|
174
139
|
|
|
175
|
-
|
|
176
|
-
STOP. Do not modify the code. First determine: **Is this greenfield or existing code?**
|
|
177
|
-
|
|
178
|
-
* **If GREENFIELD (empty directory, new project):** Do NOT offer blackbox. Create proper nodes (reverse engineering or upfront design) before implementing.
|
|
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
|
-
**Reverse engineering order:** When reverse-engineering an area, create graph elements in this order: (1) aspects, (2) flows, (3) knowledge elements, (4) model nodes. Never create model nodes before cross-cutting rules and shared wisdom exist \u2014 they depend on them.
|
|
186
|
-
|
|
187
|
-
---
|
|
140
|
+
Before preflight:
|
|
188
141
|
|
|
189
|
-
|
|
142
|
+
- Verify \`yg\` CLI is available. If not found, inform user and stop.
|
|
143
|
+
- If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
|
|
144
|
+
- If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.`;
|
|
145
|
+
var OPERATIONS = `## OPERATIONS
|
|
190
146
|
|
|
191
|
-
|
|
147
|
+
### Conversation Lifecycle
|
|
192
148
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
3.
|
|
198
|
-
4.
|
|
199
|
-
|
|
200
|
-
* W008: Stale semantic memory. Update knowledge artifacts.
|
|
149
|
+
\`\`\`
|
|
150
|
+
PREFLIGHT (every conversation, before any work):
|
|
151
|
+
- [ ] 1. yg preflight \u2192 read unified report
|
|
152
|
+
- [ ] 2. If journal entries: consolidate to graph, then yg journal-archive
|
|
153
|
+
- [ ] 3. If drift: resolve per Drift Resolution, then yg drift-sync per node
|
|
154
|
+
- [ ] 4. If validation errors: fix, re-run yg validate
|
|
155
|
+
Exception: read-only requests (explain, analyze) \u2014 skip preflight.
|
|
201
156
|
|
|
202
|
-
|
|
203
|
-
|
|
157
|
+
UNDERSTANDING mapped code (questions, research, OR planning):
|
|
158
|
+
- [ ] 1. yg owner --file <path>
|
|
159
|
+
- [ ] 2. Owner found \u2192 yg build-context --node <path>. Use context package as primary source.
|
|
160
|
+
- [ ] 3. Owner not found \u2192 use file analysis, state it is not graph-backed.
|
|
161
|
+
Never use grep or raw file reads as primary understanding when graph coverage exists.
|
|
162
|
+
Raw reads supplement the context package \u2014 they do not replace it.
|
|
204
163
|
|
|
205
|
-
-
|
|
206
|
-
- [ ]
|
|
207
|
-
- [ ]
|
|
208
|
-
- [ ]
|
|
209
|
-
- [ ]
|
|
164
|
+
WRAP-UP (user signals "done", "wrap up", "that's enough"):
|
|
165
|
+
- [ ] 1. Consolidate journal if used \u2192 yg journal-archive
|
|
166
|
+
- [ ] 2. yg drift --drifted-only \u2192 resolve
|
|
167
|
+
- [ ] 3. yg validate \u2192 fix errors
|
|
168
|
+
- [ ] 4. Report: which nodes and files were changed
|
|
169
|
+
\`\`\`
|
|
210
170
|
|
|
211
|
-
|
|
212
|
-
* **Default:** Write changes directly to graph files immediately. Do not defer.
|
|
213
|
-
* **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.
|
|
171
|
+
### Modify Source Code
|
|
214
172
|
|
|
215
|
-
|
|
173
|
+
You are not allowed to edit or create source code without establishing graph coverage first.
|
|
216
174
|
|
|
217
|
-
|
|
175
|
+
**Step 1** \u2014 Check coverage: \`yg owner --file <path>\`
|
|
218
176
|
|
|
219
|
-
|
|
220
|
-
* **Node paths** (used in CLI, relations, flow nodes): Relative to \`.yggdrasil/model/\` (e.g., \`orders/order-service\`).
|
|
221
|
-
* **File paths** (used in mapping, \`yg owner\`): Relative to the repository root (e.g., \`src/modules/orders/order.service.ts\`).
|
|
222
|
-
* **Knowledge paths** (used in node explicit refs): Relative to \`.yggdrasil/knowledge/\` (e.g., \`decisions/001-event-sourcing\`).
|
|
177
|
+
**Step 2a** \u2014 Owner found: execute checklist:
|
|
223
178
|
|
|
224
|
-
|
|
179
|
+
- [ ] 1. Read specification: \`yg build-context --node <node_path>\`
|
|
180
|
+
- [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
|
|
181
|
+
- [ ] 3. Modify source code
|
|
182
|
+
- [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes
|
|
183
|
+
- [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
|
|
184
|
+
- [ ] 6. Run \`yg drift-sync --node <node_path>\` \u2014 only after graph and code are both current
|
|
225
185
|
|
|
226
|
-
|
|
186
|
+
**Step 2b** \u2014 Owner not found: establish coverage first. Present options to the user:
|
|
227
187
|
|
|
228
|
-
|
|
188
|
+
*Partially mapped* (file unmapped but inside a mapped module): ask whether to add to existing node or create new one.
|
|
229
189
|
|
|
230
|
-
*
|
|
231
|
-
* **\`.yggdrasil/templates/\`**: Schemas for each graph layer \u2014 \`node.yaml\`, \`aspect.yaml\`, \`flow.yaml\`, \`knowledge.yaml\`.
|
|
232
|
-
* **\`.yggdrasil/model/\`**: Node tree. Each node is a directory with \`node.yaml\` and artifact files.
|
|
233
|
-
* **\`.yggdrasil/aspects/\`**: Cross-cutting rules. Directory contains \`aspect.yaml\` and \`.md\` content.
|
|
234
|
-
* **\`.yggdrasil/flows/\`**: End-to-end processes. Directory contains \`flow.yaml\` and \`.md\` content.
|
|
235
|
-
* **\`.yggdrasil/knowledge/\`**: Repo-wide wisdom. Directory contains \`knowledge.yaml\` and \`.md\` content.
|
|
190
|
+
*Existing code:*
|
|
236
191
|
|
|
237
|
-
|
|
192
|
+
- Option A \u2014 Full node: create node(s), map files, write artifacts from code analysis
|
|
193
|
+
- Option B \u2014 Blackbox: create a blackbox node at agreed granularity
|
|
194
|
+
- Option C \u2014 Abort
|
|
238
195
|
|
|
239
|
-
|
|
196
|
+
*Greenfield (new code):* Only Option A. Blackbox is forbidden for new code. Follow the graph-first workflow:
|
|
240
197
|
|
|
241
|
-
|
|
198
|
+
1. Create aspects first (cross-cutting requirements the new code must satisfy)
|
|
199
|
+
2. Create flows if the code participates in a business process
|
|
200
|
+
3. Create nodes with full artifacts \u2014 responsibility, constraints, decisions, interface, logic
|
|
201
|
+
4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
|
|
202
|
+
5. Implement code that satisfies the specification
|
|
203
|
+
6. The graph specifies WHAT and WHY; the code implements HOW (framework APIs, library choices)
|
|
242
204
|
|
|
243
|
-
|
|
205
|
+
After the user chooses, return to Step 1 and follow Step 2a.
|
|
244
206
|
|
|
245
|
-
###
|
|
246
|
-
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".
|
|
207
|
+
### Modify Graph
|
|
247
208
|
|
|
248
|
-
|
|
249
|
-
2.
|
|
250
|
-
|
|
251
|
-
|
|
209
|
+
- [ ] 1. Read the relevant schema from \`schemas/\` before touching any YAML
|
|
210
|
+
- [ ] 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
|
|
211
|
+
- [ ] 3. Make changes
|
|
212
|
+
- [ ] 4. Run \`yg validate\` immediately \u2014 fix all errors
|
|
213
|
+
- [ ] 5. Verify affected source files are consistent \u2014 update if needed
|
|
214
|
+
- [ ] 6. Run \`yg drift-sync\` for affected nodes
|
|
252
215
|
|
|
253
|
-
|
|
216
|
+
### Reverse Engineering
|
|
254
217
|
|
|
255
|
-
|
|
256
|
-
* **What goes here:** Things exclusively true for this specific node.
|
|
257
|
-
* **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\`.
|
|
258
|
-
* 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.
|
|
218
|
+
**Order:** aspects (cross-cutting patterns) \u2192 flows (business processes) \u2192 model nodes. Never create nodes before aspects and flows are understood.
|
|
259
219
|
|
|
260
|
-
|
|
220
|
+
Per area checklist:
|
|
261
221
|
|
|
262
|
-
|
|
222
|
+
- [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
|
|
223
|
+
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
224
|
+
- [ ] 3. Create node directory, read \`schemas/node.yaml\`, create \`node.yaml\`
|
|
225
|
+
- [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
|
|
226
|
+
- [ ] 5. Identify relations \u2014 add to \`node.yaml\`
|
|
227
|
+
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
228
|
+
- [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
|
|
229
|
+
- [ ] 8. \`yg validate\` \u2014 fix errors
|
|
230
|
+
- [ ] 9. \`yg drift-sync --node <path>\`
|
|
263
231
|
|
|
264
|
-
When
|
|
232
|
+
**When to ask:**
|
|
265
233
|
|
|
266
|
-
|
|
234
|
+
- Business process unclear: "This code appears to be part of a larger process. Can you describe what it means from a business perspective?"
|
|
235
|
+
- Constraint without rationale: "I see [constraint X]. Do you know why this exists? I want to record the reason, not just the rule."
|
|
236
|
+
- Unexplained architectural choice: "I see [approach X]. What was the reason for this choice?"
|
|
237
|
+
- Decision without alternatives: "You chose [X]. What alternatives did you consider, and why did you reject them?" Record the answer in \`decisions.md\`.
|
|
267
238
|
|
|
268
|
-
|
|
239
|
+
### Bootstrap Mode
|
|
269
240
|
|
|
270
|
-
|
|
241
|
+
Trigger: \`yg preflight\` shows 0 nodes, or no nodes cover the active work area.
|
|
271
242
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
243
|
+
- [ ] 1. Identify the active work area (files the user wants to modify)
|
|
244
|
+
- [ ] 2. Scan for cross-cutting patterns \u2192 create aspects
|
|
245
|
+
- [ ] 3. Ask user about business processes \u2192 create flows if applicable
|
|
246
|
+
- [ ] 4. Propose node structure for the area
|
|
247
|
+
- [ ] 5. Create node(s) with initial artifacts, map files
|
|
248
|
+
- [ ] 6. \`yg validate\`, \`yg drift-sync\`
|
|
249
|
+
- [ ] 7. Proceed with user's original request
|
|
278
250
|
|
|
279
|
-
|
|
280
|
-
* **What goes here:** Business rules shared by a family of nodes.
|
|
281
|
-
* **Routing:** Do not repeat module-wide rules in every child node. Place the child node directory *inside* a parent Module Node directory. Write the shared rules in the parent's configured artifacts. The engine inherently passes parent context to children.
|
|
251
|
+
Constraint: Do NOT map the entire repository. Focus on the active area. Expand incrementally.
|
|
282
252
|
|
|
283
|
-
###
|
|
284
|
-
* **What goes here:** Horizontal requirements like logging, auth, rate-limiting, or specific frameworks.
|
|
285
|
-
* **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.
|
|
253
|
+
### Drift Resolution
|
|
286
254
|
|
|
287
|
-
|
|
288
|
-
* **What goes here:** Global architectural decisions, design patterns, and systemic invariants.
|
|
289
|
-
* **Routing:** Read \`config.yaml\` (the \`knowledge_categories\` section) to know what categories exist.
|
|
290
|
-
* If the file implements a standard pattern: Do not describe the pattern locally. Add a \`knowledge\` reference in \`node.yaml\` to the existing pattern.
|
|
291
|
-
* 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.
|
|
255
|
+
Always ask the user before resolving drift. Never auto-resolve.
|
|
292
256
|
|
|
293
|
-
**
|
|
294
|
-
|
|
295
|
-
-
|
|
296
|
-
-
|
|
257
|
+
- **Source drift** (source files changed) \u2192 update graph artifacts to match source, then \`yg drift-sync\`
|
|
258
|
+
- **Graph drift** (graph artifacts changed) \u2192 review affected source, update if needed, then \`yg drift-sync\`
|
|
259
|
+
- **Full drift** (both changed) \u2192 present both sides to user, ask which direction wins
|
|
260
|
+
- **Missing** \u2192 ask: re-materialize or remove mapping?
|
|
261
|
+
- **Unmaterialized** \u2192 ask user how to proceed
|
|
297
262
|
|
|
298
|
-
|
|
263
|
+
Threshold: >10 drifted nodes \u2192 ask user which area to prioritize. Do not resolve all at once.
|
|
299
264
|
|
|
300
|
-
|
|
265
|
+
### Error Recovery
|
|
301
266
|
|
|
302
|
-
|
|
267
|
+
- **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
|
|
268
|
+
- **Unfixable validate errors** \u2192 if not resolved after 3 attempts, stop and report to user. Do not loop.
|
|
269
|
+
- **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.
|
|
270
|
+
- **Corrupted \`.yggdrasil/\` files** \u2192 report to user. Do not attempt repair.
|
|
271
|
+
- **Incremental sync** \u2192 run \`yg drift-sync\` every 3-5 source files during multi-file tasks. Do not defer to end.`;
|
|
272
|
+
var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
|
|
303
273
|
|
|
304
|
-
|
|
305
|
-
* \`yg build-context --node <node_path>\` -> Assemble strict specification.
|
|
306
|
-
* \`yg tree [--root <node_path>] [--depth N]\` -> Print graph structure.
|
|
307
|
-
* \`yg deps --node <node_path> [--type structural|event|all]\` -> Show dependencies.
|
|
308
|
-
* \`yg impact --node <node_path> --simulate\` -> Simulate blast radius.
|
|
309
|
-
* \`yg status\` -> Graph health metrics.
|
|
310
|
-
* \`yg validate [--scope <node_path>|all]\` -> Compile/check graph. Run after EVERY graph edit.
|
|
311
|
-
* \`yg drift [--scope <node_path>|all]\` -> Check code vs graph baseline.
|
|
312
|
-
* \`yg drift-sync --node <node_path>\` -> Save current file hash as new baseline. Run ONLY after ensuring graph artifacts match the code.
|
|
274
|
+
### Graph Structure
|
|
313
275
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
276
|
+
\`\`\`
|
|
277
|
+
.yggdrasil/
|
|
278
|
+
config.yaml \u2190 vocabulary, stack, node types, artifact rules, required aspects
|
|
279
|
+
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
280
|
+
aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
|
|
281
|
+
flows/ \u2190 why and in what process: business processes with node participation
|
|
282
|
+
schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
|
|
283
|
+
.drift-state \u2190 generated by CLI; never edit manually
|
|
284
|
+
.journal.yaml \u2190 generated by CLI; never edit manually
|
|
285
|
+
\`\`\`
|
|
286
|
+
|
|
287
|
+
Key facts:
|
|
288
|
+
|
|
289
|
+
- **Hierarchy:** nodes nest in \`model/\`. Children inherit parent context. Do not repeat parent content in children.
|
|
290
|
+
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
291
|
+
- **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
|
|
292
|
+
|
|
293
|
+
### Context Assembly
|
|
294
|
+
|
|
295
|
+
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.
|
|
296
|
+
|
|
297
|
+
### Information Routing
|
|
298
|
+
|
|
299
|
+
When you encounter information, route it to the correct location:
|
|
300
|
+
|
|
301
|
+
- **Specific to this node** \u2192 local node artifact (check \`config.yaml artifacts\` for available types)
|
|
302
|
+
- **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\`
|
|
303
|
+
- **Business process** \u2192 flow (\`flows/<name>/\` with \`flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
304
|
+
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
305
|
+
- **Technology stack or standard** \u2192 \`config.yaml\` under \`stack\` or \`standards\` (+ \`rationale\` field)
|
|
306
|
+
- **Decision (why + why NOT):** one node \u2192 \`decisions.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192 \`config.yaml\` rationale field. Always include rejected alternatives \u2014 they are the highest-value graph content.
|
|
307
|
+
|
|
308
|
+
### Creating Aspects
|
|
309
|
+
|
|
310
|
+
- [ ] 1. Read \`schemas/aspect.yaml\`
|
|
311
|
+
- [ ] 2. Create \`aspects/<id>/\` directory
|
|
312
|
+
- [ ] 3. Write \`aspect.yaml\` \u2014 name, optional description, optional implies
|
|
313
|
+
- [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
|
|
314
|
+
- [ ] 5. \`yg validate\`
|
|
315
|
+
|
|
316
|
+
Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No \u2192 local artifact.
|
|
317
|
+
|
|
318
|
+
**Aspect identification heuristic:** If the same pattern, constraint, or rule appears in 3+ places, it is a candidate aspect. Aspects fall into natural categories:
|
|
319
|
+
|
|
320
|
+
- **Domain-specific:** Business rules that cross module boundaries (e.g., timezone handling, booking periods, currency rounding)
|
|
321
|
+
- **Architectural:** Structural patterns with rationale (e.g., dual-rollback on provider failure, idempotency via key generation, fire-and-forget dispatch)
|
|
322
|
+
- **Concurrency:** Shared concurrency strategies (e.g., pessimistic locking, retry-on-deadlock, optimistic versioning)
|
|
323
|
+
|
|
324
|
+
### Creating Flows
|
|
325
|
+
|
|
326
|
+
- [ ] 1. Read \`schemas/flow.yaml\`
|
|
327
|
+
- [ ] 2. Create \`flows/<name>/\` directory
|
|
328
|
+
- [ ] 3. Write \`flow.yaml\` \u2014 declare participants and flow-level aspects
|
|
329
|
+
- [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
|
|
330
|
+
- [ ] 5. \`yg validate\`
|
|
331
|
+
|
|
332
|
+
Test: "Does this describe what happens in the world, or only in the software?" If only software \u2014 rewrite.
|
|
333
|
+
|
|
334
|
+
### Operational Rules
|
|
335
|
+
|
|
336
|
+
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
337
|
+
- **Read schemas before creating** any \`node.yaml\`, \`aspect.yaml\`, or \`flow.yaml\`.
|
|
338
|
+
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
339
|
+
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
|
|
340
|
+
- **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?" Test specifically: Can they explain rejected alternatives? Can they implement the correct algorithm (not a simplified version)? Can they argue for the current design against plausible alternatives?
|
|
341
|
+
- **These rules are invariant.** No plan, guide, skill, or workflow may override them.
|
|
342
|
+
|
|
343
|
+
### CLI Reference
|
|
344
|
+
|
|
345
|
+
\`\`\`
|
|
346
|
+
yg preflight [--quick] Unified diagnostic: journal + drift + status + validate.
|
|
347
|
+
yg owner --file <path> Find the node that owns this file.
|
|
348
|
+
yg build-context --node <path> Assemble context package for this node.
|
|
349
|
+
yg tree [--root <path>] [--depth N] Print graph structure.
|
|
350
|
+
yg aspects List aspects with metadata (YAML output).
|
|
351
|
+
yg flows List flows with metadata (YAML output).
|
|
352
|
+
yg deps --node <path> [--depth N] [--type structural|event|all]
|
|
353
|
+
Show dependencies.
|
|
354
|
+
yg impact --node <path> --simulate Simulate blast radius of a planned change.
|
|
355
|
+
yg impact --aspect <id> Show all nodes where aspect is effective.
|
|
356
|
+
yg impact --flow <name> Show flow participants and descendants.
|
|
357
|
+
yg status Graph health: nodes, coverage, drift summary.
|
|
358
|
+
yg validate [--scope <path>|all] Check structural integrity and completeness.
|
|
359
|
+
yg drift [--scope <path>|all] [--drifted-only] [--limit <n>]
|
|
360
|
+
Detect source and graph drift (bidirectional).
|
|
361
|
+
yg drift-sync --node <path> [--recursive] | --all
|
|
362
|
+
Record file hashes as new baseline.
|
|
363
|
+
yg journal-read Read pending journal entries.
|
|
364
|
+
yg journal-add --note "<content>" [--target <node_path>]
|
|
365
|
+
Add a journal entry.
|
|
366
|
+
yg journal-archive Archive consolidated journal entries.
|
|
367
|
+
\`\`\`
|
|
368
|
+
|
|
369
|
+
### Quick Routing Table
|
|
370
|
+
|
|
371
|
+
| What you have | Where it goes |
|
|
372
|
+
|---|---|
|
|
373
|
+
| Information specific to this node | Local node artifact (read \`config.yaml artifacts\` for types) |
|
|
374
|
+
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
375
|
+
| Architectural invariant for a node type | Required aspect in \`config.yaml node_types\` |
|
|
376
|
+
| Business process participation | Flow (\`flow.yaml participants\`) |
|
|
377
|
+
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
378
|
+
| Context shared across a domain | Parent node artifact |
|
|
379
|
+
| Technology stack | \`config.yaml stack\` (+ \`rationale\` field) |
|
|
380
|
+
| Global coding standards | \`config.yaml standards\` |
|
|
318
381
|
`;
|
|
382
|
+
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n");
|
|
319
383
|
|
|
320
384
|
// src/templates/platform.ts
|
|
321
385
|
var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
|
|
@@ -558,12 +622,13 @@ function escapeRegex(s) {
|
|
|
558
622
|
}
|
|
559
623
|
|
|
560
624
|
// src/cli/init.ts
|
|
561
|
-
function
|
|
625
|
+
function getGraphSchemasDir() {
|
|
562
626
|
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
563
627
|
const packageRoot = path2.join(currentDir, "..");
|
|
564
|
-
return path2.join(packageRoot, "graph-
|
|
628
|
+
return path2.join(packageRoot, "graph-schemas");
|
|
565
629
|
}
|
|
566
630
|
var GITIGNORE_CONTENT = `.journal.yaml
|
|
631
|
+
.drift-state
|
|
567
632
|
journals-archive/
|
|
568
633
|
`;
|
|
569
634
|
function registerInitCommand(program2) {
|
|
@@ -609,23 +674,20 @@ function registerInitCommand(program2) {
|
|
|
609
674
|
await mkdir2(path2.join(yggRoot, "model"), { recursive: true });
|
|
610
675
|
await mkdir2(path2.join(yggRoot, "aspects"), { recursive: true });
|
|
611
676
|
await mkdir2(path2.join(yggRoot, "flows"), { recursive: true });
|
|
612
|
-
|
|
613
|
-
await mkdir2(
|
|
614
|
-
|
|
615
|
-
const templatesDir = path2.join(yggRoot, "templates");
|
|
616
|
-
await mkdir2(templatesDir, { recursive: true });
|
|
617
|
-
const graphTemplatesDir = getGraphTemplatesDir();
|
|
677
|
+
const schemasDir = path2.join(yggRoot, "schemas");
|
|
678
|
+
await mkdir2(schemasDir, { recursive: true });
|
|
679
|
+
const graphSchemasDir = getGraphSchemasDir();
|
|
618
680
|
try {
|
|
619
|
-
const entries = await readdir(
|
|
620
|
-
const
|
|
621
|
-
for (const file of
|
|
622
|
-
const srcPath = path2.join(
|
|
681
|
+
const entries = await readdir(graphSchemasDir, { withFileTypes: true });
|
|
682
|
+
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
683
|
+
for (const file of schemaFiles) {
|
|
684
|
+
const srcPath = path2.join(graphSchemasDir, file);
|
|
623
685
|
const content = await readFile2(srcPath, "utf-8");
|
|
624
|
-
await writeFile2(path2.join(
|
|
686
|
+
await writeFile2(path2.join(schemasDir, file), content, "utf-8");
|
|
625
687
|
}
|
|
626
688
|
} catch (err) {
|
|
627
689
|
process.stderr.write(
|
|
628
|
-
`Warning: Could not copy graph
|
|
690
|
+
`Warning: Could not copy graph schemas from ${graphSchemasDir}: ${err.message}
|
|
629
691
|
`
|
|
630
692
|
);
|
|
631
693
|
}
|
|
@@ -639,10 +701,7 @@ function registerInitCommand(program2) {
|
|
|
639
701
|
process.stdout.write(" .yggdrasil/model/\n");
|
|
640
702
|
process.stdout.write(" .yggdrasil/aspects/\n");
|
|
641
703
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
642
|
-
process.stdout.write(" .yggdrasil/
|
|
643
|
-
process.stdout.write(
|
|
644
|
-
" .yggdrasil/templates/ (node, aspect, flow, knowledge)\n"
|
|
645
|
-
);
|
|
704
|
+
process.stdout.write(" .yggdrasil/schemas/ (config, node, aspect, flow)\n");
|
|
646
705
|
process.stdout.write(` ${path2.relative(projectRoot, rulesPath)} (rules)
|
|
647
706
|
|
|
648
707
|
`);
|
|
@@ -654,8 +713,8 @@ function registerInitCommand(program2) {
|
|
|
654
713
|
}
|
|
655
714
|
|
|
656
715
|
// src/core/graph-loader.ts
|
|
657
|
-
import { readdir as readdir3 } from "fs/promises";
|
|
658
|
-
import
|
|
716
|
+
import { readdir as readdir3, readFile as readFile9 } from "fs/promises";
|
|
717
|
+
import path7 from "path";
|
|
659
718
|
|
|
660
719
|
// src/io/config-parser.ts
|
|
661
720
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -663,27 +722,45 @@ import { parse as parseYaml } from "yaml";
|
|
|
663
722
|
var DEFAULT_QUALITY = {
|
|
664
723
|
min_artifact_length: 50,
|
|
665
724
|
max_direct_relations: 10,
|
|
666
|
-
context_budget: { warning: 1e4, error: 2e4 }
|
|
667
|
-
knowledge_staleness_days: 90
|
|
725
|
+
context_budget: { warning: 1e4, error: 2e4 }
|
|
668
726
|
};
|
|
669
727
|
async function parseConfig(filePath) {
|
|
670
728
|
const content = await readFile3(filePath, "utf-8");
|
|
671
729
|
const raw = parseYaml(content);
|
|
730
|
+
if (!raw || typeof raw !== "object") {
|
|
731
|
+
throw new Error(`config.yaml: file is empty or not a valid YAML mapping`);
|
|
732
|
+
}
|
|
672
733
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
673
734
|
throw new Error(`config.yaml: missing or invalid 'name' field`);
|
|
674
735
|
}
|
|
675
|
-
const
|
|
676
|
-
if (!Array.isArray(
|
|
736
|
+
const nodeTypesRaw = raw.node_types;
|
|
737
|
+
if (!Array.isArray(nodeTypesRaw) || nodeTypesRaw.length === 0) {
|
|
677
738
|
throw new Error(`config.yaml: 'node_types' must be a non-empty array`);
|
|
678
739
|
}
|
|
740
|
+
const nodeTypes = nodeTypesRaw.map((item) => {
|
|
741
|
+
if (typeof item === "string") {
|
|
742
|
+
return { name: item };
|
|
743
|
+
}
|
|
744
|
+
if (typeof item === "object" && item !== null && "name" in item && typeof item.name === "string") {
|
|
745
|
+
const obj = item;
|
|
746
|
+
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;
|
|
747
|
+
return {
|
|
748
|
+
name: obj.name,
|
|
749
|
+
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
throw new Error(
|
|
753
|
+
`config.yaml: node_types entry must be string or { name, required_aspects? }`
|
|
754
|
+
);
|
|
755
|
+
});
|
|
679
756
|
const artifacts = raw.artifacts;
|
|
680
757
|
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
|
|
681
758
|
throw new Error(`config.yaml: 'artifacts' must be a non-empty object`);
|
|
682
759
|
}
|
|
683
760
|
const artifactsMap = {};
|
|
684
761
|
for (const [key, val] of Object.entries(artifacts)) {
|
|
685
|
-
if (key === "node") {
|
|
686
|
-
throw new Error(`config.yaml: artifact name 'node' is reserved`);
|
|
762
|
+
if (key === "node.yaml") {
|
|
763
|
+
throw new Error(`config.yaml: artifact name 'node.yaml' is reserved`);
|
|
687
764
|
}
|
|
688
765
|
const a = val;
|
|
689
766
|
const required = a.required;
|
|
@@ -692,10 +769,10 @@ async function parseConfig(filePath) {
|
|
|
692
769
|
}
|
|
693
770
|
if (typeof required === "object" && required && "when" in required) {
|
|
694
771
|
const when = required.when;
|
|
695
|
-
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && when.startsWith("has_tag:");
|
|
772
|
+
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
|
|
696
773
|
if (!validWhen) {
|
|
697
774
|
throw new Error(
|
|
698
|
-
`config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or
|
|
775
|
+
`config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
699
776
|
);
|
|
700
777
|
}
|
|
701
778
|
}
|
|
@@ -705,24 +782,6 @@ async function parseConfig(filePath) {
|
|
|
705
782
|
structural_context: a.structural_context ?? false
|
|
706
783
|
};
|
|
707
784
|
}
|
|
708
|
-
if (!("knowledge_categories" in raw)) {
|
|
709
|
-
throw new Error(
|
|
710
|
-
`config.yaml: missing 'knowledge_categories' field (required, may be empty list)`
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
const knowledgeCategoriesRaw = raw.knowledge_categories;
|
|
714
|
-
if (!Array.isArray(knowledgeCategoriesRaw)) {
|
|
715
|
-
throw new Error(`config.yaml: 'knowledge_categories' must be an array`);
|
|
716
|
-
}
|
|
717
|
-
const knowledgeCategories = knowledgeCategoriesRaw;
|
|
718
|
-
const categoryNames = /* @__PURE__ */ new Set();
|
|
719
|
-
for (const kc of knowledgeCategories) {
|
|
720
|
-
if (!kc?.name || typeof kc.name !== "string") continue;
|
|
721
|
-
if (categoryNames.has(kc.name)) {
|
|
722
|
-
throw new Error(`config.yaml: duplicate knowledge category '${kc.name}'`);
|
|
723
|
-
}
|
|
724
|
-
categoryNames.add(kc.name);
|
|
725
|
-
}
|
|
726
785
|
const qualityRaw = raw.quality;
|
|
727
786
|
const quality = qualityRaw ? {
|
|
728
787
|
min_artifact_length: qualityRaw.min_artifact_length ?? DEFAULT_QUALITY.min_artifact_length,
|
|
@@ -730,30 +789,19 @@ async function parseConfig(filePath) {
|
|
|
730
789
|
context_budget: {
|
|
731
790
|
warning: qualityRaw.context_budget?.warning ?? DEFAULT_QUALITY.context_budget.warning,
|
|
732
791
|
error: qualityRaw.context_budget?.error ?? DEFAULT_QUALITY.context_budget.error
|
|
733
|
-
}
|
|
734
|
-
knowledge_staleness_days: qualityRaw.knowledge_staleness_days ?? DEFAULT_QUALITY.knowledge_staleness_days
|
|
792
|
+
}
|
|
735
793
|
} : DEFAULT_QUALITY;
|
|
736
794
|
if (quality.context_budget.error < quality.context_budget.warning) {
|
|
737
795
|
throw new Error(
|
|
738
796
|
`config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
|
|
739
797
|
);
|
|
740
798
|
}
|
|
741
|
-
if (!("tags" in raw)) {
|
|
742
|
-
throw new Error(`config.yaml: missing 'tags' field (required, may be empty list)`);
|
|
743
|
-
}
|
|
744
|
-
const tags = raw.tags;
|
|
745
|
-
if (!Array.isArray(tags)) {
|
|
746
|
-
throw new Error(`config.yaml: 'tags' must be an array`);
|
|
747
|
-
}
|
|
748
|
-
const tagsList = tags.filter((t) => typeof t === "string");
|
|
749
799
|
return {
|
|
750
800
|
name: raw.name.trim(),
|
|
751
801
|
stack: raw.stack ?? {},
|
|
752
802
|
standards: typeof raw.standards === "string" ? raw.standards : "",
|
|
753
|
-
tags: tagsList,
|
|
754
803
|
node_types: nodeTypes,
|
|
755
804
|
artifacts: artifactsMap,
|
|
756
|
-
knowledge_categories: knowledgeCategories.filter((kc) => kc?.name),
|
|
757
805
|
quality
|
|
758
806
|
};
|
|
759
807
|
}
|
|
@@ -775,6 +823,9 @@ function isValidRelationType(t) {
|
|
|
775
823
|
async function parseNodeYaml(filePath) {
|
|
776
824
|
const content = await readFile4(filePath, "utf-8");
|
|
777
825
|
const raw = parseYaml2(content);
|
|
826
|
+
if (!raw || typeof raw !== "object") {
|
|
827
|
+
throw new Error(`node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
828
|
+
}
|
|
778
829
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
779
830
|
throw new Error(`node.yaml at ${filePath}: missing or empty 'name'`);
|
|
780
831
|
}
|
|
@@ -786,10 +837,9 @@ async function parseNodeYaml(filePath) {
|
|
|
786
837
|
return {
|
|
787
838
|
name: raw.name.trim(),
|
|
788
839
|
type: raw.type.trim(),
|
|
789
|
-
|
|
840
|
+
aspects: parseStringArray(raw.aspects) ?? parseStringArray(raw.tags),
|
|
790
841
|
blackbox: raw.blackbox ?? false,
|
|
791
842
|
relations: relations.length > 0 ? relations : void 0,
|
|
792
|
-
knowledge: parseStringArray(raw.knowledge),
|
|
793
843
|
mapping
|
|
794
844
|
};
|
|
795
845
|
}
|
|
@@ -889,29 +939,47 @@ async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles
|
|
|
889
939
|
}
|
|
890
940
|
|
|
891
941
|
// src/io/aspect-parser.ts
|
|
892
|
-
async function parseAspect(aspectDir, aspectYamlPath) {
|
|
942
|
+
async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
943
|
+
const idTrimmed = id?.trim() ?? "";
|
|
944
|
+
if (!idTrimmed) {
|
|
945
|
+
throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
|
|
946
|
+
}
|
|
893
947
|
const content = await readFile6(aspectYamlPath, "utf-8");
|
|
894
948
|
const raw = parseYaml3(content);
|
|
949
|
+
if (!raw || typeof raw !== "object") {
|
|
950
|
+
throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
|
|
951
|
+
}
|
|
895
952
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
896
953
|
throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
|
|
897
954
|
}
|
|
898
|
-
|
|
899
|
-
throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'tag'`);
|
|
900
|
-
}
|
|
955
|
+
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
901
956
|
const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
|
|
957
|
+
let implies;
|
|
958
|
+
if (raw.implies !== void 0) {
|
|
959
|
+
if (!Array.isArray(raw.implies)) {
|
|
960
|
+
throw new Error(`Aspect file ${aspectYamlPath}: 'implies' must be an array of strings`);
|
|
961
|
+
}
|
|
962
|
+
implies = raw.implies.filter((t) => typeof t === "string");
|
|
963
|
+
}
|
|
902
964
|
return {
|
|
903
965
|
name: raw.name.trim(),
|
|
904
|
-
|
|
966
|
+
id: idTrimmed,
|
|
967
|
+
description,
|
|
968
|
+
implies,
|
|
905
969
|
artifacts
|
|
906
970
|
};
|
|
907
971
|
}
|
|
908
972
|
|
|
909
973
|
// src/io/flow-parser.ts
|
|
910
974
|
import { readFile as readFile7 } from "fs/promises";
|
|
975
|
+
import path4 from "path";
|
|
911
976
|
import { parse as parseYaml4 } from "yaml";
|
|
912
977
|
async function parseFlow(flowDir, flowYamlPath) {
|
|
913
978
|
const content = await readFile7(flowYamlPath, "utf-8");
|
|
914
979
|
const raw = parseYaml4(content);
|
|
980
|
+
if (!raw || typeof raw !== "object") {
|
|
981
|
+
throw new Error(`flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
982
|
+
}
|
|
915
983
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
916
984
|
throw new Error(`flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
|
|
917
985
|
}
|
|
@@ -923,79 +991,44 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
923
991
|
if (nodePaths.length === 0) {
|
|
924
992
|
throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
|
|
925
993
|
}
|
|
926
|
-
|
|
994
|
+
let aspects;
|
|
995
|
+
if (raw.aspects !== void 0) {
|
|
996
|
+
if (!Array.isArray(raw.aspects)) {
|
|
997
|
+
throw new Error(`flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
998
|
+
}
|
|
999
|
+
const aspectTags = raw.aspects.filter((a) => typeof a === "string");
|
|
1000
|
+
aspects = aspectTags.length > 0 ? aspectTags : [];
|
|
1001
|
+
}
|
|
927
1002
|
const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
|
|
928
1003
|
return {
|
|
1004
|
+
path: path4.basename(flowDir),
|
|
929
1005
|
name: raw.name.trim(),
|
|
930
1006
|
nodes: nodePaths,
|
|
931
|
-
|
|
1007
|
+
...aspects !== void 0 && { aspects },
|
|
932
1008
|
artifacts
|
|
933
1009
|
};
|
|
934
1010
|
}
|
|
935
1011
|
|
|
936
|
-
// src/io/
|
|
1012
|
+
// src/io/schema-parser.ts
|
|
937
1013
|
import { readFile as readFile8 } from "fs/promises";
|
|
1014
|
+
import path5 from "path";
|
|
938
1015
|
import { parse as parseYaml5 } from "yaml";
|
|
939
|
-
async function parseKnowledge(knowledgeDir, knowledgeYamlPath, category, relativePath) {
|
|
940
|
-
const content = await readFile8(knowledgeYamlPath, "utf-8");
|
|
941
|
-
const raw = parseYaml5(content);
|
|
942
|
-
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
943
|
-
throw new Error(`knowledge.yaml at ${knowledgeYamlPath}: missing or empty 'name'`);
|
|
944
|
-
}
|
|
945
|
-
const scope = parseScope(raw.scope, knowledgeYamlPath);
|
|
946
|
-
const artifacts = await readArtifacts(knowledgeDir, ["knowledge.yaml"]);
|
|
947
|
-
return {
|
|
948
|
-
name: raw.name.trim(),
|
|
949
|
-
scope,
|
|
950
|
-
category,
|
|
951
|
-
path: relativePath,
|
|
952
|
-
artifacts
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
function parseScope(raw, filePath) {
|
|
956
|
-
if (raw === "global") {
|
|
957
|
-
return "global";
|
|
958
|
-
}
|
|
959
|
-
if (raw && typeof raw === "object") {
|
|
960
|
-
const obj = raw;
|
|
961
|
-
if (Array.isArray(obj.tags)) {
|
|
962
|
-
const tags = obj.tags.filter((t) => typeof t === "string");
|
|
963
|
-
if (tags.length === 0) {
|
|
964
|
-
throw new Error(`knowledge.yaml at ${filePath}: scope.tags must be a non-empty array`);
|
|
965
|
-
}
|
|
966
|
-
return { tags };
|
|
967
|
-
}
|
|
968
|
-
if (Array.isArray(obj.nodes)) {
|
|
969
|
-
const nodes = obj.nodes.filter((n) => typeof n === "string");
|
|
970
|
-
if (nodes.length === 0) {
|
|
971
|
-
throw new Error(`knowledge.yaml at ${filePath}: scope.nodes must be a non-empty array`);
|
|
972
|
-
}
|
|
973
|
-
return { nodes };
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
throw new Error(`knowledge.yaml at ${filePath}: invalid 'scope' value`);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// src/io/template-parser.ts
|
|
980
|
-
import { readFile as readFile9 } from "fs/promises";
|
|
981
|
-
import path4 from "path";
|
|
982
|
-
import { parse as parseYaml6 } from "yaml";
|
|
983
1016
|
async function parseSchema(filePath) {
|
|
984
|
-
const content = await
|
|
985
|
-
|
|
986
|
-
const schemaType =
|
|
1017
|
+
const content = await readFile8(filePath, "utf-8");
|
|
1018
|
+
parseYaml5(content);
|
|
1019
|
+
const schemaType = path5.basename(filePath, path5.extname(filePath));
|
|
987
1020
|
return { schemaType };
|
|
988
1021
|
}
|
|
989
1022
|
|
|
990
1023
|
// src/utils/paths.ts
|
|
991
|
-
import
|
|
1024
|
+
import path6 from "path";
|
|
992
1025
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
993
1026
|
import { stat as stat2 } from "fs/promises";
|
|
994
1027
|
async function findYggRoot(projectRoot) {
|
|
995
|
-
let current =
|
|
996
|
-
const root =
|
|
1028
|
+
let current = path6.resolve(projectRoot);
|
|
1029
|
+
const root = path6.parse(current).root;
|
|
997
1030
|
while (true) {
|
|
998
|
-
const yggPath =
|
|
1031
|
+
const yggPath = path6.join(current, ".yggdrasil");
|
|
999
1032
|
try {
|
|
1000
1033
|
const st = await stat2(yggPath);
|
|
1001
1034
|
if (!st.isDirectory()) {
|
|
@@ -1009,7 +1042,7 @@ async function findYggRoot(projectRoot) {
|
|
|
1009
1042
|
if (current === root) {
|
|
1010
1043
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
1011
1044
|
}
|
|
1012
|
-
current =
|
|
1045
|
+
current = path6.dirname(current);
|
|
1013
1046
|
continue;
|
|
1014
1047
|
}
|
|
1015
1048
|
throw err;
|
|
@@ -1025,41 +1058,42 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
1025
1058
|
if (normalizedInput.length === 0) {
|
|
1026
1059
|
throw new Error("Path cannot be empty");
|
|
1027
1060
|
}
|
|
1028
|
-
const absolute =
|
|
1029
|
-
const relative =
|
|
1030
|
-
const isOutside = relative.startsWith("..") ||
|
|
1061
|
+
const absolute = path6.resolve(projectRoot, normalizedInput);
|
|
1062
|
+
const relative = path6.relative(projectRoot, absolute);
|
|
1063
|
+
const isOutside = relative.startsWith("..") || path6.isAbsolute(relative);
|
|
1031
1064
|
if (isOutside) {
|
|
1032
1065
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
1033
1066
|
}
|
|
1034
|
-
return relative.split(
|
|
1067
|
+
return relative.split(path6.sep).join("/");
|
|
1068
|
+
}
|
|
1069
|
+
function projectRootFromGraph(yggRootPath) {
|
|
1070
|
+
return path6.dirname(yggRootPath);
|
|
1035
1071
|
}
|
|
1036
1072
|
|
|
1037
1073
|
// src/core/graph-loader.ts
|
|
1038
1074
|
function toModelPath(absolutePath, modelDir) {
|
|
1039
|
-
return
|
|
1075
|
+
return path7.relative(modelDir, absolutePath).split(path7.sep).join("/");
|
|
1040
1076
|
}
|
|
1041
1077
|
var FALLBACK_CONFIG = {
|
|
1042
1078
|
name: "",
|
|
1043
1079
|
stack: {},
|
|
1044
1080
|
standards: "",
|
|
1045
|
-
tags: [],
|
|
1046
1081
|
node_types: [],
|
|
1047
|
-
artifacts: {}
|
|
1048
|
-
knowledge_categories: []
|
|
1082
|
+
artifacts: {}
|
|
1049
1083
|
};
|
|
1050
1084
|
async function loadGraph(projectRoot, options = {}) {
|
|
1051
1085
|
const yggRoot = await findYggRoot(projectRoot);
|
|
1052
1086
|
let configError;
|
|
1053
1087
|
let config = FALLBACK_CONFIG;
|
|
1054
1088
|
try {
|
|
1055
|
-
config = await parseConfig(
|
|
1089
|
+
config = await parseConfig(path7.join(yggRoot, "config.yaml"));
|
|
1056
1090
|
} catch (error) {
|
|
1057
1091
|
if (!options.tolerateInvalidConfig) {
|
|
1058
1092
|
throw error;
|
|
1059
1093
|
}
|
|
1060
1094
|
configError = error.message;
|
|
1061
1095
|
}
|
|
1062
|
-
const modelDir =
|
|
1096
|
+
const modelDir = path7.join(yggRoot, "model");
|
|
1063
1097
|
const nodes = /* @__PURE__ */ new Map();
|
|
1064
1098
|
const nodeParseErrors = [];
|
|
1065
1099
|
const artifactFilenames = Object.keys(config.artifacts ?? {});
|
|
@@ -1073,13 +1107,9 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1073
1107
|
}
|
|
1074
1108
|
throw err;
|
|
1075
1109
|
}
|
|
1076
|
-
const aspects = await loadAspects(
|
|
1077
|
-
const flows = await loadFlows(
|
|
1078
|
-
const
|
|
1079
|
-
path6.join(yggRoot, "knowledge"),
|
|
1080
|
-
config.knowledge_categories
|
|
1081
|
-
);
|
|
1082
|
-
const schemas = await loadSchemas(path6.join(yggRoot, "templates"));
|
|
1110
|
+
const aspects = await loadAspects(path7.join(yggRoot, "aspects"));
|
|
1111
|
+
const flows = await loadFlows(path7.join(yggRoot, "flows"));
|
|
1112
|
+
const schemas = await loadSchemas(path7.join(yggRoot, "schemas"));
|
|
1083
1113
|
return {
|
|
1084
1114
|
config,
|
|
1085
1115
|
configError,
|
|
@@ -1087,7 +1117,6 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1087
1117
|
nodes,
|
|
1088
1118
|
aspects,
|
|
1089
1119
|
flows,
|
|
1090
|
-
knowledge,
|
|
1091
1120
|
schemas,
|
|
1092
1121
|
rootPath: yggRoot
|
|
1093
1122
|
};
|
|
@@ -1100,9 +1129,12 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1100
1129
|
}
|
|
1101
1130
|
if (hasNodeYaml) {
|
|
1102
1131
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1132
|
+
const nodeYamlPath = path7.join(dirPath, "node.yaml");
|
|
1103
1133
|
let meta;
|
|
1134
|
+
let nodeYamlRaw;
|
|
1104
1135
|
try {
|
|
1105
|
-
|
|
1136
|
+
nodeYamlRaw = await readFile9(nodeYamlPath, "utf-8");
|
|
1137
|
+
meta = await parseNodeYaml(nodeYamlPath);
|
|
1106
1138
|
} catch (err) {
|
|
1107
1139
|
nodeParseErrors.push({
|
|
1108
1140
|
nodePath: graphPath,
|
|
@@ -1114,6 +1146,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1114
1146
|
const node = {
|
|
1115
1147
|
path: graphPath,
|
|
1116
1148
|
meta,
|
|
1149
|
+
nodeYamlRaw,
|
|
1117
1150
|
artifacts,
|
|
1118
1151
|
children: [],
|
|
1119
1152
|
parent
|
|
@@ -1126,7 +1159,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1126
1159
|
if (!entry.isDirectory()) continue;
|
|
1127
1160
|
if (entry.name.startsWith(".")) continue;
|
|
1128
1161
|
await scanModelDirectory(
|
|
1129
|
-
|
|
1162
|
+
path7.join(dirPath, entry.name),
|
|
1130
1163
|
modelDir,
|
|
1131
1164
|
node,
|
|
1132
1165
|
nodes,
|
|
@@ -1139,7 +1172,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1139
1172
|
if (!entry.isDirectory()) continue;
|
|
1140
1173
|
if (entry.name.startsWith(".")) continue;
|
|
1141
1174
|
await scanModelDirectory(
|
|
1142
|
-
|
|
1175
|
+
path7.join(dirPath, entry.name),
|
|
1143
1176
|
modelDir,
|
|
1144
1177
|
null,
|
|
1145
1178
|
nodes,
|
|
@@ -1151,27 +1184,36 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1151
1184
|
}
|
|
1152
1185
|
async function loadAspects(aspectsDir) {
|
|
1153
1186
|
try {
|
|
1154
|
-
const entries = await readdir3(aspectsDir, { withFileTypes: true });
|
|
1155
1187
|
const aspects = [];
|
|
1156
|
-
|
|
1157
|
-
if (!entry.isDirectory()) continue;
|
|
1158
|
-
const aspectYamlPath = path6.join(aspectsDir, entry.name, "aspect.yaml");
|
|
1159
|
-
const aspect = await parseAspect(path6.join(aspectsDir, entry.name), aspectYamlPath);
|
|
1160
|
-
aspects.push(aspect);
|
|
1161
|
-
}
|
|
1188
|
+
await scanAspectsDirectory(aspectsDir, aspectsDir, aspects);
|
|
1162
1189
|
return aspects;
|
|
1163
1190
|
} catch {
|
|
1164
1191
|
return [];
|
|
1165
1192
|
}
|
|
1166
1193
|
}
|
|
1194
|
+
async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
1195
|
+
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1196
|
+
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "aspect.yaml");
|
|
1197
|
+
if (hasAspectYaml) {
|
|
1198
|
+
const id = path7.relative(aspectsRoot, dirPath).split(path7.sep).join("/");
|
|
1199
|
+
const aspectYamlPath = path7.join(dirPath, "aspect.yaml");
|
|
1200
|
+
const aspect = await parseAspect(dirPath, aspectYamlPath, id);
|
|
1201
|
+
aspects.push(aspect);
|
|
1202
|
+
}
|
|
1203
|
+
for (const entry of entries) {
|
|
1204
|
+
if (!entry.isDirectory()) continue;
|
|
1205
|
+
if (entry.name.startsWith(".")) continue;
|
|
1206
|
+
await scanAspectsDirectory(path7.join(dirPath, entry.name), aspectsRoot, aspects);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1167
1209
|
async function loadFlows(flowsDir) {
|
|
1168
1210
|
try {
|
|
1169
1211
|
const entries = await readdir3(flowsDir, { withFileTypes: true });
|
|
1170
1212
|
const flows = [];
|
|
1171
1213
|
for (const entry of entries) {
|
|
1172
1214
|
if (!entry.isDirectory()) continue;
|
|
1173
|
-
const flowYamlPath =
|
|
1174
|
-
const flow = await parseFlow(
|
|
1215
|
+
const flowYamlPath = path7.join(flowsDir, entry.name, "flow.yaml");
|
|
1216
|
+
const flow = await parseFlow(path7.join(flowsDir, entry.name), flowYamlPath);
|
|
1175
1217
|
flows.push(flow);
|
|
1176
1218
|
}
|
|
1177
1219
|
return flows;
|
|
@@ -1179,37 +1221,14 @@ async function loadFlows(flowsDir) {
|
|
|
1179
1221
|
return [];
|
|
1180
1222
|
}
|
|
1181
1223
|
}
|
|
1182
|
-
async function
|
|
1183
|
-
const items = [];
|
|
1184
|
-
const categorySet = new Set(categories.map((c) => c.name));
|
|
1185
|
-
try {
|
|
1186
|
-
const catEntries = await readdir3(knowledgeDir, { withFileTypes: true });
|
|
1187
|
-
for (const catEntry of catEntries) {
|
|
1188
|
-
if (!catEntry.isDirectory()) continue;
|
|
1189
|
-
if (!categorySet.has(catEntry.name)) continue;
|
|
1190
|
-
const catPath = path6.join(knowledgeDir, catEntry.name);
|
|
1191
|
-
const itemEntries = await readdir3(catPath, { withFileTypes: true });
|
|
1192
|
-
for (const itemEntry of itemEntries) {
|
|
1193
|
-
if (!itemEntry.isDirectory()) continue;
|
|
1194
|
-
const itemDir = path6.join(catPath, itemEntry.name);
|
|
1195
|
-
const knowledgeYamlPath = path6.join(itemDir, "knowledge.yaml");
|
|
1196
|
-
const relativePath = `${catEntry.name}/${itemEntry.name}`;
|
|
1197
|
-
const item = await parseKnowledge(itemDir, knowledgeYamlPath, catEntry.name, relativePath);
|
|
1198
|
-
items.push(item);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
} catch {
|
|
1202
|
-
}
|
|
1203
|
-
return items;
|
|
1204
|
-
}
|
|
1205
|
-
async function loadSchemas(templatesDir) {
|
|
1224
|
+
async function loadSchemas(schemasDir) {
|
|
1206
1225
|
try {
|
|
1207
|
-
const entries = await readdir3(
|
|
1226
|
+
const entries = await readdir3(schemasDir, { withFileTypes: true });
|
|
1208
1227
|
const schemas = [];
|
|
1209
1228
|
for (const entry of entries) {
|
|
1210
1229
|
if (!entry.isFile()) continue;
|
|
1211
1230
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1212
|
-
const s = await parseSchema(
|
|
1231
|
+
const s = await parseSchema(path7.join(schemasDir, entry.name));
|
|
1213
1232
|
schemas.push(s);
|
|
1214
1233
|
}
|
|
1215
1234
|
return schemas;
|
|
@@ -1220,7 +1239,7 @@ async function loadSchemas(templatesDir) {
|
|
|
1220
1239
|
|
|
1221
1240
|
// src/core/context-builder.ts
|
|
1222
1241
|
import { readFile as readFile10 } from "fs/promises";
|
|
1223
|
-
import
|
|
1242
|
+
import path8 from "path";
|
|
1224
1243
|
|
|
1225
1244
|
// src/utils/tokens.ts
|
|
1226
1245
|
function estimateTokens(text) {
|
|
@@ -1235,47 +1254,42 @@ async function buildContext(graph, nodePath) {
|
|
|
1235
1254
|
if (!node) {
|
|
1236
1255
|
throw new Error(`Node not found: ${nodePath}`);
|
|
1237
1256
|
}
|
|
1238
|
-
const nodeTags = new Set(node.meta.tags ?? []);
|
|
1239
|
-
const seenKnowledge = /* @__PURE__ */ new Set();
|
|
1240
1257
|
const layers = [];
|
|
1241
1258
|
layers.push(buildGlobalLayer(graph.config));
|
|
1242
|
-
for (const k of collectKnowledgeItems(graph, nodePath, nodeTags, seenKnowledge)) {
|
|
1243
|
-
layers.push(buildKnowledgeLayer(k));
|
|
1244
|
-
}
|
|
1245
1259
|
const ancestors = collectAncestors(node);
|
|
1246
1260
|
for (const ancestor of ancestors) {
|
|
1247
|
-
layers.push(buildHierarchyLayer(ancestor, graph.config));
|
|
1261
|
+
layers.push(buildHierarchyLayer(ancestor, graph.config, graph));
|
|
1248
1262
|
}
|
|
1249
|
-
layers.push(await buildOwnLayer(node, graph.config, graph.rootPath));
|
|
1263
|
+
layers.push(await buildOwnLayer(node, graph.config, graph.rootPath, graph));
|
|
1264
|
+
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
1250
1265
|
for (const relation of node.meta.relations ?? []) {
|
|
1251
1266
|
const target = graph.nodes.get(relation.target);
|
|
1252
1267
|
if (!target) {
|
|
1253
1268
|
throw new Error(`Broken relation: ${nodePath} -> ${relation.target} (target not found)`);
|
|
1254
1269
|
}
|
|
1270
|
+
if (ancestorPaths.has(relation.target)) continue;
|
|
1255
1271
|
if (STRUCTURAL_RELATION_TYPES.has(relation.type)) {
|
|
1256
1272
|
layers.push(buildStructuralRelationLayer(target, relation, graph.config));
|
|
1257
1273
|
} else if (EVENT_RELATION_TYPES.has(relation.type)) {
|
|
1258
1274
|
layers.push(buildEventRelationLayer(target, relation));
|
|
1259
1275
|
}
|
|
1260
1276
|
}
|
|
1261
|
-
for (const tag of nodeTags) {
|
|
1262
|
-
for (const aspect of graph.aspects) {
|
|
1263
|
-
if (aspect.tag === tag) {
|
|
1264
|
-
layers.push(buildAspectLayer(aspect));
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
1277
|
for (const flow of collectParticipatingFlows(graph, node)) {
|
|
1269
|
-
layers.push(buildFlowLayer(flow));
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1278
|
+
layers.push(buildFlowLayer(flow, graph));
|
|
1279
|
+
}
|
|
1280
|
+
const allAspectIds = /* @__PURE__ */ new Set();
|
|
1281
|
+
for (const l of layers) {
|
|
1282
|
+
const aspects = l.attrs?.aspects;
|
|
1283
|
+
if (aspects) {
|
|
1284
|
+
for (const id of aspects.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
1285
|
+
allAspectIds.add(id);
|
|
1276
1286
|
}
|
|
1277
1287
|
}
|
|
1278
1288
|
}
|
|
1289
|
+
const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
|
|
1290
|
+
for (const aspect of aspectsToInclude) {
|
|
1291
|
+
layers.push(buildAspectLayer(aspect));
|
|
1292
|
+
}
|
|
1279
1293
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
1280
1294
|
const tokenCount = estimateTokens(fullText);
|
|
1281
1295
|
const mapping = normalizeMappingPaths(node.meta.mapping);
|
|
@@ -1289,47 +1303,46 @@ async function buildContext(graph, nodePath) {
|
|
|
1289
1303
|
tokenCount
|
|
1290
1304
|
};
|
|
1291
1305
|
}
|
|
1292
|
-
function
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1306
|
+
function collectParticipatingFlows(graph, node) {
|
|
1307
|
+
const paths = /* @__PURE__ */ new Set([node.path, ...collectAncestors(node).map((a) => a.path)]);
|
|
1308
|
+
return graph.flows.filter((f) => f.nodes.some((n) => paths.has(n)));
|
|
1309
|
+
}
|
|
1310
|
+
function expandAspects(aspectIds, aspects) {
|
|
1311
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1312
|
+
for (const a of aspects) {
|
|
1313
|
+
idToAspect.set(a.id, a);
|
|
1299
1314
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1315
|
+
const result = [];
|
|
1316
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1317
|
+
const stack = /* @__PURE__ */ new Set();
|
|
1318
|
+
function collect(id) {
|
|
1319
|
+
if (stack.has(id)) {
|
|
1320
|
+
throw new Error(`Aspect implies cycle detected involving aspect '${id}'`);
|
|
1307
1321
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1322
|
+
if (visited.has(id)) return;
|
|
1323
|
+
stack.add(id);
|
|
1324
|
+
visited.add(id);
|
|
1325
|
+
result.push(id);
|
|
1326
|
+
const aspect = idToAspect.get(id);
|
|
1327
|
+
if (aspect) {
|
|
1328
|
+
for (const implied of aspect.implies ?? []) {
|
|
1329
|
+
collect(implied);
|
|
1314
1330
|
}
|
|
1315
1331
|
}
|
|
1332
|
+
stack.delete(id);
|
|
1316
1333
|
}
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
for (const kPath of node.meta.knowledge) {
|
|
1320
|
-
const norm = kPath.replace(/\/$/, "");
|
|
1321
|
-
const k = graph.knowledge.find((item) => item.path === norm || item.path === kPath);
|
|
1322
|
-
if (k && !seenKnowledge.has(k.path)) {
|
|
1323
|
-
seenKnowledge.add(k.path);
|
|
1324
|
-
result.push(k);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1334
|
+
for (const id of aspectIds) {
|
|
1335
|
+
collect(id);
|
|
1327
1336
|
}
|
|
1328
1337
|
return result;
|
|
1329
1338
|
}
|
|
1330
|
-
function
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1339
|
+
function resolveAspects(aspectIds, aspects) {
|
|
1340
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const a of aspects) {
|
|
1342
|
+
idToAspect.set(a.id, a);
|
|
1343
|
+
}
|
|
1344
|
+
const expandedIds = expandAspects([...aspectIds], aspects);
|
|
1345
|
+
return expandedIds.map((id) => idToAspect.get(id)).filter((a) => a !== void 0);
|
|
1333
1346
|
}
|
|
1334
1347
|
function buildGlobalLayer(config) {
|
|
1335
1348
|
let content = `**Project:** ${config.name}
|
|
@@ -1347,41 +1360,39 @@ ${config.standards || "(none)"}
|
|
|
1347
1360
|
`;
|
|
1348
1361
|
return { type: "global", label: "Global Context", content };
|
|
1349
1362
|
}
|
|
1350
|
-
function buildKnowledgeLayer(k, fromFlow) {
|
|
1351
|
-
const categoryLabel = k.category.charAt(0).toUpperCase() + k.category.slice(1);
|
|
1352
|
-
const content = k.artifacts.map((a) => `### ${a.filename}
|
|
1353
|
-
${a.content}`).join("\n\n");
|
|
1354
|
-
const label = fromFlow ? `Long-term Memory (from flow): ${k.name}` : `${categoryLabel}: ${k.name}`;
|
|
1355
|
-
return {
|
|
1356
|
-
type: "knowledge",
|
|
1357
|
-
label,
|
|
1358
|
-
content
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
1363
|
function filterArtifactsByConfig(artifacts, config) {
|
|
1362
1364
|
const allowed = new Set(Object.keys(config.artifacts ?? {}));
|
|
1363
1365
|
return artifacts.filter((a) => allowed.has(a.filename));
|
|
1364
1366
|
}
|
|
1365
|
-
function buildHierarchyLayer(ancestor, config) {
|
|
1367
|
+
function buildHierarchyLayer(ancestor, config, graph) {
|
|
1366
1368
|
const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
|
|
1367
1369
|
const content = filtered.map((a) => `### ${a.filename}
|
|
1368
1370
|
${a.content}`).join("\n\n");
|
|
1371
|
+
const nodeAspects = ancestor.meta.aspects ?? [];
|
|
1372
|
+
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1373
|
+
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1369
1374
|
return {
|
|
1370
1375
|
type: "hierarchy",
|
|
1371
1376
|
label: `Module Context (${ancestor.path}/)`,
|
|
1372
|
-
content
|
|
1377
|
+
content,
|
|
1378
|
+
attrs
|
|
1373
1379
|
};
|
|
1374
1380
|
}
|
|
1375
|
-
async function buildOwnLayer(node, config, graphRootPath) {
|
|
1381
|
+
async function buildOwnLayer(node, config, graphRootPath, graph) {
|
|
1376
1382
|
const parts = [];
|
|
1377
|
-
|
|
1378
|
-
try {
|
|
1379
|
-
const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
|
|
1383
|
+
if (node.nodeYamlRaw) {
|
|
1380
1384
|
parts.push(`### node.yaml
|
|
1385
|
+
${node.nodeYamlRaw.trim()}`);
|
|
1386
|
+
} else {
|
|
1387
|
+
const nodeYamlPath = path8.join(graphRootPath, "model", node.path, "node.yaml");
|
|
1388
|
+
try {
|
|
1389
|
+
const nodeYamlContent = await readFile10(nodeYamlPath, "utf-8");
|
|
1390
|
+
parts.push(`### node.yaml
|
|
1381
1391
|
${nodeYamlContent.trim()}`);
|
|
1382
|
-
|
|
1383
|
-
|
|
1392
|
+
} catch {
|
|
1393
|
+
parts.push(`### node.yaml
|
|
1384
1394
|
(not found)`);
|
|
1395
|
+
}
|
|
1385
1396
|
}
|
|
1386
1397
|
const filtered = filterArtifactsByConfig(node.artifacts, config);
|
|
1387
1398
|
for (const a of filtered) {
|
|
@@ -1389,10 +1400,14 @@ ${nodeYamlContent.trim()}`);
|
|
|
1389
1400
|
${a.content}`);
|
|
1390
1401
|
}
|
|
1391
1402
|
const content = parts.join("\n\n");
|
|
1403
|
+
const nodeAspects = node.meta.aspects ?? [];
|
|
1404
|
+
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1405
|
+
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1392
1406
|
return {
|
|
1393
1407
|
type: "own",
|
|
1394
1408
|
label: `Node: ${node.meta.name}`,
|
|
1395
|
-
content
|
|
1409
|
+
content,
|
|
1410
|
+
attrs
|
|
1396
1411
|
};
|
|
1397
1412
|
}
|
|
1398
1413
|
function buildStructuralRelationLayer(target, relation, config) {
|
|
@@ -1420,10 +1435,17 @@ ${a.content}`).join("\n\n");
|
|
|
1420
1435
|
content += filtered.map((a) => `### ${a.filename}
|
|
1421
1436
|
${a.content}`).join("\n\n");
|
|
1422
1437
|
}
|
|
1438
|
+
const attrs = {
|
|
1439
|
+
target: target.path,
|
|
1440
|
+
type: relation.type
|
|
1441
|
+
};
|
|
1442
|
+
if (relation.consumes?.length) attrs.consumes = relation.consumes.join(", ");
|
|
1443
|
+
if (relation.failure) attrs.failure = relation.failure;
|
|
1423
1444
|
return {
|
|
1424
1445
|
type: "relational",
|
|
1425
1446
|
label: `Dependency: ${target.meta.name} (${relation.type}) \u2014 ${target.path}`,
|
|
1426
|
-
content: content.trim()
|
|
1447
|
+
content: content.trim(),
|
|
1448
|
+
attrs
|
|
1427
1449
|
};
|
|
1428
1450
|
}
|
|
1429
1451
|
function buildEventRelationLayer(target, relation) {
|
|
@@ -1436,10 +1458,17 @@ You listen for ${eventName}.`;
|
|
|
1436
1458
|
content += `
|
|
1437
1459
|
Consumes: ${relation.consumes.join(", ")}`;
|
|
1438
1460
|
}
|
|
1461
|
+
const attrs = {
|
|
1462
|
+
target: target.path,
|
|
1463
|
+
type: relation.type,
|
|
1464
|
+
"event-name": eventName
|
|
1465
|
+
};
|
|
1466
|
+
if (relation.consumes?.length) attrs.consumes = relation.consumes.join(", ");
|
|
1439
1467
|
return {
|
|
1440
1468
|
type: "relational",
|
|
1441
1469
|
label: `Event: ${eventName} [${relation.type}]`,
|
|
1442
|
-
content
|
|
1470
|
+
content,
|
|
1471
|
+
attrs
|
|
1443
1472
|
};
|
|
1444
1473
|
}
|
|
1445
1474
|
function buildAspectLayer(aspect) {
|
|
@@ -1447,17 +1476,21 @@ function buildAspectLayer(aspect) {
|
|
|
1447
1476
|
${a.content}`).join("\n\n");
|
|
1448
1477
|
return {
|
|
1449
1478
|
type: "aspects",
|
|
1450
|
-
label: `${aspect.name} (
|
|
1479
|
+
label: `${aspect.name} (aspect: ${aspect.id})`,
|
|
1451
1480
|
content
|
|
1452
1481
|
};
|
|
1453
1482
|
}
|
|
1454
|
-
function buildFlowLayer(flow) {
|
|
1483
|
+
function buildFlowLayer(flow, graph) {
|
|
1455
1484
|
const content = flow.artifacts.map((a) => `### ${a.filename}
|
|
1456
1485
|
${a.content}`).join("\n\n");
|
|
1486
|
+
const flowAspects = flow.aspects ?? [];
|
|
1487
|
+
const expanded = expandAspects(flowAspects, graph.aspects);
|
|
1488
|
+
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1457
1489
|
return {
|
|
1458
1490
|
type: "flows",
|
|
1459
1491
|
label: `Flow: ${flow.name}`,
|
|
1460
|
-
content: content || "(no artifacts)"
|
|
1492
|
+
content: content || "(no artifacts)",
|
|
1493
|
+
attrs
|
|
1461
1494
|
};
|
|
1462
1495
|
}
|
|
1463
1496
|
function buildSections(layers, mapping) {
|
|
@@ -1471,12 +1504,16 @@ function buildSections(layers, mapping) {
|
|
|
1471
1504
|
}
|
|
1472
1505
|
return [
|
|
1473
1506
|
{ key: "Global", layers: layers.filter((l) => l.type === "global") },
|
|
1474
|
-
{ key: "Knowledge", layers: layers.filter((l) => l.type === "knowledge") },
|
|
1475
1507
|
{ key: "Hierarchy", layers: layers.filter((l) => l.type === "hierarchy") },
|
|
1476
1508
|
{ key: "OwnArtifacts", layers: ownLayers },
|
|
1477
|
-
{ key: "Dependencies", layers: layers.filter((l) => l.type === "relational") },
|
|
1478
1509
|
{ key: "Aspects", layers: layers.filter((l) => l.type === "aspects") },
|
|
1479
|
-
{
|
|
1510
|
+
{
|
|
1511
|
+
key: "Relational",
|
|
1512
|
+
layers: [
|
|
1513
|
+
...layers.filter((l) => l.type === "relational"),
|
|
1514
|
+
...layers.filter((l) => l.type === "flows")
|
|
1515
|
+
]
|
|
1516
|
+
}
|
|
1480
1517
|
];
|
|
1481
1518
|
}
|
|
1482
1519
|
function collectAncestors(node) {
|
|
@@ -1488,30 +1525,27 @@ function collectAncestors(node) {
|
|
|
1488
1525
|
}
|
|
1489
1526
|
return ancestors;
|
|
1490
1527
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1506
|
-
});
|
|
1507
|
-
const ts = parseInt(out.trim(), 10);
|
|
1508
|
-
return Number.isNaN(ts) ? null : ts;
|
|
1509
|
-
} catch {
|
|
1510
|
-
return null;
|
|
1528
|
+
function collectEffectiveAspectIds(graph, nodePath) {
|
|
1529
|
+
const node = graph.nodes.get(nodePath);
|
|
1530
|
+
if (!node) return /* @__PURE__ */ new Set();
|
|
1531
|
+
const raw = new Set(node.meta.aspects ?? []);
|
|
1532
|
+
let ancestor = node.parent;
|
|
1533
|
+
while (ancestor) {
|
|
1534
|
+
for (const id of ancestor.meta.aspects ?? []) raw.add(id);
|
|
1535
|
+
ancestor = ancestor.parent;
|
|
1536
|
+
}
|
|
1537
|
+
const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
|
|
1538
|
+
for (const flow of graph.flows) {
|
|
1539
|
+
if (flow.nodes.some((n) => ancestorPaths.has(n))) {
|
|
1540
|
+
for (const id of flow.aspects ?? []) raw.add(id);
|
|
1541
|
+
}
|
|
1511
1542
|
}
|
|
1543
|
+
return new Set(expandAspects([...raw], graph.aspects));
|
|
1512
1544
|
}
|
|
1513
1545
|
|
|
1514
1546
|
// src/core/validator.ts
|
|
1547
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1548
|
+
import path9 from "path";
|
|
1515
1549
|
var RESERVED_DIRS = /* @__PURE__ */ new Set();
|
|
1516
1550
|
async function validate(graph, scope = "all") {
|
|
1517
1551
|
const issues = [];
|
|
@@ -1534,46 +1568,60 @@ async function validate(graph, scope = "all") {
|
|
|
1534
1568
|
}
|
|
1535
1569
|
if (!graph.configError) {
|
|
1536
1570
|
issues.push(...checkNodeTypes(graph));
|
|
1537
|
-
issues.push(...
|
|
1538
|
-
issues.push(...
|
|
1539
|
-
issues.push(...
|
|
1571
|
+
issues.push(...checkAspectsDefined(graph));
|
|
1572
|
+
issues.push(...checkAspectIds(graph));
|
|
1573
|
+
issues.push(...checkAspectIdUniqueness(graph));
|
|
1574
|
+
issues.push(...checkImpliedAspectsExist(graph));
|
|
1575
|
+
issues.push(...checkImpliesNoCycles(graph));
|
|
1576
|
+
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
1540
1577
|
issues.push(...checkRequiredArtifacts(graph));
|
|
1541
|
-
issues.push(...await checkUnknownKnowledgeCategories(graph));
|
|
1542
1578
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
1543
|
-
issues.push(...checkScopeTagsDefined(graph));
|
|
1544
|
-
issues.push(...await checkMissingPatternExamples(graph));
|
|
1545
1579
|
issues.push(...await checkContextBudget(graph));
|
|
1546
1580
|
issues.push(...checkHighFanOut(graph));
|
|
1547
|
-
issues.push(...await checkStaleKnowledge(graph));
|
|
1548
1581
|
}
|
|
1549
1582
|
issues.push(...checkSchemas(graph));
|
|
1550
1583
|
issues.push(...checkRelationTargets(graph));
|
|
1551
1584
|
issues.push(...checkNoCycles(graph));
|
|
1552
1585
|
issues.push(...checkMappingOverlap(graph));
|
|
1553
|
-
issues.push(...
|
|
1586
|
+
issues.push(...await checkMappingPathsExist(graph));
|
|
1554
1587
|
issues.push(...checkBrokenFlowRefs(graph));
|
|
1555
|
-
issues.push(...
|
|
1588
|
+
issues.push(...checkFlowAspectIds(graph));
|
|
1556
1589
|
issues.push(...await checkDirectoriesHaveNodeYaml(graph));
|
|
1557
1590
|
issues.push(...await checkShallowArtifacts(graph));
|
|
1558
|
-
issues.push(...await checkUnreachableKnowledge(graph));
|
|
1559
1591
|
issues.push(...checkUnpairedEvents(graph));
|
|
1560
1592
|
let filtered = issues;
|
|
1561
1593
|
let nodesScanned = graph.nodes.size;
|
|
1562
1594
|
if (scope !== "all" && scope.trim()) {
|
|
1563
1595
|
if (!graph.nodes.has(scope)) {
|
|
1596
|
+
const parseError = (graph.nodeParseErrors ?? []).find(
|
|
1597
|
+
(e) => e.nodePath === scope || scope.startsWith(e.nodePath + "/")
|
|
1598
|
+
);
|
|
1599
|
+
if (parseError) {
|
|
1600
|
+
return {
|
|
1601
|
+
issues: [{
|
|
1602
|
+
severity: "error",
|
|
1603
|
+
code: "E001",
|
|
1604
|
+
rule: "invalid-node-yaml",
|
|
1605
|
+
message: parseError.message,
|
|
1606
|
+
nodePath: parseError.nodePath
|
|
1607
|
+
}],
|
|
1608
|
+
nodesScanned: 0
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1564
1611
|
return {
|
|
1565
1612
|
issues: [{ severity: "error", rule: "invalid-scope", message: `Node not found: ${scope}` }],
|
|
1566
1613
|
nodesScanned: 0
|
|
1567
1614
|
};
|
|
1568
1615
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1616
|
+
const scopePrefix = scope + "/";
|
|
1617
|
+
filtered = issues.filter((i) => !i.nodePath || i.nodePath === scope || i.nodePath.startsWith(scopePrefix));
|
|
1618
|
+
nodesScanned = [...graph.nodes.keys()].filter((p) => p === scope || p.startsWith(scopePrefix)).length;
|
|
1571
1619
|
}
|
|
1572
1620
|
return { issues: filtered, nodesScanned };
|
|
1573
1621
|
}
|
|
1574
1622
|
function checkNodeTypes(graph) {
|
|
1575
1623
|
const issues = [];
|
|
1576
|
-
const allowedTypes = new Set(graph.config.node_types ?? []);
|
|
1624
|
+
const allowedTypes = new Set((graph.config.node_types ?? []).map((t) => t.name));
|
|
1577
1625
|
for (const [nodePath, node] of graph.nodes) {
|
|
1578
1626
|
if (!allowedTypes.has(node.meta.type)) {
|
|
1579
1627
|
issues.push({
|
|
@@ -1636,17 +1684,17 @@ function checkRelationTargets(graph) {
|
|
|
1636
1684
|
}
|
|
1637
1685
|
return issues;
|
|
1638
1686
|
}
|
|
1639
|
-
function
|
|
1687
|
+
function checkAspectsDefined(graph) {
|
|
1640
1688
|
const issues = [];
|
|
1641
|
-
const
|
|
1689
|
+
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
1642
1690
|
for (const [nodePath, node] of graph.nodes) {
|
|
1643
|
-
for (const
|
|
1644
|
-
if (!
|
|
1691
|
+
for (const aspectId of node.meta.aspects ?? []) {
|
|
1692
|
+
if (!validAspectIds.has(aspectId)) {
|
|
1645
1693
|
issues.push({
|
|
1646
1694
|
severity: "error",
|
|
1647
1695
|
code: "E003",
|
|
1648
|
-
rule: "unknown-
|
|
1649
|
-
message: `
|
|
1696
|
+
rule: "unknown-aspect",
|
|
1697
|
+
message: `Aspect '${aspectId}' has no corresponding directory in aspects/`,
|
|
1650
1698
|
nodePath
|
|
1651
1699
|
});
|
|
1652
1700
|
}
|
|
@@ -1654,40 +1702,124 @@ function checkTagsDefined(graph) {
|
|
|
1654
1702
|
}
|
|
1655
1703
|
return issues;
|
|
1656
1704
|
}
|
|
1657
|
-
function
|
|
1658
|
-
|
|
1659
|
-
const definedTags = new Set(graph.config.tags ?? []);
|
|
1660
|
-
for (const aspect of graph.aspects) {
|
|
1661
|
-
if (!definedTags.has(aspect.tag)) {
|
|
1662
|
-
issues.push({
|
|
1663
|
-
severity: "error",
|
|
1664
|
-
code: "E007",
|
|
1665
|
-
rule: "broken-aspect-tag",
|
|
1666
|
-
message: `Aspect '${aspect.name}' references undefined tag '${aspect.tag}'`
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
return issues;
|
|
1705
|
+
function checkAspectIds(_graph) {
|
|
1706
|
+
return [];
|
|
1671
1707
|
}
|
|
1672
|
-
function
|
|
1708
|
+
function checkAspectIdUniqueness(graph) {
|
|
1673
1709
|
const issues = [];
|
|
1674
|
-
const
|
|
1710
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1675
1711
|
for (const aspect of graph.aspects) {
|
|
1676
|
-
const names =
|
|
1712
|
+
const names = byId.get(aspect.id) ?? [];
|
|
1677
1713
|
names.push(aspect.name);
|
|
1678
|
-
|
|
1714
|
+
byId.set(aspect.id, names);
|
|
1679
1715
|
}
|
|
1680
|
-
for (const [
|
|
1716
|
+
for (const [id, names] of byId) {
|
|
1681
1717
|
if (names.length <= 1) continue;
|
|
1682
1718
|
issues.push({
|
|
1683
1719
|
severity: "error",
|
|
1684
1720
|
code: "E014",
|
|
1685
1721
|
rule: "duplicate-aspect-binding",
|
|
1686
|
-
message: `
|
|
1722
|
+
message: `Aspect '${id}' is bound to multiple aspects (${names.join(", ")})`
|
|
1687
1723
|
});
|
|
1688
1724
|
}
|
|
1689
1725
|
return issues;
|
|
1690
1726
|
}
|
|
1727
|
+
function checkImpliedAspectsExist(graph) {
|
|
1728
|
+
const issues = [];
|
|
1729
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1730
|
+
for (const a of graph.aspects) {
|
|
1731
|
+
idToAspect.set(a.id, { name: a.name });
|
|
1732
|
+
}
|
|
1733
|
+
for (const aspect of graph.aspects) {
|
|
1734
|
+
for (const impliedId of aspect.implies ?? []) {
|
|
1735
|
+
if (!idToAspect.has(impliedId)) {
|
|
1736
|
+
issues.push({
|
|
1737
|
+
severity: "error",
|
|
1738
|
+
code: "E016",
|
|
1739
|
+
rule: "implied-aspect-missing",
|
|
1740
|
+
message: `Aspect '${aspect.name}' implies '${impliedId}' but no aspect with that id exists in aspects/`
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return issues;
|
|
1746
|
+
}
|
|
1747
|
+
function checkImpliesNoCycles(graph) {
|
|
1748
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const a of graph.aspects) {
|
|
1750
|
+
idToAspect.set(a.id, { implies: a.implies });
|
|
1751
|
+
}
|
|
1752
|
+
const WHITE = 0;
|
|
1753
|
+
const GRAY = 1;
|
|
1754
|
+
const BLACK = 2;
|
|
1755
|
+
const color = /* @__PURE__ */ new Map();
|
|
1756
|
+
for (const id of idToAspect.keys()) color.set(id, WHITE);
|
|
1757
|
+
const issues = [];
|
|
1758
|
+
function dfs(id, pathArr) {
|
|
1759
|
+
color.set(id, GRAY);
|
|
1760
|
+
pathArr.push(id);
|
|
1761
|
+
const aspect = idToAspect.get(id);
|
|
1762
|
+
for (const implied of aspect?.implies ?? []) {
|
|
1763
|
+
if (color.get(implied) === GRAY) {
|
|
1764
|
+
const cycle = pathArr.slice(pathArr.indexOf(implied)).concat(implied);
|
|
1765
|
+
issues.push({
|
|
1766
|
+
severity: "error",
|
|
1767
|
+
code: "E017",
|
|
1768
|
+
rule: "aspect-implies-cycle",
|
|
1769
|
+
message: `Aspect implies cycle: ${cycle.join(" \u2192 ")}`
|
|
1770
|
+
});
|
|
1771
|
+
pathArr.pop();
|
|
1772
|
+
color.set(id, BLACK);
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
if (color.get(implied) === WHITE && dfs(implied, pathArr)) {
|
|
1776
|
+
pathArr.pop();
|
|
1777
|
+
color.set(id, BLACK);
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
pathArr.pop();
|
|
1782
|
+
color.set(id, BLACK);
|
|
1783
|
+
return false;
|
|
1784
|
+
}
|
|
1785
|
+
for (const id of idToAspect.keys()) {
|
|
1786
|
+
if (color.get(id) === WHITE) {
|
|
1787
|
+
dfs(id, []);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return issues;
|
|
1791
|
+
}
|
|
1792
|
+
function checkRequiredAspectsCoverage(graph) {
|
|
1793
|
+
const issues = [];
|
|
1794
|
+
const typeConfig = new Map(
|
|
1795
|
+
(graph.config.node_types ?? []).map((t) => [t.name, t.required_aspects ?? []])
|
|
1796
|
+
);
|
|
1797
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
1798
|
+
if (node.meta.blackbox) continue;
|
|
1799
|
+
const requiredAspects = typeConfig.get(node.meta.type);
|
|
1800
|
+
if (!requiredAspects || requiredAspects.length === 0) continue;
|
|
1801
|
+
const nodeAspects = node.meta.aspects ?? [];
|
|
1802
|
+
let effectiveAspects;
|
|
1803
|
+
try {
|
|
1804
|
+
effectiveAspects = resolveAspects(nodeAspects, graph.aspects);
|
|
1805
|
+
} catch {
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
const effectiveAspectIds = new Set(effectiveAspects.map((a) => a.id));
|
|
1809
|
+
for (const required of requiredAspects) {
|
|
1810
|
+
if (!effectiveAspectIds.has(required)) {
|
|
1811
|
+
issues.push({
|
|
1812
|
+
severity: "warning",
|
|
1813
|
+
code: "W011",
|
|
1814
|
+
rule: "missing-required-aspect-coverage",
|
|
1815
|
+
message: `Node '${nodePath}' (type: ${node.meta.type}) missing required aspect coverage for '${required}'`,
|
|
1816
|
+
nodePath
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return issues;
|
|
1822
|
+
}
|
|
1691
1823
|
function checkNoCycles(graph) {
|
|
1692
1824
|
const WHITE = 0;
|
|
1693
1825
|
const GRAY = 1;
|
|
@@ -1740,6 +1872,9 @@ function arePathsOverlapping(pathA, pathB) {
|
|
|
1740
1872
|
if (pathA === pathB) return true;
|
|
1741
1873
|
return pathA.startsWith(pathB + "/") || pathB.startsWith(pathA + "/");
|
|
1742
1874
|
}
|
|
1875
|
+
function isAncestorNode(possibleAncestor, possibleDescendant) {
|
|
1876
|
+
return possibleDescendant.startsWith(possibleAncestor + "/");
|
|
1877
|
+
}
|
|
1743
1878
|
function checkMappingOverlap(graph) {
|
|
1744
1879
|
const issues = [];
|
|
1745
1880
|
const ownership = [];
|
|
@@ -1755,6 +1890,9 @@ function checkMappingOverlap(graph) {
|
|
|
1755
1890
|
const candidate = ownership[nestedIndex];
|
|
1756
1891
|
if (current.nodePath === candidate.nodePath) continue;
|
|
1757
1892
|
if (!arePathsOverlapping(current.mappingPath, candidate.mappingPath)) continue;
|
|
1893
|
+
const isContainment = current.mappingPath !== candidate.mappingPath;
|
|
1894
|
+
const isHierarchical = isAncestorNode(current.nodePath, candidate.nodePath) || isAncestorNode(candidate.nodePath, current.nodePath);
|
|
1895
|
+
if (isContainment && isHierarchical) continue;
|
|
1758
1896
|
issues.push({
|
|
1759
1897
|
severity: "error",
|
|
1760
1898
|
code: "E009",
|
|
@@ -1766,6 +1904,29 @@ function checkMappingOverlap(graph) {
|
|
|
1766
1904
|
}
|
|
1767
1905
|
return issues;
|
|
1768
1906
|
}
|
|
1907
|
+
async function checkMappingPathsExist(graph) {
|
|
1908
|
+
const issues = [];
|
|
1909
|
+
const projectRoot = path9.dirname(graph.rootPath);
|
|
1910
|
+
const { access: access4 } = await import("fs/promises");
|
|
1911
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
1912
|
+
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
1913
|
+
for (const mp of mappingPaths) {
|
|
1914
|
+
const absPath = path9.join(projectRoot, mp);
|
|
1915
|
+
try {
|
|
1916
|
+
await access4(absPath);
|
|
1917
|
+
} catch {
|
|
1918
|
+
issues.push({
|
|
1919
|
+
severity: "warning",
|
|
1920
|
+
code: "W012",
|
|
1921
|
+
rule: "mapping-path-missing",
|
|
1922
|
+
message: `Mapping path '${mp}' does not exist on disk`,
|
|
1923
|
+
nodePath
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return issues;
|
|
1929
|
+
}
|
|
1769
1930
|
function getIncomingRelationSources(graph, nodePath) {
|
|
1770
1931
|
const sources = [];
|
|
1771
1932
|
for (const [srcPath, node] of graph.nodes) {
|
|
@@ -1789,9 +1950,10 @@ function artifactRequiredReason(graph, nodePath, node, required) {
|
|
|
1789
1950
|
const count = node.meta.relations?.length ?? 0;
|
|
1790
1951
|
return count > 0 ? `${count} outgoing relation(s)` : null;
|
|
1791
1952
|
}
|
|
1792
|
-
if (when.startsWith("has_tag:")) {
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1953
|
+
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
1954
|
+
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
1955
|
+
const aspectId = when.slice(prefix.length);
|
|
1956
|
+
return (node.meta.aspects ?? []).includes(aspectId) ? `node has aspect '${aspectId}'` : null;
|
|
1795
1957
|
}
|
|
1796
1958
|
return null;
|
|
1797
1959
|
}
|
|
@@ -1831,29 +1993,9 @@ function checkRequiredArtifacts(graph) {
|
|
|
1831
1993
|
}
|
|
1832
1994
|
return issues;
|
|
1833
1995
|
}
|
|
1834
|
-
function checkBrokenKnowledgeRefs(graph) {
|
|
1835
|
-
const issues = [];
|
|
1836
|
-
const knowledgePaths = new Set(graph.knowledge.map((k) => k.path));
|
|
1837
|
-
for (const [nodePath, node] of graph.nodes) {
|
|
1838
|
-
for (const kPath of node.meta.knowledge ?? []) {
|
|
1839
|
-
const norm = kPath.replace(/\/$/, "");
|
|
1840
|
-
if (!knowledgePaths.has(norm) && !knowledgePaths.has(kPath)) {
|
|
1841
|
-
issues.push({
|
|
1842
|
-
severity: "error",
|
|
1843
|
-
code: "E005",
|
|
1844
|
-
rule: "broken-knowledge-ref",
|
|
1845
|
-
message: `Knowledge ref '${kPath}' does not resolve to existing knowledge item`,
|
|
1846
|
-
nodePath
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
return issues;
|
|
1852
|
-
}
|
|
1853
1996
|
function checkBrokenFlowRefs(graph) {
|
|
1854
1997
|
const issues = [];
|
|
1855
1998
|
const nodePaths = new Set(graph.nodes.keys());
|
|
1856
|
-
const knowledgePaths = new Set(graph.knowledge.map((k) => k.path));
|
|
1857
1999
|
for (const flow of graph.flows) {
|
|
1858
2000
|
for (const n of flow.nodes) {
|
|
1859
2001
|
if (!nodePaths.has(n)) {
|
|
@@ -1865,107 +2007,43 @@ function checkBrokenFlowRefs(graph) {
|
|
|
1865
2007
|
});
|
|
1866
2008
|
}
|
|
1867
2009
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2010
|
+
}
|
|
2011
|
+
return issues;
|
|
2012
|
+
}
|
|
2013
|
+
function checkFlowAspectIds(graph) {
|
|
2014
|
+
const issues = [];
|
|
2015
|
+
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
2016
|
+
for (const flow of graph.flows) {
|
|
2017
|
+
for (const aspectId of flow.aspects ?? []) {
|
|
2018
|
+
if (!validAspectIds.has(aspectId)) {
|
|
1871
2019
|
issues.push({
|
|
1872
2020
|
severity: "error",
|
|
1873
|
-
code: "
|
|
1874
|
-
rule: "broken-
|
|
1875
|
-
message: `Flow '${flow.name}' references
|
|
1876
|
-
nodePath: `flows/${flow.name}`
|
|
2021
|
+
code: "E007",
|
|
2022
|
+
rule: "broken-aspect-ref",
|
|
2023
|
+
message: `Flow '${flow.name}' references aspect '${aspectId}' but no aspect with that id exists in aspects/`
|
|
1877
2024
|
});
|
|
1878
2025
|
}
|
|
1879
2026
|
}
|
|
1880
2027
|
}
|
|
1881
2028
|
return issues;
|
|
1882
2029
|
}
|
|
1883
|
-
function
|
|
2030
|
+
function checkInvalidArtifactConditions(graph) {
|
|
1884
2031
|
const issues = [];
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
});
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
return issues;
|
|
1901
|
-
}
|
|
1902
|
-
function checkScopeTagsDefined(graph) {
|
|
1903
|
-
const issues = [];
|
|
1904
|
-
const definedTags = new Set(graph.config.tags ?? []);
|
|
1905
|
-
for (const k of graph.knowledge) {
|
|
1906
|
-
if (typeof k.scope !== "object" || !("tags" in k.scope)) continue;
|
|
1907
|
-
for (const tag of k.scope.tags) {
|
|
1908
|
-
if (definedTags.has(tag)) continue;
|
|
1909
|
-
issues.push({
|
|
1910
|
-
severity: "error",
|
|
1911
|
-
code: "E008",
|
|
1912
|
-
rule: "broken-scope-ref",
|
|
1913
|
-
message: `Knowledge '${k.path}' scope references undefined tag '${tag}'`
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
return issues;
|
|
1918
|
-
}
|
|
1919
|
-
async function checkUnknownKnowledgeCategories(graph) {
|
|
1920
|
-
const issues = [];
|
|
1921
|
-
const categorySet = new Set((graph.config.knowledge_categories ?? []).map((c) => c.name));
|
|
1922
|
-
const knowledgeDir = path9.join(graph.rootPath, "knowledge");
|
|
1923
|
-
const existingDirs = /* @__PURE__ */ new Set();
|
|
1924
|
-
try {
|
|
1925
|
-
const entries = await readdir4(knowledgeDir, { withFileTypes: true });
|
|
1926
|
-
for (const e of entries) {
|
|
1927
|
-
if (!e.isDirectory()) continue;
|
|
1928
|
-
if (e.name.startsWith(".")) continue;
|
|
1929
|
-
existingDirs.add(e.name);
|
|
1930
|
-
if (!categorySet.has(e.name)) {
|
|
1931
|
-
issues.push({
|
|
1932
|
-
severity: "error",
|
|
1933
|
-
code: "E011",
|
|
1934
|
-
rule: "unknown-knowledge-category",
|
|
1935
|
-
message: `Directory knowledge/${e.name}/ does not match any config.knowledge_categories`
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
} catch {
|
|
1940
|
-
}
|
|
1941
|
-
for (const cat of graph.config.knowledge_categories ?? []) {
|
|
1942
|
-
if (!existingDirs.has(cat.name)) {
|
|
1943
|
-
issues.push({
|
|
1944
|
-
severity: "error",
|
|
1945
|
-
code: "E017",
|
|
1946
|
-
rule: "missing-knowledge-category-dir",
|
|
1947
|
-
message: `Category '${cat.name}' in config has no knowledge/${cat.name}/ directory`
|
|
1948
|
-
});
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
return issues;
|
|
1952
|
-
}
|
|
1953
|
-
function checkInvalidArtifactConditions(graph) {
|
|
1954
|
-
const issues = [];
|
|
1955
|
-
const definedTags = new Set(graph.config.tags ?? []);
|
|
1956
|
-
const artifacts = graph.config.artifacts ?? {};
|
|
1957
|
-
for (const [artifactName, config] of Object.entries(artifacts)) {
|
|
1958
|
-
const required = config.required;
|
|
1959
|
-
if (typeof required === "object" && required && "when" in required) {
|
|
1960
|
-
const when = required.when;
|
|
1961
|
-
if (when.startsWith("has_tag:")) {
|
|
1962
|
-
const tag = when.slice(8);
|
|
1963
|
-
if (!definedTags.has(tag)) {
|
|
2032
|
+
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
2033
|
+
const artifacts = graph.config.artifacts ?? {};
|
|
2034
|
+
for (const [artifactName, config] of Object.entries(artifacts)) {
|
|
2035
|
+
const required = config.required;
|
|
2036
|
+
if (typeof required === "object" && required && "when" in required) {
|
|
2037
|
+
const when = required.when;
|
|
2038
|
+
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
2039
|
+
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
2040
|
+
const aspectId = when.slice(prefix.length);
|
|
2041
|
+
if (!validAspectIds.has(aspectId)) {
|
|
1964
2042
|
issues.push({
|
|
1965
2043
|
severity: "error",
|
|
1966
2044
|
code: "E013",
|
|
1967
2045
|
rule: "invalid-artifact-condition",
|
|
1968
|
-
message: `Artifact '${artifactName}' condition
|
|
2046
|
+
message: `Artifact '${artifactName}' condition has_aspect:${aspectId} has no corresponding aspect in aspects/`
|
|
1969
2047
|
});
|
|
1970
2048
|
}
|
|
1971
2049
|
}
|
|
@@ -1983,7 +2061,7 @@ async function checkShallowArtifacts(graph) {
|
|
|
1983
2061
|
severity: "warning",
|
|
1984
2062
|
code: "W002",
|
|
1985
2063
|
rule: "shallow-artifact",
|
|
1986
|
-
message: `Artifact '${art.filename}' is below minimum length (${art.content.length} < ${minLen})`,
|
|
2064
|
+
message: `Artifact '${art.filename}' is below minimum length (${art.content.trim().length} < ${minLen})`,
|
|
1987
2065
|
nodePath
|
|
1988
2066
|
});
|
|
1989
2067
|
}
|
|
@@ -1991,99 +2069,6 @@ async function checkShallowArtifacts(graph) {
|
|
|
1991
2069
|
}
|
|
1992
2070
|
return issues;
|
|
1993
2071
|
}
|
|
1994
|
-
async function checkUnreachableKnowledge(graph) {
|
|
1995
|
-
const issues = [];
|
|
1996
|
-
const nodePaths = new Set(graph.nodes.keys());
|
|
1997
|
-
const nodeTags = /* @__PURE__ */ new Map();
|
|
1998
|
-
for (const [p, n] of graph.nodes) {
|
|
1999
|
-
nodeTags.set(p, new Set(n.meta.tags ?? []));
|
|
2000
|
-
}
|
|
2001
|
-
const knowledgeReachable = /* @__PURE__ */ new Set();
|
|
2002
|
-
for (const k of graph.knowledge) {
|
|
2003
|
-
if (k.scope === "global") {
|
|
2004
|
-
knowledgeReachable.add(k.path);
|
|
2005
|
-
continue;
|
|
2006
|
-
}
|
|
2007
|
-
if (typeof k.scope === "object" && "tags" in k.scope) {
|
|
2008
|
-
for (const [, tags] of nodeTags) {
|
|
2009
|
-
if (k.scope.tags.some((t) => tags.has(t))) {
|
|
2010
|
-
knowledgeReachable.add(k.path);
|
|
2011
|
-
break;
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
if (typeof k.scope === "object" && "nodes" in k.scope) {
|
|
2016
|
-
if (k.scope.nodes.some((n) => nodePaths.has(n))) {
|
|
2017
|
-
knowledgeReachable.add(k.path);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
for (const [, node] of graph.nodes) {
|
|
2022
|
-
for (const kPath of node.meta.knowledge ?? []) {
|
|
2023
|
-
const k = graph.knowledge.find(
|
|
2024
|
-
(i) => i.path === kPath || i.path === kPath.replace(/\/$/, "")
|
|
2025
|
-
);
|
|
2026
|
-
if (k) knowledgeReachable.add(k.path);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
for (const flow of graph.flows) {
|
|
2030
|
-
for (const kPath of flow.knowledge ?? []) {
|
|
2031
|
-
const k = graph.knowledge.find(
|
|
2032
|
-
(i) => i.path === kPath || i.path === kPath.replace(/\/$/, "")
|
|
2033
|
-
);
|
|
2034
|
-
if (k) knowledgeReachable.add(k.path);
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
for (const k of graph.knowledge) {
|
|
2038
|
-
if (!knowledgeReachable.has(k.path)) {
|
|
2039
|
-
issues.push({
|
|
2040
|
-
severity: "warning",
|
|
2041
|
-
code: "W003",
|
|
2042
|
-
rule: "unreachable-knowledge",
|
|
2043
|
-
message: `Knowledge '${k.path}' does not reach any context package`
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
return issues;
|
|
2048
|
-
}
|
|
2049
|
-
async function checkMissingPatternExamples(graph) {
|
|
2050
|
-
const issues = [];
|
|
2051
|
-
const hasPatterns = (graph.config.knowledge_categories ?? []).some((c) => c.name === "patterns");
|
|
2052
|
-
if (!hasPatterns) return issues;
|
|
2053
|
-
const patternsDir = path9.join(graph.rootPath, "knowledge", "patterns");
|
|
2054
|
-
try {
|
|
2055
|
-
const entries = await readdir4(patternsDir, { withFileTypes: true });
|
|
2056
|
-
const exampleExtensions = /* @__PURE__ */ new Set([
|
|
2057
|
-
".ts",
|
|
2058
|
-
".js",
|
|
2059
|
-
".tsx",
|
|
2060
|
-
".jsx",
|
|
2061
|
-
".py",
|
|
2062
|
-
".go",
|
|
2063
|
-
".rs",
|
|
2064
|
-
".java",
|
|
2065
|
-
".kt"
|
|
2066
|
-
]);
|
|
2067
|
-
for (const e of entries) {
|
|
2068
|
-
if (!e.isDirectory()) continue;
|
|
2069
|
-
const itemDir = path9.join(patternsDir, e.name);
|
|
2070
|
-
const itemEntries = await readdir4(itemDir, { withFileTypes: true });
|
|
2071
|
-
const hasExample = itemEntries.some(
|
|
2072
|
-
(f) => f.isFile() && f.name !== "knowledge.yaml" && (f.name.startsWith("example") || exampleExtensions.has(path9.extname(f.name).toLowerCase()))
|
|
2073
|
-
);
|
|
2074
|
-
if (!hasExample) {
|
|
2075
|
-
issues.push({
|
|
2076
|
-
severity: "warning",
|
|
2077
|
-
code: "W004",
|
|
2078
|
-
rule: "missing-example",
|
|
2079
|
-
message: `Pattern 'patterns/${e.name}' has no example file`
|
|
2080
|
-
});
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
} catch {
|
|
2084
|
-
}
|
|
2085
|
-
return issues;
|
|
2086
|
-
}
|
|
2087
2072
|
function checkHighFanOut(graph) {
|
|
2088
2073
|
const issues = [];
|
|
2089
2074
|
const maxRel = graph.config.quality?.max_direct_relations ?? 10;
|
|
@@ -2101,57 +2086,6 @@ function checkHighFanOut(graph) {
|
|
|
2101
2086
|
}
|
|
2102
2087
|
return issues;
|
|
2103
2088
|
}
|
|
2104
|
-
function getNodesInScope(k, graph) {
|
|
2105
|
-
if (k.scope === "global") {
|
|
2106
|
-
return [...graph.nodes.keys()];
|
|
2107
|
-
}
|
|
2108
|
-
if (typeof k.scope === "object" && "nodes" in k.scope && k.scope.nodes) {
|
|
2109
|
-
return k.scope.nodes.filter((p) => graph.nodes.has(p));
|
|
2110
|
-
}
|
|
2111
|
-
if (typeof k.scope === "object" && "tags" in k.scope && k.scope.tags) {
|
|
2112
|
-
const tagSet = new Set(k.scope.tags);
|
|
2113
|
-
return [...graph.nodes.keys()].filter((p) => {
|
|
2114
|
-
const node = graph.nodes.get(p);
|
|
2115
|
-
return (node.meta.tags ?? []).some((t) => tagSet.has(t));
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
return [];
|
|
2119
|
-
}
|
|
2120
|
-
async function checkStaleKnowledge(graph) {
|
|
2121
|
-
const issues = [];
|
|
2122
|
-
const stalenessDays = graph.config.quality?.knowledge_staleness_days ?? 90;
|
|
2123
|
-
const projectRoot = path9.dirname(graph.rootPath);
|
|
2124
|
-
const yggRel = path9.relative(projectRoot, graph.rootPath).replace(/\\/g, "/") || ".yggdrasil";
|
|
2125
|
-
for (const k of graph.knowledge) {
|
|
2126
|
-
const scopeNodes = getNodesInScope(k, graph);
|
|
2127
|
-
if (scopeNodes.length === 0) continue;
|
|
2128
|
-
const kPath = `${yggRel}/knowledge/${k.path}`;
|
|
2129
|
-
const tK = getLastCommitTimestamp(projectRoot, kPath);
|
|
2130
|
-
if (tK === null) continue;
|
|
2131
|
-
let maxTp = 0;
|
|
2132
|
-
let latestNode = "";
|
|
2133
|
-
for (const nodePath of scopeNodes) {
|
|
2134
|
-
const nodePathRel = `${yggRel}/model/${nodePath}`;
|
|
2135
|
-
const tP = getLastCommitTimestamp(projectRoot, nodePathRel);
|
|
2136
|
-
if (tP !== null && tP > maxTp) {
|
|
2137
|
-
maxTp = tP;
|
|
2138
|
-
latestNode = nodePath;
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
if (maxTp === 0) continue;
|
|
2142
|
-
const diffDays = (maxTp - tK) / (60 * 60 * 24);
|
|
2143
|
-
if (diffDays > stalenessDays) {
|
|
2144
|
-
issues.push({
|
|
2145
|
-
severity: "warning",
|
|
2146
|
-
code: "W008",
|
|
2147
|
-
rule: "stale-knowledge",
|
|
2148
|
-
message: `Knowledge '${k.path}' may be stale: node '${latestNode}' modified ${Math.floor(diffDays)} days later (Git commits)`,
|
|
2149
|
-
nodePath: latestNode
|
|
2150
|
-
});
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
return issues;
|
|
2154
|
-
}
|
|
2155
2089
|
function checkUnpairedEvents(graph) {
|
|
2156
2090
|
const issues = [];
|
|
2157
2091
|
const emitsTo = /* @__PURE__ */ new Map();
|
|
@@ -2200,7 +2134,7 @@ function checkUnpairedEvents(graph) {
|
|
|
2200
2134
|
}
|
|
2201
2135
|
return issues;
|
|
2202
2136
|
}
|
|
2203
|
-
var REQUIRED_SCHEMAS = ["node", "aspect", "flow"
|
|
2137
|
+
var REQUIRED_SCHEMAS = ["node", "aspect", "flow"];
|
|
2204
2138
|
function checkSchemas(graph) {
|
|
2205
2139
|
const issues = [];
|
|
2206
2140
|
const present = new Set(graph.schemas.map((s) => s.schemaType));
|
|
@@ -2210,7 +2144,7 @@ function checkSchemas(graph) {
|
|
|
2210
2144
|
severity: "warning",
|
|
2211
2145
|
code: "W010",
|
|
2212
2146
|
rule: "missing-schema",
|
|
2213
|
-
message: `Schema '${required}.yaml' missing from .yggdrasil/
|
|
2147
|
+
message: `Schema '${required}.yaml' missing from .yggdrasil/schemas/`
|
|
2214
2148
|
});
|
|
2215
2149
|
}
|
|
2216
2150
|
}
|
|
@@ -2224,16 +2158,27 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2224
2158
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
2225
2159
|
const dirName = path9.basename(dirPath);
|
|
2226
2160
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
2227
|
-
const
|
|
2161
|
+
const hasFiles = entries.some((e) => e.isFile());
|
|
2162
|
+
const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
|
|
2228
2163
|
const graphPath = segments.join("/");
|
|
2229
|
-
if (
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2164
|
+
if (!hasNodeYaml && graphPath !== "") {
|
|
2165
|
+
if (hasFiles) {
|
|
2166
|
+
issues.push({
|
|
2167
|
+
severity: "error",
|
|
2168
|
+
code: "E015",
|
|
2169
|
+
rule: "missing-node-yaml",
|
|
2170
|
+
message: `Directory '${graphPath}' has files but no node.yaml`,
|
|
2171
|
+
nodePath: graphPath
|
|
2172
|
+
});
|
|
2173
|
+
} else if (hasSubdirs) {
|
|
2174
|
+
issues.push({
|
|
2175
|
+
severity: "warning",
|
|
2176
|
+
code: "W013",
|
|
2177
|
+
rule: "directory-without-node",
|
|
2178
|
+
message: `Directory '${graphPath}' has subdirectories but no node.yaml \u2014 consider creating a node`,
|
|
2179
|
+
nodePath: graphPath
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2237
2182
|
}
|
|
2238
2183
|
for (const entry of entries) {
|
|
2239
2184
|
if (!entry.isDirectory()) continue;
|
|
@@ -2255,26 +2200,26 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2255
2200
|
}
|
|
2256
2201
|
async function checkContextBudget(graph) {
|
|
2257
2202
|
const issues = [];
|
|
2258
|
-
const warningThreshold = graph.config.quality?.context_budget.warning ??
|
|
2259
|
-
const errorThreshold = graph.config.quality?.context_budget.error ??
|
|
2203
|
+
const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
|
|
2204
|
+
const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
|
|
2260
2205
|
for (const [nodePath, node] of graph.nodes) {
|
|
2261
2206
|
if (node.meta.blackbox) continue;
|
|
2262
2207
|
try {
|
|
2263
|
-
const
|
|
2264
|
-
if (
|
|
2208
|
+
const pkg2 = await buildContext(graph, nodePath);
|
|
2209
|
+
if (pkg2.tokenCount >= errorThreshold) {
|
|
2265
2210
|
issues.push({
|
|
2266
2211
|
severity: "warning",
|
|
2267
2212
|
code: "W006",
|
|
2268
2213
|
rule: "budget-error",
|
|
2269
|
-
message: `Context is ${
|
|
2214
|
+
message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (error threshold: ${errorThreshold.toLocaleString()}) \u2014 blocks materialization, node must be split`,
|
|
2270
2215
|
nodePath
|
|
2271
2216
|
});
|
|
2272
|
-
} else if (
|
|
2217
|
+
} else if (pkg2.tokenCount >= warningThreshold) {
|
|
2273
2218
|
issues.push({
|
|
2274
2219
|
severity: "warning",
|
|
2275
2220
|
code: "W005",
|
|
2276
2221
|
rule: "budget-warning",
|
|
2277
|
-
message: `Context is ${
|
|
2222
|
+
message: `Context is ${pkg2.tokenCount.toLocaleString()} tokens (warning threshold: ${warningThreshold.toLocaleString()}). Consider splitting the node or reducing dependencies.`,
|
|
2278
2223
|
nodePath
|
|
2279
2224
|
});
|
|
2280
2225
|
}
|
|
@@ -2284,75 +2229,133 @@ async function checkContextBudget(graph) {
|
|
|
2284
2229
|
return issues;
|
|
2285
2230
|
}
|
|
2286
2231
|
|
|
2287
|
-
// src/formatters/
|
|
2288
|
-
function
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2232
|
+
// src/formatters/context-text.ts
|
|
2233
|
+
function escapeAttr(val) {
|
|
2234
|
+
return val.replace(/"/g, """);
|
|
2235
|
+
}
|
|
2236
|
+
function formatLayer(layer) {
|
|
2237
|
+
switch (layer.type) {
|
|
2238
|
+
case "global":
|
|
2239
|
+
return `<global>
|
|
2240
|
+
${layer.content}
|
|
2241
|
+
</global>`;
|
|
2242
|
+
case "hierarchy": {
|
|
2243
|
+
const pathMatch = layer.label.match(/\((.+)\/\)/);
|
|
2244
|
+
const pathAttr = pathMatch ? ` path="${escapeAttr(pathMatch[1])}"` : "";
|
|
2245
|
+
const aspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
|
|
2246
|
+
return `<hierarchy${pathAttr}${aspectsAttr}>
|
|
2247
|
+
${layer.content}
|
|
2248
|
+
</hierarchy>`;
|
|
2249
|
+
}
|
|
2250
|
+
case "own": {
|
|
2251
|
+
if (layer.label === "Materialization Target") {
|
|
2252
|
+
return `<materialization-target paths="${escapeAttr(layer.content)}" />`;
|
|
2253
|
+
}
|
|
2254
|
+
const ownAspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
|
|
2255
|
+
return `<own-artifacts${ownAspectsAttr}>
|
|
2256
|
+
${layer.content}
|
|
2257
|
+
</own-artifacts>`;
|
|
2258
|
+
}
|
|
2259
|
+
case "aspects": {
|
|
2260
|
+
const nameMatch = layer.label.match(/^(.+?) \(aspect: (.+)\)$/);
|
|
2261
|
+
const name = nameMatch ? escapeAttr(nameMatch[1]) : "";
|
|
2262
|
+
const id = nameMatch ? escapeAttr(nameMatch[2]) : "";
|
|
2263
|
+
return `<aspect name="${name}" id="${id}">
|
|
2264
|
+
${layer.content}
|
|
2265
|
+
</aspect>`;
|
|
2266
|
+
}
|
|
2267
|
+
case "relational": {
|
|
2268
|
+
const attrs = layer.attrs ?? {};
|
|
2269
|
+
const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
|
|
2270
|
+
const tagName = attrs.type && ["emits", "listens"].includes(attrs.type) ? "event" : "dependency";
|
|
2271
|
+
return `<${tagName}${attrStr}>
|
|
2272
|
+
${layer.content}
|
|
2273
|
+
</${tagName}>`;
|
|
2274
|
+
}
|
|
2275
|
+
case "flows": {
|
|
2276
|
+
const flowName = layer.label.replace(/^Flow: /, "").trim();
|
|
2277
|
+
const flowAspectsAttr = layer.attrs?.aspects ? ` aspects="${escapeAttr(layer.attrs.aspects)}"` : "";
|
|
2278
|
+
return `<flow name="${escapeAttr(flowName)}"${flowAspectsAttr}>
|
|
2279
|
+
${layer.content}
|
|
2280
|
+
</flow>`;
|
|
2281
|
+
}
|
|
2282
|
+
default:
|
|
2283
|
+
return layer.content;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
function formatContextText(pkg2) {
|
|
2287
|
+
const attrs = [
|
|
2288
|
+
`node-path="${escapeAttr(pkg2.nodePath)}"`,
|
|
2289
|
+
`node-name="${escapeAttr(pkg2.nodeName)}"`,
|
|
2290
|
+
`token-count="${pkg2.tokenCount}"`
|
|
2291
|
+
].join(" ");
|
|
2292
|
+
let out = `<context-package ${attrs}>
|
|
2303
2293
|
|
|
2304
2294
|
`;
|
|
2295
|
+
for (const section of pkg2.sections) {
|
|
2305
2296
|
for (const layer of section.layers) {
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
`;
|
|
2309
|
-
md += layer.content;
|
|
2310
|
-
md += `
|
|
2311
|
-
|
|
2312
|
-
`;
|
|
2297
|
+
out += formatLayer(layer) + "\n\n";
|
|
2313
2298
|
}
|
|
2314
|
-
md += `---
|
|
2315
|
-
|
|
2316
|
-
`;
|
|
2317
2299
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
md += `Layers: ${pkg.layers.map((l) => l.type).join(", ")}
|
|
2321
|
-
`;
|
|
2322
|
-
return md;
|
|
2300
|
+
out += "</context-package>";
|
|
2301
|
+
return out;
|
|
2323
2302
|
}
|
|
2324
2303
|
|
|
2325
2304
|
// src/cli/build-context.ts
|
|
2305
|
+
function collectRelevantNodePaths(graph, nodePath) {
|
|
2306
|
+
const relevant = /* @__PURE__ */ new Set();
|
|
2307
|
+
const node = graph.nodes.get(nodePath);
|
|
2308
|
+
if (!node) return relevant;
|
|
2309
|
+
relevant.add(nodePath);
|
|
2310
|
+
for (const ancestor of collectAncestors(node)) {
|
|
2311
|
+
relevant.add(ancestor.path);
|
|
2312
|
+
}
|
|
2313
|
+
for (const rel of node.meta.relations ?? []) {
|
|
2314
|
+
relevant.add(rel.target);
|
|
2315
|
+
}
|
|
2316
|
+
return relevant;
|
|
2317
|
+
}
|
|
2326
2318
|
function registerBuildCommand(program2) {
|
|
2327
2319
|
program2.command("build-context").description("Assemble a context package for one node").requiredOption("--node <node-path>", "Node path relative to .yggdrasil/model/").action(async (options) => {
|
|
2328
2320
|
try {
|
|
2329
2321
|
const graph = await loadGraph(process.cwd());
|
|
2322
|
+
const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/$/, "");
|
|
2323
|
+
const relevantNodes = collectRelevantNodePaths(graph, nodePath);
|
|
2330
2324
|
const validationResult = await validate(graph, "all");
|
|
2331
|
-
const
|
|
2332
|
-
(issue) => issue.severity === "error"
|
|
2325
|
+
const relevantErrors = validationResult.issues.filter(
|
|
2326
|
+
(issue) => issue.severity === "error" && // Global errors (no nodePath) always block — e.g., E012 invalid config
|
|
2327
|
+
(!issue.nodePath || relevantNodes.has(issue.nodePath))
|
|
2333
2328
|
);
|
|
2334
|
-
if (
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
`
|
|
2338
|
-
|
|
2329
|
+
if (relevantErrors.length > 0) {
|
|
2330
|
+
const totalErrors = validationResult.issues.filter((i) => i.severity === "error").length;
|
|
2331
|
+
const skippedErrors = totalErrors - relevantErrors.length;
|
|
2332
|
+
let msg = `Error: build-context blocked by ${relevantErrors.length} error(s) affecting this node's context.
|
|
2333
|
+
`;
|
|
2334
|
+
if (skippedErrors > 0) {
|
|
2335
|
+
msg += `(${skippedErrors} unrelated error(s) in other nodes ignored.)
|
|
2336
|
+
`;
|
|
2337
|
+
}
|
|
2338
|
+
for (const err of relevantErrors) {
|
|
2339
|
+
const loc = err.nodePath ? `${err.nodePath}: ` : "";
|
|
2340
|
+
msg += ` ${err.code ?? ""} ${loc}${err.message}
|
|
2341
|
+
`;
|
|
2342
|
+
}
|
|
2343
|
+
process.stderr.write(msg);
|
|
2339
2344
|
process.exit(1);
|
|
2340
2345
|
}
|
|
2341
|
-
const
|
|
2342
|
-
const
|
|
2343
|
-
const
|
|
2344
|
-
const
|
|
2345
|
-
|
|
2346
|
-
let output = formatContextMarkdown(pkg);
|
|
2346
|
+
const pkg2 = await buildContext(graph, nodePath);
|
|
2347
|
+
const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
|
|
2348
|
+
const errorThreshold = graph.config.quality?.context_budget.error ?? 2e4;
|
|
2349
|
+
const budgetStatus = pkg2.tokenCount >= errorThreshold ? "error" : pkg2.tokenCount >= warningThreshold ? "warning" : "ok";
|
|
2350
|
+
let output = formatContextText(pkg2);
|
|
2347
2351
|
output += `Budget status: ${budgetStatus}
|
|
2348
2352
|
`;
|
|
2349
2353
|
process.stdout.write(output);
|
|
2350
2354
|
if (budgetStatus === "error") {
|
|
2351
2355
|
process.stderr.write(
|
|
2352
|
-
`
|
|
2356
|
+
`Warning: context package exceeds error budget (${pkg2.tokenCount} >= ${errorThreshold}). Consider splitting the node.
|
|
2353
2357
|
`
|
|
2354
2358
|
);
|
|
2355
|
-
process.exit(1);
|
|
2356
2359
|
}
|
|
2357
2360
|
} catch (error) {
|
|
2358
2361
|
process.stderr.write(`Error: ${error.message}
|
|
@@ -2368,7 +2371,8 @@ function registerValidateCommand(program2) {
|
|
|
2368
2371
|
program2.command("validate").description("Validate graph structural integrity and completeness signals").option("--scope <scope>", "Scope: all or node-path (default: all)", "all").action(async (options) => {
|
|
2369
2372
|
try {
|
|
2370
2373
|
const graph = await loadGraph(process.cwd(), { tolerateInvalidConfig: true });
|
|
2371
|
-
const
|
|
2374
|
+
const rawScope = (options.scope ?? "all").trim() || "all";
|
|
2375
|
+
const scope = rawScope === "all" ? "all" : rawScope.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2372
2376
|
const result = await validate(graph, scope);
|
|
2373
2377
|
process.stdout.write(`${result.nodesScanned} nodes scanned
|
|
2374
2378
|
|
|
@@ -2411,40 +2415,33 @@ import chalk2 from "chalk";
|
|
|
2411
2415
|
|
|
2412
2416
|
// src/io/drift-state-store.ts
|
|
2413
2417
|
import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
|
|
2414
|
-
import { parse as parseYaml7, stringify as stringifyYaml } from "yaml";
|
|
2415
2418
|
import path10 from "path";
|
|
2419
|
+
import { parse as yamlParse } from "yaml";
|
|
2416
2420
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2417
|
-
function getCanonicalHash(entry) {
|
|
2418
|
-
return typeof entry === "string" ? entry : entry.hash;
|
|
2419
|
-
}
|
|
2420
|
-
function getFileHashes(entry) {
|
|
2421
|
-
return typeof entry === "object" ? entry.files : void 0;
|
|
2422
|
-
}
|
|
2423
2421
|
async function readDriftState(yggRoot) {
|
|
2424
|
-
const filePath = path10.join(yggRoot, DRIFT_STATE_FILE);
|
|
2425
2422
|
try {
|
|
2426
|
-
const content = await readFile11(
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2423
|
+
const content = await readFile11(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
|
|
2424
|
+
let raw;
|
|
2425
|
+
try {
|
|
2426
|
+
raw = JSON.parse(content);
|
|
2427
|
+
} catch {
|
|
2428
|
+
raw = yamlParse(content);
|
|
2429
|
+
}
|
|
2430
|
+
if (!raw || typeof raw !== "object") return {};
|
|
2431
|
+
const state = {};
|
|
2432
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2433
|
+
if (typeof value === "object" && value !== null && "hash" in value) {
|
|
2434
|
+
state[key] = value;
|
|
2436
2435
|
}
|
|
2437
|
-
return result;
|
|
2438
2436
|
}
|
|
2439
|
-
return
|
|
2437
|
+
return state;
|
|
2440
2438
|
} catch {
|
|
2441
2439
|
return {};
|
|
2442
2440
|
}
|
|
2443
2441
|
}
|
|
2444
2442
|
async function writeDriftState(yggRoot, state) {
|
|
2445
|
-
const
|
|
2446
|
-
|
|
2447
|
-
await writeFile3(filePath, content, "utf-8");
|
|
2443
|
+
const content = JSON.stringify(state);
|
|
2444
|
+
await writeFile3(path10.join(yggRoot, DRIFT_STATE_FILE), content, "utf-8");
|
|
2448
2445
|
}
|
|
2449
2446
|
|
|
2450
2447
|
// src/utils/hash.ts
|
|
@@ -2458,139 +2455,238 @@ async function hashFile(filePath) {
|
|
|
2458
2455
|
const content = await readFile12(filePath);
|
|
2459
2456
|
return createHash("sha256").update(content).digest("hex");
|
|
2460
2457
|
}
|
|
2461
|
-
async function
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
return
|
|
2458
|
+
async function loadRootGitignoreStack(projectRoot) {
|
|
2459
|
+
if (!projectRoot) return [];
|
|
2460
|
+
try {
|
|
2461
|
+
const content = await readFile12(path11.join(projectRoot, ".gitignore"), "utf-8");
|
|
2462
|
+
const matcher = ignoreFactory();
|
|
2463
|
+
matcher.add(content);
|
|
2464
|
+
return [{ basePath: projectRoot, matcher }];
|
|
2465
|
+
} catch {
|
|
2466
|
+
return [];
|
|
2470
2467
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
return hashString(digestInput);
|
|
2468
|
+
}
|
|
2469
|
+
function isIgnoredByStack(candidatePath, stack) {
|
|
2470
|
+
for (const { basePath, matcher } of stack) {
|
|
2471
|
+
const relativePath = path11.relative(basePath, candidatePath);
|
|
2472
|
+
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
2473
|
+
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
2478
2474
|
}
|
|
2479
|
-
|
|
2475
|
+
return false;
|
|
2480
2476
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
path: path11.relative(rootDirectoryPath, path11.join(absoluteChildPath, nestedEntry.path)),
|
|
2498
|
-
hash: nestedEntry.hash
|
|
2477
|
+
function hashString(content) {
|
|
2478
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2479
|
+
}
|
|
2480
|
+
async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes) {
|
|
2481
|
+
const fileHashes = {};
|
|
2482
|
+
const fileMtimes = {};
|
|
2483
|
+
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2484
|
+
const allFiles = [];
|
|
2485
|
+
for (const tf of trackedFiles) {
|
|
2486
|
+
const absPath = path11.join(projectRoot, tf.path);
|
|
2487
|
+
try {
|
|
2488
|
+
const st = await stat3(absPath);
|
|
2489
|
+
if (st.isDirectory()) {
|
|
2490
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2491
|
+
projectRoot,
|
|
2492
|
+
gitignoreStack
|
|
2499
2493
|
});
|
|
2494
|
+
for (const entry of dirEntries) {
|
|
2495
|
+
allFiles.push({
|
|
2496
|
+
relPath: path11.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
2497
|
+
absPath: entry.absPath,
|
|
2498
|
+
mtimeMs: entry.mtimeMs
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
} else {
|
|
2502
|
+
allFiles.push({ relPath: tf.path, absPath, mtimeMs: st.mtimeMs });
|
|
2500
2503
|
}
|
|
2504
|
+
} catch {
|
|
2501
2505
|
continue;
|
|
2502
2506
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2507
|
+
}
|
|
2508
|
+
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => entry.relPath === prefix || entry.relPath.startsWith(prefix + "/"))) : allFiles;
|
|
2509
|
+
const dirty = [];
|
|
2510
|
+
for (const entry of filtered) {
|
|
2511
|
+
const storedMtime = storedFileData?.mtimes[entry.relPath];
|
|
2512
|
+
const storedHash = storedFileData?.hashes[entry.relPath];
|
|
2513
|
+
if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
|
|
2514
|
+
fileHashes[entry.relPath] = storedHash;
|
|
2515
|
+
} else {
|
|
2516
|
+
dirty.push(entry);
|
|
2505
2517
|
}
|
|
2506
|
-
|
|
2507
|
-
path: path11.relative(rootDirectoryPath, absoluteChildPath),
|
|
2508
|
-
hash: await hashFile(absoluteChildPath)
|
|
2509
|
-
});
|
|
2518
|
+
fileMtimes[entry.relPath] = entry.mtimeMs;
|
|
2510
2519
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2520
|
+
const BATCH_SIZE = 256;
|
|
2521
|
+
for (let i = 0; i < dirty.length; i += BATCH_SIZE) {
|
|
2522
|
+
const batch = dirty.slice(i, i + BATCH_SIZE);
|
|
2523
|
+
const hashes = await Promise.all(batch.map((e) => hashFile(e.absPath)));
|
|
2524
|
+
for (let j = 0; j < batch.length; j++) {
|
|
2525
|
+
fileHashes[batch[j].relPath] = hashes[j];
|
|
2526
|
+
}
|
|
2516
2527
|
}
|
|
2528
|
+
const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
|
|
2529
|
+
const digest = sorted.map(([p, h]) => `${p}:${h}`).join("\n");
|
|
2530
|
+
const canonicalHash = hashString(digest);
|
|
2531
|
+
return { canonicalHash, fileHashes, fileMtimes };
|
|
2532
|
+
}
|
|
2533
|
+
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
2534
|
+
let stack = options.gitignoreStack ?? [];
|
|
2517
2535
|
try {
|
|
2518
|
-
const
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
matcher
|
|
2522
|
-
return matcher;
|
|
2536
|
+
const localContent = await readFile12(path11.join(directoryPath, ".gitignore"), "utf-8");
|
|
2537
|
+
const localMatcher = ignoreFactory();
|
|
2538
|
+
localMatcher.add(localContent);
|
|
2539
|
+
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
2523
2540
|
} catch {
|
|
2524
|
-
return void 0;
|
|
2525
2541
|
}
|
|
2542
|
+
const entries = await readdir5(directoryPath, { withFileTypes: true });
|
|
2543
|
+
const dirs = [];
|
|
2544
|
+
const files = [];
|
|
2545
|
+
for (const entry of entries) {
|
|
2546
|
+
const absoluteChildPath = path11.join(directoryPath, entry.name);
|
|
2547
|
+
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
2548
|
+
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
2549
|
+
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
2550
|
+
}
|
|
2551
|
+
const [dirResults, fileStats] = await Promise.all([
|
|
2552
|
+
Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
|
|
2553
|
+
projectRoot: options.projectRoot,
|
|
2554
|
+
gitignoreStack: stack
|
|
2555
|
+
}))),
|
|
2556
|
+
Promise.all(files.map(async (f) => {
|
|
2557
|
+
const fileStat = await stat3(f);
|
|
2558
|
+
return {
|
|
2559
|
+
relPath: path11.relative(rootDirectoryPath, f),
|
|
2560
|
+
absPath: f,
|
|
2561
|
+
mtimeMs: fileStat.mtimeMs
|
|
2562
|
+
};
|
|
2563
|
+
}))
|
|
2564
|
+
]);
|
|
2565
|
+
const result = [];
|
|
2566
|
+
for (const nested of dirResults) result.push(...nested);
|
|
2567
|
+
result.push(...fileStats);
|
|
2568
|
+
return result;
|
|
2526
2569
|
}
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2570
|
+
|
|
2571
|
+
// src/core/context-files.ts
|
|
2572
|
+
import path12 from "path";
|
|
2573
|
+
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2574
|
+
function collectTrackedFiles(node, graph) {
|
|
2575
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2576
|
+
const result = [];
|
|
2577
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2578
|
+
const yggPrefix = path12.relative(projectRoot, graph.rootPath);
|
|
2579
|
+
const yggPrefixNormalized = yggPrefix.split(path12.sep).join("/");
|
|
2580
|
+
const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
|
|
2581
|
+
function addFile(filePath, category) {
|
|
2582
|
+
if (seen.has(filePath)) return;
|
|
2583
|
+
seen.add(filePath);
|
|
2584
|
+
result.push({ path: filePath, category });
|
|
2530
2585
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
return false;
|
|
2586
|
+
function graphPath(...segments) {
|
|
2587
|
+
return [yggPrefixNormalized, ...segments].join("/");
|
|
2534
2588
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
async function perFileHashes(projectRoot, mapping) {
|
|
2541
|
-
const root = path11.resolve(projectRoot);
|
|
2542
|
-
const paths = mapping.paths ?? [];
|
|
2543
|
-
if (paths.length === 0) return [];
|
|
2544
|
-
const result = [];
|
|
2545
|
-
const gitignoreMatcher = await loadGitignoreMatcher(root);
|
|
2546
|
-
for (const p of paths) {
|
|
2547
|
-
const absPath = path11.join(root, p);
|
|
2548
|
-
const st = await stat3(absPath);
|
|
2549
|
-
if (st.isFile()) {
|
|
2550
|
-
result.push({ path: p, hash: await hashFile(absPath) });
|
|
2551
|
-
} else if (st.isDirectory()) {
|
|
2552
|
-
const hashes = await collectDirectoryFileHashes(absPath, absPath, {
|
|
2553
|
-
projectRoot: root,
|
|
2554
|
-
gitignoreMatcher
|
|
2555
|
-
});
|
|
2556
|
-
for (const h of hashes) {
|
|
2557
|
-
result.push({
|
|
2558
|
-
path: path11.join(p, h.path).split(path11.sep).join("/"),
|
|
2559
|
-
hash: h.hash
|
|
2560
|
-
});
|
|
2589
|
+
function addNodeFiles(n) {
|
|
2590
|
+
addFile(graphPath("model", n.path, "node.yaml"), "graph");
|
|
2591
|
+
for (const art of n.artifacts) {
|
|
2592
|
+
if (configArtifactKeys.has(art.filename)) {
|
|
2593
|
+
addFile(graphPath("model", n.path, art.filename), "graph");
|
|
2561
2594
|
}
|
|
2562
2595
|
}
|
|
2563
2596
|
}
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
const
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
} else if (st.isDirectory()) {
|
|
2577
|
-
const dirHash = await hashPath(absPath, { projectRoot: root });
|
|
2578
|
-
pairs.push({ path: p, hash: dirHash });
|
|
2597
|
+
addNodeFiles(node);
|
|
2598
|
+
const ancestors = collectAncestors(node);
|
|
2599
|
+
for (const ancestor of ancestors) {
|
|
2600
|
+
addNodeFiles(ancestor);
|
|
2601
|
+
}
|
|
2602
|
+
const allAspectIds = /* @__PURE__ */ new Set();
|
|
2603
|
+
for (const id of node.meta.aspects ?? []) {
|
|
2604
|
+
allAspectIds.add(id);
|
|
2605
|
+
}
|
|
2606
|
+
for (const ancestor of ancestors) {
|
|
2607
|
+
for (const id of ancestor.meta.aspects ?? []) {
|
|
2608
|
+
allAspectIds.add(id);
|
|
2579
2609
|
}
|
|
2580
2610
|
}
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2611
|
+
const participatingFlows = collectParticipatingFlows2(graph, node, ancestors);
|
|
2612
|
+
for (const flow of participatingFlows) {
|
|
2613
|
+
for (const id of flow.aspects ?? []) {
|
|
2614
|
+
allAspectIds.add(id);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
|
|
2618
|
+
for (const aspect of resolvedAspects) {
|
|
2619
|
+
addFile(graphPath("aspects", aspect.id, "aspect.yaml"), "graph");
|
|
2620
|
+
for (const art of aspect.artifacts) {
|
|
2621
|
+
addFile(graphPath("aspects", aspect.id, art.filename), "graph");
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
for (const relation of node.meta.relations ?? []) {
|
|
2625
|
+
if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
|
|
2626
|
+
const target = graph.nodes.get(relation.target);
|
|
2627
|
+
if (!target) continue;
|
|
2628
|
+
const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.structural_context).map(([filename]) => filename);
|
|
2629
|
+
const structuralArts = structuralFilenames.filter(
|
|
2630
|
+
(filename) => target.artifacts.some((a) => a.filename === filename)
|
|
2631
|
+
);
|
|
2632
|
+
if (structuralArts.length > 0) {
|
|
2633
|
+
for (const filename of structuralArts) {
|
|
2634
|
+
addFile(graphPath("model", target.path, filename), "graph");
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
for (const art of target.artifacts) {
|
|
2638
|
+
if (configArtifactKeys.has(art.filename)) {
|
|
2639
|
+
addFile(graphPath("model", target.path, art.filename), "graph");
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
for (const flow of participatingFlows) {
|
|
2645
|
+
addFile(graphPath("flows", flow.path, "flow.yaml"), "graph");
|
|
2646
|
+
for (const art of flow.artifacts) {
|
|
2647
|
+
addFile(graphPath("flows", flow.path, art.filename), "graph");
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2651
|
+
for (const p of mappingPaths) {
|
|
2652
|
+
addFile(p, "source");
|
|
2653
|
+
}
|
|
2654
|
+
return result;
|
|
2655
|
+
}
|
|
2656
|
+
function collectParticipatingFlows2(graph, node, ancestors) {
|
|
2657
|
+
const paths = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
|
|
2658
|
+
return graph.flows.filter((f) => f.nodes.some((n) => paths.has(n)));
|
|
2583
2659
|
}
|
|
2584
2660
|
|
|
2585
2661
|
// src/core/drift-detector.ts
|
|
2586
2662
|
import { access } from "fs/promises";
|
|
2587
|
-
import
|
|
2663
|
+
import path13 from "path";
|
|
2664
|
+
function getChildMappingExclusions(graph, nodePath) {
|
|
2665
|
+
const node = graph.nodes.get(nodePath);
|
|
2666
|
+
if (!node) return [];
|
|
2667
|
+
const parentMappings = normalizeMappingPaths(node.meta.mapping);
|
|
2668
|
+
if (parentMappings.length === 0) return [];
|
|
2669
|
+
const exclusions = [];
|
|
2670
|
+
for (const [childPath, childNode] of graph.nodes) {
|
|
2671
|
+
if (childPath === nodePath) continue;
|
|
2672
|
+
if (!childPath.startsWith(nodePath + "/")) continue;
|
|
2673
|
+
const childMappings = normalizeMappingPaths(childNode.meta.mapping);
|
|
2674
|
+
for (const cm of childMappings) {
|
|
2675
|
+
for (const pm of parentMappings) {
|
|
2676
|
+
if (cm === pm || cm.startsWith(pm + "/")) {
|
|
2677
|
+
exclusions.push(cm);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
return exclusions;
|
|
2683
|
+
}
|
|
2588
2684
|
async function detectDrift(graph, filterNodePath) {
|
|
2589
|
-
const projectRoot =
|
|
2685
|
+
const projectRoot = path13.dirname(graph.rootPath);
|
|
2590
2686
|
const driftState = await readDriftState(graph.rootPath);
|
|
2591
2687
|
const entries = [];
|
|
2592
2688
|
for (const [nodePath, node] of graph.nodes) {
|
|
2593
|
-
if (filterNodePath && nodePath !== filterNodePath) continue;
|
|
2689
|
+
if (filterNodePath && nodePath !== filterNodePath && !nodePath.startsWith(filterNodePath + "/")) continue;
|
|
2594
2690
|
const mapping = node.meta.mapping;
|
|
2595
2691
|
if (!mapping) continue;
|
|
2596
2692
|
const mappingPaths = normalizeMappingPaths(mapping);
|
|
@@ -2600,67 +2696,82 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
2600
2696
|
const allMissing = await allPathsMissing(projectRoot, mappingPaths);
|
|
2601
2697
|
entries.push({
|
|
2602
2698
|
nodePath,
|
|
2603
|
-
|
|
2604
|
-
status: allMissing ? "unmaterialized" : "drift",
|
|
2699
|
+
status: allMissing ? "unmaterialized" : "source-drift",
|
|
2605
2700
|
details: allMissing ? "No drift state recorded, files do not exist" : "No drift state recorded, files exist (run drift-sync after materialization)"
|
|
2606
2701
|
});
|
|
2607
2702
|
continue;
|
|
2608
2703
|
}
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2704
|
+
const sourceFilesMissing = await allPathsMissing(projectRoot, mappingPaths);
|
|
2705
|
+
if (sourceFilesMissing) {
|
|
2706
|
+
entries.push({
|
|
2707
|
+
nodePath,
|
|
2708
|
+
status: "missing",
|
|
2709
|
+
details: "All source mapping paths are missing"
|
|
2710
|
+
});
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
const trackedFiles = collectTrackedFiles(node, graph);
|
|
2714
|
+
const excludePrefixes = getChildMappingExclusions(graph, nodePath);
|
|
2715
|
+
const storedFileData = storedEntry.files ? { hashes: storedEntry.files, mtimes: storedEntry.mtimes ?? {} } : void 0;
|
|
2716
|
+
const { canonicalHash, fileHashes } = await hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes);
|
|
2717
|
+
if (canonicalHash === storedEntry.hash) {
|
|
2718
|
+
entries.push({ nodePath, status: "ok" });
|
|
2719
|
+
continue;
|
|
2720
|
+
}
|
|
2721
|
+
const changedFiles = [];
|
|
2722
|
+
const storedFiles = storedEntry.files;
|
|
2723
|
+
for (const [filePath, hash] of Object.entries(fileHashes)) {
|
|
2724
|
+
const storedHash = storedFiles[filePath];
|
|
2725
|
+
if (!storedHash || storedHash !== hash) {
|
|
2726
|
+
changedFiles.push({
|
|
2727
|
+
filePath,
|
|
2728
|
+
category: categorizeFile(filePath, graph.rootPath, projectRoot)
|
|
2729
|
+
});
|
|
2622
2730
|
}
|
|
2623
|
-
} catch {
|
|
2624
|
-
status = "missing";
|
|
2625
|
-
details = "Mapped path(s) do not exist";
|
|
2626
2731
|
}
|
|
2627
|
-
|
|
2732
|
+
for (const storedPath of Object.keys(storedFiles)) {
|
|
2733
|
+
if (!(storedPath in fileHashes)) {
|
|
2734
|
+
changedFiles.push({
|
|
2735
|
+
filePath: `${storedPath} (deleted)`,
|
|
2736
|
+
category: categorizeFile(storedPath, graph.rootPath, projectRoot)
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const hasSourceChanges = changedFiles.some((f) => f.category === "source");
|
|
2741
|
+
const hasGraphChanges = changedFiles.some((f) => f.category === "graph");
|
|
2742
|
+
let status;
|
|
2743
|
+
if (hasSourceChanges && hasGraphChanges) {
|
|
2744
|
+
status = "full-drift";
|
|
2745
|
+
} else if (hasGraphChanges) {
|
|
2746
|
+
status = "graph-drift";
|
|
2747
|
+
} else if (hasSourceChanges) {
|
|
2748
|
+
status = "source-drift";
|
|
2749
|
+
} else {
|
|
2750
|
+
status = "source-drift";
|
|
2751
|
+
}
|
|
2752
|
+
const details = changedFiles.length > 0 ? `Changed files: ${changedFiles.map((f) => f.filePath).join(", ")}` : "File(s) modified since last sync";
|
|
2753
|
+
entries.push({ nodePath, status, details, changedFiles });
|
|
2628
2754
|
}
|
|
2629
2755
|
return {
|
|
2630
2756
|
entries,
|
|
2631
2757
|
totalChecked: entries.length,
|
|
2632
2758
|
okCount: entries.filter((e) => e.status === "ok").length,
|
|
2633
|
-
|
|
2759
|
+
sourceDriftCount: entries.filter((e) => e.status === "source-drift").length,
|
|
2760
|
+
graphDriftCount: entries.filter((e) => e.status === "graph-drift").length,
|
|
2761
|
+
fullDriftCount: entries.filter((e) => e.status === "full-drift").length,
|
|
2634
2762
|
missingCount: entries.filter((e) => e.status === "missing").length,
|
|
2635
2763
|
unmaterializedCount: entries.filter((e) => e.status === "unmaterialized").length
|
|
2636
2764
|
};
|
|
2637
2765
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
}
|
|
2644
|
-
const changed = [];
|
|
2645
|
-
const storedPaths = new Set(Object.keys(storedFileHashes));
|
|
2646
|
-
for (const { path: filePath, hash } of currentHashes) {
|
|
2647
|
-
const stored = storedFileHashes[filePath];
|
|
2648
|
-
if (!stored || stored !== hash) {
|
|
2649
|
-
changed.push(filePath);
|
|
2650
|
-
}
|
|
2651
|
-
storedPaths.delete(filePath);
|
|
2652
|
-
}
|
|
2653
|
-
for (const removed of storedPaths) {
|
|
2654
|
-
changed.push(`${removed} (deleted)`);
|
|
2655
|
-
}
|
|
2656
|
-
return changed.sort();
|
|
2657
|
-
} catch {
|
|
2658
|
-
return [];
|
|
2659
|
-
}
|
|
2766
|
+
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
2767
|
+
const yggPrefix = path13.relative(projectRoot, _rootPath);
|
|
2768
|
+
const normalizedPrefix = yggPrefix.split(path13.sep).join("/");
|
|
2769
|
+
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
2770
|
+
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
2660
2771
|
}
|
|
2661
2772
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
2662
2773
|
for (const mp of mappingPaths) {
|
|
2663
|
-
const absPath =
|
|
2774
|
+
const absPath = path13.join(projectRoot, mp);
|
|
2664
2775
|
try {
|
|
2665
2776
|
await access(absPath);
|
|
2666
2777
|
return false;
|
|
@@ -2670,82 +2781,53 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
2670
2781
|
return true;
|
|
2671
2782
|
}
|
|
2672
2783
|
async function syncDriftState(graph, nodePath) {
|
|
2673
|
-
const projectRoot =
|
|
2784
|
+
const projectRoot = path13.dirname(graph.rootPath);
|
|
2674
2785
|
const node = graph.nodes.get(nodePath);
|
|
2675
2786
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
const
|
|
2679
|
-
const
|
|
2680
|
-
const
|
|
2681
|
-
const
|
|
2682
|
-
const fileHashes = await
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2787
|
+
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
2788
|
+
const trackedFiles = collectTrackedFiles(node, graph);
|
|
2789
|
+
const excludePrefixes = getChildMappingExclusions(graph, nodePath);
|
|
2790
|
+
const existingState = await readDriftState(graph.rootPath);
|
|
2791
|
+
const existingEntry = existingState[nodePath];
|
|
2792
|
+
const storedFileData = existingEntry?.files ? { hashes: existingEntry.files, mtimes: existingEntry.mtimes ?? {} } : void 0;
|
|
2793
|
+
const { canonicalHash, fileHashes, fileMtimes } = await hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes);
|
|
2794
|
+
const previousHash = existingEntry?.hash;
|
|
2795
|
+
existingState[nodePath] = { hash: canonicalHash, files: fileHashes, mtimes: fileMtimes };
|
|
2796
|
+
for (const key of Object.keys(existingState)) {
|
|
2797
|
+
if (!graph.nodes.has(key)) {
|
|
2798
|
+
delete existingState[key];
|
|
2799
|
+
}
|
|
2686
2800
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
await writeDriftState(graph.rootPath, driftState);
|
|
2690
|
-
return { previousHash, currentHash };
|
|
2801
|
+
await writeDriftState(graph.rootPath, existingState);
|
|
2802
|
+
return { previousHash, currentHash: canonicalHash };
|
|
2691
2803
|
}
|
|
2692
2804
|
|
|
2693
2805
|
// src/cli/drift.ts
|
|
2694
2806
|
function registerDriftCommand(program2) {
|
|
2695
|
-
program2.command("drift").description("Detect
|
|
2807
|
+
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)").option("--limit <n>", "Maximum number of entries to show per section", parseInt).action(async (opts) => {
|
|
2696
2808
|
try {
|
|
2697
2809
|
const graph = await loadGraph(process.cwd());
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2810
|
+
const rawScope = (opts.scope ?? "all").trim() || "all";
|
|
2811
|
+
const scope = rawScope === "all" ? "all" : rawScope.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2812
|
+
if (scope !== "all") {
|
|
2813
|
+
const node = graph.nodes.get(scope);
|
|
2814
|
+
if (!node) {
|
|
2815
|
+
process.stderr.write(`Error: Node not found: ${scope}
|
|
2701
2816
|
`);
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
process.stderr.write(
|
|
2708
|
-
`Error: Node has no mapping (does not participate in drift detection): ${options.scope}
|
|
2709
|
-
`
|
|
2710
|
-
);
|
|
2711
|
-
process.exit(1);
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
const scopeNode = scope === "all" ? void 0 : scope;
|
|
2715
|
-
const report = await detectDrift(graph, scopeNode);
|
|
2716
|
-
process.stdout.write("Drift:\n");
|
|
2717
|
-
for (const entry of report.entries) {
|
|
2718
|
-
const paths = entry.mappingPaths.join(", ");
|
|
2719
|
-
switch (entry.status) {
|
|
2720
|
-
case "ok":
|
|
2721
|
-
process.stdout.write(chalk2.green(` ok ${entry.nodePath} -> ${paths}
|
|
2722
|
-
`));
|
|
2723
|
-
break;
|
|
2724
|
-
case "drift":
|
|
2725
|
-
process.stdout.write(chalk2.red(` drift ${entry.nodePath} -> ${paths}
|
|
2726
|
-
`));
|
|
2727
|
-
if (entry.details) process.stdout.write(` ${entry.details}
|
|
2817
|
+
process.exit(1);
|
|
2818
|
+
}
|
|
2819
|
+
const hasAnyMapping = node.meta.mapping || [...graph.nodes.entries()].some(([p, n]) => p.startsWith(scope + "/") && n.meta.mapping);
|
|
2820
|
+
if (!hasAnyMapping) {
|
|
2821
|
+
process.stderr.write(`Error: Node has no mapping: ${scope}
|
|
2728
2822
|
`);
|
|
2729
|
-
|
|
2730
|
-
case "missing":
|
|
2731
|
-
process.stdout.write(chalk2.yellow(` missing ${entry.nodePath} -> ${paths}
|
|
2732
|
-
`));
|
|
2733
|
-
break;
|
|
2734
|
-
case "unmaterialized":
|
|
2735
|
-
process.stdout.write(chalk2.dim(` unmat. ${entry.nodePath} -> ${paths}
|
|
2736
|
-
`));
|
|
2737
|
-
break;
|
|
2823
|
+
process.exit(1);
|
|
2738
2824
|
}
|
|
2739
2825
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
);
|
|
2745
|
-
if (report.driftCount > 0 || report.missingCount > 0 || report.unmaterializedCount > 0) {
|
|
2746
|
-
process.exit(1);
|
|
2747
|
-
}
|
|
2748
|
-
process.exit(0);
|
|
2826
|
+
const scopeNode = scope === "all" ? void 0 : scope;
|
|
2827
|
+
const report = await detectDrift(graph, scopeNode);
|
|
2828
|
+
printReport(report, opts.driftedOnly ?? false, opts.limit);
|
|
2829
|
+
const hasIssues = report.sourceDriftCount > 0 || report.graphDriftCount > 0 || report.fullDriftCount > 0 || report.missingCount > 0 || report.unmaterializedCount > 0;
|
|
2830
|
+
process.exit(hasIssues ? 1 : 0);
|
|
2749
2831
|
} catch (error) {
|
|
2750
2832
|
process.stderr.write(`Error: ${error.message}
|
|
2751
2833
|
`);
|
|
@@ -2753,21 +2835,151 @@ Summary: ${report.driftCount} drift, ${report.missingCount} missing, ${report.un
|
|
|
2753
2835
|
}
|
|
2754
2836
|
});
|
|
2755
2837
|
}
|
|
2838
|
+
function printReport(report, driftedOnly, limit) {
|
|
2839
|
+
const sourceEntries = classifyForSection(report.entries, "source", driftedOnly);
|
|
2840
|
+
const graphEntries = classifyForSection(report.entries, "graph", driftedOnly);
|
|
2841
|
+
const sourceShown = limit !== void 0 ? sourceEntries.slice(0, limit) : sourceEntries;
|
|
2842
|
+
const graphShown = limit !== void 0 ? graphEntries.slice(0, limit) : graphEntries;
|
|
2843
|
+
process.stdout.write("Source drift:\n");
|
|
2844
|
+
printSectionEntries(sourceShown, "source");
|
|
2845
|
+
if (limit !== void 0 && sourceEntries.length > limit) {
|
|
2846
|
+
process.stdout.write(chalk2.dim(` ... ${sourceEntries.length - limit} more (${sourceEntries.length} total)
|
|
2847
|
+
`));
|
|
2848
|
+
}
|
|
2849
|
+
process.stdout.write("\nGraph drift:\n");
|
|
2850
|
+
printSectionEntries(graphShown, "graph");
|
|
2851
|
+
if (limit !== void 0 && graphEntries.length > limit) {
|
|
2852
|
+
process.stdout.write(chalk2.dim(` ... ${graphEntries.length - limit} more (${graphEntries.length} total)
|
|
2853
|
+
`));
|
|
2854
|
+
}
|
|
2855
|
+
const parts = [
|
|
2856
|
+
`${report.sourceDriftCount} source-drift`,
|
|
2857
|
+
`${report.graphDriftCount} graph-drift`,
|
|
2858
|
+
`${report.fullDriftCount} full-drift`,
|
|
2859
|
+
`${report.missingCount} missing`,
|
|
2860
|
+
`${report.unmaterializedCount} unmaterialized`
|
|
2861
|
+
];
|
|
2862
|
+
let summary = `
|
|
2863
|
+
Summary: ${parts.join(", ")}`;
|
|
2864
|
+
if (driftedOnly && report.okCount > 0) {
|
|
2865
|
+
summary += ` (${report.okCount} ok hidden)`;
|
|
2866
|
+
} else {
|
|
2867
|
+
summary += `, ${report.okCount} ok`;
|
|
2868
|
+
}
|
|
2869
|
+
process.stdout.write(summary + "\n");
|
|
2870
|
+
}
|
|
2871
|
+
function classifyForSection(entries, section, driftedOnly) {
|
|
2872
|
+
return entries.filter((entry) => {
|
|
2873
|
+
if (section === "source") {
|
|
2874
|
+
if (entry.status === "graph-drift") return false;
|
|
2875
|
+
if (entry.status === "ok" && driftedOnly) return false;
|
|
2876
|
+
return true;
|
|
2877
|
+
} else {
|
|
2878
|
+
if (entry.status === "source-drift" || entry.status === "missing" || entry.status === "unmaterialized")
|
|
2879
|
+
return false;
|
|
2880
|
+
if (entry.status === "ok" && driftedOnly) return false;
|
|
2881
|
+
return true;
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
function printSectionEntries(entries, section) {
|
|
2886
|
+
if (entries.length === 0) {
|
|
2887
|
+
process.stdout.write(chalk2.dim(" (none)\n"));
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
for (const entry of entries) {
|
|
2891
|
+
printEntryLine(entry);
|
|
2892
|
+
printChangedFiles(entry, section);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function printEntryLine(entry) {
|
|
2896
|
+
const pad = 13;
|
|
2897
|
+
switch (entry.status) {
|
|
2898
|
+
case "ok":
|
|
2899
|
+
process.stdout.write(chalk2.green(` ${"[ok]".padEnd(pad)}${entry.nodePath}
|
|
2900
|
+
`));
|
|
2901
|
+
break;
|
|
2902
|
+
case "source-drift":
|
|
2903
|
+
process.stdout.write(chalk2.red(` ${"[drift]".padEnd(pad)}${entry.nodePath}
|
|
2904
|
+
`));
|
|
2905
|
+
break;
|
|
2906
|
+
case "graph-drift":
|
|
2907
|
+
process.stdout.write(chalk2.magenta(` ${"[drift]".padEnd(pad)}${entry.nodePath}
|
|
2908
|
+
`));
|
|
2909
|
+
break;
|
|
2910
|
+
case "full-drift":
|
|
2911
|
+
process.stdout.write(chalk2.red(` ${"[drift]".padEnd(pad)}${entry.nodePath}
|
|
2912
|
+
`));
|
|
2913
|
+
break;
|
|
2914
|
+
case "missing":
|
|
2915
|
+
process.stdout.write(chalk2.yellow(` ${"[missing]".padEnd(pad)}${entry.nodePath}
|
|
2916
|
+
`));
|
|
2917
|
+
break;
|
|
2918
|
+
case "unmaterialized":
|
|
2919
|
+
process.stdout.write(chalk2.dim(` ${"[unmat.]".padEnd(pad)}${entry.nodePath}
|
|
2920
|
+
`));
|
|
2921
|
+
break;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function printChangedFiles(entry, section) {
|
|
2925
|
+
if (!entry.changedFiles || entry.changedFiles.length === 0) return;
|
|
2926
|
+
const indent = " ".repeat(15);
|
|
2927
|
+
const relevantFiles = entry.changedFiles.filter((f) => {
|
|
2928
|
+
if (section === "source") return f.category === "source";
|
|
2929
|
+
return f.category === "graph";
|
|
2930
|
+
});
|
|
2931
|
+
for (const file of relevantFiles) {
|
|
2932
|
+
process.stdout.write(chalk2.dim(`${indent}${file.filePath} (changed)
|
|
2933
|
+
`));
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2756
2936
|
|
|
2757
2937
|
// src/cli/drift-sync.ts
|
|
2758
2938
|
import chalk3 from "chalk";
|
|
2759
2939
|
function registerDriftSyncCommand(program2) {
|
|
2760
|
-
program2.command("drift-sync").description("Record current file hash after resolving drift").
|
|
2940
|
+
program2.command("drift-sync").description("Record current file hash after resolving drift").option("--node <path>", "Node path to sync").option("--recursive", "Also sync all descendant nodes").option("--all", "Sync all nodes with mappings").action(async (options) => {
|
|
2761
2941
|
try {
|
|
2942
|
+
if (!options.node && !options.all) {
|
|
2943
|
+
process.stderr.write("Error: either '--node <path>' or '--all' is required\n");
|
|
2944
|
+
process.exit(1);
|
|
2945
|
+
}
|
|
2762
2946
|
const graph = await loadGraph(process.cwd());
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2947
|
+
let nodesToSync;
|
|
2948
|
+
if (options.all) {
|
|
2949
|
+
nodesToSync = [...graph.nodes.entries()].filter(([, n]) => normalizeMappingPaths(n.meta.mapping).length > 0).map(([p]) => p).sort();
|
|
2950
|
+
} else {
|
|
2951
|
+
const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2952
|
+
if (!graph.nodes.has(nodePath)) {
|
|
2953
|
+
await syncDriftState(graph, nodePath);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
nodesToSync = [nodePath];
|
|
2957
|
+
if (options.recursive) {
|
|
2958
|
+
const prefix = nodePath + "/";
|
|
2959
|
+
for (const [p] of graph.nodes) {
|
|
2960
|
+
if (p.startsWith(prefix)) {
|
|
2961
|
+
nodesToSync.push(p);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
nodesToSync.sort();
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
for (const np of nodesToSync) {
|
|
2968
|
+
const node = graph.nodes.get(np);
|
|
2969
|
+
if (normalizeMappingPaths(node.meta.mapping).length === 0) {
|
|
2970
|
+
if (!options.all && !options.recursive && np === options.node) {
|
|
2971
|
+
await syncDriftState(graph, np);
|
|
2972
|
+
}
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
const { previousHash, currentHash } = await syncDriftState(graph, np);
|
|
2976
|
+
process.stdout.write(chalk3.green(`Synchronized: ${np}
|
|
2766
2977
|
`));
|
|
2767
|
-
|
|
2768
|
-
|
|
2978
|
+
process.stdout.write(
|
|
2979
|
+
` Hash: ${previousHash ? previousHash.slice(0, 8) : "none"} -> ${currentHash.slice(0, 8)}
|
|
2769
2980
|
`
|
|
2770
|
-
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2771
2983
|
} catch (error) {
|
|
2772
2984
|
process.stderr.write(`Error: ${error.message}
|
|
2773
2985
|
`);
|
|
@@ -2790,20 +3002,40 @@ function registerStatusCommand(program2) {
|
|
|
2790
3002
|
let structuralRelations = 0;
|
|
2791
3003
|
let eventRelations = 0;
|
|
2792
3004
|
const structuralTypes = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3005
|
+
let maxRelCount = 0;
|
|
3006
|
+
let maxRelNode = "";
|
|
2793
3007
|
for (const node of graph.nodes.values()) {
|
|
3008
|
+
const relCount = (node.meta.relations ?? []).length;
|
|
3009
|
+
if (relCount > maxRelCount) {
|
|
3010
|
+
maxRelCount = relCount;
|
|
3011
|
+
maxRelNode = node.path;
|
|
3012
|
+
}
|
|
2794
3013
|
for (const rel of node.meta.relations ?? []) {
|
|
2795
3014
|
if (structuralTypes.has(rel.type)) structuralRelations += 1;
|
|
2796
3015
|
else eventRelations += 1;
|
|
2797
3016
|
}
|
|
2798
3017
|
}
|
|
2799
3018
|
const flowCount = graph.flows.length;
|
|
2800
|
-
const knowledgeCount = graph.knowledge.length;
|
|
2801
3019
|
const drift = await detectDrift(graph);
|
|
2802
3020
|
const validation = await validate(graph, "all");
|
|
2803
3021
|
const errorCount = validation.issues.filter((issue) => issue.severity === "error").length;
|
|
2804
3022
|
const warningCount = validation.issues.filter(
|
|
2805
3023
|
(issue) => issue.severity === "warning"
|
|
2806
3024
|
).length;
|
|
3025
|
+
const configuredArtifactTypes = Object.keys(graph.config.artifacts ?? {});
|
|
3026
|
+
const totalSlots = graph.nodes.size * configuredArtifactTypes.length;
|
|
3027
|
+
let filledSlots = 0;
|
|
3028
|
+
let mappedNodeCount = 0;
|
|
3029
|
+
for (const node of graph.nodes.values()) {
|
|
3030
|
+
const allowed = new Set(configuredArtifactTypes);
|
|
3031
|
+
filledSlots += node.artifacts.filter((a) => allowed.has(a.filename)).length;
|
|
3032
|
+
if (normalizeMappingPaths(node.meta.mapping).length > 0) mappedNodeCount++;
|
|
3033
|
+
}
|
|
3034
|
+
let aspectCoveredNodes = 0;
|
|
3035
|
+
for (const node of graph.nodes.values()) {
|
|
3036
|
+
const effective = collectEffectiveAspectIds(graph, node.path);
|
|
3037
|
+
if (effective.size > 0) aspectCoveredNodes++;
|
|
3038
|
+
}
|
|
2807
3039
|
process.stdout.write(`Graph: ${graph.config.name}
|
|
2808
3040
|
`);
|
|
2809
3041
|
const pluralize = (word, count) => count === 1 ? word : word.endsWith("y") ? word.slice(0, -1) + "ies" : word + "s";
|
|
@@ -2817,15 +3049,37 @@ function registerStatusCommand(program2) {
|
|
|
2817
3049
|
`
|
|
2818
3050
|
);
|
|
2819
3051
|
process.stdout.write(
|
|
2820
|
-
`Aspects: ${graph.aspects.length} Flows: ${flowCount}
|
|
3052
|
+
`Aspects: ${graph.aspects.length} Flows: ${flowCount}
|
|
2821
3053
|
`
|
|
2822
3054
|
);
|
|
2823
3055
|
process.stdout.write(
|
|
2824
|
-
`Drift: ${drift.
|
|
3056
|
+
`Drift: ${drift.sourceDriftCount} source-drift, ${drift.graphDriftCount} graph-drift, ${drift.fullDriftCount} full-drift, ${drift.missingCount} missing, ${drift.unmaterializedCount} unmaterialized, ${drift.okCount} ok
|
|
2825
3057
|
`
|
|
2826
3058
|
);
|
|
2827
3059
|
process.stdout.write(`Validation: ${errorCount} errors, ${warningCount} warnings
|
|
2828
3060
|
`);
|
|
3061
|
+
const fillPct = totalSlots > 0 ? Math.round(filledSlots / totalSlots * 100) : 0;
|
|
3062
|
+
const totalRelations = structuralRelations + eventRelations;
|
|
3063
|
+
const avgRel = graph.nodes.size > 0 ? (totalRelations / graph.nodes.size).toFixed(1) : "0";
|
|
3064
|
+
process.stdout.write(`
|
|
3065
|
+
Quality:
|
|
3066
|
+
`);
|
|
3067
|
+
process.stdout.write(
|
|
3068
|
+
` Artifacts: ${filledSlots}/${totalSlots} slots filled (${fillPct}%) \u2014 ${configuredArtifactTypes.length} types \xD7 ${graph.nodes.size} nodes
|
|
3069
|
+
`
|
|
3070
|
+
);
|
|
3071
|
+
process.stdout.write(
|
|
3072
|
+
` Relations: avg ${avgRel}/node, max ${maxRelCount}${maxRelNode ? ` (${maxRelNode})` : ""}
|
|
3073
|
+
`
|
|
3074
|
+
);
|
|
3075
|
+
process.stdout.write(
|
|
3076
|
+
` Mapping: ${mappedNodeCount}/${graph.nodes.size} nodes mapped to source
|
|
3077
|
+
`
|
|
3078
|
+
);
|
|
3079
|
+
process.stdout.write(
|
|
3080
|
+
` Aspects: ${aspectCoveredNodes}/${graph.nodes.size} nodes have aspect coverage
|
|
3081
|
+
`
|
|
3082
|
+
);
|
|
2829
3083
|
} catch (error) {
|
|
2830
3084
|
process.stderr.write(`Error: ${error.message}
|
|
2831
3085
|
`);
|
|
@@ -2842,10 +3096,10 @@ function registerTreeCommand(program2) {
|
|
|
2842
3096
|
let roots;
|
|
2843
3097
|
let showProjectName;
|
|
2844
3098
|
if (options.root?.trim()) {
|
|
2845
|
-
const
|
|
2846
|
-
const node = graph.nodes.get(
|
|
3099
|
+
const path18 = options.root.trim().replace(/\/$/, "");
|
|
3100
|
+
const node = graph.nodes.get(path18);
|
|
2847
3101
|
if (!node) {
|
|
2848
|
-
process.stderr.write(`Error: path '${
|
|
3102
|
+
process.stderr.write(`Error: path '${path18}' not found
|
|
2849
3103
|
`);
|
|
2850
3104
|
process.exit(1);
|
|
2851
3105
|
}
|
|
@@ -2873,7 +3127,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
2873
3127
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
2874
3128
|
const name = node.path.split("/").pop() ?? node.path;
|
|
2875
3129
|
const type = `[${node.meta.type}]`;
|
|
2876
|
-
const tags = node.meta.
|
|
3130
|
+
const tags = node.meta.aspects?.length ? ` aspects:${node.meta.aspects.join(",")}` : "";
|
|
2877
3131
|
const blackbox = node.meta.blackbox ? " \u25A0 blackbox" : "";
|
|
2878
3132
|
const relationCount = node.meta.relations?.length ?? 0;
|
|
2879
3133
|
process.stdout.write(
|
|
@@ -2889,6 +3143,8 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
2889
3143
|
}
|
|
2890
3144
|
|
|
2891
3145
|
// src/cli/owner.ts
|
|
3146
|
+
import path14 from "path";
|
|
3147
|
+
import { access as access2 } from "fs/promises";
|
|
2892
3148
|
function normalizeForMatch(inputPath) {
|
|
2893
3149
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2894
3150
|
}
|
|
@@ -2913,12 +3169,28 @@ function findOwner(graph, projectRoot, rawPath) {
|
|
|
2913
3169
|
function registerOwnerCommand(program2) {
|
|
2914
3170
|
program2.command("owner").description("Find which graph node owns a source file").requiredOption("--file <path>", "File path (relative to repository root)").action(async (options) => {
|
|
2915
3171
|
try {
|
|
2916
|
-
const
|
|
2917
|
-
const graph = await loadGraph(
|
|
2918
|
-
const
|
|
3172
|
+
const cwd = process.cwd();
|
|
3173
|
+
const graph = await loadGraph(cwd);
|
|
3174
|
+
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3175
|
+
const rawPath = options.file.trim();
|
|
3176
|
+
const absolute = path14.resolve(cwd, rawPath);
|
|
3177
|
+
const repoRelative = path14.relative(repoRoot, absolute).split(path14.sep).join("/");
|
|
3178
|
+
const result = findOwner(graph, repoRoot, repoRelative);
|
|
2919
3179
|
if (!result.nodePath) {
|
|
2920
|
-
|
|
3180
|
+
const absPath = path14.resolve(repoRoot, result.file);
|
|
3181
|
+
let exists = true;
|
|
3182
|
+
try {
|
|
3183
|
+
await access2(absPath);
|
|
3184
|
+
} catch {
|
|
3185
|
+
exists = false;
|
|
3186
|
+
}
|
|
3187
|
+
if (exists) {
|
|
3188
|
+
process.stdout.write(`${result.file} -> no graph coverage
|
|
2921
3189
|
`);
|
|
3190
|
+
} else {
|
|
3191
|
+
process.stdout.write(`${result.file} -> no graph coverage (file not found)
|
|
3192
|
+
`);
|
|
3193
|
+
}
|
|
2922
3194
|
} else {
|
|
2923
3195
|
process.stdout.write(`${result.file} -> ${result.nodePath}
|
|
2924
3196
|
`);
|
|
@@ -2932,13 +3204,13 @@ function registerOwnerCommand(program2) {
|
|
|
2932
3204
|
}
|
|
2933
3205
|
|
|
2934
3206
|
// src/core/dependency-resolver.ts
|
|
2935
|
-
import { execSync
|
|
2936
|
-
import
|
|
2937
|
-
var
|
|
3207
|
+
import { execSync } from "child_process";
|
|
3208
|
+
import path15 from "path";
|
|
3209
|
+
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2938
3210
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
2939
3211
|
function filterRelationType(relType, filter) {
|
|
2940
3212
|
if (filter === "all") return true;
|
|
2941
|
-
if (filter === "structural") return
|
|
3213
|
+
if (filter === "structural") return STRUCTURAL_RELATION_TYPES3.has(relType);
|
|
2942
3214
|
if (filter === "event") return EVENT_RELATION_TYPES2.has(relType);
|
|
2943
3215
|
return false;
|
|
2944
3216
|
}
|
|
@@ -2993,7 +3265,7 @@ function registerDepsCommand(program2) {
|
|
|
2993
3265
|
try {
|
|
2994
3266
|
const graph = await loadGraph(process.cwd());
|
|
2995
3267
|
const typeFilter = options.type === "structural" || options.type === "event" || options.type === "all" ? options.type : "all";
|
|
2996
|
-
const nodePath = options.node.trim().replace(
|
|
3268
|
+
const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2997
3269
|
const text = formatDependencyTree(graph, nodePath, {
|
|
2998
3270
|
depth: options.depth,
|
|
2999
3271
|
relationType: typeFilter
|
|
@@ -3010,24 +3282,24 @@ function registerDepsCommand(program2) {
|
|
|
3010
3282
|
// src/core/graph-from-git.ts
|
|
3011
3283
|
import { mkdtemp, rm } from "fs/promises";
|
|
3012
3284
|
import { tmpdir } from "os";
|
|
3013
|
-
import
|
|
3014
|
-
import { execSync as
|
|
3285
|
+
import path16 from "path";
|
|
3286
|
+
import { execSync as execSync2 } from "child_process";
|
|
3015
3287
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
3016
3288
|
const yggPath = ".yggdrasil";
|
|
3017
3289
|
let tmpDir = null;
|
|
3018
3290
|
try {
|
|
3019
|
-
|
|
3291
|
+
execSync2(`git rev-parse ${ref}`, { cwd: projectRoot, stdio: "pipe" });
|
|
3020
3292
|
} catch {
|
|
3021
3293
|
return null;
|
|
3022
3294
|
}
|
|
3023
3295
|
try {
|
|
3024
|
-
tmpDir = await mkdtemp(
|
|
3025
|
-
const archivePath =
|
|
3026
|
-
|
|
3296
|
+
tmpDir = await mkdtemp(path16.join(tmpdir(), "ygg-git-"));
|
|
3297
|
+
const archivePath = path16.join(tmpDir, "archive.tar");
|
|
3298
|
+
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
3027
3299
|
cwd: projectRoot,
|
|
3028
3300
|
stdio: "pipe"
|
|
3029
3301
|
});
|
|
3030
|
-
|
|
3302
|
+
execSync2(`tar -xf "${archivePath}" -C "${tmpDir}"`, { stdio: "pipe" });
|
|
3031
3303
|
const graph = await loadGraph(tmpDir);
|
|
3032
3304
|
return graph;
|
|
3033
3305
|
} catch {
|
|
@@ -3069,14 +3341,14 @@ function collectReverseDependents(graph, targetNode) {
|
|
|
3069
3341
|
}
|
|
3070
3342
|
return {
|
|
3071
3343
|
direct,
|
|
3072
|
-
|
|
3344
|
+
allDependents: [...seen].sort(),
|
|
3073
3345
|
reverse,
|
|
3074
3346
|
relationFrom
|
|
3075
3347
|
};
|
|
3076
3348
|
}
|
|
3077
|
-
function buildTransitiveChains(targetNode, direct,
|
|
3349
|
+
function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
3078
3350
|
const directSet = new Set(direct);
|
|
3079
|
-
const transitiveOnly =
|
|
3351
|
+
const transitiveOnly = allDependents.filter((t) => !directSet.has(t));
|
|
3080
3352
|
if (transitiveOnly.length === 0) return [];
|
|
3081
3353
|
const parent = /* @__PURE__ */ new Map();
|
|
3082
3354
|
const queue = [targetNode];
|
|
@@ -3092,164 +3364,391 @@ function buildTransitiveChains(targetNode, direct, transitive, reverse) {
|
|
|
3092
3364
|
}
|
|
3093
3365
|
const chains = [];
|
|
3094
3366
|
for (const node of transitiveOnly) {
|
|
3095
|
-
const
|
|
3367
|
+
const path18 = [];
|
|
3096
3368
|
let current = node;
|
|
3097
3369
|
while (current) {
|
|
3098
|
-
|
|
3370
|
+
path18.unshift(current);
|
|
3099
3371
|
current = parent.get(current);
|
|
3100
3372
|
}
|
|
3101
|
-
if (
|
|
3102
|
-
chains.push(
|
|
3373
|
+
if (path18.length >= 3) {
|
|
3374
|
+
chains.push(path18.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
3103
3375
|
}
|
|
3104
3376
|
}
|
|
3105
3377
|
return chains.sort();
|
|
3106
3378
|
}
|
|
3107
|
-
function
|
|
3108
|
-
|
|
3379
|
+
function collectDescendants(graph, nodePath) {
|
|
3380
|
+
const node = graph.nodes.get(nodePath);
|
|
3381
|
+
if (!node) return [];
|
|
3382
|
+
const result = [];
|
|
3383
|
+
const stack = [...node.children];
|
|
3384
|
+
while (stack.length > 0) {
|
|
3385
|
+
const child = stack.pop();
|
|
3386
|
+
result.push(child.path);
|
|
3387
|
+
stack.push(...child.children);
|
|
3388
|
+
}
|
|
3389
|
+
return result.sort();
|
|
3390
|
+
}
|
|
3391
|
+
async function runSimulation(graph, nodePaths, targetNodePath) {
|
|
3392
|
+
const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
|
|
3393
|
+
process.stdout.write("\nChanges in context packages:\n\n");
|
|
3394
|
+
const baselineGraph = await loadGraphFromRef(process.cwd(), "HEAD");
|
|
3395
|
+
const driftReport = await detectDrift(graph);
|
|
3396
|
+
const driftByNode = new Map(driftReport.entries.map((e) => [e.nodePath, e]));
|
|
3397
|
+
for (const dep of nodePaths) {
|
|
3109
3398
|
try {
|
|
3110
|
-
const
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3399
|
+
const pkg2 = await buildContext(graph, dep);
|
|
3400
|
+
const status = pkg2.tokenCount >= budget.error ? "error" : pkg2.tokenCount >= budget.warning ? "warning" : "ok";
|
|
3401
|
+
let baselineTokens = null;
|
|
3402
|
+
if (baselineGraph?.nodes.has(dep)) {
|
|
3403
|
+
try {
|
|
3404
|
+
const baselinePkg = await buildContext(baselineGraph, dep);
|
|
3405
|
+
baselineTokens = baselinePkg.tokenCount;
|
|
3406
|
+
} catch {
|
|
3407
|
+
}
|
|
3116
3408
|
}
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
nodePath
|
|
3409
|
+
const hasDepOnTarget = targetNodePath && graph.nodes.get(dep)?.meta.relations?.some(
|
|
3410
|
+
(r) => r.target === targetNodePath && STRUCTURAL_TYPES.has(r.type)
|
|
3120
3411
|
);
|
|
3121
|
-
const
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3412
|
+
const changedLine = hasDepOnTarget ? ` + Changed dependency interface: ${targetNodePath}
|
|
3413
|
+
` : "";
|
|
3414
|
+
const budgetLine = baselineTokens !== null ? ` Budget: ${baselineTokens} -> ${pkg2.tokenCount} tokens (${status})
|
|
3415
|
+
` : ` Budget: ${pkg2.tokenCount} tokens (${status})
|
|
3416
|
+
`;
|
|
3417
|
+
const driftEntry = driftByNode.get(dep);
|
|
3418
|
+
const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}
|
|
3419
|
+
` : driftEntry ? ` Mapped files (on-disk): ok
|
|
3420
|
+
` : "";
|
|
3421
|
+
process.stdout.write(`${dep}:
|
|
3422
|
+
${changedLine}${budgetLine}${driftLine}
|
|
3423
|
+
`);
|
|
3424
|
+
} catch {
|
|
3425
|
+
process.stdout.write(`${dep}:
|
|
3426
|
+
failed to build context
|
|
3427
|
+
|
|
3428
|
+
`);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
async function handleAspectImpact(graph, aspectId, simulate) {
|
|
3433
|
+
const aspect = graph.aspects.find((a) => a.id === aspectId);
|
|
3434
|
+
if (!aspect) {
|
|
3435
|
+
process.stderr.write(`Aspect not found: ${aspectId}
|
|
3436
|
+
`);
|
|
3437
|
+
process.exit(1);
|
|
3438
|
+
}
|
|
3439
|
+
const affected = [];
|
|
3440
|
+
for (const [nodePath] of graph.nodes) {
|
|
3441
|
+
const effective = collectEffectiveAspectIds(graph, nodePath);
|
|
3442
|
+
if (effective.has(aspectId)) {
|
|
3443
|
+
const node = graph.nodes.get(nodePath);
|
|
3444
|
+
const ownAspects = new Set(node.meta.aspects ?? []);
|
|
3445
|
+
if (ownAspects.has(aspectId)) {
|
|
3446
|
+
affected.push({ path: nodePath, source: "own" });
|
|
3447
|
+
} else {
|
|
3448
|
+
let fromHierarchy = false;
|
|
3449
|
+
let anc = node.parent;
|
|
3450
|
+
while (anc) {
|
|
3451
|
+
if ((anc.meta.aspects ?? []).includes(aspectId)) {
|
|
3452
|
+
fromHierarchy = true;
|
|
3453
|
+
break;
|
|
3454
|
+
}
|
|
3455
|
+
anc = anc.parent;
|
|
3126
3456
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3457
|
+
if (fromHierarchy) {
|
|
3458
|
+
affected.push({ path: nodePath, source: `hierarchy from ${anc.path}` });
|
|
3459
|
+
} else {
|
|
3460
|
+
const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
|
|
3461
|
+
const flow = graph.flows.find(
|
|
3462
|
+
(f) => (f.aspects ?? []).includes(aspectId) && f.nodes.some((n) => ancestorPaths.has(n))
|
|
3463
|
+
);
|
|
3464
|
+
affected.push({ path: nodePath, source: flow ? `flow: ${flow.name}` : "implied" });
|
|
3134
3465
|
}
|
|
3135
3466
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
affected.sort((a, b) => a.path.localeCompare(b.path));
|
|
3470
|
+
const propagatingFlows = graph.flows.filter((f) => (f.aspects ?? []).includes(aspectId)).map((f) => f.name);
|
|
3471
|
+
const impliedBy = graph.aspects.filter((a) => (a.implies ?? []).includes(aspectId)).map((a) => a.id);
|
|
3472
|
+
const implies = aspect.implies ?? [];
|
|
3473
|
+
process.stdout.write(`Impact of changes in aspect ${aspectId}:
|
|
3474
|
+
|
|
3475
|
+
`);
|
|
3476
|
+
process.stdout.write(`Affected nodes (${affected.length}):
|
|
3477
|
+
`);
|
|
3478
|
+
if (affected.length === 0) {
|
|
3479
|
+
process.stdout.write(" (none)\n");
|
|
3480
|
+
} else {
|
|
3481
|
+
for (const { path: p, source } of affected) {
|
|
3482
|
+
process.stdout.write(` ${p} (${source})
|
|
3483
|
+
`);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
process.stdout.write(
|
|
3487
|
+
`
|
|
3488
|
+
Flows propagating this aspect: ${propagatingFlows.length > 0 ? propagatingFlows.join(", ") : "(none)"}
|
|
3489
|
+
`
|
|
3490
|
+
);
|
|
3491
|
+
process.stdout.write(`Implied by: ${impliedBy.length > 0 ? impliedBy.join(", ") : "(none)"}
|
|
3492
|
+
`);
|
|
3493
|
+
process.stdout.write(`Implies: ${implies.length > 0 ? implies.join(", ") : "(none)"}
|
|
3494
|
+
`);
|
|
3495
|
+
process.stdout.write(`
|
|
3496
|
+
Total scope: ${affected.length} nodes, ${propagatingFlows.length} flows
|
|
3497
|
+
`);
|
|
3498
|
+
if (simulate && affected.length > 0) {
|
|
3499
|
+
await runSimulation(
|
|
3500
|
+
graph,
|
|
3501
|
+
affected.map((a) => a.path),
|
|
3502
|
+
null
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
async function handleFlowImpact(graph, flowName, simulate) {
|
|
3507
|
+
const flow = graph.flows.find((f) => f.name === flowName || f.path === flowName);
|
|
3508
|
+
if (!flow) {
|
|
3509
|
+
process.stderr.write(`Flow not found: ${flowName}
|
|
3510
|
+
`);
|
|
3511
|
+
process.exit(1);
|
|
3512
|
+
}
|
|
3513
|
+
const participants = /* @__PURE__ */ new Set();
|
|
3514
|
+
for (const nodePath of flow.nodes) {
|
|
3515
|
+
if (graph.nodes.has(nodePath)) {
|
|
3516
|
+
participants.add(nodePath);
|
|
3517
|
+
for (const desc of collectDescendants(graph, nodePath)) {
|
|
3518
|
+
participants.add(desc);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
const sorted = [...participants].sort();
|
|
3523
|
+
const flowAspects = flow.aspects ?? [];
|
|
3524
|
+
process.stdout.write(`Impact of changes in flow ${flow.name}:
|
|
3525
|
+
|
|
3526
|
+
`);
|
|
3527
|
+
process.stdout.write("Participants:\n");
|
|
3528
|
+
if (sorted.length === 0) {
|
|
3529
|
+
process.stdout.write(" (none)\n");
|
|
3530
|
+
} else {
|
|
3531
|
+
for (const p of sorted) {
|
|
3532
|
+
const isDeclared = flow.nodes.includes(p);
|
|
3533
|
+
const suffix = isDeclared ? "" : " (descendant)";
|
|
3534
|
+
process.stdout.write(` ${p}${suffix}
|
|
3535
|
+
`);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
process.stdout.write(
|
|
3539
|
+
`
|
|
3540
|
+
Flow aspects: ${flowAspects.length > 0 ? flowAspects.join(", ") : "(none)"}
|
|
3541
|
+
`
|
|
3542
|
+
);
|
|
3543
|
+
process.stdout.write(`
|
|
3544
|
+
Total scope: ${sorted.length} nodes
|
|
3545
|
+
`);
|
|
3546
|
+
if (simulate && sorted.length > 0) {
|
|
3547
|
+
await runSimulation(graph, sorted, null);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
function registerImpactCommand(program2) {
|
|
3551
|
+
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(
|
|
3552
|
+
async (options) => {
|
|
3553
|
+
try {
|
|
3554
|
+
const modeCount = [options.node, options.aspect, options.flow].filter(Boolean).length;
|
|
3555
|
+
if (modeCount === 0) {
|
|
3556
|
+
process.stderr.write(
|
|
3557
|
+
"Error: one of --node, --aspect, or --flow is required\n"
|
|
3558
|
+
);
|
|
3559
|
+
process.exit(1);
|
|
3560
|
+
}
|
|
3561
|
+
if (modeCount > 1) {
|
|
3562
|
+
process.stderr.write(
|
|
3563
|
+
"Error: --node, --aspect, and --flow are mutually exclusive\n"
|
|
3564
|
+
);
|
|
3565
|
+
process.exit(1);
|
|
3566
|
+
}
|
|
3567
|
+
const graph = await loadGraph(process.cwd());
|
|
3568
|
+
if (options.aspect) {
|
|
3569
|
+
await handleAspectImpact(graph, options.aspect.trim(), options.simulate);
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
if (options.flow) {
|
|
3573
|
+
await handleFlowImpact(graph, options.flow.trim(), options.simulate);
|
|
3574
|
+
return;
|
|
3141
3575
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3576
|
+
const nodePath = options.node.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
3577
|
+
if (!graph.nodes.has(nodePath)) {
|
|
3578
|
+
process.stderr.write(`Node not found: ${nodePath}
|
|
3579
|
+
`);
|
|
3580
|
+
process.exit(1);
|
|
3581
|
+
}
|
|
3582
|
+
const { direct, allDependents, reverse, relationFrom } = collectReverseDependents(
|
|
3583
|
+
graph,
|
|
3584
|
+
nodePath
|
|
3585
|
+
);
|
|
3586
|
+
const chains = buildTransitiveChains(nodePath, direct, allDependents, reverse);
|
|
3587
|
+
const flows = [];
|
|
3588
|
+
for (const flow of graph.flows) {
|
|
3589
|
+
if (flow.nodes.includes(nodePath)) {
|
|
3590
|
+
flows.push(flow.name);
|
|
3145
3591
|
}
|
|
3146
|
-
continue;
|
|
3147
3592
|
}
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3593
|
+
const targetEffective = collectEffectiveAspectIds(graph, nodePath);
|
|
3594
|
+
const aspectsInScope = [];
|
|
3595
|
+
for (const aspect of graph.aspects) {
|
|
3596
|
+
if (targetEffective.has(aspect.id)) {
|
|
3597
|
+
aspectsInScope.push(aspect.name);
|
|
3151
3598
|
}
|
|
3152
3599
|
}
|
|
3153
|
-
|
|
3154
|
-
const budget = graph.config.quality?.context_budget ?? { warning: 1e4, error: 2e4 };
|
|
3155
|
-
process.stdout.write(`Impact of changes in ${nodePath}:
|
|
3600
|
+
process.stdout.write(`Impact of changes in ${nodePath}:
|
|
3156
3601
|
|
|
3157
3602
|
`);
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3603
|
+
process.stdout.write("Directly dependent:\n");
|
|
3604
|
+
if (direct.length === 0) {
|
|
3605
|
+
process.stdout.write(" (none)\n");
|
|
3606
|
+
} else {
|
|
3607
|
+
for (const dep of direct) {
|
|
3608
|
+
const rel = relationFrom.get(`${dep}->${nodePath}`);
|
|
3609
|
+
const annot = rel?.consumes?.length ? ` (${rel.type}, you consume: ${rel.consumes.join(", ")})` : rel ? ` (${rel.type})` : "";
|
|
3610
|
+
process.stdout.write(` <- ${dep}${annot}
|
|
3166
3611
|
`);
|
|
3612
|
+
}
|
|
3167
3613
|
}
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
process.stdout.write(` ${chain}
|
|
3614
|
+
process.stdout.write("\nTransitively dependent:\n");
|
|
3615
|
+
if (chains.length === 0) {
|
|
3616
|
+
process.stdout.write(" (none)\n");
|
|
3617
|
+
} else {
|
|
3618
|
+
for (const chain of chains) {
|
|
3619
|
+
process.stdout.write(` ${chain}
|
|
3175
3620
|
`);
|
|
3621
|
+
}
|
|
3176
3622
|
}
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3623
|
+
const descendants = collectDescendants(graph, nodePath);
|
|
3624
|
+
if (descendants.length > 0) {
|
|
3625
|
+
process.stdout.write("\nDescendants (hierarchy impact):\n");
|
|
3626
|
+
for (const desc of descendants) {
|
|
3627
|
+
process.stdout.write(` ${desc}
|
|
3180
3628
|
`);
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
`Knowledge (scope covers node): ${knowledgeInScope.length > 0 ? knowledgeInScope.join(", ") : "(none)"}
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
process.stdout.write(
|
|
3632
|
+
`
|
|
3633
|
+
Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
|
|
3187
3634
|
`
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
Total scope: ${transitive.length} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects, ${knowledgeInScope.length} knowledge
|
|
3635
|
+
);
|
|
3636
|
+
process.stdout.write(
|
|
3637
|
+
`Aspects (scope covers node): ${aspectsInScope.length > 0 ? aspectsInScope.join(", ") : "(none)"}
|
|
3192
3638
|
`
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
const status = pkg.tokenCount >= budget.error ? "error" : pkg.tokenCount >= budget.warning ? "warning" : "ok";
|
|
3203
|
-
let baselineTokens = null;
|
|
3204
|
-
if (baselineGraph?.nodes.has(dep)) {
|
|
3205
|
-
try {
|
|
3206
|
-
const baselinePkg = await buildContext(baselineGraph, dep);
|
|
3207
|
-
baselineTokens = baselinePkg.tokenCount;
|
|
3208
|
-
} catch {
|
|
3209
|
-
}
|
|
3639
|
+
);
|
|
3640
|
+
const coAspectNodes = [];
|
|
3641
|
+
if (targetEffective.size > 0) {
|
|
3642
|
+
for (const [p] of graph.nodes) {
|
|
3643
|
+
if (p === nodePath) continue;
|
|
3644
|
+
const nodeEffective = collectEffectiveAspectIds(graph, p);
|
|
3645
|
+
const shared = [...targetEffective].filter((id) => nodeEffective.has(id));
|
|
3646
|
+
if (shared.length > 0) {
|
|
3647
|
+
coAspectNodes.push({ path: p, shared });
|
|
3210
3648
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
const driftEntry = driftByNode.get(dep);
|
|
3220
|
-
const driftLine = driftEntry && driftEntry.status !== "ok" ? ` Mapped files (on-disk): ${driftEntry.status}${driftEntry.details ? ` (${driftEntry.details})` : ""}
|
|
3221
|
-
` : driftEntry ? ` Mapped files (on-disk): ok
|
|
3222
|
-
` : "";
|
|
3223
|
-
process.stdout.write(`${dep}:
|
|
3224
|
-
${changedLine}${budgetLine}${driftLine}
|
|
3225
|
-
`);
|
|
3226
|
-
} catch {
|
|
3227
|
-
process.stdout.write(`${dep}:
|
|
3228
|
-
failed to build context
|
|
3229
|
-
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
if (coAspectNodes.length > 0) {
|
|
3652
|
+
process.stdout.write("Nodes sharing aspects:\n");
|
|
3653
|
+
for (const { path: p, shared } of coAspectNodes.sort(
|
|
3654
|
+
(a, b) => a.path.localeCompare(b.path)
|
|
3655
|
+
)) {
|
|
3656
|
+
process.stdout.write(` ${p} (${shared.join(", ")})
|
|
3230
3657
|
`);
|
|
3231
3658
|
}
|
|
3232
3659
|
}
|
|
3660
|
+
const allAffected = /* @__PURE__ */ new Set([...allDependents, ...descendants]);
|
|
3661
|
+
process.stdout.write(
|
|
3662
|
+
`
|
|
3663
|
+
Total scope: ${allAffected.size} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects
|
|
3664
|
+
`
|
|
3665
|
+
);
|
|
3666
|
+
if (options.simulate && allAffected.size > 0) {
|
|
3667
|
+
await runSimulation(graph, allAffected, nodePath);
|
|
3668
|
+
}
|
|
3669
|
+
} catch (error) {
|
|
3670
|
+
process.stderr.write(`Error: ${error.message}
|
|
3671
|
+
`);
|
|
3672
|
+
process.exit(1);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
);
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
// src/cli/aspects.ts
|
|
3679
|
+
import { stringify as yamlStringify } from "yaml";
|
|
3680
|
+
function registerAspectsCommand(program2) {
|
|
3681
|
+
program2.command("aspects").description("List aspects with metadata (YAML output)").action(async () => {
|
|
3682
|
+
try {
|
|
3683
|
+
const yggRoot = await findYggRoot(process.cwd());
|
|
3684
|
+
const graph = await loadGraph(yggRoot);
|
|
3685
|
+
const output = graph.aspects.sort((a, b) => a.id.localeCompare(b.id)).map((aspect) => {
|
|
3686
|
+
const entry = { id: aspect.id, name: aspect.name };
|
|
3687
|
+
if (aspect.description) entry.description = aspect.description;
|
|
3688
|
+
if (aspect.implies && aspect.implies.length > 0) entry.implies = aspect.implies;
|
|
3689
|
+
return entry;
|
|
3690
|
+
});
|
|
3691
|
+
process.stdout.write(yamlStringify(output));
|
|
3692
|
+
} catch (error) {
|
|
3693
|
+
const err = error;
|
|
3694
|
+
if (err.code === "ENOENT") {
|
|
3695
|
+
process.stderr.write(
|
|
3696
|
+
`Error: No .yggdrasil/ directory found. Run 'yg init' first.
|
|
3697
|
+
`
|
|
3698
|
+
);
|
|
3699
|
+
} else {
|
|
3700
|
+
process.stderr.write(`Error: ${error.message}
|
|
3701
|
+
`);
|
|
3233
3702
|
}
|
|
3703
|
+
process.exit(1);
|
|
3704
|
+
}
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// src/cli/flows.ts
|
|
3709
|
+
import { stringify as yamlStringify2 } from "yaml";
|
|
3710
|
+
function registerFlowsCommand(program2) {
|
|
3711
|
+
program2.command("flows").description("List flows with metadata (YAML output)").action(async () => {
|
|
3712
|
+
try {
|
|
3713
|
+
const yggRoot = await findYggRoot(process.cwd());
|
|
3714
|
+
const graph = await loadGraph(yggRoot);
|
|
3715
|
+
const output = graph.flows.sort((a, b) => a.name.localeCompare(b.name)).map((flow) => {
|
|
3716
|
+
const entry = {
|
|
3717
|
+
name: flow.name,
|
|
3718
|
+
participants: flow.nodes.length,
|
|
3719
|
+
nodes: flow.nodes.sort()
|
|
3720
|
+
};
|
|
3721
|
+
if (flow.aspects && flow.aspects.length > 0) entry.aspects = flow.aspects;
|
|
3722
|
+
return entry;
|
|
3723
|
+
});
|
|
3724
|
+
process.stdout.write(yamlStringify2(output));
|
|
3234
3725
|
} catch (error) {
|
|
3235
|
-
|
|
3726
|
+
const err = error;
|
|
3727
|
+
if (err.code === "ENOENT") {
|
|
3728
|
+
process.stderr.write(
|
|
3729
|
+
`Error: No .yggdrasil/ directory found. Run 'yg init' first.
|
|
3730
|
+
`
|
|
3731
|
+
);
|
|
3732
|
+
} else {
|
|
3733
|
+
process.stderr.write(`Error: ${error.message}
|
|
3236
3734
|
`);
|
|
3735
|
+
}
|
|
3237
3736
|
process.exit(1);
|
|
3238
3737
|
}
|
|
3239
3738
|
});
|
|
3240
3739
|
}
|
|
3241
3740
|
|
|
3242
3741
|
// src/io/journal-store.ts
|
|
3243
|
-
import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as
|
|
3244
|
-
import { parse as
|
|
3245
|
-
import
|
|
3742
|
+
import { readFile as readFile13, writeFile as writeFile4, mkdir as mkdir3, rename, access as access3 } from "fs/promises";
|
|
3743
|
+
import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
|
|
3744
|
+
import path17 from "path";
|
|
3246
3745
|
var JOURNAL_FILE = ".journal.yaml";
|
|
3247
3746
|
var ARCHIVE_DIR = "journals-archive";
|
|
3248
3747
|
async function readJournal(yggRoot) {
|
|
3249
|
-
const filePath =
|
|
3748
|
+
const filePath = path17.join(yggRoot, JOURNAL_FILE);
|
|
3250
3749
|
try {
|
|
3251
3750
|
const content = await readFile13(filePath, "utf-8");
|
|
3252
|
-
const raw =
|
|
3751
|
+
const raw = parseYaml6(content);
|
|
3253
3752
|
const entries = raw.entries ?? [];
|
|
3254
3753
|
return Array.isArray(entries) ? entries : [];
|
|
3255
3754
|
} catch {
|
|
@@ -3261,26 +3760,26 @@ async function appendJournalEntry(yggRoot, note, target) {
|
|
|
3261
3760
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3262
3761
|
const entry = target ? { at, target, note } : { at, note };
|
|
3263
3762
|
entries.push(entry);
|
|
3264
|
-
const filePath =
|
|
3265
|
-
const content =
|
|
3763
|
+
const filePath = path17.join(yggRoot, JOURNAL_FILE);
|
|
3764
|
+
const content = stringifyYaml({ entries });
|
|
3266
3765
|
await writeFile4(filePath, content, "utf-8");
|
|
3267
3766
|
return entry;
|
|
3268
3767
|
}
|
|
3269
3768
|
async function archiveJournal(yggRoot) {
|
|
3270
|
-
const journalPath =
|
|
3769
|
+
const journalPath = path17.join(yggRoot, JOURNAL_FILE);
|
|
3271
3770
|
try {
|
|
3272
|
-
await
|
|
3771
|
+
await access3(journalPath);
|
|
3273
3772
|
} catch {
|
|
3274
3773
|
return null;
|
|
3275
3774
|
}
|
|
3276
3775
|
const entries = await readJournal(yggRoot);
|
|
3277
3776
|
if (entries.length === 0) return null;
|
|
3278
|
-
const archiveDir =
|
|
3777
|
+
const archiveDir = path17.join(yggRoot, ARCHIVE_DIR);
|
|
3279
3778
|
await mkdir3(archiveDir, { recursive: true });
|
|
3280
3779
|
const now = /* @__PURE__ */ new Date();
|
|
3281
3780
|
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")}`;
|
|
3282
3781
|
const archiveName = `.journal.${timestamp}.yaml`;
|
|
3283
|
-
const archivePath =
|
|
3782
|
+
const archivePath = path17.join(archiveDir, archiveName);
|
|
3284
3783
|
await rename(journalPath, archivePath);
|
|
3285
3784
|
return { archiveName, entryCount: entries.length };
|
|
3286
3785
|
}
|
|
@@ -3356,9 +3855,93 @@ function registerJournalArchiveCommand(program2) {
|
|
|
3356
3855
|
});
|
|
3357
3856
|
}
|
|
3358
3857
|
|
|
3858
|
+
// src/cli/preflight.ts
|
|
3859
|
+
function registerPreflightCommand(program2) {
|
|
3860
|
+
program2.command("preflight").description("Unified diagnostic report: journal, drift, status, validation").option("--quick", "Skip drift detection for faster results").action(async (options) => {
|
|
3861
|
+
try {
|
|
3862
|
+
const cwd = process.cwd();
|
|
3863
|
+
const graph = await loadGraph(cwd);
|
|
3864
|
+
const yggRoot = await findYggRoot(cwd);
|
|
3865
|
+
const journalEntries = await readJournal(yggRoot);
|
|
3866
|
+
const driftedEntries = options.quick ? [] : (await detectDrift(graph)).entries.filter((e) => e.status !== "ok");
|
|
3867
|
+
const nodeCount = graph.nodes.size;
|
|
3868
|
+
const aspectCount = graph.aspects.length;
|
|
3869
|
+
const flowCount = graph.flows.length;
|
|
3870
|
+
let mappedPathCount = 0;
|
|
3871
|
+
for (const node of graph.nodes.values()) {
|
|
3872
|
+
mappedPathCount += normalizeMappingPaths(node.meta.mapping).length;
|
|
3873
|
+
}
|
|
3874
|
+
const validation = await validate(graph, "all");
|
|
3875
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
3876
|
+
const warnings = validation.issues.filter((i) => i.severity === "warning");
|
|
3877
|
+
const lines = [];
|
|
3878
|
+
lines.push("=== Preflight Report ===");
|
|
3879
|
+
lines.push("");
|
|
3880
|
+
if (journalEntries.length === 0) {
|
|
3881
|
+
lines.push("Journal: clean");
|
|
3882
|
+
} else {
|
|
3883
|
+
lines.push(`Journal: ${journalEntries.length} pending entries`);
|
|
3884
|
+
for (const entry of journalEntries) {
|
|
3885
|
+
const target = entry.target ? ` [${entry.target}]` : "";
|
|
3886
|
+
lines.push(` - ${entry.note}${target}`);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
lines.push("");
|
|
3890
|
+
if (options.quick) {
|
|
3891
|
+
lines.push("Drift: skipped (--quick)");
|
|
3892
|
+
} else if (driftedEntries.length === 0) {
|
|
3893
|
+
lines.push("Drift: clean");
|
|
3894
|
+
} else {
|
|
3895
|
+
lines.push(`Drift: ${driftedEntries.length} nodes need attention`);
|
|
3896
|
+
for (const entry of driftedEntries) {
|
|
3897
|
+
lines.push(` - ${entry.nodePath}: ${entry.status}`);
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
lines.push("");
|
|
3901
|
+
lines.push(
|
|
3902
|
+
`Status: ${nodeCount} nodes, ${aspectCount} aspects, ${flowCount} flows, ${mappedPathCount} mapped paths`
|
|
3903
|
+
);
|
|
3904
|
+
if (nodeCount === 0) {
|
|
3905
|
+
lines.push("");
|
|
3906
|
+
lines.push(" \u26A1 No nodes found. Enter BOOTSTRAP MODE:");
|
|
3907
|
+
lines.push(" Create nodes under .yggdrasil/model/ for your active work area.");
|
|
3908
|
+
lines.push(" See: yg help build-context");
|
|
3909
|
+
}
|
|
3910
|
+
lines.push("");
|
|
3911
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
3912
|
+
lines.push("Validation: clean");
|
|
3913
|
+
} else {
|
|
3914
|
+
const parts = [];
|
|
3915
|
+
if (errors.length > 0) parts.push(`${errors.length} errors`);
|
|
3916
|
+
if (warnings.length > 0) parts.push(`${warnings.length} warnings`);
|
|
3917
|
+
lines.push(`Validation: ${parts.join(", ")}`);
|
|
3918
|
+
for (const issue of [...errors, ...warnings]) {
|
|
3919
|
+
const code = issue.code ? `[${issue.code}] ` : "";
|
|
3920
|
+
const loc = issue.nodePath ? `${issue.nodePath} -> ` : "";
|
|
3921
|
+
lines.push(` - ${code}${loc}${issue.message}`);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
lines.push("");
|
|
3925
|
+
process.stdout.write(lines.join("\n"));
|
|
3926
|
+
const hasIssues = journalEntries.length > 0 || !options.quick && driftedEntries.length > 0 || errors.length > 0;
|
|
3927
|
+
process.exit(hasIssues ? 1 : 0);
|
|
3928
|
+
} catch (error) {
|
|
3929
|
+
process.stderr.write(`Error: ${error.message}
|
|
3930
|
+
`);
|
|
3931
|
+
process.exit(1);
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3359
3936
|
// src/bin.ts
|
|
3937
|
+
import { readFileSync } from "fs";
|
|
3938
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3939
|
+
import { dirname, join } from "path";
|
|
3940
|
+
var __filename = fileURLToPath3(import.meta.url);
|
|
3941
|
+
var __dirname = dirname(__filename);
|
|
3942
|
+
var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
3360
3943
|
var program = new Command();
|
|
3361
|
-
program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(
|
|
3944
|
+
program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
|
|
3362
3945
|
registerInitCommand(program);
|
|
3363
3946
|
registerBuildCommand(program);
|
|
3364
3947
|
registerValidateCommand(program);
|
|
@@ -3369,8 +3952,11 @@ registerTreeCommand(program);
|
|
|
3369
3952
|
registerOwnerCommand(program);
|
|
3370
3953
|
registerDepsCommand(program);
|
|
3371
3954
|
registerImpactCommand(program);
|
|
3955
|
+
registerAspectsCommand(program);
|
|
3956
|
+
registerFlowsCommand(program);
|
|
3372
3957
|
registerJournalAddCommand(program);
|
|
3373
3958
|
registerJournalReadCommand(program);
|
|
3374
3959
|
registerJournalArchiveCommand(program);
|
|
3960
|
+
registerPreflightCommand(program);
|
|
3375
3961
|
program.parse();
|
|
3376
3962
|
//# sourceMappingURL=bin.js.map
|