@chrisdudek/yg 2.11.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +397 -349
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +1 -15
- package/dist/templates/rules.ts +100 -35
- package/graph-schemas/yg-config.yaml +3 -12
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
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 writeFile5, readdir as readdir2, readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
8
|
+
import path5 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import { readFileSync } from "fs";
|
|
11
11
|
import { gt as gt2, valid as valid2 } from "semver";
|
|
12
12
|
|
|
13
13
|
// src/templates/default-config.ts
|
|
14
|
-
var DEFAULT_CONFIG = `version: "
|
|
14
|
+
var DEFAULT_CONFIG = `version: "3.0.0"
|
|
15
15
|
|
|
16
16
|
name: ""
|
|
17
17
|
|
|
@@ -25,20 +25,6 @@ node_types:
|
|
|
25
25
|
infrastructure:
|
|
26
26
|
description: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
|
|
27
27
|
|
|
28
|
-
artifacts:
|
|
29
|
-
responsibility.md:
|
|
30
|
-
required: always
|
|
31
|
-
description: "What this node is responsible for, and what it is not"
|
|
32
|
-
included_in_relations: true
|
|
33
|
-
interface.md:
|
|
34
|
-
required:
|
|
35
|
-
when: has_incoming_relations
|
|
36
|
-
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures"
|
|
37
|
-
included_in_relations: true
|
|
38
|
-
internals.md:
|
|
39
|
-
required: never
|
|
40
|
-
description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
|
|
41
|
-
|
|
42
28
|
quality:
|
|
43
29
|
min_artifact_length: 50
|
|
44
30
|
max_direct_relations: 10
|
|
@@ -59,11 +45,32 @@ var PROTOCOL = `## PROTOCOL
|
|
|
59
45
|
This is your operating manual for working in a Yggdrasil-managed repository.
|
|
60
46
|
|
|
61
47
|
<critical_protocol>
|
|
48
|
+
BEFORE starting any task \u2014 brainstorming, design, planning, OR implementation:
|
|
49
|
+
\`yg select --task "<goal>"\` \u2192 \`yg build-context\` on each result \u2192 read artifact files.
|
|
50
|
+
This is the READING phase \u2014 collect constraints that shape your design:
|
|
51
|
+
- Aspects = cross-cutting requirements your work MUST satisfy. Read their content files \u2014 not just the YAML description. The rules are inside.
|
|
52
|
+
- Flows = business processes your work must not break. Read invariants.
|
|
53
|
+
- Relations = interfaces your code consumes or that consume your code. Changes without checking dependents break contracts silently.
|
|
54
|
+
- Parent artifacts = inherited context not repeated in child nodes.
|
|
55
|
+
Internalize these constraints BEFORE designing your approach. This is the moment that determines quality \u2014 everything after follows from what you learn here.
|
|
56
|
+
|
|
62
57
|
BEFORE reading, analyzing, or modifying ANY source file:
|
|
63
58
|
\`yg build-context --file <path>\`
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
Resolves owner, gives you local context (node artifacts, dependencies).
|
|
60
|
+
If you have NOT done the task-level READING phase above \u2014 stop and do it now. File-level work without task-level constraints leads to code that violates cross-cutting requirements.
|
|
61
|
+
|
|
62
|
+
WHEN spec/external documents are provided AND work is greenfield:
|
|
63
|
+
BEFORE creating any feature aspect, node, or code \u2014 do this first:
|
|
64
|
+
1. Read ALL spec documents completely.
|
|
65
|
+
2. Separate two categories of knowledge in the specs:
|
|
66
|
+
a) Knowledge that WILL map to source files (feature behavior, UI, API) \u2192 this goes to node artifacts later.
|
|
67
|
+
b) Knowledge that will NEVER appear in source code (business strategy, target audience, pricing rationale, quality targets, what the system deliberately does NOT do and why) \u2192 this must go to the graph NOW, because there will be no source-file trigger to capture it later.
|
|
68
|
+
3. For category (b): create root node and/or aspects immediately. This is the only moment this knowledge can be captured. Once you start coding, you will forget it.
|
|
69
|
+
4. NOW create feature aspects, flows, and implementation nodes for category (a).
|
|
70
|
+
Test before moving on: if all spec files were deleted right now, does the graph contain everything a future agent needs \u2014 not just HOW the system works, but WHY it exists, WHO it serves, and WHAT value it delivers? If no \u2192 add the missing knowledge before continuing.
|
|
71
|
+
|
|
72
|
+
All three triggers apply regardless of what instructed the task \u2014 skills, plans, workflows, user requests.
|
|
73
|
+
The graph captures intent and relations that source files cannot \u2014 without it, you will make wrong decisions and the user will have to re-explain what the graph already knows.
|
|
67
74
|
</critical_protocol>
|
|
68
75
|
|
|
69
76
|
Every rule below is mandatory \u2014 no skill, plan, workflow, or instruction overrides these requirements.
|
|
@@ -76,18 +83,25 @@ Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the r
|
|
|
76
83
|
\`\`\`
|
|
77
84
|
EVERY conversation: yg preflight \u2014 no exceptions.
|
|
78
85
|
|
|
86
|
+
BEFORE any task (brainstorming, design, planning, implementation):
|
|
87
|
+
yg select --task "<goal>" \u2192 yg build-context on results
|
|
88
|
+
READ phase \u2014 collect constraints before designing:
|
|
89
|
+
- Aspects: read content files (not just YAML description). Rules are inside.
|
|
90
|
+
- Flows: read invariants. Your changes must not break business processes.
|
|
91
|
+
- Relations: check interfaces \u2014 who depends on you, who you depend on.
|
|
92
|
+
- Parent artifacts: inherited context not repeated in child nodes.
|
|
93
|
+
This is the moment that determines quality. Everything after follows from here.
|
|
94
|
+
|
|
79
95
|
BEFORE any source file interaction:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
For blast radius: also run yg impact --file <path>.
|
|
85
|
-
Don't know where to start? yg select --task "<goal>"
|
|
96
|
+
yg build-context --file <path>
|
|
97
|
+
Resolves owner. Read local node artifacts.
|
|
98
|
+
If you skipped the task-level READ phase above \u2014 do it now before proceeding.
|
|
99
|
+
For blast radius: also run yg impact --file <path>.
|
|
86
100
|
|
|
87
101
|
AFTER modifying:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
Update graph artifacts (per file, not batched)
|
|
103
|
+
yg validate \u2014 fix all errors
|
|
104
|
+
yg drift-sync --node <owner>
|
|
91
105
|
|
|
92
106
|
ALWAYS: establish graph coverage before modifying code.
|
|
93
107
|
ALWAYS: run yg build-context --file before reading source.
|
|
@@ -105,9 +119,9 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
105
119
|
|
|
106
120
|
**Step 2a** \u2014 Owner found: execute checklist:
|
|
107
121
|
|
|
108
|
-
- [ ] 1. Read the context package (already
|
|
122
|
+
- [ ] 1. Read local node artifacts (responsibility, interface, internals) and dependency interfaces from the context package. Cross-cutting constraints (aspects, flows) should already be internalized from the task-level READ phase \u2014 if not, stop and do it now.
|
|
109
123
|
- [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
|
|
110
|
-
- [ ] 3. Modify source code
|
|
124
|
+
- [ ] 3. Modify source code. When implementing logic subject to an aspect (e.g., writing a save function on a node with the autosave aspect), re-read that aspect's content file NOW \u2014 don't rely on memory from the task-level READ phase. Aspect rules are specific (exact timings, error handling patterns, UI details) and fade from working memory. Read them at the moment you need them.
|
|
111
125
|
- [ ] 4. Sync graph artifacts \u2014 edit artifact files to reflect the changes (after each file, not batched \u2014 context is freshest immediately after the change). If the node's purpose changed, update \`description\` in \`yg-node.yaml\`.
|
|
112
126
|
- [ ] 4b. If you split, merged, or renamed a node: run \`yg flows\` and update any flow \`nodes\` lists that referenced the old node path to point to the correct child/new nodes.
|
|
113
127
|
- [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
|
|
@@ -125,12 +139,14 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
125
139
|
|
|
126
140
|
*Greenfield (new code):* Only Option A. Blackbox is forbidden for new code. Follow the graph-first workflow:
|
|
127
141
|
|
|
142
|
+
0. **If spec/external documents exist:** route ALL knowledge from specs to the graph per the Information Routing table BEFORE any feature work. Use the appropriate location for each piece of knowledge \u2014 root node, aspects, flows, or node artifacts depending on its nature.
|
|
128
143
|
1. Create aspects first (cross-cutting requirements the new code must satisfy)
|
|
129
144
|
2. Create flows if the code participates in a business process
|
|
130
145
|
3. Create nodes with full artifacts \u2014 description in \`yg-node.yaml\`, responsibility, interface, internals
|
|
131
146
|
4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
|
|
132
|
-
5. Implement code that satisfies the specification
|
|
133
|
-
6.
|
|
147
|
+
5. Implement code that satisfies the specification. Every source file must be mapped \u2014 including shared utilities, types, and helpers.
|
|
148
|
+
6. After implementing each node, write \`internals.md\` with a ## Decisions section. Record every design choice: "Chose X over Y because Z." This is required in greenfield \u2014 not optional. Every node has design decisions (data model shape, algorithm, library, UI pattern). If you made a choice between alternatives, document it now \u2014 you will not remember later.
|
|
149
|
+
7. The graph specifies WHAT and WHY; the code implements HOW (framework APIs, library choices)
|
|
134
150
|
|
|
135
151
|
**Node sizing rule:** One node per cohesive feature area, NOT per directory. If a node would map >10 source files or cover >3 distinct user workflows, split it into child nodes. Example: an admin panel should be \`admin/blog\`, \`admin/gallery\`, \`admin/clients\`, \`admin/orders\` \u2014 not one \`admin-pages\` node. The CLI enforces this with W017, but plan granularity upfront rather than splitting after the fact.
|
|
136
152
|
|
|
@@ -185,13 +201,15 @@ Result: graph is stale, next agent asks user the same questions
|
|
|
185
201
|
User: "Here are the spec docs. Implement the admin blog editor."
|
|
186
202
|
|
|
187
203
|
1. Read ALL spec docs (blog-editor.md, autosave.md, user-persona.md, version-history.md)
|
|
188
|
-
2.
|
|
189
|
-
3.
|
|
190
|
-
4. Create
|
|
191
|
-
5.
|
|
192
|
-
6.
|
|
193
|
-
7.
|
|
194
|
-
8.
|
|
204
|
+
2. Route all knowledge from spec docs to the graph per Information Routing table \u2014 business context to root node artifacts, cross-cutting requirements to aspects, business processes to flows, feature specs to node artifacts
|
|
205
|
+
3. Extract cross-cutting patterns \u2192 create aspects (admin-ux-rules, autosave, version-history) if they don't exist
|
|
206
|
+
4. Create flow if the blog participates in a business process
|
|
207
|
+
5. Create node admin/blog with artifacts populated from spec (responsibility, interface, internals)
|
|
208
|
+
6. Run yg build-context \u2192 the context package is now the behavioral specification
|
|
209
|
+
7. Implement code that satisfies the specification
|
|
210
|
+
8. Update artifacts with any implementation details that emerged during coding
|
|
211
|
+
9. yg validate, yg drift-sync
|
|
212
|
+
Test: if spec files disappeared today, does the graph contain everything a future agent needs to understand the system?
|
|
195
213
|
|
|
196
214
|
</example_correct>
|
|
197
215
|
|
|
@@ -199,13 +217,36 @@ User: "Here are the spec docs. Implement the admin blog editor."
|
|
|
199
217
|
|
|
200
218
|
User: "Here are the spec docs. Implement the admin blog editor."
|
|
201
219
|
|
|
202
|
-
1. Read
|
|
203
|
-
2.
|
|
204
|
-
3. Create node admin
|
|
220
|
+
1. Read spec docs
|
|
221
|
+
2. Create aspects and flow for the blog feature \u2190 INCOMPLETE: knowledge from spec docs not routed to graph per Information Routing table
|
|
222
|
+
3. Create node admin/blog, implement code
|
|
205
223
|
4. Write responsibility.md summarizing what the code does \u2190 WRONG: describes code, not spec intent
|
|
206
|
-
5.
|
|
224
|
+
5. Knowledge from spec docs lost \u2190 WRONG: spec treated as consumed input, not persisted to graph
|
|
207
225
|
|
|
208
|
-
Result: graph mirrors code but misses
|
|
226
|
+
Result: graph mirrors code structure but misses everything spec docs contained that has no corresponding source file. Future agent must re-read spec files or ask the user.
|
|
227
|
+
|
|
228
|
+
</example_wrong>
|
|
229
|
+
|
|
230
|
+
<example_correct>
|
|
231
|
+
|
|
232
|
+
User: "Let's design a soft delete feature for blog posts"
|
|
233
|
+
|
|
234
|
+
1. yg select --task "blog soft delete" \u2192 find relevant nodes
|
|
235
|
+
2. yg build-context on each result \u2192 read ALL artifacts (aspects, flows, conventions)
|
|
236
|
+
3. Now read source files WITH graph context
|
|
237
|
+
4. Propose design informed by admin-ux-rules, existing flows, database conventions
|
|
238
|
+
|
|
239
|
+
</example_correct>
|
|
240
|
+
|
|
241
|
+
<example_wrong>
|
|
242
|
+
|
|
243
|
+
User: "Let's design a soft delete feature for blog posts"
|
|
244
|
+
|
|
245
|
+
1. Read BlogEditor.tsx to understand current behavior \u2190 WRONG: no graph context
|
|
246
|
+
2. Read database schema \u2190 WRONG: graph has conventions, aspects, flows
|
|
247
|
+
3. Propose design based on raw code \u2190 WRONG: missed admin-ux-rules aspect, existing flows
|
|
248
|
+
|
|
249
|
+
Result: design misses cross-cutting requirements the graph already captured.
|
|
209
250
|
|
|
210
251
|
</example_wrong>
|
|
211
252
|
|
|
@@ -269,7 +310,7 @@ var REFERENCE = `## REFERENCE
|
|
|
269
310
|
|
|
270
311
|
\`\`\`
|
|
271
312
|
.yggdrasil/
|
|
272
|
-
yg-config.yaml \u2190 version, vocabulary, node types,
|
|
313
|
+
yg-config.yaml \u2190 version, vocabulary, node types, required aspects
|
|
273
314
|
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
274
315
|
aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
|
|
275
316
|
flows/ \u2190 why and in what process: business processes with node participation
|
|
@@ -295,7 +336,7 @@ Three artifacts capture node knowledge at three levels:
|
|
|
295
336
|
|
|
296
337
|
**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\`.
|
|
297
338
|
|
|
298
|
-
|
|
339
|
+
These three artifacts are built into the CLI and are not configurable. \`responsibility.md\` is always required, \`interface.md\` is required when the node has incoming relations, and \`internals.md\` is always optional.
|
|
299
340
|
|
|
300
341
|
### Context Assembly
|
|
301
342
|
|
|
@@ -310,9 +351,14 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
|
|
|
310
351
|
|
|
311
352
|
All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
|
|
312
353
|
|
|
313
|
-
**Default mode (paths-only):** Use for all graph operations. Read the YAML map
|
|
354
|
+
**Default mode (paths-only):** Use for all graph operations. Read the YAML map, then read artifact files with purpose:
|
|
314
355
|
|
|
315
|
-
|
|
356
|
+
1. **Glossary first** \u2014 defines aspects and flows. Aspects are constraints your implementation must satisfy (not background reading). Flows are business processes whose invariants you must not break.
|
|
357
|
+
2. **Node section** \u2014 your target's own artifacts. Read before modifying.
|
|
358
|
+
3. **Hierarchy** \u2014 parent artifacts contain inherited requirements not repeated in child nodes.
|
|
359
|
+
4. **Dependencies** \u2014 interfaces you consume or that consume you. Read before changing contracts.
|
|
360
|
+
|
|
361
|
+
A typical context package is ~8K tokens (less than a single source file). Read ALL artifact files listed \u2014 the cost is low, the risk of skipping is high (violating constraints you didn't know about).
|
|
316
362
|
|
|
317
363
|
**Full mode (\`--full\`):** Use only when you cannot read files individually \u2014 e.g., when pasting context into a prompt, sharing with a user, or when you have no Read tool available.
|
|
318
364
|
|
|
@@ -322,7 +368,7 @@ Artifact paths are stable identifiers within a session. When building context fo
|
|
|
322
368
|
|
|
323
369
|
When you encounter information, route it to the correct location:
|
|
324
370
|
|
|
325
|
-
- **Specific to this node** \u2192 local node artifact (
|
|
371
|
+
- **Specific to this node** \u2192 local node artifact (\`responsibility.md\`, \`interface.md\`, or \`internals.md\` depending on the knowledge type)
|
|
326
372
|
- **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\`
|
|
327
373
|
- **Business process** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
328
374
|
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
@@ -433,6 +479,7 @@ yg owner --file <path> Find the node that owns this file (quick che
|
|
|
433
479
|
yg build-context --file <path> Resolve owner + assemble context in one step.
|
|
434
480
|
yg build-context --node <path> Assemble context map for a known node.
|
|
435
481
|
yg build-context --node <path> --full Same map + file contents appended below separator.
|
|
482
|
+
yg build-context --file <path> --self Own artifacts only (no hierarchy/deps/aspects/flows).
|
|
436
483
|
yg tree [--root <path>] [--depth N] Print graph structure.
|
|
437
484
|
yg aspects List aspects with metadata (YAML output).
|
|
438
485
|
yg flows List flows with metadata (YAML output).
|
|
@@ -457,7 +504,7 @@ yg drift-sync --node <path> [--recursive] | --all
|
|
|
457
504
|
|
|
458
505
|
| What you have | Where it goes |
|
|
459
506
|
|---|---|
|
|
460
|
-
| Information specific to this node | Local node artifact (
|
|
507
|
+
| Information specific to this node | Local node artifact (\`responsibility.md\`, \`interface.md\`, or \`internals.md\`) |
|
|
461
508
|
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
462
509
|
| Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
|
|
463
510
|
| Business process participation | Flow (\`yg-flow.yaml nodes\`) |
|
|
@@ -532,11 +579,15 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
|
|
|
532
579
|
| "Flows can wait until I understand the full system" | Flows capture business processes from specs. Create them BEFORE implementing \u2014 they are part of the specification, not an afterthought. |
|
|
533
580
|
| "I split the node but the flow still works" | Flow participants reference specific node paths. After a split, old paths are stale. Run \`yg flows\` and update. |
|
|
534
581
|
| "This is just CRUD, not a business process" | A user performing a sequence of steps toward a goal IS a business process \u2014 even single-actor workflows (publish blog, manage portfolio, fulfill order). Create a flow. |
|
|
582
|
+
| "The context package is too large to read" | A typical context package is ~8K tokens \u2014 less than one source file. Read ALL of it. |
|
|
583
|
+
| "I have a plan, I don't need graph context" | A plan is not a substitute for graph context. Plans capture task steps; the graph captures cross-cutting aspects, flows, and conventions that plans may not repeat. Always run \`build-context\`. |
|
|
584
|
+
| "The user told me what to do, that's my plan" | A verbal instruction is not a written plan. And even a written plan does not exempt you from the graph protocol. |
|
|
535
585
|
|
|
536
586
|
### Failure States
|
|
537
587
|
|
|
538
588
|
You have broken Yggdrasil if you do any of the following:
|
|
539
589
|
|
|
590
|
+
- \u274C Started brainstorming, design, or planning without running \`yg select --task\` and reading graph context first. The graph contains aspects, flows, and conventions that MUST inform design decisions.
|
|
540
591
|
- \u274C Worked on a source file without running \`yg build-context --file\` first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
|
|
541
592
|
- \u274C Modified source code without updating graph artifacts before moving to the next file, or vice versa.
|
|
542
593
|
- \u274C Batched graph updates to "do later" \u2014 deferred = forgotten. Update after EACH file.
|
|
@@ -566,7 +617,7 @@ Per area checklist:
|
|
|
566
617
|
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
567
618
|
- [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
|
|
568
619
|
- [ ] 3b. Write \`description\` in \`yg-node.yaml\` \u2014 a short summary of what the node does
|
|
569
|
-
- [ ] 4. Analyze source \u2014
|
|
620
|
+
- [ ] 4. Analyze source \u2014 write \`responsibility.md\`, \`interface.md\`, and \`internals.md\` from code analysis, do not invent
|
|
570
621
|
- [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
|
|
571
622
|
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
572
623
|
- [ ] 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\`
|
|
@@ -1188,38 +1239,73 @@ async function transformSingleNode(filePath, actions, warnings) {
|
|
|
1188
1239
|
}
|
|
1189
1240
|
}
|
|
1190
1241
|
|
|
1242
|
+
// src/migrations/to-3.0.0.ts
|
|
1243
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1244
|
+
import path4 from "path";
|
|
1245
|
+
import { parse as parseYaml3, stringify as stringifyYaml2 } from "yaml";
|
|
1246
|
+
var STANDARD_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
|
|
1247
|
+
"responsibility.md",
|
|
1248
|
+
"interface.md",
|
|
1249
|
+
"internals.md"
|
|
1250
|
+
]);
|
|
1251
|
+
async function migrateTo3(yggRoot) {
|
|
1252
|
+
const actions = [];
|
|
1253
|
+
const warnings = [];
|
|
1254
|
+
const configPath = path4.join(yggRoot, "yg-config.yaml");
|
|
1255
|
+
const content = await readFile4(configPath, "utf-8");
|
|
1256
|
+
const raw = parseYaml3(content);
|
|
1257
|
+
if (raw.artifacts && typeof raw.artifacts === "object") {
|
|
1258
|
+
const artifactKeys = Object.keys(raw.artifacts);
|
|
1259
|
+
const custom = artifactKeys.filter((k) => !STANDARD_ARTIFACT_NAMES.has(k));
|
|
1260
|
+
if (custom.length > 0) {
|
|
1261
|
+
warnings.push(
|
|
1262
|
+
`Custom artifacts removed from config: ${custom.join(", ")}. Files remain on disk but CLI will ignore them.`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
delete raw.artifacts;
|
|
1266
|
+
await writeFile4(configPath, stringifyYaml2(raw, { lineWidth: 0 }), "utf-8");
|
|
1267
|
+
actions.push("Removed artifacts section from yg-config.yaml");
|
|
1268
|
+
}
|
|
1269
|
+
return { actions, warnings };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1191
1272
|
// src/migrations/index.ts
|
|
1192
1273
|
var MIGRATIONS = [
|
|
1193
1274
|
{
|
|
1194
1275
|
to: "2.0.0",
|
|
1195
1276
|
description: "Rename YAML files to yg-* prefix, restructure config, convert aspects format",
|
|
1196
1277
|
run: migrateTo2
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
to: "3.0.0",
|
|
1281
|
+
description: "Remove artifacts section from config (now hardcoded in CLI)",
|
|
1282
|
+
run: migrateTo3
|
|
1197
1283
|
}
|
|
1198
1284
|
];
|
|
1199
1285
|
|
|
1200
1286
|
// src/cli/init.ts
|
|
1201
1287
|
function getGraphSchemasDir() {
|
|
1202
|
-
const currentDir =
|
|
1203
|
-
const packageRoot =
|
|
1204
|
-
return
|
|
1288
|
+
const currentDir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1289
|
+
const packageRoot = path5.join(currentDir, "..");
|
|
1290
|
+
return path5.join(packageRoot, "graph-schemas");
|
|
1205
1291
|
}
|
|
1206
1292
|
function getCliVersion() {
|
|
1207
|
-
const currentDir =
|
|
1208
|
-
const packageRoot =
|
|
1209
|
-
const pkg2 = JSON.parse(readFileSync(
|
|
1293
|
+
const currentDir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1294
|
+
const packageRoot = path5.join(currentDir, "..");
|
|
1295
|
+
const pkg2 = JSON.parse(readFileSync(path5.join(packageRoot, "package.json"), "utf-8"));
|
|
1210
1296
|
return pkg2.version;
|
|
1211
1297
|
}
|
|
1212
1298
|
async function refreshSchemas(yggRoot) {
|
|
1213
|
-
const schemasDir =
|
|
1299
|
+
const schemasDir = path5.join(yggRoot, "schemas");
|
|
1214
1300
|
await mkdir2(schemasDir, { recursive: true });
|
|
1215
1301
|
const graphSchemasDir = getGraphSchemasDir();
|
|
1216
1302
|
try {
|
|
1217
1303
|
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
1218
1304
|
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
1219
1305
|
for (const file of schemaFiles) {
|
|
1220
|
-
const srcPath =
|
|
1221
|
-
const content = await
|
|
1222
|
-
await
|
|
1306
|
+
const srcPath = path5.join(graphSchemasDir, file);
|
|
1307
|
+
const content = await readFile5(srcPath, "utf-8");
|
|
1308
|
+
await writeFile5(path5.join(schemasDir, file), content, "utf-8");
|
|
1223
1309
|
}
|
|
1224
1310
|
} catch {
|
|
1225
1311
|
}
|
|
@@ -1232,7 +1318,7 @@ function registerInitCommand(program2) {
|
|
|
1232
1318
|
"generic"
|
|
1233
1319
|
).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
|
|
1234
1320
|
const projectRoot = process.cwd();
|
|
1235
|
-
const yggRoot =
|
|
1321
|
+
const yggRoot = path5.join(projectRoot, ".yggdrasil");
|
|
1236
1322
|
let upgradeMode = false;
|
|
1237
1323
|
try {
|
|
1238
1324
|
const statResult = await stat2(yggRoot);
|
|
@@ -1295,23 +1381,23 @@ function registerInitCommand(program2) {
|
|
|
1295
1381
|
await refreshSchemas(yggRoot);
|
|
1296
1382
|
const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
|
|
1297
1383
|
process.stdout.write("\u2713 Rules refreshed.\n");
|
|
1298
|
-
process.stdout.write(` ${
|
|
1384
|
+
process.stdout.write(` ${path5.relative(projectRoot, rulesPath2)}
|
|
1299
1385
|
`);
|
|
1300
1386
|
return;
|
|
1301
1387
|
}
|
|
1302
|
-
await mkdir2(
|
|
1303
|
-
await mkdir2(
|
|
1304
|
-
await mkdir2(
|
|
1305
|
-
const schemasDir =
|
|
1388
|
+
await mkdir2(path5.join(yggRoot, "model"), { recursive: true });
|
|
1389
|
+
await mkdir2(path5.join(yggRoot, "aspects"), { recursive: true });
|
|
1390
|
+
await mkdir2(path5.join(yggRoot, "flows"), { recursive: true });
|
|
1391
|
+
const schemasDir = path5.join(yggRoot, "schemas");
|
|
1306
1392
|
await mkdir2(schemasDir, { recursive: true });
|
|
1307
1393
|
const graphSchemasDir = getGraphSchemasDir();
|
|
1308
1394
|
try {
|
|
1309
1395
|
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
1310
1396
|
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
1311
1397
|
for (const file of schemaFiles) {
|
|
1312
|
-
const srcPath =
|
|
1313
|
-
const content = await
|
|
1314
|
-
await
|
|
1398
|
+
const srcPath = path5.join(graphSchemasDir, file);
|
|
1399
|
+
const content = await readFile5(srcPath, "utf-8");
|
|
1400
|
+
await writeFile5(path5.join(schemasDir, file), content, "utf-8");
|
|
1315
1401
|
}
|
|
1316
1402
|
} catch (err) {
|
|
1317
1403
|
process.stderr.write(
|
|
@@ -1319,8 +1405,8 @@ function registerInitCommand(program2) {
|
|
|
1319
1405
|
`
|
|
1320
1406
|
);
|
|
1321
1407
|
}
|
|
1322
|
-
await
|
|
1323
|
-
await
|
|
1408
|
+
await writeFile5(path5.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
|
|
1409
|
+
await writeFile5(path5.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
|
|
1324
1410
|
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
1325
1411
|
process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
|
|
1326
1412
|
process.stdout.write("Created:\n");
|
|
@@ -1330,7 +1416,7 @@ function registerInitCommand(program2) {
|
|
|
1330
1416
|
process.stdout.write(" .yggdrasil/aspects/\n");
|
|
1331
1417
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
1332
1418
|
process.stdout.write(" .yggdrasil/schemas/ (yg-config, yg-node, yg-aspect, yg-flow)\n");
|
|
1333
|
-
process.stdout.write(` ${
|
|
1419
|
+
process.stdout.write(` ${path5.relative(projectRoot, rulesPath)} (rules)
|
|
1334
1420
|
|
|
1335
1421
|
`);
|
|
1336
1422
|
process.stdout.write("Next steps:\n");
|
|
@@ -1341,20 +1427,39 @@ function registerInitCommand(program2) {
|
|
|
1341
1427
|
}
|
|
1342
1428
|
|
|
1343
1429
|
// src/core/graph-loader.ts
|
|
1344
|
-
import { readdir as readdir4, readFile as
|
|
1345
|
-
import
|
|
1430
|
+
import { readdir as readdir4, readFile as readFile12 } from "fs/promises";
|
|
1431
|
+
import path10 from "path";
|
|
1432
|
+
|
|
1433
|
+
// src/model/types.ts
|
|
1434
|
+
var STANDARD_ARTIFACTS2 = {
|
|
1435
|
+
"responsibility.md": {
|
|
1436
|
+
required: "always",
|
|
1437
|
+
description: "What this node is responsible for, and what it is not",
|
|
1438
|
+
included_in_relations: true
|
|
1439
|
+
},
|
|
1440
|
+
"interface.md": {
|
|
1441
|
+
required: { when: "has_incoming_relations" },
|
|
1442
|
+
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes",
|
|
1443
|
+
included_in_relations: true
|
|
1444
|
+
},
|
|
1445
|
+
"internals.md": {
|
|
1446
|
+
required: "never",
|
|
1447
|
+
description: "How the node works and why \u2014 algorithms, business rules, design decisions",
|
|
1448
|
+
included_in_relations: false
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1346
1451
|
|
|
1347
1452
|
// src/io/config-parser.ts
|
|
1348
|
-
import { readFile as
|
|
1349
|
-
import { parse as
|
|
1453
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1454
|
+
import { parse as parseYaml4 } from "yaml";
|
|
1350
1455
|
var DEFAULT_QUALITY = {
|
|
1351
1456
|
min_artifact_length: 50,
|
|
1352
1457
|
max_direct_relations: 10,
|
|
1353
1458
|
context_budget: { warning: 1e4, error: 2e4, own_warning: void 0 }
|
|
1354
1459
|
};
|
|
1355
1460
|
async function parseConfig(filePath) {
|
|
1356
|
-
const content = await
|
|
1357
|
-
const raw =
|
|
1461
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1462
|
+
const raw = parseYaml4(content);
|
|
1358
1463
|
if (!raw || typeof raw !== "object") {
|
|
1359
1464
|
throw new Error(`yg-config.yaml: file is empty or not a valid YAML mapping`);
|
|
1360
1465
|
}
|
|
@@ -1380,35 +1485,6 @@ async function parseConfig(filePath) {
|
|
|
1380
1485
|
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
1381
1486
|
};
|
|
1382
1487
|
}
|
|
1383
|
-
const artifacts = raw.artifacts;
|
|
1384
|
-
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
|
|
1385
|
-
throw new Error(`yg-config.yaml: 'artifacts' must be a non-empty object`);
|
|
1386
|
-
}
|
|
1387
|
-
const artifactsMap = {};
|
|
1388
|
-
for (const [key, val] of Object.entries(artifacts)) {
|
|
1389
|
-
if (key === "yg-node.yaml") {
|
|
1390
|
-
throw new Error(`yg-config.yaml: artifact name 'yg-node.yaml' is reserved`);
|
|
1391
|
-
}
|
|
1392
|
-
const a = val;
|
|
1393
|
-
const required = a.required;
|
|
1394
|
-
if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
|
|
1395
|
-
throw new Error(`yg-config.yaml: artifact '${key}' has invalid 'required' field`);
|
|
1396
|
-
}
|
|
1397
|
-
if (typeof required === "object" && required && "when" in required) {
|
|
1398
|
-
const when = required.when;
|
|
1399
|
-
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
|
|
1400
|
-
if (!validWhen) {
|
|
1401
|
-
throw new Error(
|
|
1402
|
-
`yg-config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
artifactsMap[key] = {
|
|
1407
|
-
required,
|
|
1408
|
-
description: a.description ?? "",
|
|
1409
|
-
included_in_relations: a.included_in_relations ?? false
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
1488
|
const qualityRaw = raw.quality;
|
|
1413
1489
|
const quality = qualityRaw ? {
|
|
1414
1490
|
min_artifact_length: qualityRaw.min_artifact_length ?? DEFAULT_QUALITY.min_artifact_length,
|
|
@@ -1431,14 +1507,13 @@ async function parseConfig(filePath) {
|
|
|
1431
1507
|
version,
|
|
1432
1508
|
name: raw.name.trim(),
|
|
1433
1509
|
node_types: nodeTypes,
|
|
1434
|
-
artifacts: artifactsMap,
|
|
1435
1510
|
quality
|
|
1436
1511
|
};
|
|
1437
1512
|
}
|
|
1438
1513
|
|
|
1439
1514
|
// src/io/node-parser.ts
|
|
1440
|
-
import { readFile as
|
|
1441
|
-
import { parse as
|
|
1515
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1516
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1442
1517
|
var RELATION_TYPES = [
|
|
1443
1518
|
"uses",
|
|
1444
1519
|
"calls",
|
|
@@ -1451,8 +1526,8 @@ function isValidRelationType(t) {
|
|
|
1451
1526
|
return typeof t === "string" && RELATION_TYPES.includes(t);
|
|
1452
1527
|
}
|
|
1453
1528
|
async function parseNodeYaml(filePath) {
|
|
1454
|
-
const content = await
|
|
1455
|
-
const raw =
|
|
1529
|
+
const content = await readFile7(filePath, "utf-8");
|
|
1530
|
+
const raw = parseYaml5(content);
|
|
1456
1531
|
if (!raw || typeof raw !== "object") {
|
|
1457
1532
|
throw new Error(`yg-node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
1458
1533
|
}
|
|
@@ -1597,12 +1672,12 @@ function parseMapping(rawMapping, filePath) {
|
|
|
1597
1672
|
}
|
|
1598
1673
|
|
|
1599
1674
|
// src/io/aspect-parser.ts
|
|
1600
|
-
import { readFile as
|
|
1601
|
-
import { parse as
|
|
1675
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1676
|
+
import { parse as parseYaml6 } from "yaml";
|
|
1602
1677
|
|
|
1603
1678
|
// src/io/artifact-reader.ts
|
|
1604
|
-
import { readFile as
|
|
1605
|
-
import
|
|
1679
|
+
import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
|
|
1680
|
+
import path6 from "path";
|
|
1606
1681
|
async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFiles) {
|
|
1607
1682
|
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1608
1683
|
const artifacts = [];
|
|
@@ -1611,8 +1686,8 @@ async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFi
|
|
|
1611
1686
|
if (!entry.isFile()) continue;
|
|
1612
1687
|
if (excludeFiles.includes(entry.name)) continue;
|
|
1613
1688
|
if (includeSet && !includeSet.has(entry.name)) continue;
|
|
1614
|
-
const filePath =
|
|
1615
|
-
const content = await
|
|
1689
|
+
const filePath = path6.join(dirPath, entry.name);
|
|
1690
|
+
const content = await readFile8(filePath, "utf-8");
|
|
1616
1691
|
artifacts.push({ filename: entry.name, content });
|
|
1617
1692
|
}
|
|
1618
1693
|
artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
@@ -1626,8 +1701,8 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1626
1701
|
if (!idTrimmed) {
|
|
1627
1702
|
throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
|
|
1628
1703
|
}
|
|
1629
|
-
const content = await
|
|
1630
|
-
const raw =
|
|
1704
|
+
const content = await readFile9(aspectYamlPath, "utf-8");
|
|
1705
|
+
const raw = parseYaml6(content);
|
|
1631
1706
|
if (!raw || typeof raw !== "object") {
|
|
1632
1707
|
throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1633
1708
|
}
|
|
@@ -1663,12 +1738,12 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1663
1738
|
}
|
|
1664
1739
|
|
|
1665
1740
|
// src/io/flow-parser.ts
|
|
1666
|
-
import { readFile as
|
|
1667
|
-
import
|
|
1668
|
-
import { parse as
|
|
1741
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1742
|
+
import path7 from "path";
|
|
1743
|
+
import { parse as parseYaml7 } from "yaml";
|
|
1669
1744
|
async function parseFlow(flowDir, flowYamlPath) {
|
|
1670
|
-
const content = await
|
|
1671
|
-
const raw =
|
|
1745
|
+
const content = await readFile10(flowYamlPath, "utf-8");
|
|
1746
|
+
const raw = parseYaml7(content);
|
|
1672
1747
|
if (!raw || typeof raw !== "object") {
|
|
1673
1748
|
throw new Error(`yg-flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1674
1749
|
}
|
|
@@ -1698,7 +1773,7 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1698
1773
|
}
|
|
1699
1774
|
const artifacts = await readArtifacts(flowDir, ["yg-flow.yaml"]);
|
|
1700
1775
|
return {
|
|
1701
|
-
path:
|
|
1776
|
+
path: path7.basename(flowDir),
|
|
1702
1777
|
name: raw.name.trim(),
|
|
1703
1778
|
description,
|
|
1704
1779
|
nodes: nodePaths,
|
|
@@ -1708,26 +1783,26 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1708
1783
|
}
|
|
1709
1784
|
|
|
1710
1785
|
// src/io/schema-parser.ts
|
|
1711
|
-
import { readFile as
|
|
1712
|
-
import
|
|
1713
|
-
import { parse as
|
|
1786
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1787
|
+
import path8 from "path";
|
|
1788
|
+
import { parse as parseYaml8 } from "yaml";
|
|
1714
1789
|
async function parseSchema(filePath) {
|
|
1715
|
-
const content = await
|
|
1716
|
-
|
|
1717
|
-
const rawName =
|
|
1790
|
+
const content = await readFile11(filePath, "utf-8");
|
|
1791
|
+
parseYaml8(content);
|
|
1792
|
+
const rawName = path8.basename(filePath, path8.extname(filePath));
|
|
1718
1793
|
const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
|
|
1719
1794
|
return { schemaType };
|
|
1720
1795
|
}
|
|
1721
1796
|
|
|
1722
1797
|
// src/utils/paths.ts
|
|
1723
|
-
import
|
|
1798
|
+
import path9 from "path";
|
|
1724
1799
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1725
1800
|
import { stat as stat3 } from "fs/promises";
|
|
1726
1801
|
async function findYggRoot(projectRoot) {
|
|
1727
|
-
let current =
|
|
1728
|
-
const root =
|
|
1802
|
+
let current = path9.resolve(projectRoot);
|
|
1803
|
+
const root = path9.parse(current).root;
|
|
1729
1804
|
while (true) {
|
|
1730
|
-
const yggPath =
|
|
1805
|
+
const yggPath = path9.join(current, ".yggdrasil");
|
|
1731
1806
|
try {
|
|
1732
1807
|
const st = await stat3(yggPath);
|
|
1733
1808
|
if (!st.isDirectory()) {
|
|
@@ -1741,7 +1816,7 @@ async function findYggRoot(projectRoot) {
|
|
|
1741
1816
|
if (current === root) {
|
|
1742
1817
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
1743
1818
|
}
|
|
1744
|
-
current =
|
|
1819
|
+
current = path9.dirname(current);
|
|
1745
1820
|
continue;
|
|
1746
1821
|
}
|
|
1747
1822
|
throw err;
|
|
@@ -1757,43 +1832,42 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
1757
1832
|
if (normalizedInput.length === 0) {
|
|
1758
1833
|
throw new Error("Path cannot be empty");
|
|
1759
1834
|
}
|
|
1760
|
-
const absolute =
|
|
1761
|
-
const relative =
|
|
1762
|
-
const isOutside = relative.startsWith("..") ||
|
|
1835
|
+
const absolute = path9.resolve(projectRoot, normalizedInput);
|
|
1836
|
+
const relative = path9.relative(projectRoot, absolute);
|
|
1837
|
+
const isOutside = relative.startsWith("..") || path9.isAbsolute(relative);
|
|
1763
1838
|
if (isOutside) {
|
|
1764
1839
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
1765
1840
|
}
|
|
1766
|
-
return relative.split(
|
|
1841
|
+
return relative.split(path9.sep).join("/");
|
|
1767
1842
|
}
|
|
1768
1843
|
function projectRootFromGraph(yggRootPath) {
|
|
1769
|
-
return
|
|
1844
|
+
return path9.dirname(yggRootPath);
|
|
1770
1845
|
}
|
|
1771
1846
|
|
|
1772
1847
|
// src/core/graph-loader.ts
|
|
1773
1848
|
function toModelPath(absolutePath, modelDir) {
|
|
1774
|
-
return
|
|
1849
|
+
return path10.relative(modelDir, absolutePath).split(path10.sep).join("/");
|
|
1775
1850
|
}
|
|
1776
1851
|
var FALLBACK_CONFIG = {
|
|
1777
1852
|
name: "",
|
|
1778
|
-
node_types: {}
|
|
1779
|
-
artifacts: {}
|
|
1853
|
+
node_types: {}
|
|
1780
1854
|
};
|
|
1781
1855
|
async function loadGraph(projectRoot, options = {}) {
|
|
1782
1856
|
const yggRoot = await findYggRoot(projectRoot);
|
|
1783
1857
|
let configError;
|
|
1784
1858
|
let config = FALLBACK_CONFIG;
|
|
1785
1859
|
try {
|
|
1786
|
-
config = await parseConfig(
|
|
1860
|
+
config = await parseConfig(path10.join(yggRoot, "yg-config.yaml"));
|
|
1787
1861
|
} catch (error) {
|
|
1788
1862
|
if (!options.tolerateInvalidConfig) {
|
|
1789
1863
|
throw error;
|
|
1790
1864
|
}
|
|
1791
1865
|
configError = error.message;
|
|
1792
1866
|
}
|
|
1793
|
-
const modelDir =
|
|
1867
|
+
const modelDir = path10.join(yggRoot, "model");
|
|
1794
1868
|
const nodes = /* @__PURE__ */ new Map();
|
|
1795
1869
|
const nodeParseErrors = [];
|
|
1796
|
-
const artifactFilenames = Object.keys(
|
|
1870
|
+
const artifactFilenames = Object.keys(STANDARD_ARTIFACTS2);
|
|
1797
1871
|
try {
|
|
1798
1872
|
await scanModelDirectory(modelDir, modelDir, null, nodes, nodeParseErrors, artifactFilenames);
|
|
1799
1873
|
} catch (err) {
|
|
@@ -1804,9 +1878,9 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1804
1878
|
}
|
|
1805
1879
|
throw err;
|
|
1806
1880
|
}
|
|
1807
|
-
const aspects = await loadAspects(
|
|
1808
|
-
const flows = await loadFlows(
|
|
1809
|
-
const schemas = await loadSchemas(
|
|
1881
|
+
const aspects = await loadAspects(path10.join(yggRoot, "aspects"));
|
|
1882
|
+
const flows = await loadFlows(path10.join(yggRoot, "flows"));
|
|
1883
|
+
const schemas = await loadSchemas(path10.join(yggRoot, "schemas"));
|
|
1810
1884
|
return {
|
|
1811
1885
|
config,
|
|
1812
1886
|
configError,
|
|
@@ -1826,11 +1900,11 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1826
1900
|
}
|
|
1827
1901
|
if (hasNodeYaml) {
|
|
1828
1902
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1829
|
-
const nodeYamlPath =
|
|
1903
|
+
const nodeYamlPath = path10.join(dirPath, "yg-node.yaml");
|
|
1830
1904
|
let meta;
|
|
1831
1905
|
let nodeYamlRaw;
|
|
1832
1906
|
try {
|
|
1833
|
-
nodeYamlRaw = await
|
|
1907
|
+
nodeYamlRaw = await readFile12(nodeYamlPath, "utf-8");
|
|
1834
1908
|
meta = await parseNodeYaml(nodeYamlPath);
|
|
1835
1909
|
} catch (err) {
|
|
1836
1910
|
nodeParseErrors.push({
|
|
@@ -1856,7 +1930,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1856
1930
|
if (!entry.isDirectory()) continue;
|
|
1857
1931
|
if (entry.name.startsWith(".")) continue;
|
|
1858
1932
|
await scanModelDirectory(
|
|
1859
|
-
|
|
1933
|
+
path10.join(dirPath, entry.name),
|
|
1860
1934
|
modelDir,
|
|
1861
1935
|
node,
|
|
1862
1936
|
nodes,
|
|
@@ -1869,7 +1943,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1869
1943
|
if (!entry.isDirectory()) continue;
|
|
1870
1944
|
if (entry.name.startsWith(".")) continue;
|
|
1871
1945
|
await scanModelDirectory(
|
|
1872
|
-
|
|
1946
|
+
path10.join(dirPath, entry.name),
|
|
1873
1947
|
modelDir,
|
|
1874
1948
|
null,
|
|
1875
1949
|
nodes,
|
|
@@ -1892,15 +1966,15 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
|
1892
1966
|
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
1893
1967
|
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
|
|
1894
1968
|
if (hasAspectYaml) {
|
|
1895
|
-
const id =
|
|
1896
|
-
const aspectYamlPath =
|
|
1969
|
+
const id = path10.relative(aspectsRoot, dirPath).split(path10.sep).join("/");
|
|
1970
|
+
const aspectYamlPath = path10.join(dirPath, "yg-aspect.yaml");
|
|
1897
1971
|
const aspect = await parseAspect(dirPath, aspectYamlPath, id);
|
|
1898
1972
|
aspects.push(aspect);
|
|
1899
1973
|
}
|
|
1900
1974
|
for (const entry of entries) {
|
|
1901
1975
|
if (!entry.isDirectory()) continue;
|
|
1902
1976
|
if (entry.name.startsWith(".")) continue;
|
|
1903
|
-
await scanAspectsDirectory(
|
|
1977
|
+
await scanAspectsDirectory(path10.join(dirPath, entry.name), aspectsRoot, aspects);
|
|
1904
1978
|
}
|
|
1905
1979
|
}
|
|
1906
1980
|
async function loadFlows(flowsDir) {
|
|
@@ -1913,8 +1987,8 @@ async function loadFlows(flowsDir) {
|
|
|
1913
1987
|
const flows = [];
|
|
1914
1988
|
for (const entry of entries) {
|
|
1915
1989
|
if (!entry.isDirectory()) continue;
|
|
1916
|
-
const flowYamlPath =
|
|
1917
|
-
const flow = await parseFlow(
|
|
1990
|
+
const flowYamlPath = path10.join(flowsDir, entry.name, "yg-flow.yaml");
|
|
1991
|
+
const flow = await parseFlow(path10.join(flowsDir, entry.name), flowYamlPath);
|
|
1918
1992
|
flows.push(flow);
|
|
1919
1993
|
}
|
|
1920
1994
|
return flows;
|
|
@@ -1926,7 +2000,7 @@ async function loadSchemas(schemasDir) {
|
|
|
1926
2000
|
for (const entry of entries) {
|
|
1927
2001
|
if (!entry.isFile()) continue;
|
|
1928
2002
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1929
|
-
const s = await parseSchema(
|
|
2003
|
+
const s = await parseSchema(path10.join(schemasDir, entry.name));
|
|
1930
2004
|
schemas.push(s);
|
|
1931
2005
|
}
|
|
1932
2006
|
return schemas;
|
|
@@ -1936,8 +2010,8 @@ async function loadSchemas(schemasDir) {
|
|
|
1936
2010
|
}
|
|
1937
2011
|
|
|
1938
2012
|
// src/core/context-builder.ts
|
|
1939
|
-
import { readFile as
|
|
1940
|
-
import
|
|
2013
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2014
|
+
import path11 from "path";
|
|
1941
2015
|
|
|
1942
2016
|
// src/utils/tokens.ts
|
|
1943
2017
|
function estimateTokens(text) {
|
|
@@ -1948,48 +2022,53 @@ function estimateTokens(text) {
|
|
|
1948
2022
|
var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
1949
2023
|
var EVENT_RELATION_TYPES = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
1950
2024
|
var YG_YAML_FILES = /* @__PURE__ */ new Set(["yg-node.yaml", "yg-aspect.yaml", "yg-flow.yaml"]);
|
|
1951
|
-
async function buildContext(graph, nodePath) {
|
|
2025
|
+
async function buildContext(graph, nodePath, options) {
|
|
1952
2026
|
const node = graph.nodes.get(nodePath);
|
|
1953
2027
|
if (!node) {
|
|
1954
2028
|
throw new Error(`Node not found: ${nodePath}`);
|
|
1955
2029
|
}
|
|
2030
|
+
const selfOnly = options?.selfOnly ?? false;
|
|
1956
2031
|
const layers = [];
|
|
1957
2032
|
layers.push(buildGlobalLayer(graph.config));
|
|
1958
2033
|
const ancestors = collectAncestors(node);
|
|
1959
|
-
|
|
1960
|
-
|
|
2034
|
+
if (!selfOnly) {
|
|
2035
|
+
for (const ancestor of ancestors) {
|
|
2036
|
+
layers.push(buildHierarchyLayer(ancestor, graph.config, graph));
|
|
2037
|
+
}
|
|
1961
2038
|
}
|
|
1962
2039
|
layers.push(await buildOwnLayer(node, graph.config, graph.rootPath, graph));
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
2040
|
+
if (!selfOnly) {
|
|
2041
|
+
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
2042
|
+
for (const relation of node.meta.relations ?? []) {
|
|
2043
|
+
const target = graph.nodes.get(relation.target);
|
|
2044
|
+
if (!target) {
|
|
2045
|
+
throw new Error(`Broken relation: ${nodePath} -> ${relation.target} (target not found)`);
|
|
2046
|
+
}
|
|
2047
|
+
if (ancestorPaths.has(relation.target)) continue;
|
|
2048
|
+
if (STRUCTURAL_RELATION_TYPES.has(relation.type)) {
|
|
2049
|
+
layers.push(buildStructuralRelationLayer(target, relation));
|
|
2050
|
+
} else if (EVENT_RELATION_TYPES.has(relation.type)) {
|
|
2051
|
+
layers.push(buildEventRelationLayer(target, relation));
|
|
2052
|
+
}
|
|
1968
2053
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
layers.push(buildStructuralRelationLayer(target, relation, graph.config));
|
|
1972
|
-
} else if (EVENT_RELATION_TYPES.has(relation.type)) {
|
|
1973
|
-
layers.push(buildEventRelationLayer(target, relation));
|
|
2054
|
+
for (const flow of collectParticipatingFlows(graph, node)) {
|
|
2055
|
+
layers.push(buildFlowLayer(flow, graph));
|
|
1974
2056
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
if (aspects) {
|
|
1983
|
-
for (const id of aspects.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
1984
|
-
allAspectIds.add(id);
|
|
2057
|
+
const allAspectIds = /* @__PURE__ */ new Set();
|
|
2058
|
+
for (const l of layers) {
|
|
2059
|
+
const aspects = l.attrs?.aspects;
|
|
2060
|
+
if (aspects) {
|
|
2061
|
+
for (const id of aspects.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
2062
|
+
allAspectIds.add(id);
|
|
2063
|
+
}
|
|
1985
2064
|
}
|
|
1986
2065
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2066
|
+
const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
|
|
2067
|
+
for (const aspect of aspectsToInclude) {
|
|
2068
|
+
const entry = node.meta.aspects?.find((a) => a.aspect === aspect.id);
|
|
2069
|
+
const exceptionNote = entry?.exceptions?.join("; ");
|
|
2070
|
+
layers.push(buildAspectLayer(aspect, exceptionNote));
|
|
2071
|
+
}
|
|
1993
2072
|
}
|
|
1994
2073
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
1995
2074
|
const tokenCount = estimateTokens(fullText);
|
|
@@ -2050,12 +2129,12 @@ function buildGlobalLayer(config) {
|
|
|
2050
2129
|
`;
|
|
2051
2130
|
return { type: "global", label: "Global Context", content };
|
|
2052
2131
|
}
|
|
2053
|
-
function
|
|
2054
|
-
const allowed = new Set(Object.keys(
|
|
2132
|
+
function filterByStandardArtifacts(artifacts) {
|
|
2133
|
+
const allowed = new Set(Object.keys(STANDARD_ARTIFACTS2));
|
|
2055
2134
|
return artifacts.filter((a) => allowed.has(a.filename));
|
|
2056
2135
|
}
|
|
2057
2136
|
function buildHierarchyLayer(ancestor, config, graph) {
|
|
2058
|
-
const filtered =
|
|
2137
|
+
const filtered = filterByStandardArtifacts(ancestor.artifacts);
|
|
2059
2138
|
const content = filtered.map((a) => `### ${a.filename}
|
|
2060
2139
|
${a.content}`).join("\n\n");
|
|
2061
2140
|
const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
|
|
@@ -2074,9 +2153,9 @@ async function buildOwnLayer(node, config, graphRootPath, graph) {
|
|
|
2074
2153
|
parts.push(`### yg-node.yaml
|
|
2075
2154
|
${node.nodeYamlRaw.trim()}`);
|
|
2076
2155
|
} else {
|
|
2077
|
-
const nodeYamlPath =
|
|
2156
|
+
const nodeYamlPath = path11.join(graphRootPath, "model", node.path, "yg-node.yaml");
|
|
2078
2157
|
try {
|
|
2079
|
-
const nodeYamlContent = await
|
|
2158
|
+
const nodeYamlContent = await readFile13(nodeYamlPath, "utf-8");
|
|
2080
2159
|
parts.push(`### yg-node.yaml
|
|
2081
2160
|
${nodeYamlContent.trim()}`);
|
|
2082
2161
|
} catch {
|
|
@@ -2084,7 +2163,7 @@ ${nodeYamlContent.trim()}`);
|
|
|
2084
2163
|
(not found)`);
|
|
2085
2164
|
}
|
|
2086
2165
|
}
|
|
2087
|
-
const filtered =
|
|
2166
|
+
const filtered = filterByStandardArtifacts(node.artifacts);
|
|
2088
2167
|
for (const a of filtered) {
|
|
2089
2168
|
parts.push(`### ${a.filename}
|
|
2090
2169
|
${a.content}`);
|
|
@@ -2100,7 +2179,7 @@ ${a.content}`);
|
|
|
2100
2179
|
attrs
|
|
2101
2180
|
};
|
|
2102
2181
|
}
|
|
2103
|
-
function buildStructuralRelationLayer(target, relation
|
|
2182
|
+
function buildStructuralRelationLayer(target, relation) {
|
|
2104
2183
|
let content = "";
|
|
2105
2184
|
if (relation.consumes?.length) {
|
|
2106
2185
|
content += `Consumes: ${relation.consumes.join(", ")}
|
|
@@ -2112,7 +2191,7 @@ function buildStructuralRelationLayer(target, relation, config) {
|
|
|
2112
2191
|
|
|
2113
2192
|
`;
|
|
2114
2193
|
}
|
|
2115
|
-
const structuralArtifactFilenames = Object.entries(
|
|
2194
|
+
const structuralArtifactFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
2116
2195
|
const structuralArts = structuralArtifactFilenames.map((filename) => {
|
|
2117
2196
|
const art = target.artifacts.find((a) => a.filename === filename);
|
|
2118
2197
|
return art ? { filename: art.filename, content: art.content } : null;
|
|
@@ -2121,7 +2200,7 @@ function buildStructuralRelationLayer(target, relation, config) {
|
|
|
2121
2200
|
content += structuralArts.map((a) => `### ${a.filename}
|
|
2122
2201
|
${a.content}`).join("\n\n");
|
|
2123
2202
|
} else {
|
|
2124
|
-
const filtered =
|
|
2203
|
+
const filtered = filterByStandardArtifacts(target.artifacts);
|
|
2125
2204
|
content += filtered.map((a) => `### ${a.filename}
|
|
2126
2205
|
${a.content}`).join("\n\n");
|
|
2127
2206
|
}
|
|
@@ -2226,8 +2305,8 @@ function collectAncestors(node) {
|
|
|
2226
2305
|
}
|
|
2227
2306
|
function collectDependencyAncestors(target, config, graph) {
|
|
2228
2307
|
const ancestors = collectAncestors(target);
|
|
2229
|
-
const structuralFilenames = Object.entries(
|
|
2230
|
-
const configArtifactKeys = [...Object.keys(
|
|
2308
|
+
const structuralFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
2309
|
+
const configArtifactKeys = [...Object.keys(STANDARD_ARTIFACTS2)];
|
|
2231
2310
|
return ancestors.map((ancestor) => {
|
|
2232
2311
|
const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
|
|
2233
2312
|
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
@@ -2295,7 +2374,7 @@ function computeBudgetBreakdown(pkg2, graph) {
|
|
|
2295
2374
|
const total = own + hierarchy + aspects + flows + dependencies;
|
|
2296
2375
|
return { own, hierarchy, aspects, flows, dependencies, total };
|
|
2297
2376
|
}
|
|
2298
|
-
function toContextMapOutput(pkg2, graph) {
|
|
2377
|
+
function toContextMapOutput(pkg2, graph, options) {
|
|
2299
2378
|
const node = graph.nodes.get(pkg2.nodePath);
|
|
2300
2379
|
const config = graph.config;
|
|
2301
2380
|
const nodeAspects = (node.meta.aspects ?? []).map((entry) => {
|
|
@@ -2304,48 +2383,51 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2304
2383
|
if (entry.exceptions?.length) ref.exceptions = entry.exceptions;
|
|
2305
2384
|
return ref;
|
|
2306
2385
|
});
|
|
2307
|
-
const
|
|
2386
|
+
const selfOnly = options?.selfOnly ?? false;
|
|
2387
|
+
const participatingFlows = selfOnly ? [] : collectParticipatingFlows(graph, node);
|
|
2308
2388
|
const flowRefs = participatingFlows.map((f) => {
|
|
2309
2389
|
const ref = { path: f.path };
|
|
2310
2390
|
if (f.aspects?.length) ref.aspects = f.aspects;
|
|
2311
2391
|
return ref;
|
|
2312
2392
|
});
|
|
2313
2393
|
const ancestors = collectAncestors(node);
|
|
2314
|
-
const hierarchyRefs = ancestors.map((a) => {
|
|
2394
|
+
const hierarchyRefs = selfOnly ? [] : ancestors.map((a) => {
|
|
2315
2395
|
const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2316
2396
|
const expanded = expandAspects(nodeAspectIds, graph.aspects);
|
|
2317
2397
|
return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: buildNodeFiles(a, config, `model/${a.path}`) };
|
|
2318
2398
|
});
|
|
2319
|
-
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
2320
2399
|
const depRefs = [];
|
|
2321
|
-
|
|
2322
|
-
const
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2400
|
+
if (!selfOnly) {
|
|
2401
|
+
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
2402
|
+
for (const relation of node.meta.relations ?? []) {
|
|
2403
|
+
const target = graph.nodes.get(relation.target);
|
|
2404
|
+
if (!target) continue;
|
|
2405
|
+
if (ancestorPaths.has(relation.target)) continue;
|
|
2406
|
+
const depAncestors = collectAncestors(target);
|
|
2407
|
+
const depHierarchy = depAncestors.map((a) => {
|
|
2408
|
+
const ids = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2409
|
+
const expanded = expandAspects(ids, graph.aspects);
|
|
2410
|
+
const ancestorNode = graph.nodes.get(a.path);
|
|
2411
|
+
return { path: a.path, name: a.meta.name, type: a.meta.type, description: a.meta.description, aspects: expanded, files: ancestorNode ? buildDepNodeFiles(ancestorNode, config, `model/${a.path}`) : [] };
|
|
2412
|
+
});
|
|
2413
|
+
const depEffectiveAspects = [...collectEffectiveAspectIds(graph, target.path)];
|
|
2414
|
+
const ref = {
|
|
2415
|
+
path: target.path,
|
|
2416
|
+
name: target.meta.name,
|
|
2417
|
+
type: target.meta.type,
|
|
2418
|
+
description: target.meta.description,
|
|
2419
|
+
relation: relation.type,
|
|
2420
|
+
aspects: depEffectiveAspects,
|
|
2421
|
+
hierarchy: depHierarchy,
|
|
2422
|
+
files: buildDepNodeFiles(target, config, `model/${target.path}`)
|
|
2423
|
+
};
|
|
2424
|
+
if (relation.consumes?.length) ref.consumes = relation.consumes;
|
|
2425
|
+
if (relation.failure) ref.failure = relation.failure;
|
|
2426
|
+
if (relation.event_name) ref["event-name"] = relation.event_name;
|
|
2427
|
+
depRefs.push(ref);
|
|
2428
|
+
}
|
|
2347
2429
|
}
|
|
2348
|
-
const glossary = buildGlossary(node, depRefs, graph);
|
|
2430
|
+
const glossary = selfOnly ? { aspects: {}, flows: {} } : buildGlossary(node, depRefs, graph);
|
|
2349
2431
|
const breakdown = computeBudgetBreakdown(pkg2, graph);
|
|
2350
2432
|
const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
|
|
2351
2433
|
const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
|
|
@@ -2368,13 +2450,13 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2368
2450
|
glossary
|
|
2369
2451
|
};
|
|
2370
2452
|
}
|
|
2371
|
-
function buildNodeFiles(node,
|
|
2372
|
-
const configKeys = Object.keys(
|
|
2453
|
+
function buildNodeFiles(node, _config, prefix) {
|
|
2454
|
+
const configKeys = Object.keys(STANDARD_ARTIFACTS2);
|
|
2373
2455
|
return configKeys.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
|
|
2374
2456
|
}
|
|
2375
|
-
function buildDepNodeFiles(node,
|
|
2376
|
-
const structural = Object.entries(
|
|
2377
|
-
const filenames = structural.length > 0 ? structural : Object.keys(
|
|
2457
|
+
function buildDepNodeFiles(node, _config, prefix) {
|
|
2458
|
+
const structural = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([f]) => f);
|
|
2459
|
+
const filenames = structural.length > 0 ? structural : Object.keys(STANDARD_ARTIFACTS2);
|
|
2378
2460
|
return filenames.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
|
|
2379
2461
|
}
|
|
2380
2462
|
function buildGlossary(node, dependencies, graph) {
|
|
@@ -2479,8 +2561,8 @@ ${file.content}
|
|
|
2479
2561
|
}
|
|
2480
2562
|
|
|
2481
2563
|
// src/core/validator.ts
|
|
2482
|
-
import { readdir as readdir5, readFile as
|
|
2483
|
-
import
|
|
2564
|
+
import { readdir as readdir5, readFile as readFile14, stat as stat4 } from "fs/promises";
|
|
2565
|
+
import path12 from "path";
|
|
2484
2566
|
function getAspectIds(aspects) {
|
|
2485
2567
|
return (aspects ?? []).map((a) => a.aspect);
|
|
2486
2568
|
}
|
|
@@ -2514,7 +2596,6 @@ async function validate(graph, scope = "all") {
|
|
|
2514
2596
|
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
2515
2597
|
issues.push(...await checkAnchorPresence(graph));
|
|
2516
2598
|
issues.push(...checkRequiredArtifacts(graph));
|
|
2517
|
-
issues.push(...checkInvalidArtifactConditions(graph));
|
|
2518
2599
|
issues.push(...await checkContextBudget(graph));
|
|
2519
2600
|
issues.push(...checkHighFanOut(graph));
|
|
2520
2601
|
issues.push(...checkMissingDescriptions(graph));
|
|
@@ -2847,12 +2928,12 @@ function checkMappingOverlap(graph) {
|
|
|
2847
2928
|
}
|
|
2848
2929
|
async function checkMappingPathsExist(graph) {
|
|
2849
2930
|
const issues = [];
|
|
2850
|
-
const projectRoot =
|
|
2931
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2851
2932
|
const { access: access4 } = await import("fs/promises");
|
|
2852
2933
|
for (const [nodePath, node] of graph.nodes) {
|
|
2853
2934
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2854
2935
|
for (const mp of mappingPaths) {
|
|
2855
|
-
const absPath =
|
|
2936
|
+
const absPath = path12.join(projectRoot, mp);
|
|
2856
2937
|
try {
|
|
2857
2938
|
await access4(absPath);
|
|
2858
2939
|
} catch {
|
|
@@ -2887,15 +2968,6 @@ function artifactRequiredReason(graph, nodePath, node, required) {
|
|
|
2887
2968
|
const sources = getIncomingRelationSources(graph, nodePath);
|
|
2888
2969
|
return sources.length > 0 ? `${sources.length} incoming relation(s): ${sources.join(", ")}` : null;
|
|
2889
2970
|
}
|
|
2890
|
-
if (when === "has_outgoing_relations") {
|
|
2891
|
-
const count = node.meta.relations?.length ?? 0;
|
|
2892
|
-
return count > 0 ? `${count} outgoing relation(s)` : null;
|
|
2893
|
-
}
|
|
2894
|
-
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
2895
|
-
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
2896
|
-
const aspectId = when.slice(prefix.length);
|
|
2897
|
-
return (node.meta.aspects ?? []).some((a) => a.aspect === aspectId) ? `node has aspect '${aspectId}'` : null;
|
|
2898
|
-
}
|
|
2899
2971
|
return null;
|
|
2900
2972
|
}
|
|
2901
2973
|
function getIncomingRelations(graph, nodePath) {
|
|
@@ -2912,7 +2984,7 @@ function getIncomingRelations(graph, nodePath) {
|
|
|
2912
2984
|
}
|
|
2913
2985
|
function checkRequiredArtifacts(graph) {
|
|
2914
2986
|
const issues = [];
|
|
2915
|
-
const artifacts =
|
|
2987
|
+
const artifacts = STANDARD_ARTIFACTS2;
|
|
2916
2988
|
for (const [nodePath, node] of graph.nodes) {
|
|
2917
2989
|
for (const [filename, config] of Object.entries(artifacts)) {
|
|
2918
2990
|
const hasArtifact = node.artifacts.some((a) => a.filename === filename);
|
|
@@ -2968,30 +3040,6 @@ function checkFlowAspectIds(graph) {
|
|
|
2968
3040
|
}
|
|
2969
3041
|
return issues;
|
|
2970
3042
|
}
|
|
2971
|
-
function checkInvalidArtifactConditions(graph) {
|
|
2972
|
-
const issues = [];
|
|
2973
|
-
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
2974
|
-
const artifacts = graph.config.artifacts ?? {};
|
|
2975
|
-
for (const [artifactName, config] of Object.entries(artifacts)) {
|
|
2976
|
-
const required = config.required;
|
|
2977
|
-
if (typeof required === "object" && required && "when" in required) {
|
|
2978
|
-
const when = required.when;
|
|
2979
|
-
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
2980
|
-
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
2981
|
-
const aspectId = when.slice(prefix.length);
|
|
2982
|
-
if (!validAspectIds.has(aspectId)) {
|
|
2983
|
-
issues.push({
|
|
2984
|
-
severity: "error",
|
|
2985
|
-
code: "E013",
|
|
2986
|
-
rule: "invalid-artifact-condition",
|
|
2987
|
-
message: `Artifact '${artifactName}' condition has_aspect:${aspectId} has no corresponding aspect in aspects/`
|
|
2988
|
-
});
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
}
|
|
2993
|
-
return issues;
|
|
2994
|
-
}
|
|
2995
3043
|
async function checkShallowArtifacts(graph) {
|
|
2996
3044
|
const issues = [];
|
|
2997
3045
|
const minLen = graph.config.quality?.min_artifact_length ?? 50;
|
|
@@ -3013,7 +3061,7 @@ async function checkShallowArtifacts(graph) {
|
|
|
3013
3061
|
async function checkWideNodes(graph) {
|
|
3014
3062
|
const issues = [];
|
|
3015
3063
|
const maxFiles = graph.config.quality?.max_mapping_source_files ?? 10;
|
|
3016
|
-
const projectRoot =
|
|
3064
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
3017
3065
|
for (const [nodePath, node] of graph.nodes) {
|
|
3018
3066
|
if (node.meta.blackbox) continue;
|
|
3019
3067
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
@@ -3116,11 +3164,11 @@ function checkSchemas(graph) {
|
|
|
3116
3164
|
}
|
|
3117
3165
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
3118
3166
|
const issues = [];
|
|
3119
|
-
const modelDir =
|
|
3167
|
+
const modelDir = path12.join(graph.rootPath, "model");
|
|
3120
3168
|
async function scanDir(dirPath, segments) {
|
|
3121
3169
|
const entries = await readdir5(dirPath, { withFileTypes: true });
|
|
3122
3170
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
3123
|
-
const dirName =
|
|
3171
|
+
const dirName = path12.basename(dirPath);
|
|
3124
3172
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
3125
3173
|
const hasFiles = entries.some((e) => e.isFile());
|
|
3126
3174
|
const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
|
|
@@ -3148,7 +3196,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
3148
3196
|
if (!entry.isDirectory()) continue;
|
|
3149
3197
|
if (RESERVED_DIRS.has(entry.name)) continue;
|
|
3150
3198
|
if (entry.name.startsWith(".")) continue;
|
|
3151
|
-
await scanDir(
|
|
3199
|
+
await scanDir(path12.join(dirPath, entry.name), [...segments, entry.name]);
|
|
3152
3200
|
}
|
|
3153
3201
|
}
|
|
3154
3202
|
try {
|
|
@@ -3156,7 +3204,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
3156
3204
|
for (const entry of rootEntries) {
|
|
3157
3205
|
if (!entry.isDirectory()) continue;
|
|
3158
3206
|
if (entry.name.startsWith(".")) continue;
|
|
3159
|
-
await scanDir(
|
|
3207
|
+
await scanDir(path12.join(modelDir, entry.name), [entry.name]);
|
|
3160
3208
|
}
|
|
3161
3209
|
} catch {
|
|
3162
3210
|
}
|
|
@@ -3173,7 +3221,7 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
3173
3221
|
const entries = await readdir5(absPath, { withFileTypes: true });
|
|
3174
3222
|
for (const entry of entries) {
|
|
3175
3223
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3176
|
-
const entryPath =
|
|
3224
|
+
const entryPath = path12.join(absPath, entry.name);
|
|
3177
3225
|
if (entry.isFile()) {
|
|
3178
3226
|
files.push(entryPath);
|
|
3179
3227
|
} else if (entry.isDirectory()) {
|
|
@@ -3185,13 +3233,13 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
3185
3233
|
}
|
|
3186
3234
|
}
|
|
3187
3235
|
for (const mp of mappingPaths) {
|
|
3188
|
-
await collectFiles(
|
|
3236
|
+
await collectFiles(path12.join(projectRoot, mp));
|
|
3189
3237
|
}
|
|
3190
3238
|
return files;
|
|
3191
3239
|
}
|
|
3192
3240
|
async function checkAnchorPresence(graph) {
|
|
3193
3241
|
const issues = [];
|
|
3194
|
-
const projectRoot =
|
|
3242
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
3195
3243
|
for (const [nodePath, node] of graph.nodes) {
|
|
3196
3244
|
const aspectsWithAnchors = (node.meta.aspects ?? []).filter((a) => a.anchors && a.anchors.length > 0);
|
|
3197
3245
|
if (aspectsWithAnchors.length === 0) continue;
|
|
@@ -3202,7 +3250,7 @@ async function checkAnchorPresence(graph) {
|
|
|
3202
3250
|
const fileContents = [];
|
|
3203
3251
|
for (const filePath of sourceFiles) {
|
|
3204
3252
|
try {
|
|
3205
|
-
const content = await
|
|
3253
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3206
3254
|
fileContents.push(content);
|
|
3207
3255
|
} catch {
|
|
3208
3256
|
}
|
|
@@ -3311,7 +3359,7 @@ function checkMissingDescriptions(graph) {
|
|
|
3311
3359
|
}
|
|
3312
3360
|
|
|
3313
3361
|
// src/cli/owner.ts
|
|
3314
|
-
import
|
|
3362
|
+
import path13 from "path";
|
|
3315
3363
|
import { access as access2 } from "fs/promises";
|
|
3316
3364
|
function normalizeForMatch(inputPath) {
|
|
3317
3365
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -3341,11 +3389,11 @@ function registerOwnerCommand(program2) {
|
|
|
3341
3389
|
const graph = await loadGraph(cwd);
|
|
3342
3390
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3343
3391
|
const rawPath = options.file.trim();
|
|
3344
|
-
const absolute =
|
|
3345
|
-
const repoRelative =
|
|
3392
|
+
const absolute = path13.resolve(cwd, rawPath);
|
|
3393
|
+
const repoRelative = path13.relative(repoRoot, absolute).split(path13.sep).join("/");
|
|
3346
3394
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3347
3395
|
if (!result.nodePath) {
|
|
3348
|
-
const absPath =
|
|
3396
|
+
const absPath = path13.resolve(repoRoot, result.file);
|
|
3349
3397
|
let exists = true;
|
|
3350
3398
|
try {
|
|
3351
3399
|
await access2(absPath);
|
|
@@ -3398,7 +3446,7 @@ function collectRelevantNodePaths(graph, nodePath) {
|
|
|
3398
3446
|
return relevant;
|
|
3399
3447
|
}
|
|
3400
3448
|
function registerBuildCommand(program2) {
|
|
3401
|
-
program2.command("build-context").description("Assemble a context package for one node").option("--node <node-path>", "Node path relative to .yggdrasil/model/").option("--file <file-path>", "Source file path \u2014 resolves owner node automatically").option("--full", "Include artifact file contents in output").action(async (options) => {
|
|
3449
|
+
program2.command("build-context").description("Assemble a context package for one node").option("--node <node-path>", "Node path relative to .yggdrasil/model/").option("--file <file-path>", "Source file path \u2014 resolves owner node automatically").option("--full", "Include artifact file contents in output").option("--self", "Only include the node\u2019s own artifacts (no hierarchy, dependencies, aspects, flows)").action(async (options) => {
|
|
3402
3450
|
try {
|
|
3403
3451
|
if (!options.node && !options.file) {
|
|
3404
3452
|
process.stderr.write("Error: either '--node <path>' or '--file <path>' is required\n");
|
|
@@ -3446,8 +3494,8 @@ function registerBuildCommand(program2) {
|
|
|
3446
3494
|
process.stderr.write(msg);
|
|
3447
3495
|
process.exit(1);
|
|
3448
3496
|
}
|
|
3449
|
-
const pkg2 = await buildContext(graph, nodePath);
|
|
3450
|
-
const mapOutput = toContextMapOutput(pkg2, graph);
|
|
3497
|
+
const pkg2 = await buildContext(graph, nodePath, { selfOnly: options.self });
|
|
3498
|
+
const mapOutput = toContextMapOutput(pkg2, graph, { selfOnly: options.self });
|
|
3451
3499
|
let output = formatContextYaml(mapOutput);
|
|
3452
3500
|
if (options.full) {
|
|
3453
3501
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3569,12 +3617,12 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
3569
3617
|
import chalk2 from "chalk";
|
|
3570
3618
|
|
|
3571
3619
|
// src/io/drift-state-store.ts
|
|
3572
|
-
import { readFile as
|
|
3573
|
-
import
|
|
3620
|
+
import { readFile as readFile15, writeFile as writeFile6, stat as stat5, readdir as readdir6, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
3621
|
+
import path14 from "path";
|
|
3574
3622
|
import { parse as yamlParse } from "yaml";
|
|
3575
3623
|
var DRIFT_STATE_DIR = ".drift-state";
|
|
3576
3624
|
function nodeStatePath(yggRoot, nodePath) {
|
|
3577
|
-
return
|
|
3625
|
+
return path14.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
|
|
3578
3626
|
}
|
|
3579
3627
|
async function scanJsonFiles(dir, baseDir) {
|
|
3580
3628
|
const results = [];
|
|
@@ -3585,12 +3633,12 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3585
3633
|
return results;
|
|
3586
3634
|
}
|
|
3587
3635
|
for (const entry of entries) {
|
|
3588
|
-
const fullPath =
|
|
3636
|
+
const fullPath = path14.join(dir, entry.name);
|
|
3589
3637
|
if (entry.isDirectory()) {
|
|
3590
3638
|
const nested = await scanJsonFiles(fullPath, baseDir);
|
|
3591
3639
|
results.push(...nested);
|
|
3592
3640
|
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
3593
|
-
const relPath =
|
|
3641
|
+
const relPath = path14.relative(baseDir, fullPath);
|
|
3594
3642
|
const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
|
|
3595
3643
|
results.push(nodePath);
|
|
3596
3644
|
}
|
|
@@ -3598,13 +3646,13 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3598
3646
|
return results;
|
|
3599
3647
|
}
|
|
3600
3648
|
async function removeEmptyParents(filePath, stopDir) {
|
|
3601
|
-
let dir =
|
|
3649
|
+
let dir = path14.dirname(filePath);
|
|
3602
3650
|
while (dir !== stopDir && dir.startsWith(stopDir)) {
|
|
3603
3651
|
try {
|
|
3604
3652
|
const entries = await readdir6(dir);
|
|
3605
3653
|
if (entries.length === 0) {
|
|
3606
3654
|
await rm2(dir, { recursive: true });
|
|
3607
|
-
dir =
|
|
3655
|
+
dir = path14.dirname(dir);
|
|
3608
3656
|
} else {
|
|
3609
3657
|
break;
|
|
3610
3658
|
}
|
|
@@ -3616,7 +3664,7 @@ async function removeEmptyParents(filePath, stopDir) {
|
|
|
3616
3664
|
async function readNodeDriftState(yggRoot, nodePath) {
|
|
3617
3665
|
try {
|
|
3618
3666
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3619
|
-
const content = await
|
|
3667
|
+
const content = await readFile15(filePath, "utf-8");
|
|
3620
3668
|
const parsed = JSON.parse(content);
|
|
3621
3669
|
return parsed;
|
|
3622
3670
|
} catch {
|
|
@@ -3625,12 +3673,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
|
|
|
3625
3673
|
}
|
|
3626
3674
|
async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
3627
3675
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3628
|
-
await mkdir3(
|
|
3676
|
+
await mkdir3(path14.dirname(filePath), { recursive: true });
|
|
3629
3677
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
3630
|
-
await
|
|
3678
|
+
await writeFile6(filePath, content, "utf-8");
|
|
3631
3679
|
}
|
|
3632
3680
|
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
3633
|
-
const driftDir =
|
|
3681
|
+
const driftDir = path14.join(yggRoot, DRIFT_STATE_DIR);
|
|
3634
3682
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
3635
3683
|
const removed = [];
|
|
3636
3684
|
for (const nodePath of allNodePaths) {
|
|
@@ -3644,7 +3692,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
|
3644
3692
|
return removed.sort();
|
|
3645
3693
|
}
|
|
3646
3694
|
async function readDriftState(yggRoot) {
|
|
3647
|
-
const driftPath =
|
|
3695
|
+
const driftPath = path14.join(yggRoot, DRIFT_STATE_DIR);
|
|
3648
3696
|
let driftStat;
|
|
3649
3697
|
try {
|
|
3650
3698
|
driftStat = await stat5(driftPath);
|
|
@@ -3652,7 +3700,7 @@ async function readDriftState(yggRoot) {
|
|
|
3652
3700
|
return {};
|
|
3653
3701
|
}
|
|
3654
3702
|
if (driftStat.isFile()) {
|
|
3655
|
-
const content = await
|
|
3703
|
+
const content = await readFile15(driftPath, "utf-8");
|
|
3656
3704
|
let raw;
|
|
3657
3705
|
try {
|
|
3658
3706
|
raw = JSON.parse(content);
|
|
@@ -3684,20 +3732,20 @@ async function readDriftState(yggRoot) {
|
|
|
3684
3732
|
}
|
|
3685
3733
|
|
|
3686
3734
|
// src/utils/hash.ts
|
|
3687
|
-
import { readFile as
|
|
3688
|
-
import
|
|
3735
|
+
import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
3736
|
+
import path15 from "path";
|
|
3689
3737
|
import { createHash } from "crypto";
|
|
3690
3738
|
import { createRequire } from "module";
|
|
3691
3739
|
var require2 = createRequire(import.meta.url);
|
|
3692
3740
|
var ignoreFactory = require2("ignore");
|
|
3693
3741
|
async function hashFile(filePath) {
|
|
3694
|
-
const content = await
|
|
3742
|
+
const content = await readFile16(filePath);
|
|
3695
3743
|
return createHash("sha256").update(content).digest("hex");
|
|
3696
3744
|
}
|
|
3697
3745
|
async function loadRootGitignoreStack(projectRoot) {
|
|
3698
3746
|
if (!projectRoot) return [];
|
|
3699
3747
|
try {
|
|
3700
|
-
const content = await
|
|
3748
|
+
const content = await readFile16(path15.join(projectRoot, ".gitignore"), "utf-8");
|
|
3701
3749
|
const matcher = ignoreFactory();
|
|
3702
3750
|
matcher.add(content);
|
|
3703
3751
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -3707,7 +3755,7 @@ async function loadRootGitignoreStack(projectRoot) {
|
|
|
3707
3755
|
}
|
|
3708
3756
|
function isIgnoredByStack(candidatePath, stack) {
|
|
3709
3757
|
for (const { basePath, matcher } of stack) {
|
|
3710
|
-
const relativePath =
|
|
3758
|
+
const relativePath = path15.relative(basePath, candidatePath);
|
|
3711
3759
|
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
3712
3760
|
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
3713
3761
|
}
|
|
@@ -3722,7 +3770,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3722
3770
|
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
3723
3771
|
const allFiles = [];
|
|
3724
3772
|
for (const tf of trackedFiles) {
|
|
3725
|
-
const absPath =
|
|
3773
|
+
const absPath = path15.join(projectRoot, tf.path);
|
|
3726
3774
|
try {
|
|
3727
3775
|
const st = await stat6(absPath);
|
|
3728
3776
|
if (st.isDirectory()) {
|
|
@@ -3732,7 +3780,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3732
3780
|
});
|
|
3733
3781
|
for (const entry of dirEntries) {
|
|
3734
3782
|
allFiles.push({
|
|
3735
|
-
relPath:
|
|
3783
|
+
relPath: path15.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
3736
3784
|
absPath: entry.absPath,
|
|
3737
3785
|
mtimeMs: entry.mtimeMs
|
|
3738
3786
|
});
|
|
@@ -3772,7 +3820,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3772
3820
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
3773
3821
|
let stack = options.gitignoreStack ?? [];
|
|
3774
3822
|
try {
|
|
3775
|
-
const localContent = await
|
|
3823
|
+
const localContent = await readFile16(path15.join(directoryPath, ".gitignore"), "utf-8");
|
|
3776
3824
|
const localMatcher = ignoreFactory();
|
|
3777
3825
|
localMatcher.add(localContent);
|
|
3778
3826
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
@@ -3782,7 +3830,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3782
3830
|
const dirs = [];
|
|
3783
3831
|
const files = [];
|
|
3784
3832
|
for (const entry of entries) {
|
|
3785
|
-
const absoluteChildPath =
|
|
3833
|
+
const absoluteChildPath = path15.join(directoryPath, entry.name);
|
|
3786
3834
|
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
3787
3835
|
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
3788
3836
|
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
@@ -3795,7 +3843,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3795
3843
|
Promise.all(files.map(async (f) => {
|
|
3796
3844
|
const fileStat = await stat6(f);
|
|
3797
3845
|
return {
|
|
3798
|
-
relPath:
|
|
3846
|
+
relPath: path15.relative(rootDirectoryPath, f),
|
|
3799
3847
|
absPath: f,
|
|
3800
3848
|
mtimeMs: fileStat.mtimeMs
|
|
3801
3849
|
};
|
|
@@ -3808,15 +3856,15 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3808
3856
|
}
|
|
3809
3857
|
|
|
3810
3858
|
// src/core/context-files.ts
|
|
3811
|
-
import
|
|
3859
|
+
import path16 from "path";
|
|
3812
3860
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3813
3861
|
function collectTrackedFiles(node, graph) {
|
|
3814
3862
|
const seen = /* @__PURE__ */ new Set();
|
|
3815
3863
|
const result = [];
|
|
3816
|
-
const projectRoot =
|
|
3817
|
-
const yggPrefix =
|
|
3818
|
-
const yggPrefixNormalized = yggPrefix.split(
|
|
3819
|
-
const configArtifactKeys = new Set(Object.keys(
|
|
3864
|
+
const projectRoot = path16.dirname(graph.rootPath);
|
|
3865
|
+
const yggPrefix = path16.relative(projectRoot, graph.rootPath);
|
|
3866
|
+
const yggPrefixNormalized = yggPrefix.split(path16.sep).join("/");
|
|
3867
|
+
const configArtifactKeys = new Set(Object.keys(STANDARD_ARTIFACTS2));
|
|
3820
3868
|
function addFile(filePath, category) {
|
|
3821
3869
|
if (seen.has(filePath)) return;
|
|
3822
3870
|
seen.add(filePath);
|
|
@@ -3864,7 +3912,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
3864
3912
|
if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
|
|
3865
3913
|
const target = graph.nodes.get(relation.target);
|
|
3866
3914
|
if (!target) continue;
|
|
3867
|
-
const structuralFilenames = Object.entries(
|
|
3915
|
+
const structuralFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
3868
3916
|
const structuralArts = structuralFilenames.filter(
|
|
3869
3917
|
(filename) => target.artifacts.some((a) => a.filename === filename)
|
|
3870
3918
|
);
|
|
@@ -3893,7 +3941,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
3893
3941
|
if (relation.type !== "emits" && relation.type !== "listens") continue;
|
|
3894
3942
|
const target = graph.nodes.get(relation.target);
|
|
3895
3943
|
if (!target) continue;
|
|
3896
|
-
const structuralFilenames = Object.entries(
|
|
3944
|
+
const structuralFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
3897
3945
|
const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : [...configArtifactKeys];
|
|
3898
3946
|
for (const filename of filterFilenames) {
|
|
3899
3947
|
if (target.artifacts.some((a) => a.filename === filename)) {
|
|
@@ -3928,7 +3976,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
|
|
|
3928
3976
|
|
|
3929
3977
|
// src/core/drift-detector.ts
|
|
3930
3978
|
import { access as access3 } from "fs/promises";
|
|
3931
|
-
import
|
|
3979
|
+
import path17 from "path";
|
|
3932
3980
|
function getChildMappingExclusions(graph, nodePath) {
|
|
3933
3981
|
const node = graph.nodes.get(nodePath);
|
|
3934
3982
|
if (!node) return [];
|
|
@@ -3950,7 +3998,7 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
3950
3998
|
return exclusions;
|
|
3951
3999
|
}
|
|
3952
4000
|
async function detectDrift(graph, filterNodePath) {
|
|
3953
|
-
const projectRoot =
|
|
4001
|
+
const projectRoot = path17.dirname(graph.rootPath);
|
|
3954
4002
|
const driftState = await readDriftState(graph.rootPath);
|
|
3955
4003
|
const entries = [];
|
|
3956
4004
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -4033,14 +4081,14 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
4033
4081
|
};
|
|
4034
4082
|
}
|
|
4035
4083
|
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
4036
|
-
const yggPrefix =
|
|
4037
|
-
const normalizedPrefix = yggPrefix.split(
|
|
4084
|
+
const yggPrefix = path17.relative(projectRoot, _rootPath);
|
|
4085
|
+
const normalizedPrefix = yggPrefix.split(path17.sep).join("/");
|
|
4038
4086
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
4039
4087
|
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
4040
4088
|
}
|
|
4041
4089
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
4042
4090
|
for (const mp of mappingPaths) {
|
|
4043
|
-
const absPath =
|
|
4091
|
+
const absPath = path17.join(projectRoot, mp);
|
|
4044
4092
|
try {
|
|
4045
4093
|
await access3(absPath);
|
|
4046
4094
|
return false;
|
|
@@ -4050,7 +4098,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
4050
4098
|
return true;
|
|
4051
4099
|
}
|
|
4052
4100
|
async function syncDriftState(graph, nodePath) {
|
|
4053
|
-
const projectRoot =
|
|
4101
|
+
const projectRoot = path17.dirname(graph.rootPath);
|
|
4054
4102
|
const node = graph.nodes.get(nodePath);
|
|
4055
4103
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
4056
4104
|
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
@@ -4065,7 +4113,7 @@ async function syncDriftState(graph, nodePath) {
|
|
|
4065
4113
|
if (previousHash && previousHash !== canonicalHash && existingEntry?.files) {
|
|
4066
4114
|
let hasSourceChange = false;
|
|
4067
4115
|
let hasGraphChange = false;
|
|
4068
|
-
const yggPrefix =
|
|
4116
|
+
const yggPrefix = path17.relative(projectRoot, graph.rootPath).split(path17.sep).join("/");
|
|
4069
4117
|
for (const [filePath, hash] of Object.entries(fileHashes)) {
|
|
4070
4118
|
const storedHash = existingEntry.files[filePath];
|
|
4071
4119
|
if (storedHash && storedHash === hash) continue;
|
|
@@ -4331,7 +4379,7 @@ function registerStatusCommand(program2) {
|
|
|
4331
4379
|
const warningCount = validation.issues.filter(
|
|
4332
4380
|
(issue) => issue.severity === "warning"
|
|
4333
4381
|
).length;
|
|
4334
|
-
const configuredArtifactTypes = Object.keys(
|
|
4382
|
+
const configuredArtifactTypes = Object.keys(STANDARD_ARTIFACTS2);
|
|
4335
4383
|
const totalSlots = graph.nodes.size * configuredArtifactTypes.length;
|
|
4336
4384
|
let filledSlots = 0;
|
|
4337
4385
|
let mappedNodeCount = 0;
|
|
@@ -4405,10 +4453,10 @@ function registerTreeCommand(program2) {
|
|
|
4405
4453
|
let roots;
|
|
4406
4454
|
let showProjectName;
|
|
4407
4455
|
if (options.root?.trim()) {
|
|
4408
|
-
const
|
|
4409
|
-
const node = graph.nodes.get(
|
|
4456
|
+
const path20 = options.root.trim().replace(/\/$/, "");
|
|
4457
|
+
const node = graph.nodes.get(path20);
|
|
4410
4458
|
if (!node) {
|
|
4411
|
-
process.stderr.write(`Error: path '${
|
|
4459
|
+
process.stderr.write(`Error: path '${path20}' not found
|
|
4412
4460
|
`);
|
|
4413
4461
|
process.exit(1);
|
|
4414
4462
|
}
|
|
@@ -4453,7 +4501,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
4453
4501
|
|
|
4454
4502
|
// src/core/dependency-resolver.ts
|
|
4455
4503
|
import { execSync } from "child_process";
|
|
4456
|
-
import
|
|
4504
|
+
import path18 from "path";
|
|
4457
4505
|
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
4458
4506
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
4459
4507
|
function filterRelationType(relType, filter) {
|
|
@@ -4530,7 +4578,7 @@ function registerDepsCommand(program2) {
|
|
|
4530
4578
|
// src/core/graph-from-git.ts
|
|
4531
4579
|
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
4532
4580
|
import { tmpdir } from "os";
|
|
4533
|
-
import
|
|
4581
|
+
import path19 from "path";
|
|
4534
4582
|
import { execSync as execSync2 } from "child_process";
|
|
4535
4583
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
4536
4584
|
const yggPath = ".yggdrasil";
|
|
@@ -4541,8 +4589,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
4541
4589
|
return null;
|
|
4542
4590
|
}
|
|
4543
4591
|
try {
|
|
4544
|
-
tmpDir = await mkdtemp(
|
|
4545
|
-
const archivePath =
|
|
4592
|
+
tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
|
|
4593
|
+
const archivePath = path19.join(tmpDir, "archive.tar");
|
|
4546
4594
|
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
4547
4595
|
cwd: projectRoot,
|
|
4548
4596
|
stdio: "pipe"
|
|
@@ -4612,14 +4660,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
4612
4660
|
}
|
|
4613
4661
|
const chains = [];
|
|
4614
4662
|
for (const node of transitiveOnly) {
|
|
4615
|
-
const
|
|
4663
|
+
const path20 = [];
|
|
4616
4664
|
let current = node;
|
|
4617
4665
|
while (current) {
|
|
4618
|
-
|
|
4666
|
+
path20.unshift(current);
|
|
4619
4667
|
current = parent.get(current);
|
|
4620
4668
|
}
|
|
4621
|
-
if (
|
|
4622
|
-
chains.push(
|
|
4669
|
+
if (path20.length >= 3) {
|
|
4670
|
+
chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
4623
4671
|
}
|
|
4624
4672
|
}
|
|
4625
4673
|
return chains.sort();
|
|
@@ -4663,14 +4711,14 @@ function collectIndirectDependents(graph, directlyAffected) {
|
|
|
4663
4711
|
}
|
|
4664
4712
|
for (const [node] of parent) {
|
|
4665
4713
|
if (directSet.has(node)) continue;
|
|
4666
|
-
const
|
|
4714
|
+
const path20 = [node];
|
|
4667
4715
|
let current = node;
|
|
4668
4716
|
while (parent.has(current)) {
|
|
4669
4717
|
current = parent.get(current);
|
|
4670
|
-
|
|
4718
|
+
path20.push(current);
|
|
4671
4719
|
}
|
|
4672
|
-
const chain =
|
|
4673
|
-
const depth =
|
|
4720
|
+
const chain = path20.map((p) => `<- ${p}`).join(" ");
|
|
4721
|
+
const depth = path20.length;
|
|
4674
4722
|
const existing = bestChain.get(node);
|
|
4675
4723
|
if (!existing || depth < existing.depth) {
|
|
4676
4724
|
bestChain.set(node, { chain, depth });
|