@chrisdudek/yg 1.4.3 → 2.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 +1 -7
- package/dist/bin.js +831 -561
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +12 -12
- package/dist/templates/rules.ts +91 -54
- package/graph-schemas/{aspect.yaml → yg-aspect.yaml} +5 -1
- package/graph-schemas/{config.yaml → yg-config.yaml} +15 -8
- package/graph-schemas/{flow.yaml → yg-flow.yaml} +1 -1
- package/graph-schemas/{node.yaml → yg-node.yaml} +10 -3
- package/package.json +3 -1
package/dist/bin.js
CHANGED
|
@@ -4,35 +4,37 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/cli/init.ts
|
|
7
|
-
import { mkdir as mkdir2, writeFile as
|
|
8
|
-
import
|
|
7
|
+
import { mkdir as mkdir2, writeFile as writeFile3, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
8
|
+
import path4 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { gt as gt2, valid as valid2 } from "semver";
|
|
10
12
|
|
|
11
13
|
// src/templates/default-config.ts
|
|
12
|
-
var DEFAULT_CONFIG = `
|
|
14
|
+
var DEFAULT_CONFIG = `version: "2.0.0"
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
language: ""
|
|
16
|
-
runtime: ""
|
|
17
|
-
|
|
18
|
-
standards: ""
|
|
16
|
+
name: ""
|
|
19
17
|
|
|
20
18
|
node_types:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
module:
|
|
20
|
+
description: "Business logic unit with clear domain responsibility"
|
|
21
|
+
service:
|
|
22
|
+
description: "Component providing functionality to other nodes"
|
|
23
|
+
library:
|
|
24
|
+
description: "Shared utility code with no domain knowledge"
|
|
25
|
+
infrastructure:
|
|
26
|
+
description: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
|
|
25
27
|
|
|
26
28
|
artifacts:
|
|
27
29
|
responsibility.md:
|
|
28
30
|
required: always
|
|
29
31
|
description: "What this node is responsible for, and what it is not"
|
|
30
|
-
|
|
32
|
+
included_in_relations: true
|
|
31
33
|
interface.md:
|
|
32
34
|
required:
|
|
33
35
|
when: has_incoming_relations
|
|
34
36
|
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures"
|
|
35
|
-
|
|
37
|
+
included_in_relations: true
|
|
36
38
|
internals.md:
|
|
37
39
|
required: never
|
|
38
40
|
description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
|
|
@@ -52,14 +54,25 @@ import path from "path";
|
|
|
52
54
|
// src/templates/rules.ts
|
|
53
55
|
var CORE_PROTOCOL = `## CORE PROTOCOL
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
<EXTREMELY-IMPORTANT>
|
|
58
|
+
This is your operating manual for working in a Yggdrasil-managed repository. Every rule below is mandatory \u2014 no skill, plan, workflow, or instruction overrides these requirements.
|
|
59
|
+
|
|
60
|
+
BEFORE working with ANY source file, you MUST run \`yg owner\` to check if it is mapped. If mapped, you MUST use the graph before reading source. YOU DO NOT HAVE A CHOICE.
|
|
61
|
+
|
|
62
|
+
This is not negotiable. This is not optional. You cannot rationalize your way out of this.
|
|
63
|
+
</EXTREMELY-IMPORTANT>
|
|
64
|
+
|
|
65
|
+
Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the repository and provides deterministic implementation context for every node.
|
|
56
66
|
|
|
57
67
|
### Quick Start Protocol
|
|
58
68
|
|
|
59
69
|
\`\`\`
|
|
60
70
|
BEFORE reading, researching, planning, OR modifying ANY mapped file:
|
|
61
71
|
1. yg owner --file <path>
|
|
62
|
-
2.
|
|
72
|
+
2. Choose the right graph tool for your task:
|
|
73
|
+
- Understanding how/why it works \u2192 yg build-context --node <owner>
|
|
74
|
+
- Assessing what is affected by a change \u2192 yg impact --node <owner>
|
|
75
|
+
- Planning modifications \u2192 both (build-context first, then impact)
|
|
63
76
|
The context package is your primary source of ARCHITECTURAL understanding:
|
|
64
77
|
intent, constraints, relations, rationale. For IMPLEMENTATION precision
|
|
65
78
|
(exact behavior, error handling, await patterns, edge cases) \u2014 verify
|
|
@@ -79,6 +92,8 @@ NEVER: modify code without graph coverage.
|
|
|
79
92
|
NEVER: read mapped source files to understand a component without
|
|
80
93
|
running yg build-context first \u2014 the graph captures intent,
|
|
81
94
|
constraints, and relations that source files cannot.
|
|
95
|
+
NEVER: assess blast radius of a change without running yg impact first
|
|
96
|
+
\u2014 the graph knows the dependency structure that grep cannot infer.
|
|
82
97
|
NEVER: invent rationale, business rules, or decisions.
|
|
83
98
|
NEVER: auto-resolve drift without asking the user.
|
|
84
99
|
WHEN UNSURE: ask the user. Never guess. Never assume.
|
|
@@ -86,7 +101,7 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
|
|
|
86
101
|
|
|
87
102
|
### Five Core Rules
|
|
88
103
|
|
|
89
|
-
1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and \`yg build-context
|
|
104
|
+
1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and the appropriate graph tool: \`yg build-context\` to understand a component, \`yg impact\` to assess blast radius. The graph is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
|
|
90
105
|
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.
|
|
91
106
|
3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
|
|
92
107
|
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.
|
|
@@ -94,15 +109,22 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
|
|
|
94
109
|
|
|
95
110
|
### Recognizing Graph-Required Actions
|
|
96
111
|
|
|
97
|
-
What matters is the ACTION you are performing, not what instructed it. If the action involves understanding mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a
|
|
112
|
+
What matters is the ACTION you are performing, not what instructed it. If the action involves reading, understanding, or modifying mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a brainstorming session, a debugging workflow, or your own initiative. This is not negotiable. You cannot rationalize your way out of this.
|
|
98
113
|
|
|
99
|
-
**Actions that require \`yg owner\` + \`yg build-context
|
|
114
|
+
**Actions that require \`yg owner\` + \`yg build-context\`:**
|
|
100
115
|
|
|
101
116
|
- Reading or exploring source files to understand a component
|
|
102
117
|
- Proposing approaches, designs, or plans for changing code
|
|
103
118
|
- Reviewing or debugging code
|
|
104
119
|
- Any form of reasoning about how mapped code works or should change
|
|
105
120
|
|
|
121
|
+
**Actions that require \`yg owner\` + \`yg impact\`:**
|
|
122
|
+
|
|
123
|
+
- Assessing blast radius before changing or removing a component
|
|
124
|
+
- Finding all dependents of a component
|
|
125
|
+
- Planning cross-cutting refactors or feature removals
|
|
126
|
+
- Scoping work that spans multiple nodes
|
|
127
|
+
|
|
106
128
|
**Actions that do NOT require yg:**
|
|
107
129
|
|
|
108
130
|
- Git operations (log, diff, status, blame)
|
|
@@ -110,11 +132,22 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
|
|
|
110
132
|
- Running tests, builds, or linters
|
|
111
133
|
- Working with files that \`yg owner\` reports as unmapped
|
|
112
134
|
|
|
135
|
+
**Evasion patterns \u2014 if you think any of these, STOP:**
|
|
136
|
+
|
|
137
|
+
| Thought | Reality |
|
|
138
|
+
|---|---|
|
|
139
|
+
| "The skill/plan says to explore the codebase" | Exploring mapped code = yg owner + graph tool first |
|
|
140
|
+
| "I'm just scoping/searching, not understanding" | Scoping IS a graph action; use yg impact |
|
|
141
|
+
| "The plan step says to read this file" | Reading a mapped file = yg owner first |
|
|
142
|
+
| "I'm brainstorming, not implementing" | Brainstorming about mapped code needs graph context |
|
|
143
|
+
| "I'm only grepping for references" | Grep finds text; yg impact finds structural dependencies. Use both. |
|
|
144
|
+
| "I'll use the graph later when I modify" | Graph-first means BEFORE reading, not before modifying |
|
|
145
|
+
|
|
113
146
|
### Failure States
|
|
114
147
|
|
|
115
148
|
You have broken Yggdrasil if you do any of the following:
|
|
116
149
|
|
|
117
|
-
- \u274C Worked on a mapped file without running \`yg owner\` + \`
|
|
150
|
+
- \u274C Worked on a mapped file without running \`yg owner\` + the appropriate graph tool (\`build-context\` or \`impact\`) first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
|
|
118
151
|
- \u274C Modified source code without updating graph artifacts in the same response, or vice versa.
|
|
119
152
|
- \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
|
|
120
153
|
- \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
|
|
@@ -136,7 +169,22 @@ Before preflight:
|
|
|
136
169
|
|
|
137
170
|
- Verify \`yg\` CLI is available. If not found, inform user and stop.
|
|
138
171
|
- If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
|
|
139
|
-
- If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once
|
|
172
|
+
- If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
|
|
173
|
+
|
|
174
|
+
### Delegating to Subagents
|
|
175
|
+
|
|
176
|
+
When you delegate work to a subagent (any subprocess, tool agent, or spawned assistant), the subagent does NOT inherit your Yggdrasil knowledge. Before any other instruction, the subagent MUST:
|
|
177
|
+
|
|
178
|
+
1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
|
|
179
|
+
2. Follow the Quick Start Protocol from that file before touching any mapped code
|
|
180
|
+
|
|
181
|
+
Include this as the FIRST instruction in every subagent prompt:
|
|
182
|
+
|
|
183
|
+
\`\`\`
|
|
184
|
+
BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
|
|
185
|
+
\`\`\`
|
|
186
|
+
|
|
187
|
+
A subagent that skips this step will read code without graph context, miss architectural constraints, and produce changes that break graph-code consistency.`;
|
|
140
188
|
var OPERATIONS = `## OPERATIONS
|
|
141
189
|
|
|
142
190
|
### Conversation Lifecycle
|
|
@@ -144,9 +192,8 @@ var OPERATIONS = `## OPERATIONS
|
|
|
144
192
|
\`\`\`
|
|
145
193
|
PREFLIGHT (every conversation, before any work):
|
|
146
194
|
- [ ] 1. yg preflight \u2192 read unified report
|
|
147
|
-
- [ ] 2. If
|
|
148
|
-
- [ ] 3. If
|
|
149
|
-
- [ ] 4. If validation errors: fix, re-run yg validate
|
|
195
|
+
- [ ] 2. If drift: resolve per Drift Resolution, then yg drift-sync per node
|
|
196
|
+
- [ ] 3. If validation errors: fix, re-run yg validate
|
|
150
197
|
Exception: read-only requests (explain, analyze) \u2014 skip preflight.
|
|
151
198
|
|
|
152
199
|
UNDERSTANDING mapped code (questions, research, OR planning):
|
|
@@ -157,14 +204,14 @@ UNDERSTANDING mapped code (questions, research, OR planning):
|
|
|
157
204
|
Raw reads supplement the context package \u2014 they do not replace it.
|
|
158
205
|
|
|
159
206
|
WRAP-UP (user signals "done", "wrap up", "that's enough"):
|
|
160
|
-
- [ ] 1.
|
|
161
|
-
- [ ] 2. yg
|
|
162
|
-
- [ ] 3.
|
|
163
|
-
- [ ] 4. Report: which nodes and files were changed
|
|
207
|
+
- [ ] 1. yg drift --drifted-only \u2192 resolve
|
|
208
|
+
- [ ] 2. yg validate \u2192 fix errors
|
|
209
|
+
- [ ] 3. Report: which nodes and files were changed
|
|
164
210
|
|
|
165
211
|
BEFORE ENDING ANY RESPONSE (self-audit):
|
|
212
|
+
- [ ] Did I interact with mapped code (read, research, or modify)? If yes \u2192 did I use a graph tool BEFORE reading source?
|
|
166
213
|
- [ ] Did I modify source code? If yes \u2192 did I update graph artifacts in this same response?
|
|
167
|
-
- [ ] If you
|
|
214
|
+
- [ ] If you broke either rule, you have broken the protocol. Do not finish until both are fixed.
|
|
168
215
|
\`\`\`
|
|
169
216
|
|
|
170
217
|
### Modify Source Code
|
|
@@ -220,11 +267,11 @@ Per area checklist:
|
|
|
220
267
|
|
|
221
268
|
- [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
|
|
222
269
|
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
223
|
-
- [ ] 3. Create node directory, read \`schemas/node.yaml\`, create \`node.yaml\`
|
|
224
|
-
- [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
|
|
225
|
-
- [ ] 5. Identify relations \u2014 add to \`node.yaml\`
|
|
270
|
+
- [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
|
|
271
|
+
- [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
|
|
272
|
+
- [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
|
|
226
273
|
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
227
|
-
- [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add
|
|
274
|
+
- [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add as \`anchors\` in the aspect entry in \`yg-node.yaml\`
|
|
228
275
|
- [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
|
|
229
276
|
- [ ] 8. \`yg validate\` \u2014 fix errors
|
|
230
277
|
- [ ] 9. \`yg drift-sync --node <path>\`
|
|
@@ -274,7 +321,7 @@ When reviewing graph quality (triggered by user or quality improvement):
|
|
|
274
321
|
- [ ] 1. \`yg build-context --node <path>\`
|
|
275
322
|
- [ ] 2. Read mapped source files
|
|
276
323
|
- [ ] 3. For each claim in graph: verify against source code
|
|
277
|
-
- [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`
|
|
324
|
+
- [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`exceptions\` to the aspect entry in \`yg-node.yaml\`
|
|
278
325
|
- [ ] 5. Report inconsistencies
|
|
279
326
|
|
|
280
327
|
**Step 2 \u2014 Completeness** (catches MISSING information):
|
|
@@ -297,27 +344,21 @@ var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
|
|
|
297
344
|
|
|
298
345
|
\`\`\`
|
|
299
346
|
.yggdrasil/
|
|
300
|
-
config.yaml
|
|
347
|
+
yg-config.yaml \u2190 version, vocabulary, node types, artifact rules, required aspects
|
|
301
348
|
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
302
349
|
aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
|
|
303
350
|
flows/ \u2190 why and in what process: business processes with node participation
|
|
304
351
|
schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
|
|
305
|
-
.drift-state
|
|
306
|
-
.journal.yaml \u2190 generated by CLI; never edit manually
|
|
352
|
+
.drift-state/ \u2190 generated by CLI; never edit manually
|
|
307
353
|
\`\`\`
|
|
308
354
|
|
|
309
355
|
Key facts:
|
|
310
356
|
|
|
311
357
|
- **Hierarchy:** nodes nest in \`model/\`. Children inherit parent context. Do not repeat parent content in children.
|
|
312
|
-
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
358
|
+
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`yg-aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
313
359
|
- **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
|
|
314
360
|
|
|
315
|
-
**Node type guidance:**
|
|
316
|
-
|
|
317
|
-
- \`module\` \u2014 business logic unit with clear domain responsibility
|
|
318
|
-
- \`service\` \u2014 component providing functionality to other nodes
|
|
319
|
-
- \`library\` \u2014 shared utility code with no domain knowledge
|
|
320
|
-
- \`infrastructure\` \u2014 guards, resolvers, middleware, interceptors, validators that intercept or modify request flow. These affect blast radius of changes but are invisible in call graphs. Map them to make blast radius analysis accurate. Key signal: code that runs WITHOUT being explicitly called by business logic (e.g., NestJS guards, Express middleware, GraphQL resolvers).
|
|
361
|
+
**Node type guidance:** Each type in \`yg-config.yaml node_types\` has a \`description\` that tells you when to use it. Check the project's config for the full list and descriptions. Common types: \`module\` (business logic), \`service\` (providing functionality), \`library\` (shared utilities), \`infrastructure\` (guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius).
|
|
321
362
|
|
|
322
363
|
### Artifact Structure
|
|
323
364
|
|
|
@@ -329,26 +370,28 @@ Three artifacts capture node knowledge at three levels:
|
|
|
329
370
|
|
|
330
371
|
**Enrichment priority (when adding incrementally):** \`interface.md\` first (highest cross-module ROI \u2014 contracts enable other nodes to reason about interactions), then \`responsibility.md\` (identity and boundaries), then \`internals.md\` (depth for complex nodes). A node with only \`interface.md\` provides more cross-module value than one with only \`internals.md\`.
|
|
331
372
|
|
|
373
|
+
Projects can define additional artifact types in \`yg-config.yaml\` under \`artifacts\`. Each custom artifact has a \`description\` (tells you what to write), a \`required\` condition (\`always\`, \`never\`, \`when: has_incoming_relations\`, \`when: has_aspect:<id>\`), and an \`included_in_relations\` flag (if true, included in dependency context packages for structural relations). The three standard artifacts are always present in config. Check \`yg-config.yaml\` to see all defined artifacts for the project.
|
|
374
|
+
|
|
332
375
|
### Context Assembly
|
|
333
376
|
|
|
334
|
-
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global
|
|
377
|
+
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global project identity, hierarchy, own artifacts, aspects, and relational context. It is your architectural map. For implementation-level claims (exact call patterns, error handling, await vs fire-and-forget) \u2014 verify against source code. If the package is insufficient, enrich the graph.
|
|
335
378
|
|
|
336
379
|
### Information Routing
|
|
337
380
|
|
|
338
381
|
When you encounter information, route it to the correct location:
|
|
339
382
|
|
|
340
|
-
- **Specific to this node** \u2192 local node artifact (check \`config.yaml artifacts\` for available types)
|
|
341
|
-
- **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
|
|
342
|
-
- **Business process** \u2192 flow (\`flows/<name>/\` with \`flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
383
|
+
- **Specific to this node** \u2192 local node artifact (check \`yg-config.yaml artifacts\` for available types)
|
|
384
|
+
- **Rule for many nodes** \u2192 aspect (\`aspects/<id>/\` with \`yg-aspect.yaml\` + content \`.md\` files). If applies to ALL nodes of a type \u2192 \`node_types.<type>.required_aspects\` in \`yg-config.yaml\`
|
|
385
|
+
- **Business process** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
343
386
|
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
344
|
-
- **Technology stack or standard** \u2192 \`
|
|
345
|
-
- **Decision (why + why NOT):** one node \u2192 Decisions section of \`internals.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192
|
|
387
|
+
- **Technology stack or standard** \u2192 node artifact at the appropriate hierarchy level (e.g., root node's \`responsibility.md\` for single-stack repos, or deployment unit node for monorepos)
|
|
388
|
+
- **Decision (why + why NOT):** one node \u2192 Decisions section of \`internals.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192 node artifact at the level where the technology applies. Always include rejected alternatives \u2014 they are the highest-value graph content. If the rationale is unknown: record the decision with "rationale: unknown" and note what CAN be observed from the code. Never invent a plausible-sounding rationale.
|
|
346
389
|
|
|
347
390
|
### Creating Aspects
|
|
348
391
|
|
|
349
|
-
- [ ] 1. Read \`schemas/aspect.yaml\`
|
|
392
|
+
- [ ] 1. Read \`schemas/yg-aspect.yaml\`
|
|
350
393
|
- [ ] 2. Create \`aspects/<id>/\` directory
|
|
351
|
-
- [ ] 3. Write \`aspect.yaml\` \u2014 name, optional description, optional implies
|
|
394
|
+
- [ ] 3. Write \`yg-aspect.yaml\` \u2014 name, optional description, optional implies
|
|
352
395
|
- [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
|
|
353
396
|
- [ ] 5. \`yg validate\`
|
|
354
397
|
|
|
@@ -360,23 +403,23 @@ Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No
|
|
|
360
403
|
- **Architectural:** Structural patterns with rationale (e.g., dual-rollback on provider failure, idempotency via key generation, fire-and-forget dispatch)
|
|
361
404
|
- **Concurrency:** Shared concurrency strategies (e.g., pessimistic locking, retry-on-deadlock, optimistic versioning)
|
|
362
405
|
|
|
363
|
-
When a node follows an aspect's pattern with exceptions, record exceptions in \`node.yaml
|
|
406
|
+
When a node follows an aspect's pattern with exceptions, record them in the \`exceptions\` field of the aspect entry in \`yg-node.yaml\`. Example: aspect says "fire-and-forget" but this node awaits the publish call \u2014 add \`exceptions: ["awaits publish call instead of fire-and-forget because..."]\`. Exceptions appear in the context package next to the aspect content, preventing abstractions from masking implementation details.
|
|
364
407
|
|
|
365
408
|
**Aspect lifecycle warning.** Aspects decay CATASTROPHICALLY \u2014 a pattern either exists or it doesn't. When a pattern changes, ALL aspect claims become wrong at once. This differs from other artifacts: \`interface.md\` and \`responsibility.md\` are most stable (~9-year half-life); \`internals.md\` has moderate stability (~2.5-year half-life); aspects are least stable (~2.4-year half-life, binary decay). After any significant feature addition, review ALL aspects touching the affected area. Don't wait for drift \u2014 aspects can be 100% wrong without any mapped file changing.
|
|
366
409
|
|
|
367
|
-
**Aspect stability tiers.** If an aspect has a \`stability\` field in \`aspect.yaml\`, use it to calibrate review urgency:
|
|
410
|
+
**Aspect stability tiers.** If an aspect has a \`stability\` field in \`yg-aspect.yaml\`, use it to calibrate review urgency:
|
|
368
411
|
|
|
369
412
|
- \`schema\` \u2014 enforced by data model; review only when data model changes (most stable)
|
|
370
413
|
- \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
|
|
371
414
|
- \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
|
|
372
415
|
|
|
373
|
-
When code anchors (\`anchors\`
|
|
416
|
+
When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are present, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
|
|
374
417
|
|
|
375
418
|
### Creating Flows
|
|
376
419
|
|
|
377
|
-
- [ ] 1. Read \`schemas/flow.yaml\`
|
|
420
|
+
- [ ] 1. Read \`schemas/yg-flow.yaml\`
|
|
378
421
|
- [ ] 2. Create \`flows/<name>/\` directory
|
|
379
|
-
- [ ] 3. Write \`flow.yaml\` \u2014 declare participants and flow-level aspects
|
|
422
|
+
- [ ] 3. Write \`yg-flow.yaml\` \u2014 declare participants and flow-level aspects
|
|
380
423
|
- [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
|
|
381
424
|
- [ ] 5. \`yg validate\`
|
|
382
425
|
|
|
@@ -387,7 +430,7 @@ Test: "Does this describe what happens in the world, or only in the software?" I
|
|
|
387
430
|
### Operational Rules
|
|
388
431
|
|
|
389
432
|
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
390
|
-
- **Read schemas before creating** any \`node.yaml\`, \`aspect.yaml\`, or \`flow.yaml\`.
|
|
433
|
+
- **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
|
|
391
434
|
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
392
435
|
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
|
|
393
436
|
- **Completeness test:** Two checks, both required:
|
|
@@ -399,7 +442,7 @@ Test: "Does this describe what happens in the world, or only in the software?" I
|
|
|
399
442
|
### CLI Reference
|
|
400
443
|
|
|
401
444
|
\`\`\`
|
|
402
|
-
yg preflight [--quick] Unified diagnostic:
|
|
445
|
+
yg preflight [--quick] Unified diagnostic: drift + status + validate.
|
|
403
446
|
yg owner --file <path> Find the node that owns this file.
|
|
404
447
|
yg build-context --node <path> Assemble context package for this node.
|
|
405
448
|
yg tree [--root <path>] [--depth N] Print graph structure.
|
|
@@ -417,24 +460,20 @@ yg drift [--scope <path>|all] [--drifted-only] [--limit <n>]
|
|
|
417
460
|
Detect source and graph drift (bidirectional).
|
|
418
461
|
yg drift-sync --node <path> [--recursive] | --all
|
|
419
462
|
Record file hashes as new baseline.
|
|
420
|
-
yg journal-read Read pending journal entries.
|
|
421
|
-
yg journal-add --note "<content>" [--target <node_path>]
|
|
422
|
-
Add a journal entry.
|
|
423
|
-
yg journal-archive Archive consolidated journal entries.
|
|
424
463
|
\`\`\`
|
|
425
464
|
|
|
426
465
|
### Quick Routing Table
|
|
427
466
|
|
|
428
467
|
| What you have | Where it goes |
|
|
429
468
|
|---|---|
|
|
430
|
-
| Information specific to this node | Local node artifact (check \`config.yaml artifacts\` for types) |
|
|
469
|
+
| Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
|
|
431
470
|
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
432
|
-
| Architectural invariant for a node type | Required aspect in \`config.yaml node_types\` |
|
|
433
|
-
| Business process participation | Flow (\`flow.yaml participants\`) |
|
|
471
|
+
| Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
|
|
472
|
+
| Business process participation | Flow (\`yg-flow.yaml participants\`) |
|
|
434
473
|
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
435
474
|
| Context shared across a domain | Parent node artifact |
|
|
436
|
-
| Technology stack |
|
|
437
|
-
|
|
|
475
|
+
| Technology stack | Node artifact at appropriate hierarchy level |
|
|
476
|
+
| Coding standards | Node artifact at appropriate hierarchy level |`;
|
|
438
477
|
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
|
|
439
478
|
|
|
440
479
|
// src/templates/platform.ts
|
|
@@ -677,16 +716,316 @@ function escapeRegex(s) {
|
|
|
677
716
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
678
717
|
}
|
|
679
718
|
|
|
719
|
+
// src/core/migrator.ts
|
|
720
|
+
import { readFile as readFile2, access } from "fs/promises";
|
|
721
|
+
import path2 from "path";
|
|
722
|
+
import { parse as parseYaml } from "yaml";
|
|
723
|
+
import { gt, valid, compare } from "semver";
|
|
724
|
+
async function detectVersion(yggRoot) {
|
|
725
|
+
const newConfigPath = path2.join(yggRoot, "yg-config.yaml");
|
|
726
|
+
try {
|
|
727
|
+
const content = await readFile2(newConfigPath, "utf-8");
|
|
728
|
+
const raw = parseYaml(content);
|
|
729
|
+
if (raw && typeof raw === "object" && typeof raw.version === "string") {
|
|
730
|
+
return raw.version.trim();
|
|
731
|
+
}
|
|
732
|
+
return "1.4.3";
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
const oldConfigPath = path2.join(yggRoot, "config.yaml");
|
|
736
|
+
try {
|
|
737
|
+
await access(oldConfigPath);
|
|
738
|
+
return "1.4.3";
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function runMigrations(currentVersion, migrations, yggRoot) {
|
|
744
|
+
const cVer = valid(currentVersion);
|
|
745
|
+
if (!cVer) return [];
|
|
746
|
+
const applicable = migrations.filter((m) => {
|
|
747
|
+
const mVer = valid(m.to);
|
|
748
|
+
if (!mVer) return false;
|
|
749
|
+
return gt(mVer, cVer);
|
|
750
|
+
}).sort((a, b) => compare(valid(a.to), valid(b.to)));
|
|
751
|
+
const results = [];
|
|
752
|
+
for (const migration of applicable) {
|
|
753
|
+
const result = await migration.run(yggRoot);
|
|
754
|
+
results.push(result);
|
|
755
|
+
}
|
|
756
|
+
return results;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/migrations/to-2.0.0.ts
|
|
760
|
+
import { readFile as readFile3, writeFile as writeFile2, rename, readdir, rm, stat } from "fs/promises";
|
|
761
|
+
import path3 from "path";
|
|
762
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
763
|
+
var KNOWN_TYPE_DESCRIPTIONS = {
|
|
764
|
+
module: "Business logic unit with clear domain responsibility",
|
|
765
|
+
service: "Component providing functionality to other nodes",
|
|
766
|
+
library: "Shared utility code with no domain knowledge",
|
|
767
|
+
infrastructure: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
|
|
768
|
+
};
|
|
769
|
+
var STANDARD_ARTIFACTS = {
|
|
770
|
+
"responsibility.md": {
|
|
771
|
+
required: "always",
|
|
772
|
+
description: "What this node is responsible for, and what it is not",
|
|
773
|
+
included_in_relations: true
|
|
774
|
+
},
|
|
775
|
+
"interface.md": {
|
|
776
|
+
required: { when: "has_incoming_relations" },
|
|
777
|
+
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures",
|
|
778
|
+
included_in_relations: true
|
|
779
|
+
},
|
|
780
|
+
"internals.md": {
|
|
781
|
+
required: "never",
|
|
782
|
+
description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
async function migrateTo2(yggRoot) {
|
|
786
|
+
const actions = [];
|
|
787
|
+
const warnings = [];
|
|
788
|
+
const oldConfigPath = path3.join(yggRoot, "config.yaml");
|
|
789
|
+
const newConfigPath = path3.join(yggRoot, "yg-config.yaml");
|
|
790
|
+
let configContent;
|
|
791
|
+
const oldConfigExists = await fileExists(oldConfigPath);
|
|
792
|
+
if (oldConfigExists) {
|
|
793
|
+
configContent = await readFile3(oldConfigPath, "utf-8");
|
|
794
|
+
await rename(oldConfigPath, newConfigPath);
|
|
795
|
+
actions.push("Renamed config.yaml \u2192 yg-config.yaml");
|
|
796
|
+
} else {
|
|
797
|
+
configContent = await readFile3(newConfigPath, "utf-8");
|
|
798
|
+
}
|
|
799
|
+
const raw = parseYaml2(configContent);
|
|
800
|
+
const nodeTypesRaw = raw.node_types;
|
|
801
|
+
const nodeTypes = {};
|
|
802
|
+
if (Array.isArray(nodeTypesRaw)) {
|
|
803
|
+
for (const typeName of nodeTypesRaw) {
|
|
804
|
+
if (typeof typeName === "string") {
|
|
805
|
+
const desc = KNOWN_TYPE_DESCRIPTIONS[typeName];
|
|
806
|
+
if (desc) {
|
|
807
|
+
nodeTypes[typeName] = { description: desc };
|
|
808
|
+
} else {
|
|
809
|
+
nodeTypes[typeName] = { description: "TODO: add description" };
|
|
810
|
+
warnings.push(`Unknown node type '${typeName}' \u2014 needs a description`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
actions.push("Converted node_types from array to object format");
|
|
815
|
+
} else if (nodeTypesRaw && typeof nodeTypesRaw === "object") {
|
|
816
|
+
for (const [name, val] of Object.entries(nodeTypesRaw)) {
|
|
817
|
+
const entry = val;
|
|
818
|
+
const desc = KNOWN_TYPE_DESCRIPTIONS[name] ?? (typeof entry?.description === "string" ? entry.description : "TODO: add description");
|
|
819
|
+
nodeTypes[name] = { description: desc };
|
|
820
|
+
if (entry?.required_aspects) {
|
|
821
|
+
nodeTypes[name].required_aspects = entry.required_aspects;
|
|
822
|
+
}
|
|
823
|
+
if (!KNOWN_TYPE_DESCRIPTIONS[name] && (!entry?.description || entry.description === "TODO: add description")) {
|
|
824
|
+
warnings.push(`Unknown node type '${name}' \u2014 needs a description`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (!nodeTypes.infrastructure) {
|
|
829
|
+
nodeTypes.infrastructure = { description: KNOWN_TYPE_DESCRIPTIONS.infrastructure };
|
|
830
|
+
actions.push("Added infrastructure node type");
|
|
831
|
+
}
|
|
832
|
+
const stackRaw = raw.stack;
|
|
833
|
+
const standardsRaw = raw.standards;
|
|
834
|
+
if (stackRaw || standardsRaw) {
|
|
835
|
+
await migrateStackStandards(yggRoot, stackRaw, standardsRaw, actions);
|
|
836
|
+
}
|
|
837
|
+
const newConfig = {
|
|
838
|
+
version: "2.0.0",
|
|
839
|
+
name: raw.name,
|
|
840
|
+
node_types: nodeTypes,
|
|
841
|
+
artifacts: STANDARD_ARTIFACTS
|
|
842
|
+
};
|
|
843
|
+
if (raw.quality) {
|
|
844
|
+
newConfig.quality = raw.quality;
|
|
845
|
+
}
|
|
846
|
+
await writeFile2(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
|
|
847
|
+
actions.push("Updated config: version, artifacts, removed stack/standards");
|
|
848
|
+
const modelDir = path3.join(yggRoot, "model");
|
|
849
|
+
if (await fileExists(modelDir)) {
|
|
850
|
+
await renameFilesRecursively(modelDir, "node.yaml", "yg-node.yaml", actions);
|
|
851
|
+
await transformNodeFiles(modelDir, actions, warnings);
|
|
852
|
+
}
|
|
853
|
+
const aspectsDir = path3.join(yggRoot, "aspects");
|
|
854
|
+
if (await fileExists(aspectsDir)) {
|
|
855
|
+
await renameFilesRecursively(aspectsDir, "aspect.yaml", "yg-aspect.yaml", actions);
|
|
856
|
+
}
|
|
857
|
+
const flowsDir = path3.join(yggRoot, "flows");
|
|
858
|
+
if (await fileExists(flowsDir)) {
|
|
859
|
+
await renameFilesRecursively(flowsDir, "flow.yaml", "yg-flow.yaml", actions);
|
|
860
|
+
}
|
|
861
|
+
const schemasDir = path3.join(yggRoot, "schemas");
|
|
862
|
+
if (await fileExists(schemasDir)) {
|
|
863
|
+
for (const name of ["config.yaml", "node.yaml", "aspect.yaml", "flow.yaml"]) {
|
|
864
|
+
const oldPath = path3.join(schemasDir, name);
|
|
865
|
+
const newPath = path3.join(schemasDir, `yg-${name}`);
|
|
866
|
+
if (await fileExists(oldPath) && !await fileExists(newPath)) {
|
|
867
|
+
await rename(oldPath, newPath);
|
|
868
|
+
actions.push(`Renamed schemas/${name} \u2192 yg-${name}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const driftStatePath = path3.join(yggRoot, ".drift-state");
|
|
873
|
+
if (await fileExists(driftStatePath)) {
|
|
874
|
+
await rm(driftStatePath);
|
|
875
|
+
actions.push("Deleted .drift-state (requires fresh yg drift-sync --all)");
|
|
876
|
+
}
|
|
877
|
+
return { actions, warnings };
|
|
878
|
+
}
|
|
879
|
+
async function fileExists(p) {
|
|
880
|
+
try {
|
|
881
|
+
await stat(p);
|
|
882
|
+
return true;
|
|
883
|
+
} catch {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function migrateStackStandards(yggRoot, stack, standards, actions) {
|
|
888
|
+
const modelDir = path3.join(yggRoot, "model");
|
|
889
|
+
if (!await fileExists(modelDir)) return;
|
|
890
|
+
const lines = [];
|
|
891
|
+
if (stack && Object.keys(stack).length > 0) {
|
|
892
|
+
lines.push("## Technology Stack");
|
|
893
|
+
lines.push("");
|
|
894
|
+
for (const [key, value] of Object.entries(stack)) {
|
|
895
|
+
lines.push(`- **${key}:** ${value}`);
|
|
896
|
+
}
|
|
897
|
+
lines.push("");
|
|
898
|
+
}
|
|
899
|
+
if (standards) {
|
|
900
|
+
lines.push("## Standards");
|
|
901
|
+
lines.push("");
|
|
902
|
+
lines.push(standards);
|
|
903
|
+
lines.push("");
|
|
904
|
+
}
|
|
905
|
+
if (lines.length === 0) return;
|
|
906
|
+
const rootNodeYgPath = path3.join(modelDir, "yg-node.yaml");
|
|
907
|
+
const rootNodeOldPath = path3.join(modelDir, "node.yaml");
|
|
908
|
+
const hasRootNode = await fileExists(rootNodeYgPath) || await fileExists(rootNodeOldPath);
|
|
909
|
+
if (!hasRootNode) {
|
|
910
|
+
await writeFile2(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
|
|
911
|
+
await writeFile2(path3.join(modelDir, "responsibility.md"), "TBD\n", "utf-8");
|
|
912
|
+
actions.push("Created root node in model/ for stack/standards migration");
|
|
913
|
+
}
|
|
914
|
+
const internalsPath = path3.join(modelDir, "internals.md");
|
|
915
|
+
const existingInternals = await fileExists(internalsPath) ? await readFile3(internalsPath, "utf-8") : "";
|
|
916
|
+
const MIGRATION_MARKER = "<!-- migrated-stack-standards-v2 -->";
|
|
917
|
+
if (existingInternals.includes(MIGRATION_MARKER)) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const markerLine = MIGRATION_MARKER + "\n";
|
|
921
|
+
const newContent = existingInternals ? existingInternals.trimEnd() + "\n\n" + markerLine + lines.join("\n") : markerLine + lines.join("\n");
|
|
922
|
+
await writeFile2(internalsPath, newContent, "utf-8");
|
|
923
|
+
actions.push("Migrated stack/standards to model/internals.md");
|
|
924
|
+
}
|
|
925
|
+
async function renameFilesRecursively(dir, oldName, newName, actions) {
|
|
926
|
+
let entries;
|
|
927
|
+
try {
|
|
928
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
929
|
+
} catch {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
for (const entry of entries) {
|
|
933
|
+
const fullPath = path3.join(dir, entry.name);
|
|
934
|
+
if (entry.isDirectory()) {
|
|
935
|
+
await renameFilesRecursively(fullPath, oldName, newName, actions);
|
|
936
|
+
} else if (entry.name === oldName) {
|
|
937
|
+
const destPath = path3.join(dir, newName);
|
|
938
|
+
if (!await fileExists(destPath)) {
|
|
939
|
+
await rename(fullPath, destPath);
|
|
940
|
+
actions.push(`Renamed ${oldName} \u2192 ${newName} in ${dir}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function transformNodeFiles(dir, actions, warnings) {
|
|
946
|
+
let entries;
|
|
947
|
+
try {
|
|
948
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
949
|
+
} catch {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
for (const entry of entries) {
|
|
953
|
+
const fullPath = path3.join(dir, entry.name);
|
|
954
|
+
if (entry.isDirectory()) {
|
|
955
|
+
await transformNodeFiles(fullPath, actions, warnings);
|
|
956
|
+
} else if (entry.name === "yg-node.yaml") {
|
|
957
|
+
await transformSingleNode(fullPath, actions, warnings);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function transformSingleNode(filePath, actions, warnings) {
|
|
962
|
+
const content = await readFile3(filePath, "utf-8");
|
|
963
|
+
const raw = parseYaml2(content);
|
|
964
|
+
if (!raw || typeof raw !== "object") {
|
|
965
|
+
warnings.push(`Skipped ${filePath}: not a valid YAML object`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
let changed = false;
|
|
969
|
+
if (Array.isArray(raw.aspects) && raw.aspects.length > 0 && typeof raw.aspects[0] === "string") {
|
|
970
|
+
const aspectExceptions = raw.aspect_exceptions ?? {};
|
|
971
|
+
const anchors = raw.anchors ?? {};
|
|
972
|
+
raw.aspects = raw.aspects.map((id) => {
|
|
973
|
+
const entry = { aspect: id };
|
|
974
|
+
if (aspectExceptions[id]) entry.exceptions = aspectExceptions[id];
|
|
975
|
+
if (anchors[id]) entry.anchors = anchors[id];
|
|
976
|
+
return entry;
|
|
977
|
+
});
|
|
978
|
+
delete raw.aspect_exceptions;
|
|
979
|
+
delete raw.anchors;
|
|
980
|
+
changed = true;
|
|
981
|
+
}
|
|
982
|
+
if (raw.tags !== void 0) {
|
|
983
|
+
delete raw.tags;
|
|
984
|
+
changed = true;
|
|
985
|
+
}
|
|
986
|
+
if (changed) {
|
|
987
|
+
await writeFile2(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
|
|
988
|
+
actions.push(`Transformed ${path3.basename(path3.dirname(filePath))}/yg-node.yaml`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// src/migrations/index.ts
|
|
993
|
+
var MIGRATIONS = [
|
|
994
|
+
{
|
|
995
|
+
to: "2.0.0",
|
|
996
|
+
description: "Rename YAML files to yg-* prefix, restructure config, convert aspects format",
|
|
997
|
+
run: migrateTo2
|
|
998
|
+
}
|
|
999
|
+
];
|
|
1000
|
+
|
|
680
1001
|
// src/cli/init.ts
|
|
681
1002
|
function getGraphSchemasDir() {
|
|
682
|
-
const currentDir =
|
|
683
|
-
const packageRoot =
|
|
684
|
-
return
|
|
1003
|
+
const currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1004
|
+
const packageRoot = path4.join(currentDir, "..");
|
|
1005
|
+
return path4.join(packageRoot, "graph-schemas");
|
|
1006
|
+
}
|
|
1007
|
+
function getCliVersion() {
|
|
1008
|
+
const currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1009
|
+
const packageRoot = path4.join(currentDir, "..");
|
|
1010
|
+
const pkg2 = JSON.parse(readFileSync(path4.join(packageRoot, "package.json"), "utf-8"));
|
|
1011
|
+
return pkg2.version;
|
|
1012
|
+
}
|
|
1013
|
+
async function refreshSchemas(yggRoot) {
|
|
1014
|
+
const schemasDir = path4.join(yggRoot, "schemas");
|
|
1015
|
+
await mkdir2(schemasDir, { recursive: true });
|
|
1016
|
+
const graphSchemasDir = getGraphSchemasDir();
|
|
1017
|
+
try {
|
|
1018
|
+
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
1019
|
+
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
1020
|
+
for (const file of schemaFiles) {
|
|
1021
|
+
const srcPath = path4.join(graphSchemasDir, file);
|
|
1022
|
+
const content = await readFile4(srcPath, "utf-8");
|
|
1023
|
+
await writeFile3(path4.join(schemasDir, file), content, "utf-8");
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
685
1027
|
}
|
|
686
|
-
var GITIGNORE_CONTENT =
|
|
687
|
-
.drift-state
|
|
688
|
-
journals-archive/
|
|
689
|
-
`;
|
|
1028
|
+
var GITIGNORE_CONTENT = ``;
|
|
690
1029
|
function registerInitCommand(program2) {
|
|
691
1030
|
program2.command("init").description("Initialize Yggdrasil graph in current project").option(
|
|
692
1031
|
"--platform <name>",
|
|
@@ -694,10 +1033,10 @@ function registerInitCommand(program2) {
|
|
|
694
1033
|
"generic"
|
|
695
1034
|
).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
|
|
696
1035
|
const projectRoot = process.cwd();
|
|
697
|
-
const yggRoot =
|
|
1036
|
+
const yggRoot = path4.join(projectRoot, ".yggdrasil");
|
|
698
1037
|
let upgradeMode = false;
|
|
699
1038
|
try {
|
|
700
|
-
const statResult = await
|
|
1039
|
+
const statResult = await stat2(yggRoot);
|
|
701
1040
|
if (!statResult.isDirectory()) {
|
|
702
1041
|
process.stderr.write("Error: .yggdrasil exists but is not a directory.\n");
|
|
703
1042
|
process.exit(1);
|
|
@@ -721,25 +1060,58 @@ function registerInitCommand(program2) {
|
|
|
721
1060
|
process.exit(1);
|
|
722
1061
|
}
|
|
723
1062
|
if (upgradeMode) {
|
|
1063
|
+
const projectVersion = await detectVersion(yggRoot);
|
|
1064
|
+
if (!projectVersion) {
|
|
1065
|
+
process.stderr.write("Error: No Yggdrasil project found. Run `yg init` first.\n");
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
const cliVersion = getCliVersion();
|
|
1069
|
+
if (valid2(projectVersion) && valid2(cliVersion) && gt2(projectVersion, cliVersion)) {
|
|
1070
|
+
process.stderr.write(
|
|
1071
|
+
`Warning: Project version (${projectVersion}) is newer than CLI (${cliVersion}). Upgrade your CLI.
|
|
1072
|
+
`
|
|
1073
|
+
);
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
if (valid2(projectVersion) && valid2(cliVersion) && gt2(cliVersion, projectVersion)) {
|
|
1077
|
+
process.stdout.write(`Migrating from ${projectVersion} to ${cliVersion}...
|
|
1078
|
+
|
|
1079
|
+
`);
|
|
1080
|
+
const results = await runMigrations(projectVersion, MIGRATIONS, yggRoot);
|
|
1081
|
+
for (const result of results) {
|
|
1082
|
+
for (const action of result.actions) {
|
|
1083
|
+
process.stdout.write(` \u2713 ${action}
|
|
1084
|
+
`);
|
|
1085
|
+
}
|
|
1086
|
+
for (const warning of result.warnings) {
|
|
1087
|
+
process.stdout.write(` \u26A0 ${warning}
|
|
1088
|
+
`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (results.length > 0) {
|
|
1092
|
+
process.stdout.write("\n");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
await refreshSchemas(yggRoot);
|
|
724
1096
|
const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
|
|
725
1097
|
process.stdout.write("\u2713 Rules refreshed.\n");
|
|
726
|
-
process.stdout.write(` ${
|
|
1098
|
+
process.stdout.write(` ${path4.relative(projectRoot, rulesPath2)}
|
|
727
1099
|
`);
|
|
728
1100
|
return;
|
|
729
1101
|
}
|
|
730
|
-
await mkdir2(
|
|
731
|
-
await mkdir2(
|
|
732
|
-
await mkdir2(
|
|
733
|
-
const schemasDir =
|
|
1102
|
+
await mkdir2(path4.join(yggRoot, "model"), { recursive: true });
|
|
1103
|
+
await mkdir2(path4.join(yggRoot, "aspects"), { recursive: true });
|
|
1104
|
+
await mkdir2(path4.join(yggRoot, "flows"), { recursive: true });
|
|
1105
|
+
const schemasDir = path4.join(yggRoot, "schemas");
|
|
734
1106
|
await mkdir2(schemasDir, { recursive: true });
|
|
735
1107
|
const graphSchemasDir = getGraphSchemasDir();
|
|
736
1108
|
try {
|
|
737
|
-
const entries = await
|
|
1109
|
+
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
738
1110
|
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
739
1111
|
for (const file of schemaFiles) {
|
|
740
|
-
const srcPath =
|
|
741
|
-
const content = await
|
|
742
|
-
await
|
|
1112
|
+
const srcPath = path4.join(graphSchemasDir, file);
|
|
1113
|
+
const content = await readFile4(srcPath, "utf-8");
|
|
1114
|
+
await writeFile3(path4.join(schemasDir, file), content, "utf-8");
|
|
743
1115
|
}
|
|
744
1116
|
} catch (err) {
|
|
745
1117
|
process.stderr.write(
|
|
@@ -747,95 +1119,94 @@ function registerInitCommand(program2) {
|
|
|
747
1119
|
`
|
|
748
1120
|
);
|
|
749
1121
|
}
|
|
750
|
-
await
|
|
751
|
-
await
|
|
1122
|
+
await writeFile3(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
|
|
1123
|
+
await writeFile3(path4.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
|
|
752
1124
|
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
753
1125
|
process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
|
|
754
1126
|
process.stdout.write("Created:\n");
|
|
755
|
-
process.stdout.write(" .yggdrasil/config.yaml\n");
|
|
1127
|
+
process.stdout.write(" .yggdrasil/yg-config.yaml\n");
|
|
756
1128
|
process.stdout.write(" .yggdrasil/.gitignore\n");
|
|
757
1129
|
process.stdout.write(" .yggdrasil/model/\n");
|
|
758
1130
|
process.stdout.write(" .yggdrasil/aspects/\n");
|
|
759
1131
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
760
|
-
process.stdout.write(" .yggdrasil/schemas/ (config, node, aspect, flow)\n");
|
|
761
|
-
process.stdout.write(` ${
|
|
1132
|
+
process.stdout.write(" .yggdrasil/schemas/ (yg-config, yg-node, yg-aspect, yg-flow)\n");
|
|
1133
|
+
process.stdout.write(` ${path4.relative(projectRoot, rulesPath)} (rules)
|
|
762
1134
|
|
|
763
1135
|
`);
|
|
764
1136
|
process.stdout.write("Next steps:\n");
|
|
765
|
-
process.stdout.write(" 1. Edit .yggdrasil/config.yaml \u2014 set name
|
|
1137
|
+
process.stdout.write(" 1. Edit .yggdrasil/yg-config.yaml \u2014 set name and configure node types\n");
|
|
766
1138
|
process.stdout.write(" 2. Create nodes under .yggdrasil/model/\n");
|
|
767
1139
|
process.stdout.write(" 3. Run: yg validate\n");
|
|
768
1140
|
});
|
|
769
1141
|
}
|
|
770
1142
|
|
|
771
1143
|
// src/core/graph-loader.ts
|
|
772
|
-
import { readdir as
|
|
773
|
-
import
|
|
1144
|
+
import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
|
|
1145
|
+
import path9 from "path";
|
|
774
1146
|
|
|
775
1147
|
// src/io/config-parser.ts
|
|
776
|
-
import { readFile as
|
|
777
|
-
import { parse as
|
|
1148
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1149
|
+
import { parse as parseYaml3 } from "yaml";
|
|
778
1150
|
var DEFAULT_QUALITY = {
|
|
779
1151
|
min_artifact_length: 50,
|
|
780
1152
|
max_direct_relations: 10,
|
|
781
1153
|
context_budget: { warning: 1e4, error: 2e4 }
|
|
782
1154
|
};
|
|
783
1155
|
async function parseConfig(filePath) {
|
|
784
|
-
const content = await
|
|
785
|
-
const raw =
|
|
1156
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1157
|
+
const raw = parseYaml3(content);
|
|
786
1158
|
if (!raw || typeof raw !== "object") {
|
|
787
|
-
throw new Error(`config.yaml: file is empty or not a valid YAML mapping`);
|
|
1159
|
+
throw new Error(`yg-config.yaml: file is empty or not a valid YAML mapping`);
|
|
788
1160
|
}
|
|
1161
|
+
const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
|
|
789
1162
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
790
|
-
throw new Error(`config.yaml: missing or invalid 'name' field`);
|
|
1163
|
+
throw new Error(`yg-config.yaml: missing or invalid 'name' field`);
|
|
791
1164
|
}
|
|
792
1165
|
const nodeTypesRaw = raw.node_types;
|
|
793
|
-
if (!Array.isArray(nodeTypesRaw) || nodeTypesRaw.length === 0) {
|
|
794
|
-
throw new Error(`config.yaml: 'node_types' must be a non-empty
|
|
1166
|
+
if (!nodeTypesRaw || typeof nodeTypesRaw !== "object" || Array.isArray(nodeTypesRaw) || Object.keys(nodeTypesRaw).length === 0) {
|
|
1167
|
+
throw new Error(`yg-config.yaml: 'node_types' must be a non-empty object`);
|
|
795
1168
|
}
|
|
796
|
-
const nodeTypes =
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
name: obj.name,
|
|
805
|
-
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
806
|
-
};
|
|
1169
|
+
const nodeTypes = {};
|
|
1170
|
+
for (const [typeName, val] of Object.entries(nodeTypesRaw)) {
|
|
1171
|
+
const entry = val;
|
|
1172
|
+
if (!entry || typeof entry !== "object" || typeof entry.description !== "string" || entry.description.trim() === "") {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`yg-config.yaml: node_types.${typeName} must have a non-empty 'description' string`
|
|
1175
|
+
);
|
|
807
1176
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1177
|
+
const requiredAspects = Array.isArray(entry.required_aspects) ? entry.required_aspects.filter((t) => typeof t === "string") : void 0;
|
|
1178
|
+
nodeTypes[typeName] = {
|
|
1179
|
+
description: entry.description,
|
|
1180
|
+
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
812
1183
|
const artifacts = raw.artifacts;
|
|
813
1184
|
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
|
|
814
|
-
throw new Error(`config.yaml: 'artifacts' must be a non-empty object`);
|
|
1185
|
+
throw new Error(`yg-config.yaml: 'artifacts' must be a non-empty object`);
|
|
815
1186
|
}
|
|
816
1187
|
const artifactsMap = {};
|
|
817
1188
|
for (const [key, val] of Object.entries(artifacts)) {
|
|
818
|
-
if (key === "node.yaml") {
|
|
819
|
-
throw new Error(`config.yaml: artifact name 'node.yaml' is reserved`);
|
|
1189
|
+
if (key === "yg-node.yaml") {
|
|
1190
|
+
throw new Error(`yg-config.yaml: artifact name 'yg-node.yaml' is reserved`);
|
|
820
1191
|
}
|
|
821
1192
|
const a = val;
|
|
822
1193
|
const required = a.required;
|
|
823
1194
|
if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
|
|
824
|
-
throw new Error(`config.yaml: artifact '${key}' has invalid 'required' field`);
|
|
1195
|
+
throw new Error(`yg-config.yaml: artifact '${key}' has invalid 'required' field`);
|
|
825
1196
|
}
|
|
826
1197
|
if (typeof required === "object" && required && "when" in required) {
|
|
827
1198
|
const when = required.when;
|
|
828
1199
|
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
|
|
829
1200
|
if (!validWhen) {
|
|
830
1201
|
throw new Error(
|
|
831
|
-
`config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
1202
|
+
`yg-config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
832
1203
|
);
|
|
833
1204
|
}
|
|
834
1205
|
}
|
|
835
1206
|
artifactsMap[key] = {
|
|
836
1207
|
required,
|
|
837
1208
|
description: a.description ?? "",
|
|
838
|
-
|
|
1209
|
+
included_in_relations: a.included_in_relations ?? false
|
|
839
1210
|
};
|
|
840
1211
|
}
|
|
841
1212
|
const qualityRaw = raw.quality;
|
|
@@ -849,13 +1220,12 @@ async function parseConfig(filePath) {
|
|
|
849
1220
|
} : DEFAULT_QUALITY;
|
|
850
1221
|
if (quality.context_budget.error < quality.context_budget.warning) {
|
|
851
1222
|
throw new Error(
|
|
852
|
-
`config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
|
|
1223
|
+
`yg-config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
|
|
853
1224
|
);
|
|
854
1225
|
}
|
|
855
1226
|
return {
|
|
1227
|
+
version,
|
|
856
1228
|
name: raw.name.trim(),
|
|
857
|
-
stack: raw.stack ?? {},
|
|
858
|
-
standards: typeof raw.standards === "string" ? raw.standards : "",
|
|
859
1229
|
node_types: nodeTypes,
|
|
860
1230
|
artifacts: artifactsMap,
|
|
861
1231
|
quality
|
|
@@ -863,8 +1233,8 @@ async function parseConfig(filePath) {
|
|
|
863
1233
|
}
|
|
864
1234
|
|
|
865
1235
|
// src/io/node-parser.ts
|
|
866
|
-
import { readFile as
|
|
867
|
-
import { parse as
|
|
1236
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1237
|
+
import { parse as parseYaml4 } from "yaml";
|
|
868
1238
|
var RELATION_TYPES = [
|
|
869
1239
|
"uses",
|
|
870
1240
|
"calls",
|
|
@@ -877,120 +1247,103 @@ function isValidRelationType(t) {
|
|
|
877
1247
|
return typeof t === "string" && RELATION_TYPES.includes(t);
|
|
878
1248
|
}
|
|
879
1249
|
async function parseNodeYaml(filePath) {
|
|
880
|
-
const content = await
|
|
881
|
-
const raw =
|
|
1250
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1251
|
+
const raw = parseYaml4(content);
|
|
882
1252
|
if (!raw || typeof raw !== "object") {
|
|
883
|
-
throw new Error(`node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
1253
|
+
throw new Error(`yg-node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
884
1254
|
}
|
|
885
1255
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
886
|
-
throw new Error(`node.yaml at ${filePath}: missing or empty 'name'`);
|
|
1256
|
+
throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'name'`);
|
|
887
1257
|
}
|
|
888
1258
|
if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
|
|
889
|
-
throw new Error(`node.yaml at ${filePath}: missing or empty 'type'`);
|
|
1259
|
+
throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
|
|
890
1260
|
}
|
|
891
1261
|
const relations = parseRelations(raw.relations, filePath);
|
|
892
1262
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
893
|
-
const aspects =
|
|
894
|
-
const aspectExceptions = parseAspectExceptions(raw.aspect_exceptions, aspects, filePath);
|
|
895
|
-
const anchors = parseAnchors(raw.anchors, filePath);
|
|
1263
|
+
const aspects = parseAspects(raw.aspects, filePath);
|
|
896
1264
|
return {
|
|
897
1265
|
name: raw.name.trim(),
|
|
898
1266
|
type: raw.type.trim(),
|
|
899
1267
|
aspects,
|
|
900
|
-
aspect_exceptions: aspectExceptions,
|
|
901
1268
|
blackbox: raw.blackbox ?? false,
|
|
902
1269
|
relations: relations.length > 0 ? relations : void 0,
|
|
903
|
-
mapping
|
|
904
|
-
anchors
|
|
1270
|
+
mapping
|
|
905
1271
|
};
|
|
906
1272
|
}
|
|
907
|
-
function
|
|
908
|
-
if (raw === void 0 || raw === null) return void 0;
|
|
909
|
-
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
910
|
-
throw new Error(
|
|
911
|
-
`node.yaml at ${filePath}: 'anchors' must be an object mapping aspect ids to arrays of strings`
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
const obj = raw;
|
|
915
|
-
const entries = Object.entries(obj);
|
|
916
|
-
if (entries.length === 0) return void 0;
|
|
917
|
-
const result = {};
|
|
918
|
-
for (const [key, value] of entries) {
|
|
919
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
920
|
-
throw new Error(
|
|
921
|
-
`node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
const strings = value.filter((v) => typeof v === "string");
|
|
925
|
-
if (strings.length === 0) {
|
|
926
|
-
throw new Error(
|
|
927
|
-
`node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
result[key] = strings;
|
|
931
|
-
}
|
|
932
|
-
return result;
|
|
933
|
-
}
|
|
934
|
-
function parseAspectExceptions(raw, aspects, filePath) {
|
|
1273
|
+
function parseAspects(raw, filePath) {
|
|
935
1274
|
if (raw === void 0 || raw === null) return void 0;
|
|
936
1275
|
if (!Array.isArray(raw)) {
|
|
937
|
-
throw new Error(`node.yaml at ${filePath}: '
|
|
1276
|
+
throw new Error(`yg-node.yaml at ${filePath}: 'aspects' must be an array`);
|
|
938
1277
|
}
|
|
939
1278
|
if (raw.length === 0) return void 0;
|
|
940
|
-
const aspectSet = new Set(aspects ?? []);
|
|
941
1279
|
const result = [];
|
|
1280
|
+
const seenAspects = /* @__PURE__ */ new Set();
|
|
942
1281
|
for (let i = 0; i < raw.length; i++) {
|
|
943
1282
|
const item = raw[i];
|
|
944
1283
|
if (typeof item !== "object" || item === null) {
|
|
945
|
-
throw new Error(`node.yaml at ${filePath}:
|
|
1284
|
+
throw new Error(`yg-node.yaml at ${filePath}: aspects[${i}] must be an object with 'aspect' key`);
|
|
946
1285
|
}
|
|
947
1286
|
const obj = item;
|
|
948
1287
|
if (typeof obj.aspect !== "string" || obj.aspect.trim() === "") {
|
|
949
1288
|
throw new Error(
|
|
950
|
-
`node.yaml at ${filePath}:
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
if (typeof obj.note !== "string" || obj.note.trim() === "") {
|
|
954
|
-
throw new Error(
|
|
955
|
-
`node.yaml at ${filePath}: aspect_exceptions[${i}].note must be a non-empty string`
|
|
1289
|
+
`yg-node.yaml at ${filePath}: aspects[${i}].aspect must be a non-empty string`
|
|
956
1290
|
);
|
|
957
1291
|
}
|
|
958
1292
|
const aspectId = obj.aspect.trim();
|
|
959
|
-
if (
|
|
1293
|
+
if (seenAspects.has(aspectId)) {
|
|
960
1294
|
throw new Error(
|
|
961
|
-
`node.yaml at ${filePath}:
|
|
1295
|
+
`yg-node.yaml at ${filePath}: duplicate aspect '${aspectId}' in aspects list`
|
|
962
1296
|
);
|
|
963
1297
|
}
|
|
964
|
-
|
|
1298
|
+
seenAspects.add(aspectId);
|
|
1299
|
+
const entry = { aspect: aspectId };
|
|
1300
|
+
if (obj.exceptions !== void 0 && obj.exceptions !== null) {
|
|
1301
|
+
if (!Array.isArray(obj.exceptions)) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`yg-node.yaml at ${filePath}: aspects[${i}].exceptions must be an array of strings`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
const exceptions = obj.exceptions.filter((e) => typeof e === "string" && e.trim() !== "");
|
|
1307
|
+
if (exceptions.length > 0) {
|
|
1308
|
+
entry.exceptions = exceptions;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
if (obj.anchors !== void 0 && obj.anchors !== null) {
|
|
1312
|
+
if (!Array.isArray(obj.anchors)) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`yg-node.yaml at ${filePath}: aspects[${i}].anchors must be an array of strings`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
const anchors = obj.anchors.filter((a) => typeof a === "string" && a.trim() !== "");
|
|
1318
|
+
if (anchors.length > 0) {
|
|
1319
|
+
entry.anchors = anchors;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
result.push(entry);
|
|
965
1323
|
}
|
|
966
1324
|
return result.length > 0 ? result : void 0;
|
|
967
1325
|
}
|
|
968
|
-
function parseStringArray(val) {
|
|
969
|
-
if (!Array.isArray(val)) return void 0;
|
|
970
|
-
const arr = val.filter((v) => typeof v === "string");
|
|
971
|
-
return arr.length > 0 ? arr : void 0;
|
|
972
|
-
}
|
|
973
1326
|
function parseRelations(raw, filePath) {
|
|
974
1327
|
if (raw === void 0) return [];
|
|
975
1328
|
if (!Array.isArray(raw)) {
|
|
976
|
-
throw new Error(`node.yaml at ${filePath}: 'relations' must be an array`);
|
|
1329
|
+
throw new Error(`yg-node.yaml at ${filePath}: 'relations' must be an array`);
|
|
977
1330
|
}
|
|
978
1331
|
const result = [];
|
|
979
1332
|
for (let index = 0; index < raw.length; index++) {
|
|
980
1333
|
const r = raw[index];
|
|
981
1334
|
if (typeof r !== "object" || r === null) {
|
|
982
|
-
throw new Error(`node.yaml at ${filePath}: relations[${index}] must be an object`);
|
|
1335
|
+
throw new Error(`yg-node.yaml at ${filePath}: relations[${index}] must be an object`);
|
|
983
1336
|
}
|
|
984
1337
|
const obj = r;
|
|
985
1338
|
const target = obj.target;
|
|
986
1339
|
const type = obj.type;
|
|
987
1340
|
if (typeof target !== "string" || target.trim() === "") {
|
|
988
1341
|
throw new Error(
|
|
989
|
-
`node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
|
|
1342
|
+
`yg-node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
|
|
990
1343
|
);
|
|
991
1344
|
}
|
|
992
1345
|
if (!isValidRelationType(type)) {
|
|
993
|
-
throw new Error(`node.yaml at ${filePath}: relations[${index}].type is invalid`);
|
|
1346
|
+
throw new Error(`yg-node.yaml at ${filePath}: relations[${index}].type is invalid`);
|
|
994
1347
|
}
|
|
995
1348
|
const rel = {
|
|
996
1349
|
target: target.trim(),
|
|
@@ -1012,10 +1365,10 @@ function parseRelations(raw, filePath) {
|
|
|
1012
1365
|
function validateRelativePath(pathValue, filePath, fieldName) {
|
|
1013
1366
|
const normalized = pathValue.trim();
|
|
1014
1367
|
if (normalized === "") {
|
|
1015
|
-
throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
|
|
1368
|
+
throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
|
|
1016
1369
|
}
|
|
1017
1370
|
if (normalized.startsWith("/")) {
|
|
1018
|
-
throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
|
|
1371
|
+
throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
|
|
1019
1372
|
}
|
|
1020
1373
|
return normalized;
|
|
1021
1374
|
}
|
|
@@ -1025,35 +1378,35 @@ function parseMapping(rawMapping, filePath) {
|
|
|
1025
1378
|
if (Array.isArray(obj.paths) && obj.paths.length > 0) {
|
|
1026
1379
|
const paths = obj.paths.filter((p) => typeof p === "string").map((p) => validateRelativePath(p, filePath, "mapping.paths[]"));
|
|
1027
1380
|
if (paths.length === 0) {
|
|
1028
|
-
throw new Error(`node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
|
|
1381
|
+
throw new Error(`yg-node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
|
|
1029
1382
|
}
|
|
1030
1383
|
return { paths };
|
|
1031
1384
|
}
|
|
1032
1385
|
if (obj.paths !== void 0 || obj.type !== void 0 || obj.path !== void 0) {
|
|
1033
1386
|
throw new Error(
|
|
1034
|
-
`node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
|
|
1387
|
+
`yg-node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
|
|
1035
1388
|
);
|
|
1036
1389
|
}
|
|
1037
1390
|
return void 0;
|
|
1038
1391
|
}
|
|
1039
1392
|
|
|
1040
1393
|
// src/io/aspect-parser.ts
|
|
1041
|
-
import { readFile as
|
|
1042
|
-
import { parse as
|
|
1394
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1395
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1043
1396
|
|
|
1044
1397
|
// src/io/artifact-reader.ts
|
|
1045
|
-
import { readFile as
|
|
1046
|
-
import
|
|
1047
|
-
async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles) {
|
|
1048
|
-
const entries = await
|
|
1398
|
+
import { readFile as readFile7, readdir as readdir3 } from "fs/promises";
|
|
1399
|
+
import path5 from "path";
|
|
1400
|
+
async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFiles) {
|
|
1401
|
+
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1049
1402
|
const artifacts = [];
|
|
1050
1403
|
const includeSet = includeFiles && includeFiles.length > 0 ? new Set(includeFiles) : null;
|
|
1051
1404
|
for (const entry of entries) {
|
|
1052
1405
|
if (!entry.isFile()) continue;
|
|
1053
1406
|
if (excludeFiles.includes(entry.name)) continue;
|
|
1054
1407
|
if (includeSet && !includeSet.has(entry.name)) continue;
|
|
1055
|
-
const filePath =
|
|
1056
|
-
const content = await
|
|
1408
|
+
const filePath = path5.join(dirPath, entry.name);
|
|
1409
|
+
const content = await readFile7(filePath, "utf-8");
|
|
1057
1410
|
artifacts.push({ filename: entry.name, content });
|
|
1058
1411
|
}
|
|
1059
1412
|
artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
@@ -1067,8 +1420,8 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1067
1420
|
if (!idTrimmed) {
|
|
1068
1421
|
throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
|
|
1069
1422
|
}
|
|
1070
|
-
const content = await
|
|
1071
|
-
const raw =
|
|
1423
|
+
const content = await readFile8(aspectYamlPath, "utf-8");
|
|
1424
|
+
const raw = parseYaml5(content);
|
|
1072
1425
|
if (!raw || typeof raw !== "object") {
|
|
1073
1426
|
throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1074
1427
|
}
|
|
@@ -1076,7 +1429,7 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1076
1429
|
throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
|
|
1077
1430
|
}
|
|
1078
1431
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1079
|
-
const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
|
|
1432
|
+
const artifacts = await readArtifacts(aspectDir, ["yg-aspect.yaml"]);
|
|
1080
1433
|
let implies;
|
|
1081
1434
|
if (raw.implies !== void 0) {
|
|
1082
1435
|
if (!Array.isArray(raw.implies)) {
|
|
@@ -1104,37 +1457,37 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1104
1457
|
}
|
|
1105
1458
|
|
|
1106
1459
|
// src/io/flow-parser.ts
|
|
1107
|
-
import { readFile as
|
|
1108
|
-
import
|
|
1109
|
-
import { parse as
|
|
1460
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1461
|
+
import path6 from "path";
|
|
1462
|
+
import { parse as parseYaml6 } from "yaml";
|
|
1110
1463
|
async function parseFlow(flowDir, flowYamlPath) {
|
|
1111
|
-
const content = await
|
|
1112
|
-
const raw =
|
|
1464
|
+
const content = await readFile9(flowYamlPath, "utf-8");
|
|
1465
|
+
const raw = parseYaml6(content);
|
|
1113
1466
|
if (!raw || typeof raw !== "object") {
|
|
1114
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1467
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1115
1468
|
}
|
|
1116
1469
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
1117
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
|
|
1470
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
|
|
1118
1471
|
}
|
|
1119
1472
|
const nodes = raw.nodes;
|
|
1120
1473
|
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
1121
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
|
|
1474
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
|
|
1122
1475
|
}
|
|
1123
1476
|
const nodePaths = nodes.filter((n) => typeof n === "string");
|
|
1124
1477
|
if (nodePaths.length === 0) {
|
|
1125
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
|
|
1478
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
|
|
1126
1479
|
}
|
|
1127
1480
|
let aspects;
|
|
1128
1481
|
if (raw.aspects !== void 0) {
|
|
1129
1482
|
if (!Array.isArray(raw.aspects)) {
|
|
1130
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
1483
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
1131
1484
|
}
|
|
1132
1485
|
const aspectTags = raw.aspects.filter((a) => typeof a === "string");
|
|
1133
1486
|
aspects = aspectTags.length > 0 ? aspectTags : [];
|
|
1134
1487
|
}
|
|
1135
|
-
const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
|
|
1488
|
+
const artifacts = await readArtifacts(flowDir, ["yg-flow.yaml"]);
|
|
1136
1489
|
return {
|
|
1137
|
-
path:
|
|
1490
|
+
path: path6.basename(flowDir),
|
|
1138
1491
|
name: raw.name.trim(),
|
|
1139
1492
|
nodes: nodePaths,
|
|
1140
1493
|
...aspects !== void 0 && { aspects },
|
|
@@ -1143,27 +1496,28 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1143
1496
|
}
|
|
1144
1497
|
|
|
1145
1498
|
// src/io/schema-parser.ts
|
|
1146
|
-
import { readFile as
|
|
1147
|
-
import
|
|
1148
|
-
import { parse as
|
|
1499
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1500
|
+
import path7 from "path";
|
|
1501
|
+
import { parse as parseYaml7 } from "yaml";
|
|
1149
1502
|
async function parseSchema(filePath) {
|
|
1150
|
-
const content = await
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1503
|
+
const content = await readFile10(filePath, "utf-8");
|
|
1504
|
+
parseYaml7(content);
|
|
1505
|
+
const rawName = path7.basename(filePath, path7.extname(filePath));
|
|
1506
|
+
const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
|
|
1153
1507
|
return { schemaType };
|
|
1154
1508
|
}
|
|
1155
1509
|
|
|
1156
1510
|
// src/utils/paths.ts
|
|
1157
|
-
import
|
|
1511
|
+
import path8 from "path";
|
|
1158
1512
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1159
|
-
import { stat as
|
|
1513
|
+
import { stat as stat3 } from "fs/promises";
|
|
1160
1514
|
async function findYggRoot(projectRoot) {
|
|
1161
|
-
let current =
|
|
1162
|
-
const root =
|
|
1515
|
+
let current = path8.resolve(projectRoot);
|
|
1516
|
+
const root = path8.parse(current).root;
|
|
1163
1517
|
while (true) {
|
|
1164
|
-
const yggPath =
|
|
1518
|
+
const yggPath = path8.join(current, ".yggdrasil");
|
|
1165
1519
|
try {
|
|
1166
|
-
const st = await
|
|
1520
|
+
const st = await stat3(yggPath);
|
|
1167
1521
|
if (!st.isDirectory()) {
|
|
1168
1522
|
throw new Error(
|
|
1169
1523
|
`.yggdrasil exists but is not a directory (${yggPath}). Run 'yg init' in a clean location.`
|
|
@@ -1175,7 +1529,7 @@ async function findYggRoot(projectRoot) {
|
|
|
1175
1529
|
if (current === root) {
|
|
1176
1530
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
1177
1531
|
}
|
|
1178
|
-
current =
|
|
1532
|
+
current = path8.dirname(current);
|
|
1179
1533
|
continue;
|
|
1180
1534
|
}
|
|
1181
1535
|
throw err;
|
|
@@ -1191,27 +1545,25 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
1191
1545
|
if (normalizedInput.length === 0) {
|
|
1192
1546
|
throw new Error("Path cannot be empty");
|
|
1193
1547
|
}
|
|
1194
|
-
const absolute =
|
|
1195
|
-
const relative =
|
|
1196
|
-
const isOutside = relative.startsWith("..") ||
|
|
1548
|
+
const absolute = path8.resolve(projectRoot, normalizedInput);
|
|
1549
|
+
const relative = path8.relative(projectRoot, absolute);
|
|
1550
|
+
const isOutside = relative.startsWith("..") || path8.isAbsolute(relative);
|
|
1197
1551
|
if (isOutside) {
|
|
1198
1552
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
1199
1553
|
}
|
|
1200
|
-
return relative.split(
|
|
1554
|
+
return relative.split(path8.sep).join("/");
|
|
1201
1555
|
}
|
|
1202
1556
|
function projectRootFromGraph(yggRootPath) {
|
|
1203
|
-
return
|
|
1557
|
+
return path8.dirname(yggRootPath);
|
|
1204
1558
|
}
|
|
1205
1559
|
|
|
1206
1560
|
// src/core/graph-loader.ts
|
|
1207
1561
|
function toModelPath(absolutePath, modelDir) {
|
|
1208
|
-
return
|
|
1562
|
+
return path9.relative(modelDir, absolutePath).split(path9.sep).join("/");
|
|
1209
1563
|
}
|
|
1210
1564
|
var FALLBACK_CONFIG = {
|
|
1211
1565
|
name: "",
|
|
1212
|
-
|
|
1213
|
-
standards: "",
|
|
1214
|
-
node_types: [],
|
|
1566
|
+
node_types: {},
|
|
1215
1567
|
artifacts: {}
|
|
1216
1568
|
};
|
|
1217
1569
|
async function loadGraph(projectRoot, options = {}) {
|
|
@@ -1219,14 +1571,14 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1219
1571
|
let configError;
|
|
1220
1572
|
let config = FALLBACK_CONFIG;
|
|
1221
1573
|
try {
|
|
1222
|
-
config = await parseConfig(
|
|
1574
|
+
config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
|
|
1223
1575
|
} catch (error) {
|
|
1224
1576
|
if (!options.tolerateInvalidConfig) {
|
|
1225
1577
|
throw error;
|
|
1226
1578
|
}
|
|
1227
1579
|
configError = error.message;
|
|
1228
1580
|
}
|
|
1229
|
-
const modelDir =
|
|
1581
|
+
const modelDir = path9.join(yggRoot, "model");
|
|
1230
1582
|
const nodes = /* @__PURE__ */ new Map();
|
|
1231
1583
|
const nodeParseErrors = [];
|
|
1232
1584
|
const artifactFilenames = Object.keys(config.artifacts ?? {});
|
|
@@ -1240,9 +1592,9 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1240
1592
|
}
|
|
1241
1593
|
throw err;
|
|
1242
1594
|
}
|
|
1243
|
-
const aspects = await loadAspects(
|
|
1244
|
-
const flows = await loadFlows(
|
|
1245
|
-
const schemas = await loadSchemas(
|
|
1595
|
+
const aspects = await loadAspects(path9.join(yggRoot, "aspects"));
|
|
1596
|
+
const flows = await loadFlows(path9.join(yggRoot, "flows"));
|
|
1597
|
+
const schemas = await loadSchemas(path9.join(yggRoot, "schemas"));
|
|
1246
1598
|
return {
|
|
1247
1599
|
config,
|
|
1248
1600
|
configError,
|
|
@@ -1255,18 +1607,18 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1255
1607
|
};
|
|
1256
1608
|
}
|
|
1257
1609
|
async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErrors, artifactFilenames) {
|
|
1258
|
-
const entries = await
|
|
1259
|
-
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
1610
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
1611
|
+
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
1260
1612
|
if (!hasNodeYaml && dirPath !== modelDir) {
|
|
1261
1613
|
return;
|
|
1262
1614
|
}
|
|
1263
1615
|
if (hasNodeYaml) {
|
|
1264
1616
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1265
|
-
const nodeYamlPath =
|
|
1617
|
+
const nodeYamlPath = path9.join(dirPath, "yg-node.yaml");
|
|
1266
1618
|
let meta;
|
|
1267
1619
|
let nodeYamlRaw;
|
|
1268
1620
|
try {
|
|
1269
|
-
nodeYamlRaw = await
|
|
1621
|
+
nodeYamlRaw = await readFile11(nodeYamlPath, "utf-8");
|
|
1270
1622
|
meta = await parseNodeYaml(nodeYamlPath);
|
|
1271
1623
|
} catch (err) {
|
|
1272
1624
|
nodeParseErrors.push({
|
|
@@ -1275,7 +1627,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1275
1627
|
});
|
|
1276
1628
|
return;
|
|
1277
1629
|
}
|
|
1278
|
-
const artifacts = await readArtifacts(dirPath, ["node.yaml"], artifactFilenames);
|
|
1630
|
+
const artifacts = await readArtifacts(dirPath, ["yg-node.yaml"], artifactFilenames);
|
|
1279
1631
|
const node = {
|
|
1280
1632
|
path: graphPath,
|
|
1281
1633
|
meta,
|
|
@@ -1292,7 +1644,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1292
1644
|
if (!entry.isDirectory()) continue;
|
|
1293
1645
|
if (entry.name.startsWith(".")) continue;
|
|
1294
1646
|
await scanModelDirectory(
|
|
1295
|
-
|
|
1647
|
+
path9.join(dirPath, entry.name),
|
|
1296
1648
|
modelDir,
|
|
1297
1649
|
node,
|
|
1298
1650
|
nodes,
|
|
@@ -1305,7 +1657,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1305
1657
|
if (!entry.isDirectory()) continue;
|
|
1306
1658
|
if (entry.name.startsWith(".")) continue;
|
|
1307
1659
|
await scanModelDirectory(
|
|
1308
|
-
|
|
1660
|
+
path9.join(dirPath, entry.name),
|
|
1309
1661
|
modelDir,
|
|
1310
1662
|
null,
|
|
1311
1663
|
nodes,
|
|
@@ -1325,28 +1677,28 @@ async function loadAspects(aspectsDir) {
|
|
|
1325
1677
|
}
|
|
1326
1678
|
}
|
|
1327
1679
|
async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
1328
|
-
const entries = await
|
|
1329
|
-
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "aspect.yaml");
|
|
1680
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
1681
|
+
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
|
|
1330
1682
|
if (hasAspectYaml) {
|
|
1331
|
-
const id =
|
|
1332
|
-
const aspectYamlPath =
|
|
1683
|
+
const id = path9.relative(aspectsRoot, dirPath).split(path9.sep).join("/");
|
|
1684
|
+
const aspectYamlPath = path9.join(dirPath, "yg-aspect.yaml");
|
|
1333
1685
|
const aspect = await parseAspect(dirPath, aspectYamlPath, id);
|
|
1334
1686
|
aspects.push(aspect);
|
|
1335
1687
|
}
|
|
1336
1688
|
for (const entry of entries) {
|
|
1337
1689
|
if (!entry.isDirectory()) continue;
|
|
1338
1690
|
if (entry.name.startsWith(".")) continue;
|
|
1339
|
-
await scanAspectsDirectory(
|
|
1691
|
+
await scanAspectsDirectory(path9.join(dirPath, entry.name), aspectsRoot, aspects);
|
|
1340
1692
|
}
|
|
1341
1693
|
}
|
|
1342
1694
|
async function loadFlows(flowsDir) {
|
|
1343
1695
|
try {
|
|
1344
|
-
const entries = await
|
|
1696
|
+
const entries = await readdir4(flowsDir, { withFileTypes: true });
|
|
1345
1697
|
const flows = [];
|
|
1346
1698
|
for (const entry of entries) {
|
|
1347
1699
|
if (!entry.isDirectory()) continue;
|
|
1348
|
-
const flowYamlPath =
|
|
1349
|
-
const flow = await parseFlow(
|
|
1700
|
+
const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
|
|
1701
|
+
const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
|
|
1350
1702
|
flows.push(flow);
|
|
1351
1703
|
}
|
|
1352
1704
|
return flows;
|
|
@@ -1356,12 +1708,12 @@ async function loadFlows(flowsDir) {
|
|
|
1356
1708
|
}
|
|
1357
1709
|
async function loadSchemas(schemasDir) {
|
|
1358
1710
|
try {
|
|
1359
|
-
const entries = await
|
|
1711
|
+
const entries = await readdir4(schemasDir, { withFileTypes: true });
|
|
1360
1712
|
const schemas = [];
|
|
1361
1713
|
for (const entry of entries) {
|
|
1362
1714
|
if (!entry.isFile()) continue;
|
|
1363
1715
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1364
|
-
const s = await parseSchema(
|
|
1716
|
+
const s = await parseSchema(path9.join(schemasDir, entry.name));
|
|
1365
1717
|
schemas.push(s);
|
|
1366
1718
|
}
|
|
1367
1719
|
return schemas;
|
|
@@ -1371,8 +1723,8 @@ async function loadSchemas(schemasDir) {
|
|
|
1371
1723
|
}
|
|
1372
1724
|
|
|
1373
1725
|
// src/core/context-builder.ts
|
|
1374
|
-
import { readFile as
|
|
1375
|
-
import
|
|
1726
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1727
|
+
import path10 from "path";
|
|
1376
1728
|
|
|
1377
1729
|
// src/utils/tokens.ts
|
|
1378
1730
|
function estimateTokens(text) {
|
|
@@ -1421,8 +1773,9 @@ async function buildContext(graph, nodePath) {
|
|
|
1421
1773
|
}
|
|
1422
1774
|
const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
|
|
1423
1775
|
for (const aspect of aspectsToInclude) {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1776
|
+
const entry = node.meta.aspects?.find((a) => a.aspect === aspect.id);
|
|
1777
|
+
const exceptionNote = entry?.exceptions?.join("; ");
|
|
1778
|
+
layers.push(buildAspectLayer(aspect, exceptionNote));
|
|
1426
1779
|
}
|
|
1427
1780
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
1428
1781
|
const tokenCount = estimateTokens(fullText);
|
|
@@ -1479,18 +1832,7 @@ function resolveAspects(aspectIds, aspects) {
|
|
|
1479
1832
|
return expandedIds.map((id) => idToAspect.get(id)).filter((a) => a !== void 0);
|
|
1480
1833
|
}
|
|
1481
1834
|
function buildGlobalLayer(config) {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
`;
|
|
1485
|
-
content += `**Stack:**
|
|
1486
|
-
`;
|
|
1487
|
-
for (const [key, value] of Object.entries(config.stack)) {
|
|
1488
|
-
content += `- ${key}: ${value}
|
|
1489
|
-
`;
|
|
1490
|
-
}
|
|
1491
|
-
content += `
|
|
1492
|
-
**Standards:**
|
|
1493
|
-
${config.standards || "(none)"}
|
|
1835
|
+
const content = `**Project:** ${config.name}
|
|
1494
1836
|
`;
|
|
1495
1837
|
return { type: "global", label: "Global Context", content };
|
|
1496
1838
|
}
|
|
@@ -1502,7 +1844,7 @@ function buildHierarchyLayer(ancestor, config, graph) {
|
|
|
1502
1844
|
const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
|
|
1503
1845
|
const content = filtered.map((a) => `### ${a.filename}
|
|
1504
1846
|
${a.content}`).join("\n\n");
|
|
1505
|
-
const nodeAspects = ancestor.meta.aspects ?? [];
|
|
1847
|
+
const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
|
|
1506
1848
|
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1507
1849
|
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1508
1850
|
return {
|
|
@@ -1515,16 +1857,16 @@ ${a.content}`).join("\n\n");
|
|
|
1515
1857
|
async function buildOwnLayer(node, config, graphRootPath, graph) {
|
|
1516
1858
|
const parts = [];
|
|
1517
1859
|
if (node.nodeYamlRaw) {
|
|
1518
|
-
parts.push(`### node.yaml
|
|
1860
|
+
parts.push(`### yg-node.yaml
|
|
1519
1861
|
${node.nodeYamlRaw.trim()}`);
|
|
1520
1862
|
} else {
|
|
1521
|
-
const nodeYamlPath =
|
|
1863
|
+
const nodeYamlPath = path10.join(graphRootPath, "model", node.path, "yg-node.yaml");
|
|
1522
1864
|
try {
|
|
1523
|
-
const nodeYamlContent = await
|
|
1524
|
-
parts.push(`### node.yaml
|
|
1865
|
+
const nodeYamlContent = await readFile12(nodeYamlPath, "utf-8");
|
|
1866
|
+
parts.push(`### yg-node.yaml
|
|
1525
1867
|
${nodeYamlContent.trim()}`);
|
|
1526
1868
|
} catch {
|
|
1527
|
-
parts.push(`### node.yaml
|
|
1869
|
+
parts.push(`### yg-node.yaml
|
|
1528
1870
|
(not found)`);
|
|
1529
1871
|
}
|
|
1530
1872
|
}
|
|
@@ -1534,7 +1876,7 @@ ${nodeYamlContent.trim()}`);
|
|
|
1534
1876
|
${a.content}`);
|
|
1535
1877
|
}
|
|
1536
1878
|
const content = parts.join("\n\n");
|
|
1537
|
-
const nodeAspects = node.meta.aspects ?? [];
|
|
1879
|
+
const nodeAspects = (node.meta.aspects ?? []).map((a) => a.aspect);
|
|
1538
1880
|
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1539
1881
|
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1540
1882
|
return {
|
|
@@ -1556,7 +1898,7 @@ function buildStructuralRelationLayer(target, relation, config) {
|
|
|
1556
1898
|
|
|
1557
1899
|
`;
|
|
1558
1900
|
}
|
|
1559
|
-
const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.
|
|
1901
|
+
const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
1560
1902
|
const structuralArts = structuralArtifactFilenames.map((filename) => {
|
|
1561
1903
|
const art = target.artifacts.find((a) => a.filename === filename);
|
|
1562
1904
|
return art ? { filename: art.filename, content: art.content } : null;
|
|
@@ -1671,10 +2013,10 @@ function collectAncestors(node) {
|
|
|
1671
2013
|
function collectEffectiveAspectIds(graph, nodePath) {
|
|
1672
2014
|
const node = graph.nodes.get(nodePath);
|
|
1673
2015
|
if (!node) return /* @__PURE__ */ new Set();
|
|
1674
|
-
const raw = new Set(node.meta.aspects ?? []);
|
|
2016
|
+
const raw = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
|
|
1675
2017
|
let ancestor = node.parent;
|
|
1676
2018
|
while (ancestor) {
|
|
1677
|
-
for (const
|
|
2019
|
+
for (const entry of ancestor.meta.aspects ?? []) raw.add(entry.aspect);
|
|
1678
2020
|
ancestor = ancestor.parent;
|
|
1679
2021
|
}
|
|
1680
2022
|
const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
|
|
@@ -1687,8 +2029,11 @@ function collectEffectiveAspectIds(graph, nodePath) {
|
|
|
1687
2029
|
}
|
|
1688
2030
|
|
|
1689
2031
|
// src/core/validator.ts
|
|
1690
|
-
import { readdir as
|
|
1691
|
-
import
|
|
2032
|
+
import { readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
|
|
2033
|
+
import path11 from "path";
|
|
2034
|
+
function getAspectIds(aspects) {
|
|
2035
|
+
return (aspects ?? []).map((a) => a.aspect);
|
|
2036
|
+
}
|
|
1692
2037
|
var RESERVED_DIRS = /* @__PURE__ */ new Set();
|
|
1693
2038
|
async function validate(graph, scope = "all") {
|
|
1694
2039
|
const issues = [];
|
|
@@ -1717,7 +2062,6 @@ async function validate(graph, scope = "all") {
|
|
|
1717
2062
|
issues.push(...checkImpliedAspectsExist(graph));
|
|
1718
2063
|
issues.push(...checkImpliesNoCycles(graph));
|
|
1719
2064
|
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
1720
|
-
issues.push(...checkAspectExceptions(graph));
|
|
1721
2065
|
issues.push(...await checkAnchorPresence(graph));
|
|
1722
2066
|
issues.push(...checkRequiredArtifacts(graph));
|
|
1723
2067
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
@@ -1766,7 +2110,7 @@ async function validate(graph, scope = "all") {
|
|
|
1766
2110
|
}
|
|
1767
2111
|
function checkNodeTypes(graph) {
|
|
1768
2112
|
const issues = [];
|
|
1769
|
-
const allowedTypes = new Set((graph.config.node_types ??
|
|
2113
|
+
const allowedTypes = new Set(Object.keys(graph.config.node_types ?? {}));
|
|
1770
2114
|
for (const [nodePath, node] of graph.nodes) {
|
|
1771
2115
|
if (!allowedTypes.has(node.meta.type)) {
|
|
1772
2116
|
issues.push({
|
|
@@ -1833,7 +2177,7 @@ function checkAspectsDefined(graph) {
|
|
|
1833
2177
|
const issues = [];
|
|
1834
2178
|
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
1835
2179
|
for (const [nodePath, node] of graph.nodes) {
|
|
1836
|
-
for (const aspectId of node.meta.aspects
|
|
2180
|
+
for (const aspectId of getAspectIds(node.meta.aspects)) {
|
|
1837
2181
|
if (!validAspectIds.has(aspectId)) {
|
|
1838
2182
|
issues.push({
|
|
1839
2183
|
severity: "error",
|
|
@@ -1937,16 +2281,16 @@ function checkImpliesNoCycles(graph) {
|
|
|
1937
2281
|
function checkRequiredAspectsCoverage(graph) {
|
|
1938
2282
|
const issues = [];
|
|
1939
2283
|
const typeConfig = new Map(
|
|
1940
|
-
(graph.config.node_types ??
|
|
2284
|
+
Object.entries(graph.config.node_types ?? {}).map(([name, cfg]) => [name, cfg.required_aspects ?? []])
|
|
1941
2285
|
);
|
|
1942
2286
|
for (const [nodePath, node] of graph.nodes) {
|
|
1943
2287
|
if (node.meta.blackbox) continue;
|
|
1944
2288
|
const requiredAspects = typeConfig.get(node.meta.type);
|
|
1945
2289
|
if (!requiredAspects || requiredAspects.length === 0) continue;
|
|
1946
|
-
const
|
|
2290
|
+
const nodeAspectIds = getAspectIds(node.meta.aspects);
|
|
1947
2291
|
let effectiveAspects;
|
|
1948
2292
|
try {
|
|
1949
|
-
effectiveAspects = resolveAspects(
|
|
2293
|
+
effectiveAspects = resolveAspects(nodeAspectIds, graph.aspects);
|
|
1950
2294
|
} catch {
|
|
1951
2295
|
continue;
|
|
1952
2296
|
}
|
|
@@ -1965,24 +2309,6 @@ function checkRequiredAspectsCoverage(graph) {
|
|
|
1965
2309
|
}
|
|
1966
2310
|
return issues;
|
|
1967
2311
|
}
|
|
1968
|
-
function checkAspectExceptions(graph) {
|
|
1969
|
-
const issues = [];
|
|
1970
|
-
for (const [nodePath, node] of graph.nodes) {
|
|
1971
|
-
for (const exception of node.meta.aspect_exceptions ?? []) {
|
|
1972
|
-
const nodeAspects = node.meta.aspects ?? [];
|
|
1973
|
-
if (!nodeAspects.includes(exception.aspect)) {
|
|
1974
|
-
issues.push({
|
|
1975
|
-
severity: "error",
|
|
1976
|
-
code: "E018",
|
|
1977
|
-
rule: "invalid-aspect-exception",
|
|
1978
|
-
message: `aspect_exceptions references aspect '${exception.aspect}' which is not in this node's aspects list (${nodeAspects.join(", ") || "none"})`,
|
|
1979
|
-
nodePath
|
|
1980
|
-
});
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
return issues;
|
|
1985
|
-
}
|
|
1986
2312
|
function checkNoCycles(graph) {
|
|
1987
2313
|
const WHITE = 0;
|
|
1988
2314
|
const GRAY = 1;
|
|
@@ -2069,12 +2395,12 @@ function checkMappingOverlap(graph) {
|
|
|
2069
2395
|
}
|
|
2070
2396
|
async function checkMappingPathsExist(graph) {
|
|
2071
2397
|
const issues = [];
|
|
2072
|
-
const projectRoot =
|
|
2398
|
+
const projectRoot = path11.dirname(graph.rootPath);
|
|
2073
2399
|
const { access: access4 } = await import("fs/promises");
|
|
2074
2400
|
for (const [nodePath, node] of graph.nodes) {
|
|
2075
2401
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2076
2402
|
for (const mp of mappingPaths) {
|
|
2077
|
-
const absPath =
|
|
2403
|
+
const absPath = path11.join(projectRoot, mp);
|
|
2078
2404
|
try {
|
|
2079
2405
|
await access4(absPath);
|
|
2080
2406
|
} catch {
|
|
@@ -2116,7 +2442,7 @@ function artifactRequiredReason(graph, nodePath, node, required) {
|
|
|
2116
2442
|
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
2117
2443
|
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
2118
2444
|
const aspectId = when.slice(prefix.length);
|
|
2119
|
-
return (node.meta.aspects ?? []).
|
|
2445
|
+
return (node.meta.aspects ?? []).some((a) => a.aspect === aspectId) ? `node has aspect '${aspectId}'` : null;
|
|
2120
2446
|
}
|
|
2121
2447
|
return null;
|
|
2122
2448
|
}
|
|
@@ -2307,7 +2633,7 @@ function checkSchemas(graph) {
|
|
|
2307
2633
|
severity: "warning",
|
|
2308
2634
|
code: "W010",
|
|
2309
2635
|
rule: "missing-schema",
|
|
2310
|
-
message: `Schema '
|
|
2636
|
+
message: `Schema 'yg-${required}.yaml' missing from .yggdrasil/schemas/`
|
|
2311
2637
|
});
|
|
2312
2638
|
}
|
|
2313
2639
|
}
|
|
@@ -2315,11 +2641,11 @@ function checkSchemas(graph) {
|
|
|
2315
2641
|
}
|
|
2316
2642
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
2317
2643
|
const issues = [];
|
|
2318
|
-
const modelDir =
|
|
2644
|
+
const modelDir = path11.join(graph.rootPath, "model");
|
|
2319
2645
|
async function scanDir(dirPath, segments) {
|
|
2320
|
-
const entries = await
|
|
2321
|
-
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
2322
|
-
const dirName =
|
|
2646
|
+
const entries = await readdir5(dirPath, { withFileTypes: true });
|
|
2647
|
+
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
2648
|
+
const dirName = path11.basename(dirPath);
|
|
2323
2649
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
2324
2650
|
const hasFiles = entries.some((e) => e.isFile());
|
|
2325
2651
|
const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
|
|
@@ -2330,7 +2656,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2330
2656
|
severity: "error",
|
|
2331
2657
|
code: "E015",
|
|
2332
2658
|
rule: "missing-node-yaml",
|
|
2333
|
-
message: `Directory '${graphPath}' has files but no node.yaml`,
|
|
2659
|
+
message: `Directory '${graphPath}' has files but no yg-node.yaml`,
|
|
2334
2660
|
nodePath: graphPath
|
|
2335
2661
|
});
|
|
2336
2662
|
} else if (hasSubdirs) {
|
|
@@ -2338,7 +2664,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2338
2664
|
severity: "warning",
|
|
2339
2665
|
code: "W013",
|
|
2340
2666
|
rule: "directory-without-node",
|
|
2341
|
-
message: `Directory '${graphPath}' has subdirectories but no node.yaml \u2014 consider creating a node`,
|
|
2667
|
+
message: `Directory '${graphPath}' has subdirectories but no yg-node.yaml \u2014 consider creating a node`,
|
|
2342
2668
|
nodePath: graphPath
|
|
2343
2669
|
});
|
|
2344
2670
|
}
|
|
@@ -2347,15 +2673,15 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2347
2673
|
if (!entry.isDirectory()) continue;
|
|
2348
2674
|
if (RESERVED_DIRS.has(entry.name)) continue;
|
|
2349
2675
|
if (entry.name.startsWith(".")) continue;
|
|
2350
|
-
await scanDir(
|
|
2676
|
+
await scanDir(path11.join(dirPath, entry.name), [...segments, entry.name]);
|
|
2351
2677
|
}
|
|
2352
2678
|
}
|
|
2353
2679
|
try {
|
|
2354
|
-
const rootEntries = await
|
|
2680
|
+
const rootEntries = await readdir5(modelDir, { withFileTypes: true });
|
|
2355
2681
|
for (const entry of rootEntries) {
|
|
2356
2682
|
if (!entry.isDirectory()) continue;
|
|
2357
2683
|
if (entry.name.startsWith(".")) continue;
|
|
2358
|
-
await scanDir(
|
|
2684
|
+
await scanDir(path11.join(modelDir, entry.name), [entry.name]);
|
|
2359
2685
|
}
|
|
2360
2686
|
} catch {
|
|
2361
2687
|
}
|
|
@@ -2365,14 +2691,14 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
2365
2691
|
const files = [];
|
|
2366
2692
|
async function collectFiles(absPath) {
|
|
2367
2693
|
try {
|
|
2368
|
-
const s = await
|
|
2694
|
+
const s = await stat4(absPath);
|
|
2369
2695
|
if (s.isFile()) {
|
|
2370
2696
|
files.push(absPath);
|
|
2371
2697
|
} else if (s.isDirectory()) {
|
|
2372
|
-
const entries = await
|
|
2698
|
+
const entries = await readdir5(absPath, { withFileTypes: true });
|
|
2373
2699
|
for (const entry of entries) {
|
|
2374
2700
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2375
|
-
const entryPath =
|
|
2701
|
+
const entryPath = path11.join(absPath, entry.name);
|
|
2376
2702
|
if (entry.isFile()) {
|
|
2377
2703
|
files.push(entryPath);
|
|
2378
2704
|
} else if (entry.isDirectory()) {
|
|
@@ -2384,28 +2710,16 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
2384
2710
|
}
|
|
2385
2711
|
}
|
|
2386
2712
|
for (const mp of mappingPaths) {
|
|
2387
|
-
await collectFiles(
|
|
2713
|
+
await collectFiles(path11.join(projectRoot, mp));
|
|
2388
2714
|
}
|
|
2389
2715
|
return files;
|
|
2390
2716
|
}
|
|
2391
2717
|
async function checkAnchorPresence(graph) {
|
|
2392
2718
|
const issues = [];
|
|
2393
|
-
const projectRoot =
|
|
2719
|
+
const projectRoot = path11.dirname(graph.rootPath);
|
|
2394
2720
|
for (const [nodePath, node] of graph.nodes) {
|
|
2395
|
-
const
|
|
2396
|
-
if (
|
|
2397
|
-
const nodeAspects = new Set(node.meta.aspects ?? []);
|
|
2398
|
-
for (const aspectId of Object.keys(anchors)) {
|
|
2399
|
-
if (!nodeAspects.has(aspectId)) {
|
|
2400
|
-
issues.push({
|
|
2401
|
-
severity: "error",
|
|
2402
|
-
code: "E019",
|
|
2403
|
-
rule: "invalid-anchor-ref",
|
|
2404
|
-
message: `anchors references aspect '${aspectId}' which is not in this node's aspects list (${[...nodeAspects].join(", ") || "none"})`,
|
|
2405
|
-
nodePath
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2721
|
+
const aspectsWithAnchors = (node.meta.aspects ?? []).filter((a) => a.anchors && a.anchors.length > 0);
|
|
2722
|
+
if (aspectsWithAnchors.length === 0) continue;
|
|
2409
2723
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2410
2724
|
if (mappingPaths.length === 0) continue;
|
|
2411
2725
|
const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
|
|
@@ -2413,21 +2727,20 @@ async function checkAnchorPresence(graph) {
|
|
|
2413
2727
|
const fileContents = [];
|
|
2414
2728
|
for (const filePath of sourceFiles) {
|
|
2415
2729
|
try {
|
|
2416
|
-
const content = await
|
|
2730
|
+
const content = await readFile13(filePath, "utf-8");
|
|
2417
2731
|
fileContents.push(content);
|
|
2418
2732
|
} catch {
|
|
2419
2733
|
}
|
|
2420
2734
|
}
|
|
2421
|
-
for (const
|
|
2422
|
-
|
|
2423
|
-
for (const anchor of anchorList) {
|
|
2735
|
+
for (const entry of aspectsWithAnchors) {
|
|
2736
|
+
for (const anchor of entry.anchors) {
|
|
2424
2737
|
const found = fileContents.some((content) => content.includes(anchor));
|
|
2425
2738
|
if (!found) {
|
|
2426
2739
|
issues.push({
|
|
2427
2740
|
severity: "warning",
|
|
2428
2741
|
code: "W014",
|
|
2429
2742
|
rule: "anchor-not-found",
|
|
2430
|
-
message: `Anchor '${anchor}' for aspect '${
|
|
2743
|
+
message: `Anchor '${anchor}' for aspect '${entry.aspect}' not found in mapped source files`,
|
|
2431
2744
|
nodePath
|
|
2432
2745
|
});
|
|
2433
2746
|
}
|
|
@@ -2652,13 +2965,90 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
2652
2965
|
import chalk2 from "chalk";
|
|
2653
2966
|
|
|
2654
2967
|
// src/io/drift-state-store.ts
|
|
2655
|
-
import { readFile as
|
|
2656
|
-
import
|
|
2968
|
+
import { readFile as readFile14, writeFile as writeFile4, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
2969
|
+
import path12 from "path";
|
|
2657
2970
|
import { parse as yamlParse } from "yaml";
|
|
2658
|
-
var
|
|
2971
|
+
var DRIFT_STATE_DIR = ".drift-state";
|
|
2972
|
+
function nodeStatePath(yggRoot, nodePath) {
|
|
2973
|
+
return path12.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
|
|
2974
|
+
}
|
|
2975
|
+
async function scanJsonFiles(dir, baseDir) {
|
|
2976
|
+
const results = [];
|
|
2977
|
+
let entries;
|
|
2978
|
+
try {
|
|
2979
|
+
entries = await readdir6(dir, { withFileTypes: true });
|
|
2980
|
+
} catch {
|
|
2981
|
+
return results;
|
|
2982
|
+
}
|
|
2983
|
+
for (const entry of entries) {
|
|
2984
|
+
const fullPath = path12.join(dir, entry.name);
|
|
2985
|
+
if (entry.isDirectory()) {
|
|
2986
|
+
const nested = await scanJsonFiles(fullPath, baseDir);
|
|
2987
|
+
results.push(...nested);
|
|
2988
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
2989
|
+
const relPath = path12.relative(baseDir, fullPath);
|
|
2990
|
+
const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
|
|
2991
|
+
results.push(nodePath);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return results;
|
|
2995
|
+
}
|
|
2996
|
+
async function removeEmptyParents(filePath, stopDir) {
|
|
2997
|
+
let dir = path12.dirname(filePath);
|
|
2998
|
+
while (dir !== stopDir && dir.startsWith(stopDir)) {
|
|
2999
|
+
try {
|
|
3000
|
+
const entries = await readdir6(dir);
|
|
3001
|
+
if (entries.length === 0) {
|
|
3002
|
+
await rm2(dir, { recursive: true });
|
|
3003
|
+
dir = path12.dirname(dir);
|
|
3004
|
+
} else {
|
|
3005
|
+
break;
|
|
3006
|
+
}
|
|
3007
|
+
} catch {
|
|
3008
|
+
break;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
async function readNodeDriftState(yggRoot, nodePath) {
|
|
3013
|
+
try {
|
|
3014
|
+
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3015
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3016
|
+
const parsed = JSON.parse(content);
|
|
3017
|
+
return parsed;
|
|
3018
|
+
} catch {
|
|
3019
|
+
return void 0;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
3023
|
+
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3024
|
+
await mkdir3(path12.dirname(filePath), { recursive: true });
|
|
3025
|
+
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
3026
|
+
await writeFile4(filePath, content, "utf-8");
|
|
3027
|
+
}
|
|
3028
|
+
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
3029
|
+
const driftDir = path12.join(yggRoot, DRIFT_STATE_DIR);
|
|
3030
|
+
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
3031
|
+
const removed = [];
|
|
3032
|
+
for (const nodePath of allNodePaths) {
|
|
3033
|
+
if (!validNodePaths.has(nodePath)) {
|
|
3034
|
+
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3035
|
+
await rm2(filePath);
|
|
3036
|
+
await removeEmptyParents(filePath, driftDir);
|
|
3037
|
+
removed.push(nodePath);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
return removed.sort();
|
|
3041
|
+
}
|
|
2659
3042
|
async function readDriftState(yggRoot) {
|
|
3043
|
+
const driftPath = path12.join(yggRoot, DRIFT_STATE_DIR);
|
|
3044
|
+
let driftStat;
|
|
2660
3045
|
try {
|
|
2661
|
-
|
|
3046
|
+
driftStat = await stat5(driftPath);
|
|
3047
|
+
} catch {
|
|
3048
|
+
return {};
|
|
3049
|
+
}
|
|
3050
|
+
if (driftStat.isFile()) {
|
|
3051
|
+
const content = await readFile14(driftPath, "utf-8");
|
|
2662
3052
|
let raw;
|
|
2663
3053
|
try {
|
|
2664
3054
|
raw = JSON.parse(content);
|
|
@@ -2666,37 +3056,44 @@ async function readDriftState(yggRoot) {
|
|
|
2666
3056
|
raw = yamlParse(content);
|
|
2667
3057
|
}
|
|
2668
3058
|
if (!raw || typeof raw !== "object") return {};
|
|
2669
|
-
const
|
|
3059
|
+
const state2 = {};
|
|
2670
3060
|
for (const [key, value] of Object.entries(raw)) {
|
|
2671
3061
|
if (typeof value === "object" && value !== null && "hash" in value) {
|
|
2672
|
-
|
|
3062
|
+
state2[key] = value;
|
|
2673
3063
|
}
|
|
2674
3064
|
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3065
|
+
await rm2(driftPath);
|
|
3066
|
+
for (const [nodePath, nodeState] of Object.entries(state2)) {
|
|
3067
|
+
await writeNodeDriftState(yggRoot, nodePath, nodeState);
|
|
3068
|
+
}
|
|
3069
|
+
return state2;
|
|
2678
3070
|
}
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
const
|
|
2682
|
-
|
|
3071
|
+
const nodePaths = await scanJsonFiles(driftPath, driftPath);
|
|
3072
|
+
const state = {};
|
|
3073
|
+
for (const nodePath of nodePaths) {
|
|
3074
|
+
const nodeState = await readNodeDriftState(yggRoot, nodePath);
|
|
3075
|
+
if (nodeState) {
|
|
3076
|
+
state[nodePath] = nodeState;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
return state;
|
|
2683
3080
|
}
|
|
2684
3081
|
|
|
2685
3082
|
// src/utils/hash.ts
|
|
2686
|
-
import { readFile as
|
|
2687
|
-
import
|
|
3083
|
+
import { readFile as readFile15, readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
3084
|
+
import path13 from "path";
|
|
2688
3085
|
import { createHash } from "crypto";
|
|
2689
3086
|
import { createRequire } from "module";
|
|
2690
3087
|
var require2 = createRequire(import.meta.url);
|
|
2691
3088
|
var ignoreFactory = require2("ignore");
|
|
2692
3089
|
async function hashFile(filePath) {
|
|
2693
|
-
const content = await
|
|
3090
|
+
const content = await readFile15(filePath);
|
|
2694
3091
|
return createHash("sha256").update(content).digest("hex");
|
|
2695
3092
|
}
|
|
2696
3093
|
async function loadRootGitignoreStack(projectRoot) {
|
|
2697
3094
|
if (!projectRoot) return [];
|
|
2698
3095
|
try {
|
|
2699
|
-
const content = await
|
|
3096
|
+
const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
|
|
2700
3097
|
const matcher = ignoreFactory();
|
|
2701
3098
|
matcher.add(content);
|
|
2702
3099
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -2706,7 +3103,7 @@ async function loadRootGitignoreStack(projectRoot) {
|
|
|
2706
3103
|
}
|
|
2707
3104
|
function isIgnoredByStack(candidatePath, stack) {
|
|
2708
3105
|
for (const { basePath, matcher } of stack) {
|
|
2709
|
-
const relativePath =
|
|
3106
|
+
const relativePath = path13.relative(basePath, candidatePath);
|
|
2710
3107
|
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
2711
3108
|
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
2712
3109
|
}
|
|
@@ -2721,9 +3118,9 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2721
3118
|
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2722
3119
|
const allFiles = [];
|
|
2723
3120
|
for (const tf of trackedFiles) {
|
|
2724
|
-
const absPath =
|
|
3121
|
+
const absPath = path13.join(projectRoot, tf.path);
|
|
2725
3122
|
try {
|
|
2726
|
-
const st = await
|
|
3123
|
+
const st = await stat6(absPath);
|
|
2727
3124
|
if (st.isDirectory()) {
|
|
2728
3125
|
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2729
3126
|
projectRoot,
|
|
@@ -2731,7 +3128,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2731
3128
|
});
|
|
2732
3129
|
for (const entry of dirEntries) {
|
|
2733
3130
|
allFiles.push({
|
|
2734
|
-
relPath:
|
|
3131
|
+
relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
2735
3132
|
absPath: entry.absPath,
|
|
2736
3133
|
mtimeMs: entry.mtimeMs
|
|
2737
3134
|
});
|
|
@@ -2771,17 +3168,17 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2771
3168
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
2772
3169
|
let stack = options.gitignoreStack ?? [];
|
|
2773
3170
|
try {
|
|
2774
|
-
const localContent = await
|
|
3171
|
+
const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
|
|
2775
3172
|
const localMatcher = ignoreFactory();
|
|
2776
3173
|
localMatcher.add(localContent);
|
|
2777
3174
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
2778
3175
|
} catch {
|
|
2779
3176
|
}
|
|
2780
|
-
const entries = await
|
|
3177
|
+
const entries = await readdir7(directoryPath, { withFileTypes: true });
|
|
2781
3178
|
const dirs = [];
|
|
2782
3179
|
const files = [];
|
|
2783
3180
|
for (const entry of entries) {
|
|
2784
|
-
const absoluteChildPath =
|
|
3181
|
+
const absoluteChildPath = path13.join(directoryPath, entry.name);
|
|
2785
3182
|
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
2786
3183
|
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
2787
3184
|
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
@@ -2792,9 +3189,9 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
2792
3189
|
gitignoreStack: stack
|
|
2793
3190
|
}))),
|
|
2794
3191
|
Promise.all(files.map(async (f) => {
|
|
2795
|
-
const fileStat = await
|
|
3192
|
+
const fileStat = await stat6(f);
|
|
2796
3193
|
return {
|
|
2797
|
-
relPath:
|
|
3194
|
+
relPath: path13.relative(rootDirectoryPath, f),
|
|
2798
3195
|
absPath: f,
|
|
2799
3196
|
mtimeMs: fileStat.mtimeMs
|
|
2800
3197
|
};
|
|
@@ -2807,14 +3204,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
2807
3204
|
}
|
|
2808
3205
|
|
|
2809
3206
|
// src/core/context-files.ts
|
|
2810
|
-
import
|
|
3207
|
+
import path14 from "path";
|
|
2811
3208
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2812
3209
|
function collectTrackedFiles(node, graph) {
|
|
2813
3210
|
const seen = /* @__PURE__ */ new Set();
|
|
2814
3211
|
const result = [];
|
|
2815
|
-
const projectRoot =
|
|
2816
|
-
const yggPrefix =
|
|
2817
|
-
const yggPrefixNormalized = yggPrefix.split(
|
|
3212
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3213
|
+
const yggPrefix = path14.relative(projectRoot, graph.rootPath);
|
|
3214
|
+
const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
|
|
2818
3215
|
const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
|
|
2819
3216
|
function addFile(filePath, category) {
|
|
2820
3217
|
if (seen.has(filePath)) return;
|
|
@@ -2825,7 +3222,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2825
3222
|
return [yggPrefixNormalized, ...segments].join("/");
|
|
2826
3223
|
}
|
|
2827
3224
|
function addNodeFiles(n) {
|
|
2828
|
-
addFile(graphPath("model", n.path, "node.yaml"), "graph");
|
|
3225
|
+
addFile(graphPath("model", n.path, "yg-node.yaml"), "graph");
|
|
2829
3226
|
for (const art of n.artifacts) {
|
|
2830
3227
|
if (configArtifactKeys.has(art.filename)) {
|
|
2831
3228
|
addFile(graphPath("model", n.path, art.filename), "graph");
|
|
@@ -2838,12 +3235,12 @@ function collectTrackedFiles(node, graph) {
|
|
|
2838
3235
|
addNodeFiles(ancestor);
|
|
2839
3236
|
}
|
|
2840
3237
|
const allAspectIds = /* @__PURE__ */ new Set();
|
|
2841
|
-
for (const
|
|
2842
|
-
allAspectIds.add(
|
|
3238
|
+
for (const entry of node.meta.aspects ?? []) {
|
|
3239
|
+
allAspectIds.add(entry.aspect);
|
|
2843
3240
|
}
|
|
2844
3241
|
for (const ancestor of ancestors) {
|
|
2845
|
-
for (const
|
|
2846
|
-
allAspectIds.add(
|
|
3242
|
+
for (const entry of ancestor.meta.aspects ?? []) {
|
|
3243
|
+
allAspectIds.add(entry.aspect);
|
|
2847
3244
|
}
|
|
2848
3245
|
}
|
|
2849
3246
|
const participatingFlows = collectParticipatingFlows2(graph, node, ancestors);
|
|
@@ -2854,7 +3251,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2854
3251
|
}
|
|
2855
3252
|
const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
|
|
2856
3253
|
for (const aspect of resolvedAspects) {
|
|
2857
|
-
addFile(graphPath("aspects", aspect.id, "aspect.yaml"), "graph");
|
|
3254
|
+
addFile(graphPath("aspects", aspect.id, "yg-aspect.yaml"), "graph");
|
|
2858
3255
|
for (const art of aspect.artifacts) {
|
|
2859
3256
|
addFile(graphPath("aspects", aspect.id, art.filename), "graph");
|
|
2860
3257
|
}
|
|
@@ -2863,7 +3260,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2863
3260
|
if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
|
|
2864
3261
|
const target = graph.nodes.get(relation.target);
|
|
2865
3262
|
if (!target) continue;
|
|
2866
|
-
const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.
|
|
3263
|
+
const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
2867
3264
|
const structuralArts = structuralFilenames.filter(
|
|
2868
3265
|
(filename) => target.artifacts.some((a) => a.filename === filename)
|
|
2869
3266
|
);
|
|
@@ -2880,7 +3277,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2880
3277
|
}
|
|
2881
3278
|
}
|
|
2882
3279
|
for (const flow of participatingFlows) {
|
|
2883
|
-
addFile(graphPath("flows", flow.path, "flow.yaml"), "graph");
|
|
3280
|
+
addFile(graphPath("flows", flow.path, "yg-flow.yaml"), "graph");
|
|
2884
3281
|
for (const art of flow.artifacts) {
|
|
2885
3282
|
addFile(graphPath("flows", flow.path, art.filename), "graph");
|
|
2886
3283
|
}
|
|
@@ -2897,8 +3294,8 @@ function collectParticipatingFlows2(graph, node, ancestors) {
|
|
|
2897
3294
|
}
|
|
2898
3295
|
|
|
2899
3296
|
// src/core/drift-detector.ts
|
|
2900
|
-
import { access } from "fs/promises";
|
|
2901
|
-
import
|
|
3297
|
+
import { access as access2 } from "fs/promises";
|
|
3298
|
+
import path15 from "path";
|
|
2902
3299
|
function getChildMappingExclusions(graph, nodePath) {
|
|
2903
3300
|
const node = graph.nodes.get(nodePath);
|
|
2904
3301
|
if (!node) return [];
|
|
@@ -2920,7 +3317,7 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
2920
3317
|
return exclusions;
|
|
2921
3318
|
}
|
|
2922
3319
|
async function detectDrift(graph, filterNodePath) {
|
|
2923
|
-
const projectRoot =
|
|
3320
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
2924
3321
|
const driftState = await readDriftState(graph.rootPath);
|
|
2925
3322
|
const entries = [];
|
|
2926
3323
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -3002,16 +3399,16 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
3002
3399
|
};
|
|
3003
3400
|
}
|
|
3004
3401
|
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
3005
|
-
const yggPrefix =
|
|
3006
|
-
const normalizedPrefix = yggPrefix.split(
|
|
3402
|
+
const yggPrefix = path15.relative(projectRoot, _rootPath);
|
|
3403
|
+
const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
|
|
3007
3404
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
3008
3405
|
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
3009
3406
|
}
|
|
3010
3407
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
3011
3408
|
for (const mp of mappingPaths) {
|
|
3012
|
-
const absPath =
|
|
3409
|
+
const absPath = path15.join(projectRoot, mp);
|
|
3013
3410
|
try {
|
|
3014
|
-
await
|
|
3411
|
+
await access2(absPath);
|
|
3015
3412
|
return false;
|
|
3016
3413
|
} catch {
|
|
3017
3414
|
}
|
|
@@ -3019,24 +3416,21 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
3019
3416
|
return true;
|
|
3020
3417
|
}
|
|
3021
3418
|
async function syncDriftState(graph, nodePath) {
|
|
3022
|
-
const projectRoot =
|
|
3419
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
3023
3420
|
const node = graph.nodes.get(nodePath);
|
|
3024
3421
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
3025
3422
|
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
3026
3423
|
const trackedFiles = collectTrackedFiles(node, graph);
|
|
3027
3424
|
const excludePrefixes = getChildMappingExclusions(graph, nodePath);
|
|
3028
|
-
const
|
|
3029
|
-
const existingEntry = existingState[nodePath];
|
|
3425
|
+
const existingEntry = await readNodeDriftState(graph.rootPath, nodePath);
|
|
3030
3426
|
const storedFileData = existingEntry?.files ? { hashes: existingEntry.files, mtimes: existingEntry.mtimes ?? {} } : void 0;
|
|
3031
3427
|
const { canonicalHash, fileHashes, fileMtimes } = await hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes);
|
|
3032
3428
|
const previousHash = existingEntry?.hash;
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
}
|
|
3039
|
-
await writeDriftState(graph.rootPath, existingState);
|
|
3429
|
+
await writeNodeDriftState(graph.rootPath, nodePath, {
|
|
3430
|
+
hash: canonicalHash,
|
|
3431
|
+
files: fileHashes,
|
|
3432
|
+
mtimes: fileMtimes
|
|
3433
|
+
});
|
|
3040
3434
|
return { previousHash, currentHash: canonicalHash };
|
|
3041
3435
|
}
|
|
3042
3436
|
|
|
@@ -3218,6 +3612,14 @@ function registerDriftSyncCommand(program2) {
|
|
|
3218
3612
|
`
|
|
3219
3613
|
);
|
|
3220
3614
|
}
|
|
3615
|
+
if (options.all) {
|
|
3616
|
+
const validPaths = new Set(nodesToSync);
|
|
3617
|
+
const removed = await garbageCollectDriftState(graph.rootPath, validPaths);
|
|
3618
|
+
for (const r of removed) {
|
|
3619
|
+
process.stdout.write(chalk3.dim(`Removed orphaned drift state: ${r}
|
|
3620
|
+
`));
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3221
3623
|
} catch (error) {
|
|
3222
3624
|
process.stderr.write(`Error: ${error.message}
|
|
3223
3625
|
`);
|
|
@@ -3334,10 +3736,10 @@ function registerTreeCommand(program2) {
|
|
|
3334
3736
|
let roots;
|
|
3335
3737
|
let showProjectName;
|
|
3336
3738
|
if (options.root?.trim()) {
|
|
3337
|
-
const
|
|
3338
|
-
const node = graph.nodes.get(
|
|
3739
|
+
const path19 = options.root.trim().replace(/\/$/, "");
|
|
3740
|
+
const node = graph.nodes.get(path19);
|
|
3339
3741
|
if (!node) {
|
|
3340
|
-
process.stderr.write(`Error: path '${
|
|
3742
|
+
process.stderr.write(`Error: path '${path19}' not found
|
|
3341
3743
|
`);
|
|
3342
3744
|
process.exit(1);
|
|
3343
3745
|
}
|
|
@@ -3381,8 +3783,8 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
3381
3783
|
}
|
|
3382
3784
|
|
|
3383
3785
|
// src/cli/owner.ts
|
|
3384
|
-
import
|
|
3385
|
-
import { access as
|
|
3786
|
+
import path16 from "path";
|
|
3787
|
+
import { access as access3 } from "fs/promises";
|
|
3386
3788
|
function normalizeForMatch(inputPath) {
|
|
3387
3789
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3388
3790
|
}
|
|
@@ -3411,14 +3813,14 @@ function registerOwnerCommand(program2) {
|
|
|
3411
3813
|
const graph = await loadGraph(cwd);
|
|
3412
3814
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3413
3815
|
const rawPath = options.file.trim();
|
|
3414
|
-
const absolute =
|
|
3415
|
-
const repoRelative =
|
|
3816
|
+
const absolute = path16.resolve(cwd, rawPath);
|
|
3817
|
+
const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
|
|
3416
3818
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3417
3819
|
if (!result.nodePath) {
|
|
3418
|
-
const absPath =
|
|
3820
|
+
const absPath = path16.resolve(repoRoot, result.file);
|
|
3419
3821
|
let exists = true;
|
|
3420
3822
|
try {
|
|
3421
|
-
await
|
|
3823
|
+
await access3(absPath);
|
|
3422
3824
|
} catch {
|
|
3423
3825
|
exists = false;
|
|
3424
3826
|
}
|
|
@@ -3449,7 +3851,7 @@ function registerOwnerCommand(program2) {
|
|
|
3449
3851
|
|
|
3450
3852
|
// src/core/dependency-resolver.ts
|
|
3451
3853
|
import { execSync } from "child_process";
|
|
3452
|
-
import
|
|
3854
|
+
import path17 from "path";
|
|
3453
3855
|
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3454
3856
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
3455
3857
|
function filterRelationType(relType, filter) {
|
|
@@ -3524,9 +3926,9 @@ function registerDepsCommand(program2) {
|
|
|
3524
3926
|
}
|
|
3525
3927
|
|
|
3526
3928
|
// src/core/graph-from-git.ts
|
|
3527
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3929
|
+
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
3528
3930
|
import { tmpdir } from "os";
|
|
3529
|
-
import
|
|
3931
|
+
import path18 from "path";
|
|
3530
3932
|
import { execSync as execSync2 } from "child_process";
|
|
3531
3933
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
3532
3934
|
const yggPath = ".yggdrasil";
|
|
@@ -3537,8 +3939,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
3537
3939
|
return null;
|
|
3538
3940
|
}
|
|
3539
3941
|
try {
|
|
3540
|
-
tmpDir = await mkdtemp(
|
|
3541
|
-
const archivePath =
|
|
3942
|
+
tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
|
|
3943
|
+
const archivePath = path18.join(tmpDir, "archive.tar");
|
|
3542
3944
|
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
3543
3945
|
cwd: projectRoot,
|
|
3544
3946
|
stdio: "pipe"
|
|
@@ -3550,7 +3952,7 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
3550
3952
|
return null;
|
|
3551
3953
|
} finally {
|
|
3552
3954
|
if (tmpDir) {
|
|
3553
|
-
await
|
|
3955
|
+
await rm3(tmpDir, { recursive: true, force: true });
|
|
3554
3956
|
}
|
|
3555
3957
|
}
|
|
3556
3958
|
}
|
|
@@ -3608,14 +4010,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
3608
4010
|
}
|
|
3609
4011
|
const chains = [];
|
|
3610
4012
|
for (const node of transitiveOnly) {
|
|
3611
|
-
const
|
|
4013
|
+
const path19 = [];
|
|
3612
4014
|
let current = node;
|
|
3613
4015
|
while (current) {
|
|
3614
|
-
|
|
4016
|
+
path19.unshift(current);
|
|
3615
4017
|
current = parent.get(current);
|
|
3616
4018
|
}
|
|
3617
|
-
if (
|
|
3618
|
-
chains.push(
|
|
4019
|
+
if (path19.length >= 3) {
|
|
4020
|
+
chains.push(path19.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
3619
4021
|
}
|
|
3620
4022
|
}
|
|
3621
4023
|
return chains.sort();
|
|
@@ -3685,14 +4087,14 @@ async function handleAspectImpact(graph, aspectId, simulate) {
|
|
|
3685
4087
|
const effective = collectEffectiveAspectIds(graph, nodePath);
|
|
3686
4088
|
if (effective.has(aspectId)) {
|
|
3687
4089
|
const node = graph.nodes.get(nodePath);
|
|
3688
|
-
const
|
|
3689
|
-
if (
|
|
4090
|
+
const ownAspectIds = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
|
|
4091
|
+
if (ownAspectIds.has(aspectId)) {
|
|
3690
4092
|
affected.push({ path: nodePath, source: "own" });
|
|
3691
4093
|
} else {
|
|
3692
4094
|
let fromHierarchy = false;
|
|
3693
4095
|
let anc = node.parent;
|
|
3694
4096
|
while (anc) {
|
|
3695
|
-
if ((anc.meta.aspects ?? []).
|
|
4097
|
+
if ((anc.meta.aspects ?? []).some((a) => a.aspect === aspectId)) {
|
|
3696
4098
|
fromHierarchy = true;
|
|
3697
4099
|
break;
|
|
3698
4100
|
}
|
|
@@ -4036,131 +4438,12 @@ function registerFlowsCommand(program2) {
|
|
|
4036
4438
|
});
|
|
4037
4439
|
}
|
|
4038
4440
|
|
|
4039
|
-
// src/io/journal-store.ts
|
|
4040
|
-
import { readFile as readFile14, writeFile as writeFile4, mkdir as mkdir3, rename, access as access3 } from "fs/promises";
|
|
4041
|
-
import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
|
|
4042
|
-
import path17 from "path";
|
|
4043
|
-
var JOURNAL_FILE = ".journal.yaml";
|
|
4044
|
-
var ARCHIVE_DIR = "journals-archive";
|
|
4045
|
-
async function readJournal(yggRoot) {
|
|
4046
|
-
const filePath = path17.join(yggRoot, JOURNAL_FILE);
|
|
4047
|
-
try {
|
|
4048
|
-
const content = await readFile14(filePath, "utf-8");
|
|
4049
|
-
const raw = parseYaml6(content);
|
|
4050
|
-
const entries = raw.entries ?? [];
|
|
4051
|
-
return Array.isArray(entries) ? entries : [];
|
|
4052
|
-
} catch {
|
|
4053
|
-
return [];
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4056
|
-
async function appendJournalEntry(yggRoot, note, target) {
|
|
4057
|
-
const entries = await readJournal(yggRoot);
|
|
4058
|
-
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4059
|
-
const entry = target ? { at, target, note } : { at, note };
|
|
4060
|
-
entries.push(entry);
|
|
4061
|
-
const filePath = path17.join(yggRoot, JOURNAL_FILE);
|
|
4062
|
-
const content = stringifyYaml({ entries });
|
|
4063
|
-
await writeFile4(filePath, content, "utf-8");
|
|
4064
|
-
return entry;
|
|
4065
|
-
}
|
|
4066
|
-
async function archiveJournal(yggRoot) {
|
|
4067
|
-
const journalPath = path17.join(yggRoot, JOURNAL_FILE);
|
|
4068
|
-
try {
|
|
4069
|
-
await access3(journalPath);
|
|
4070
|
-
} catch {
|
|
4071
|
-
return null;
|
|
4072
|
-
}
|
|
4073
|
-
const entries = await readJournal(yggRoot);
|
|
4074
|
-
if (entries.length === 0) return null;
|
|
4075
|
-
const archiveDir = path17.join(yggRoot, ARCHIVE_DIR);
|
|
4076
|
-
await mkdir3(archiveDir, { recursive: true });
|
|
4077
|
-
const now = /* @__PURE__ */ new Date();
|
|
4078
|
-
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")}`;
|
|
4079
|
-
const archiveName = `.journal.${timestamp}.yaml`;
|
|
4080
|
-
const archivePath = path17.join(archiveDir, archiveName);
|
|
4081
|
-
await rename(journalPath, archivePath);
|
|
4082
|
-
return { archiveName, entryCount: entries.length };
|
|
4083
|
-
}
|
|
4084
|
-
|
|
4085
|
-
// src/cli/journal-add.ts
|
|
4086
|
-
function registerJournalAddCommand(program2) {
|
|
4087
|
-
program2.command("journal-add").description("Add a note to the session journal").requiredOption("--note <text>", "Note content").option("--target <node-path>", "Node path this note relates to").action(async (options) => {
|
|
4088
|
-
try {
|
|
4089
|
-
const projectRoot = process.cwd();
|
|
4090
|
-
const yggRoot = await findYggRoot(projectRoot);
|
|
4091
|
-
await appendJournalEntry(yggRoot, options.note, options.target);
|
|
4092
|
-
const entries = await readJournal(yggRoot);
|
|
4093
|
-
process.stdout.write(`Note added to journal (${entries.length} entries total)
|
|
4094
|
-
`);
|
|
4095
|
-
} catch (error) {
|
|
4096
|
-
process.stderr.write(`Error: ${error.message}
|
|
4097
|
-
`);
|
|
4098
|
-
process.exit(1);
|
|
4099
|
-
}
|
|
4100
|
-
});
|
|
4101
|
-
}
|
|
4102
|
-
|
|
4103
|
-
// src/cli/journal-read.ts
|
|
4104
|
-
function registerJournalReadCommand(program2) {
|
|
4105
|
-
program2.command("journal-read").description("List pending journal entries").action(async () => {
|
|
4106
|
-
try {
|
|
4107
|
-
const projectRoot = process.cwd();
|
|
4108
|
-
const yggRoot = await findYggRoot(projectRoot);
|
|
4109
|
-
const entries = await readJournal(yggRoot);
|
|
4110
|
-
if (entries.length === 0) {
|
|
4111
|
-
process.stdout.write("Session journal: empty (clean state)\n");
|
|
4112
|
-
return;
|
|
4113
|
-
}
|
|
4114
|
-
process.stdout.write(`Session journal (${entries.length} entries):
|
|
4115
|
-
|
|
4116
|
-
`);
|
|
4117
|
-
for (const e of entries) {
|
|
4118
|
-
const date = e.at.slice(0, 19).replace("T", " ");
|
|
4119
|
-
const target = e.target ? ` ${e.target}` : "";
|
|
4120
|
-
process.stdout.write(`[${date}]${target}
|
|
4121
|
-
${e.note}
|
|
4122
|
-
|
|
4123
|
-
`);
|
|
4124
|
-
}
|
|
4125
|
-
} catch (error) {
|
|
4126
|
-
process.stderr.write(`Error: ${error.message}
|
|
4127
|
-
`);
|
|
4128
|
-
process.exit(1);
|
|
4129
|
-
}
|
|
4130
|
-
});
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
|
-
// src/cli/journal-archive.ts
|
|
4134
|
-
function registerJournalArchiveCommand(program2) {
|
|
4135
|
-
program2.command("journal-archive").description("Archive journal after consolidating notes to graph").action(async () => {
|
|
4136
|
-
try {
|
|
4137
|
-
const projectRoot = process.cwd();
|
|
4138
|
-
const yggRoot = await findYggRoot(projectRoot);
|
|
4139
|
-
const result = await archiveJournal(yggRoot);
|
|
4140
|
-
if (!result) {
|
|
4141
|
-
process.stdout.write("No active journal - nothing to archive.\n");
|
|
4142
|
-
return;
|
|
4143
|
-
}
|
|
4144
|
-
process.stdout.write(
|
|
4145
|
-
`Archived journal (${result.entryCount} entries) -> journals-archive/${result.archiveName}
|
|
4146
|
-
`
|
|
4147
|
-
);
|
|
4148
|
-
} catch (error) {
|
|
4149
|
-
process.stderr.write(`Error: ${error.message}
|
|
4150
|
-
`);
|
|
4151
|
-
process.exit(1);
|
|
4152
|
-
}
|
|
4153
|
-
});
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
4441
|
// src/cli/preflight.ts
|
|
4157
4442
|
function registerPreflightCommand(program2) {
|
|
4158
|
-
program2.command("preflight").description("Unified diagnostic report:
|
|
4443
|
+
program2.command("preflight").description("Unified diagnostic report: drift, status, validation").option("--quick", "Skip drift detection for faster results").action(async (options) => {
|
|
4159
4444
|
try {
|
|
4160
4445
|
const cwd = process.cwd();
|
|
4161
4446
|
const graph = await loadGraph(cwd);
|
|
4162
|
-
const yggRoot = await findYggRoot(cwd);
|
|
4163
|
-
const journalEntries = await readJournal(yggRoot);
|
|
4164
4447
|
const driftedEntries = options.quick ? [] : (await detectDrift(graph)).entries.filter((e) => e.status !== "ok");
|
|
4165
4448
|
const nodeCount = graph.nodes.size;
|
|
4166
4449
|
const aspectCount = graph.aspects.length;
|
|
@@ -4175,16 +4458,6 @@ function registerPreflightCommand(program2) {
|
|
|
4175
4458
|
const lines = [];
|
|
4176
4459
|
lines.push("=== Preflight Report ===");
|
|
4177
4460
|
lines.push("");
|
|
4178
|
-
if (journalEntries.length === 0) {
|
|
4179
|
-
lines.push("Journal: clean");
|
|
4180
|
-
} else {
|
|
4181
|
-
lines.push(`Journal: ${journalEntries.length} pending entries`);
|
|
4182
|
-
for (const entry of journalEntries) {
|
|
4183
|
-
const target = entry.target ? ` [${entry.target}]` : "";
|
|
4184
|
-
lines.push(` - ${entry.note}${target}`);
|
|
4185
|
-
}
|
|
4186
|
-
}
|
|
4187
|
-
lines.push("");
|
|
4188
4461
|
if (options.quick) {
|
|
4189
4462
|
lines.push("Drift: skipped (--quick)");
|
|
4190
4463
|
} else if (driftedEntries.length === 0) {
|
|
@@ -4221,7 +4494,7 @@ function registerPreflightCommand(program2) {
|
|
|
4221
4494
|
}
|
|
4222
4495
|
lines.push("");
|
|
4223
4496
|
process.stdout.write(lines.join("\n"));
|
|
4224
|
-
const hasIssues =
|
|
4497
|
+
const hasIssues = !options.quick && driftedEntries.length > 0 || errors.length > 0;
|
|
4225
4498
|
process.exit(hasIssues ? 1 : 0);
|
|
4226
4499
|
} catch (error) {
|
|
4227
4500
|
process.stderr.write(`Error: ${error.message}
|
|
@@ -4232,12 +4505,12 @@ function registerPreflightCommand(program2) {
|
|
|
4232
4505
|
}
|
|
4233
4506
|
|
|
4234
4507
|
// src/bin.ts
|
|
4235
|
-
import { readFileSync } from "fs";
|
|
4508
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4236
4509
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4237
4510
|
import { dirname, join } from "path";
|
|
4238
4511
|
var __filename = fileURLToPath3(import.meta.url);
|
|
4239
4512
|
var __dirname = dirname(__filename);
|
|
4240
|
-
var pkg = JSON.parse(
|
|
4513
|
+
var pkg = JSON.parse(readFileSync2(join(__dirname, "..", "package.json"), "utf-8"));
|
|
4241
4514
|
var program = new Command();
|
|
4242
4515
|
program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
|
|
4243
4516
|
registerInitCommand(program);
|
|
@@ -4252,9 +4525,6 @@ registerDepsCommand(program);
|
|
|
4252
4525
|
registerImpactCommand(program);
|
|
4253
4526
|
registerAspectsCommand(program);
|
|
4254
4527
|
registerFlowsCommand(program);
|
|
4255
|
-
registerJournalAddCommand(program);
|
|
4256
|
-
registerJournalReadCommand(program);
|
|
4257
|
-
registerJournalArchiveCommand(program);
|
|
4258
4528
|
registerPreflightCommand(program);
|
|
4259
4529
|
program.parse();
|
|
4260
4530
|
//# sourceMappingURL=bin.js.map
|