@chrisdudek/yg 2.12.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 +369 -353
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +1 -15
- package/dist/templates/rules.ts +72 -39
- 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
|
|
@@ -60,14 +46,30 @@ This is your operating manual for working in a Yggdrasil-managed repository.
|
|
|
60
46
|
|
|
61
47
|
<critical_protocol>
|
|
62
48
|
BEFORE starting any task \u2014 brainstorming, design, planning, OR implementation:
|
|
63
|
-
\`yg select --task "<goal>"\` \u2192 \`yg build-context\` on each result \u2192 read
|
|
64
|
-
This is
|
|
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.
|
|
65
56
|
|
|
66
57
|
BEFORE reading, analyzing, or modifying ANY source file:
|
|
67
58
|
\`yg build-context --file <path>\`
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
71
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.
|
|
72
74
|
</critical_protocol>
|
|
73
75
|
|
|
@@ -82,21 +84,24 @@ Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the r
|
|
|
82
84
|
EVERY conversation: yg preflight \u2014 no exceptions.
|
|
83
85
|
|
|
84
86
|
BEFORE any task (brainstorming, design, planning, implementation):
|
|
85
|
-
yg select --task "<goal>" \u2192 yg build-context on results
|
|
86
|
-
|
|
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.
|
|
87
94
|
|
|
88
95
|
BEFORE any source file interaction:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
For blast radius: also run yg impact --file <path>.
|
|
94
|
-
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>.
|
|
95
100
|
|
|
96
101
|
AFTER modifying:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
Update graph artifacts (per file, not batched)
|
|
103
|
+
yg validate \u2014 fix all errors
|
|
104
|
+
yg drift-sync --node <owner>
|
|
100
105
|
|
|
101
106
|
ALWAYS: establish graph coverage before modifying code.
|
|
102
107
|
ALWAYS: run yg build-context --file before reading source.
|
|
@@ -114,9 +119,9 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
114
119
|
|
|
115
120
|
**Step 2a** \u2014 Owner found: execute checklist:
|
|
116
121
|
|
|
117
|
-
- [ ] 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.
|
|
118
123
|
- [ ] 2. Assess blast radius: \`yg impact --node <node_path>\` \u2014 review dependents, descendants, and co-aspect nodes before changing interfaces or shared behavior
|
|
119
|
-
- [ ] 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.
|
|
120
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\`.
|
|
121
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.
|
|
122
127
|
- [ ] 5. Run \`yg validate\` \u2014 fix all errors (if unfixable after 3 attempts \u2192 stop, report to user)
|
|
@@ -134,12 +139,14 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
134
139
|
|
|
135
140
|
*Greenfield (new code):* Only Option A. Blackbox is forbidden for new code. Follow the graph-first workflow:
|
|
136
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.
|
|
137
143
|
1. Create aspects first (cross-cutting requirements the new code must satisfy)
|
|
138
144
|
2. Create flows if the code participates in a business process
|
|
139
145
|
3. Create nodes with full artifacts \u2014 description in \`yg-node.yaml\`, responsibility, interface, internals
|
|
140
146
|
4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
|
|
141
|
-
5. Implement code that satisfies the specification
|
|
142
|
-
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)
|
|
143
150
|
|
|
144
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.
|
|
145
152
|
|
|
@@ -194,13 +201,15 @@ Result: graph is stale, next agent asks user the same questions
|
|
|
194
201
|
User: "Here are the spec docs. Implement the admin blog editor."
|
|
195
202
|
|
|
196
203
|
1. Read ALL spec docs (blog-editor.md, autosave.md, user-persona.md, version-history.md)
|
|
197
|
-
2.
|
|
198
|
-
3.
|
|
199
|
-
4. Create
|
|
200
|
-
5.
|
|
201
|
-
6.
|
|
202
|
-
7.
|
|
203
|
-
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?
|
|
204
213
|
|
|
205
214
|
</example_correct>
|
|
206
215
|
|
|
@@ -208,13 +217,13 @@ User: "Here are the spec docs. Implement the admin blog editor."
|
|
|
208
217
|
|
|
209
218
|
User: "Here are the spec docs. Implement the admin blog editor."
|
|
210
219
|
|
|
211
|
-
1. Read
|
|
212
|
-
2.
|
|
213
|
-
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
|
|
214
223
|
4. Write responsibility.md summarizing what the code does \u2190 WRONG: describes code, not spec intent
|
|
215
|
-
5.
|
|
224
|
+
5. Knowledge from spec docs lost \u2190 WRONG: spec treated as consumed input, not persisted to graph
|
|
216
225
|
|
|
217
|
-
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.
|
|
218
227
|
|
|
219
228
|
</example_wrong>
|
|
220
229
|
|
|
@@ -301,7 +310,7 @@ var REFERENCE = `## REFERENCE
|
|
|
301
310
|
|
|
302
311
|
\`\`\`
|
|
303
312
|
.yggdrasil/
|
|
304
|
-
yg-config.yaml \u2190 version, vocabulary, node types,
|
|
313
|
+
yg-config.yaml \u2190 version, vocabulary, node types, required aspects
|
|
305
314
|
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
306
315
|
aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
|
|
307
316
|
flows/ \u2190 why and in what process: business processes with node participation
|
|
@@ -327,7 +336,7 @@ Three artifacts capture node knowledge at three levels:
|
|
|
327
336
|
|
|
328
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\`.
|
|
329
338
|
|
|
330
|
-
|
|
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.
|
|
331
340
|
|
|
332
341
|
### Context Assembly
|
|
333
342
|
|
|
@@ -342,9 +351,14 @@ Projects can define additional artifact types in \`yg-config.yaml\` under \`arti
|
|
|
342
351
|
|
|
343
352
|
All artifact paths are relative to \`.yggdrasil/\` \u2014 construct full path as \`.yggdrasil/<path>\`.
|
|
344
353
|
|
|
345
|
-
**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:
|
|
355
|
+
|
|
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.
|
|
346
360
|
|
|
347
|
-
|
|
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).
|
|
348
362
|
|
|
349
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.
|
|
350
364
|
|
|
@@ -354,7 +368,7 @@ Artifact paths are stable identifiers within a session. When building context fo
|
|
|
354
368
|
|
|
355
369
|
When you encounter information, route it to the correct location:
|
|
356
370
|
|
|
357
|
-
- **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)
|
|
358
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\`
|
|
359
373
|
- **Business process** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
360
374
|
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
@@ -465,6 +479,7 @@ yg owner --file <path> Find the node that owns this file (quick che
|
|
|
465
479
|
yg build-context --file <path> Resolve owner + assemble context in one step.
|
|
466
480
|
yg build-context --node <path> Assemble context map for a known node.
|
|
467
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).
|
|
468
483
|
yg tree [--root <path>] [--depth N] Print graph structure.
|
|
469
484
|
yg aspects List aspects with metadata (YAML output).
|
|
470
485
|
yg flows List flows with metadata (YAML output).
|
|
@@ -489,7 +504,7 @@ yg drift-sync --node <path> [--recursive] | --all
|
|
|
489
504
|
|
|
490
505
|
| What you have | Where it goes |
|
|
491
506
|
|---|---|
|
|
492
|
-
| Information specific to this node | Local node artifact (
|
|
507
|
+
| Information specific to this node | Local node artifact (\`responsibility.md\`, \`interface.md\`, or \`internals.md\`) |
|
|
493
508
|
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
494
509
|
| Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
|
|
495
510
|
| Business process participation | Flow (\`yg-flow.yaml nodes\`) |
|
|
@@ -564,11 +579,15 @@ What matters is the ACTION you are performing, not what instructed it. If the ac
|
|
|
564
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. |
|
|
565
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. |
|
|
566
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. |
|
|
567
585
|
|
|
568
586
|
### Failure States
|
|
569
587
|
|
|
570
588
|
You have broken Yggdrasil if you do any of the following:
|
|
571
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.
|
|
572
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).
|
|
573
592
|
- \u274C Modified source code without updating graph artifacts before moving to the next file, or vice versa.
|
|
574
593
|
- \u274C Batched graph updates to "do later" \u2014 deferred = forgotten. Update after EACH file.
|
|
@@ -598,7 +617,7 @@ Per area checklist:
|
|
|
598
617
|
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
599
618
|
- [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
|
|
600
619
|
- [ ] 3b. Write \`description\` in \`yg-node.yaml\` \u2014 a short summary of what the node does
|
|
601
|
-
- [ ] 4. Analyze source \u2014
|
|
620
|
+
- [ ] 4. Analyze source \u2014 write \`responsibility.md\`, \`interface.md\`, and \`internals.md\` from code analysis, do not invent
|
|
602
621
|
- [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
|
|
603
622
|
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
604
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\`
|
|
@@ -1220,38 +1239,73 @@ async function transformSingleNode(filePath, actions, warnings) {
|
|
|
1220
1239
|
}
|
|
1221
1240
|
}
|
|
1222
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
|
+
|
|
1223
1272
|
// src/migrations/index.ts
|
|
1224
1273
|
var MIGRATIONS = [
|
|
1225
1274
|
{
|
|
1226
1275
|
to: "2.0.0",
|
|
1227
1276
|
description: "Rename YAML files to yg-* prefix, restructure config, convert aspects format",
|
|
1228
1277
|
run: migrateTo2
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
to: "3.0.0",
|
|
1281
|
+
description: "Remove artifacts section from config (now hardcoded in CLI)",
|
|
1282
|
+
run: migrateTo3
|
|
1229
1283
|
}
|
|
1230
1284
|
];
|
|
1231
1285
|
|
|
1232
1286
|
// src/cli/init.ts
|
|
1233
1287
|
function getGraphSchemasDir() {
|
|
1234
|
-
const currentDir =
|
|
1235
|
-
const packageRoot =
|
|
1236
|
-
return
|
|
1288
|
+
const currentDir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1289
|
+
const packageRoot = path5.join(currentDir, "..");
|
|
1290
|
+
return path5.join(packageRoot, "graph-schemas");
|
|
1237
1291
|
}
|
|
1238
1292
|
function getCliVersion() {
|
|
1239
|
-
const currentDir =
|
|
1240
|
-
const packageRoot =
|
|
1241
|
-
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"));
|
|
1242
1296
|
return pkg2.version;
|
|
1243
1297
|
}
|
|
1244
1298
|
async function refreshSchemas(yggRoot) {
|
|
1245
|
-
const schemasDir =
|
|
1299
|
+
const schemasDir = path5.join(yggRoot, "schemas");
|
|
1246
1300
|
await mkdir2(schemasDir, { recursive: true });
|
|
1247
1301
|
const graphSchemasDir = getGraphSchemasDir();
|
|
1248
1302
|
try {
|
|
1249
1303
|
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
1250
1304
|
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
1251
1305
|
for (const file of schemaFiles) {
|
|
1252
|
-
const srcPath =
|
|
1253
|
-
const content = await
|
|
1254
|
-
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");
|
|
1255
1309
|
}
|
|
1256
1310
|
} catch {
|
|
1257
1311
|
}
|
|
@@ -1264,7 +1318,7 @@ function registerInitCommand(program2) {
|
|
|
1264
1318
|
"generic"
|
|
1265
1319
|
).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
|
|
1266
1320
|
const projectRoot = process.cwd();
|
|
1267
|
-
const yggRoot =
|
|
1321
|
+
const yggRoot = path5.join(projectRoot, ".yggdrasil");
|
|
1268
1322
|
let upgradeMode = false;
|
|
1269
1323
|
try {
|
|
1270
1324
|
const statResult = await stat2(yggRoot);
|
|
@@ -1327,23 +1381,23 @@ function registerInitCommand(program2) {
|
|
|
1327
1381
|
await refreshSchemas(yggRoot);
|
|
1328
1382
|
const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
|
|
1329
1383
|
process.stdout.write("\u2713 Rules refreshed.\n");
|
|
1330
|
-
process.stdout.write(` ${
|
|
1384
|
+
process.stdout.write(` ${path5.relative(projectRoot, rulesPath2)}
|
|
1331
1385
|
`);
|
|
1332
1386
|
return;
|
|
1333
1387
|
}
|
|
1334
|
-
await mkdir2(
|
|
1335
|
-
await mkdir2(
|
|
1336
|
-
await mkdir2(
|
|
1337
|
-
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");
|
|
1338
1392
|
await mkdir2(schemasDir, { recursive: true });
|
|
1339
1393
|
const graphSchemasDir = getGraphSchemasDir();
|
|
1340
1394
|
try {
|
|
1341
1395
|
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
1342
1396
|
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
1343
1397
|
for (const file of schemaFiles) {
|
|
1344
|
-
const srcPath =
|
|
1345
|
-
const content = await
|
|
1346
|
-
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");
|
|
1347
1401
|
}
|
|
1348
1402
|
} catch (err) {
|
|
1349
1403
|
process.stderr.write(
|
|
@@ -1351,8 +1405,8 @@ function registerInitCommand(program2) {
|
|
|
1351
1405
|
`
|
|
1352
1406
|
);
|
|
1353
1407
|
}
|
|
1354
|
-
await
|
|
1355
|
-
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");
|
|
1356
1410
|
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
1357
1411
|
process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
|
|
1358
1412
|
process.stdout.write("Created:\n");
|
|
@@ -1362,7 +1416,7 @@ function registerInitCommand(program2) {
|
|
|
1362
1416
|
process.stdout.write(" .yggdrasil/aspects/\n");
|
|
1363
1417
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
1364
1418
|
process.stdout.write(" .yggdrasil/schemas/ (yg-config, yg-node, yg-aspect, yg-flow)\n");
|
|
1365
|
-
process.stdout.write(` ${
|
|
1419
|
+
process.stdout.write(` ${path5.relative(projectRoot, rulesPath)} (rules)
|
|
1366
1420
|
|
|
1367
1421
|
`);
|
|
1368
1422
|
process.stdout.write("Next steps:\n");
|
|
@@ -1373,20 +1427,39 @@ function registerInitCommand(program2) {
|
|
|
1373
1427
|
}
|
|
1374
1428
|
|
|
1375
1429
|
// src/core/graph-loader.ts
|
|
1376
|
-
import { readdir as readdir4, readFile as
|
|
1377
|
-
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
|
+
};
|
|
1378
1451
|
|
|
1379
1452
|
// src/io/config-parser.ts
|
|
1380
|
-
import { readFile as
|
|
1381
|
-
import { parse as
|
|
1453
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1454
|
+
import { parse as parseYaml4 } from "yaml";
|
|
1382
1455
|
var DEFAULT_QUALITY = {
|
|
1383
1456
|
min_artifact_length: 50,
|
|
1384
1457
|
max_direct_relations: 10,
|
|
1385
1458
|
context_budget: { warning: 1e4, error: 2e4, own_warning: void 0 }
|
|
1386
1459
|
};
|
|
1387
1460
|
async function parseConfig(filePath) {
|
|
1388
|
-
const content = await
|
|
1389
|
-
const raw =
|
|
1461
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1462
|
+
const raw = parseYaml4(content);
|
|
1390
1463
|
if (!raw || typeof raw !== "object") {
|
|
1391
1464
|
throw new Error(`yg-config.yaml: file is empty or not a valid YAML mapping`);
|
|
1392
1465
|
}
|
|
@@ -1412,35 +1485,6 @@ async function parseConfig(filePath) {
|
|
|
1412
1485
|
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
1413
1486
|
};
|
|
1414
1487
|
}
|
|
1415
|
-
const artifacts = raw.artifacts;
|
|
1416
|
-
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
|
|
1417
|
-
throw new Error(`yg-config.yaml: 'artifacts' must be a non-empty object`);
|
|
1418
|
-
}
|
|
1419
|
-
const artifactsMap = {};
|
|
1420
|
-
for (const [key, val] of Object.entries(artifacts)) {
|
|
1421
|
-
if (key === "yg-node.yaml") {
|
|
1422
|
-
throw new Error(`yg-config.yaml: artifact name 'yg-node.yaml' is reserved`);
|
|
1423
|
-
}
|
|
1424
|
-
const a = val;
|
|
1425
|
-
const required = a.required;
|
|
1426
|
-
if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
|
|
1427
|
-
throw new Error(`yg-config.yaml: artifact '${key}' has invalid 'required' field`);
|
|
1428
|
-
}
|
|
1429
|
-
if (typeof required === "object" && required && "when" in required) {
|
|
1430
|
-
const when = required.when;
|
|
1431
|
-
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
|
|
1432
|
-
if (!validWhen) {
|
|
1433
|
-
throw new Error(
|
|
1434
|
-
`yg-config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
artifactsMap[key] = {
|
|
1439
|
-
required,
|
|
1440
|
-
description: a.description ?? "",
|
|
1441
|
-
included_in_relations: a.included_in_relations ?? false
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
1488
|
const qualityRaw = raw.quality;
|
|
1445
1489
|
const quality = qualityRaw ? {
|
|
1446
1490
|
min_artifact_length: qualityRaw.min_artifact_length ?? DEFAULT_QUALITY.min_artifact_length,
|
|
@@ -1463,14 +1507,13 @@ async function parseConfig(filePath) {
|
|
|
1463
1507
|
version,
|
|
1464
1508
|
name: raw.name.trim(),
|
|
1465
1509
|
node_types: nodeTypes,
|
|
1466
|
-
artifacts: artifactsMap,
|
|
1467
1510
|
quality
|
|
1468
1511
|
};
|
|
1469
1512
|
}
|
|
1470
1513
|
|
|
1471
1514
|
// src/io/node-parser.ts
|
|
1472
|
-
import { readFile as
|
|
1473
|
-
import { parse as
|
|
1515
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1516
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1474
1517
|
var RELATION_TYPES = [
|
|
1475
1518
|
"uses",
|
|
1476
1519
|
"calls",
|
|
@@ -1483,8 +1526,8 @@ function isValidRelationType(t) {
|
|
|
1483
1526
|
return typeof t === "string" && RELATION_TYPES.includes(t);
|
|
1484
1527
|
}
|
|
1485
1528
|
async function parseNodeYaml(filePath) {
|
|
1486
|
-
const content = await
|
|
1487
|
-
const raw =
|
|
1529
|
+
const content = await readFile7(filePath, "utf-8");
|
|
1530
|
+
const raw = parseYaml5(content);
|
|
1488
1531
|
if (!raw || typeof raw !== "object") {
|
|
1489
1532
|
throw new Error(`yg-node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
1490
1533
|
}
|
|
@@ -1629,12 +1672,12 @@ function parseMapping(rawMapping, filePath) {
|
|
|
1629
1672
|
}
|
|
1630
1673
|
|
|
1631
1674
|
// src/io/aspect-parser.ts
|
|
1632
|
-
import { readFile as
|
|
1633
|
-
import { parse as
|
|
1675
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1676
|
+
import { parse as parseYaml6 } from "yaml";
|
|
1634
1677
|
|
|
1635
1678
|
// src/io/artifact-reader.ts
|
|
1636
|
-
import { readFile as
|
|
1637
|
-
import
|
|
1679
|
+
import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
|
|
1680
|
+
import path6 from "path";
|
|
1638
1681
|
async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFiles) {
|
|
1639
1682
|
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1640
1683
|
const artifacts = [];
|
|
@@ -1643,8 +1686,8 @@ async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFi
|
|
|
1643
1686
|
if (!entry.isFile()) continue;
|
|
1644
1687
|
if (excludeFiles.includes(entry.name)) continue;
|
|
1645
1688
|
if (includeSet && !includeSet.has(entry.name)) continue;
|
|
1646
|
-
const filePath =
|
|
1647
|
-
const content = await
|
|
1689
|
+
const filePath = path6.join(dirPath, entry.name);
|
|
1690
|
+
const content = await readFile8(filePath, "utf-8");
|
|
1648
1691
|
artifacts.push({ filename: entry.name, content });
|
|
1649
1692
|
}
|
|
1650
1693
|
artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
@@ -1658,8 +1701,8 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1658
1701
|
if (!idTrimmed) {
|
|
1659
1702
|
throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
|
|
1660
1703
|
}
|
|
1661
|
-
const content = await
|
|
1662
|
-
const raw =
|
|
1704
|
+
const content = await readFile9(aspectYamlPath, "utf-8");
|
|
1705
|
+
const raw = parseYaml6(content);
|
|
1663
1706
|
if (!raw || typeof raw !== "object") {
|
|
1664
1707
|
throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1665
1708
|
}
|
|
@@ -1695,12 +1738,12 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1695
1738
|
}
|
|
1696
1739
|
|
|
1697
1740
|
// src/io/flow-parser.ts
|
|
1698
|
-
import { readFile as
|
|
1699
|
-
import
|
|
1700
|
-
import { parse as
|
|
1741
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1742
|
+
import path7 from "path";
|
|
1743
|
+
import { parse as parseYaml7 } from "yaml";
|
|
1701
1744
|
async function parseFlow(flowDir, flowYamlPath) {
|
|
1702
|
-
const content = await
|
|
1703
|
-
const raw =
|
|
1745
|
+
const content = await readFile10(flowYamlPath, "utf-8");
|
|
1746
|
+
const raw = parseYaml7(content);
|
|
1704
1747
|
if (!raw || typeof raw !== "object") {
|
|
1705
1748
|
throw new Error(`yg-flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1706
1749
|
}
|
|
@@ -1730,7 +1773,7 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1730
1773
|
}
|
|
1731
1774
|
const artifacts = await readArtifacts(flowDir, ["yg-flow.yaml"]);
|
|
1732
1775
|
return {
|
|
1733
|
-
path:
|
|
1776
|
+
path: path7.basename(flowDir),
|
|
1734
1777
|
name: raw.name.trim(),
|
|
1735
1778
|
description,
|
|
1736
1779
|
nodes: nodePaths,
|
|
@@ -1740,26 +1783,26 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1740
1783
|
}
|
|
1741
1784
|
|
|
1742
1785
|
// src/io/schema-parser.ts
|
|
1743
|
-
import { readFile as
|
|
1744
|
-
import
|
|
1745
|
-
import { parse as
|
|
1786
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1787
|
+
import path8 from "path";
|
|
1788
|
+
import { parse as parseYaml8 } from "yaml";
|
|
1746
1789
|
async function parseSchema(filePath) {
|
|
1747
|
-
const content = await
|
|
1748
|
-
|
|
1749
|
-
const rawName =
|
|
1790
|
+
const content = await readFile11(filePath, "utf-8");
|
|
1791
|
+
parseYaml8(content);
|
|
1792
|
+
const rawName = path8.basename(filePath, path8.extname(filePath));
|
|
1750
1793
|
const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
|
|
1751
1794
|
return { schemaType };
|
|
1752
1795
|
}
|
|
1753
1796
|
|
|
1754
1797
|
// src/utils/paths.ts
|
|
1755
|
-
import
|
|
1798
|
+
import path9 from "path";
|
|
1756
1799
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1757
1800
|
import { stat as stat3 } from "fs/promises";
|
|
1758
1801
|
async function findYggRoot(projectRoot) {
|
|
1759
|
-
let current =
|
|
1760
|
-
const root =
|
|
1802
|
+
let current = path9.resolve(projectRoot);
|
|
1803
|
+
const root = path9.parse(current).root;
|
|
1761
1804
|
while (true) {
|
|
1762
|
-
const yggPath =
|
|
1805
|
+
const yggPath = path9.join(current, ".yggdrasil");
|
|
1763
1806
|
try {
|
|
1764
1807
|
const st = await stat3(yggPath);
|
|
1765
1808
|
if (!st.isDirectory()) {
|
|
@@ -1773,7 +1816,7 @@ async function findYggRoot(projectRoot) {
|
|
|
1773
1816
|
if (current === root) {
|
|
1774
1817
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
1775
1818
|
}
|
|
1776
|
-
current =
|
|
1819
|
+
current = path9.dirname(current);
|
|
1777
1820
|
continue;
|
|
1778
1821
|
}
|
|
1779
1822
|
throw err;
|
|
@@ -1789,43 +1832,42 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
1789
1832
|
if (normalizedInput.length === 0) {
|
|
1790
1833
|
throw new Error("Path cannot be empty");
|
|
1791
1834
|
}
|
|
1792
|
-
const absolute =
|
|
1793
|
-
const relative =
|
|
1794
|
-
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);
|
|
1795
1838
|
if (isOutside) {
|
|
1796
1839
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
1797
1840
|
}
|
|
1798
|
-
return relative.split(
|
|
1841
|
+
return relative.split(path9.sep).join("/");
|
|
1799
1842
|
}
|
|
1800
1843
|
function projectRootFromGraph(yggRootPath) {
|
|
1801
|
-
return
|
|
1844
|
+
return path9.dirname(yggRootPath);
|
|
1802
1845
|
}
|
|
1803
1846
|
|
|
1804
1847
|
// src/core/graph-loader.ts
|
|
1805
1848
|
function toModelPath(absolutePath, modelDir) {
|
|
1806
|
-
return
|
|
1849
|
+
return path10.relative(modelDir, absolutePath).split(path10.sep).join("/");
|
|
1807
1850
|
}
|
|
1808
1851
|
var FALLBACK_CONFIG = {
|
|
1809
1852
|
name: "",
|
|
1810
|
-
node_types: {}
|
|
1811
|
-
artifacts: {}
|
|
1853
|
+
node_types: {}
|
|
1812
1854
|
};
|
|
1813
1855
|
async function loadGraph(projectRoot, options = {}) {
|
|
1814
1856
|
const yggRoot = await findYggRoot(projectRoot);
|
|
1815
1857
|
let configError;
|
|
1816
1858
|
let config = FALLBACK_CONFIG;
|
|
1817
1859
|
try {
|
|
1818
|
-
config = await parseConfig(
|
|
1860
|
+
config = await parseConfig(path10.join(yggRoot, "yg-config.yaml"));
|
|
1819
1861
|
} catch (error) {
|
|
1820
1862
|
if (!options.tolerateInvalidConfig) {
|
|
1821
1863
|
throw error;
|
|
1822
1864
|
}
|
|
1823
1865
|
configError = error.message;
|
|
1824
1866
|
}
|
|
1825
|
-
const modelDir =
|
|
1867
|
+
const modelDir = path10.join(yggRoot, "model");
|
|
1826
1868
|
const nodes = /* @__PURE__ */ new Map();
|
|
1827
1869
|
const nodeParseErrors = [];
|
|
1828
|
-
const artifactFilenames = Object.keys(
|
|
1870
|
+
const artifactFilenames = Object.keys(STANDARD_ARTIFACTS2);
|
|
1829
1871
|
try {
|
|
1830
1872
|
await scanModelDirectory(modelDir, modelDir, null, nodes, nodeParseErrors, artifactFilenames);
|
|
1831
1873
|
} catch (err) {
|
|
@@ -1836,9 +1878,9 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1836
1878
|
}
|
|
1837
1879
|
throw err;
|
|
1838
1880
|
}
|
|
1839
|
-
const aspects = await loadAspects(
|
|
1840
|
-
const flows = await loadFlows(
|
|
1841
|
-
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"));
|
|
1842
1884
|
return {
|
|
1843
1885
|
config,
|
|
1844
1886
|
configError,
|
|
@@ -1858,11 +1900,11 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1858
1900
|
}
|
|
1859
1901
|
if (hasNodeYaml) {
|
|
1860
1902
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1861
|
-
const nodeYamlPath =
|
|
1903
|
+
const nodeYamlPath = path10.join(dirPath, "yg-node.yaml");
|
|
1862
1904
|
let meta;
|
|
1863
1905
|
let nodeYamlRaw;
|
|
1864
1906
|
try {
|
|
1865
|
-
nodeYamlRaw = await
|
|
1907
|
+
nodeYamlRaw = await readFile12(nodeYamlPath, "utf-8");
|
|
1866
1908
|
meta = await parseNodeYaml(nodeYamlPath);
|
|
1867
1909
|
} catch (err) {
|
|
1868
1910
|
nodeParseErrors.push({
|
|
@@ -1888,7 +1930,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1888
1930
|
if (!entry.isDirectory()) continue;
|
|
1889
1931
|
if (entry.name.startsWith(".")) continue;
|
|
1890
1932
|
await scanModelDirectory(
|
|
1891
|
-
|
|
1933
|
+
path10.join(dirPath, entry.name),
|
|
1892
1934
|
modelDir,
|
|
1893
1935
|
node,
|
|
1894
1936
|
nodes,
|
|
@@ -1901,7 +1943,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1901
1943
|
if (!entry.isDirectory()) continue;
|
|
1902
1944
|
if (entry.name.startsWith(".")) continue;
|
|
1903
1945
|
await scanModelDirectory(
|
|
1904
|
-
|
|
1946
|
+
path10.join(dirPath, entry.name),
|
|
1905
1947
|
modelDir,
|
|
1906
1948
|
null,
|
|
1907
1949
|
nodes,
|
|
@@ -1924,15 +1966,15 @@ async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
|
1924
1966
|
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
1925
1967
|
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
|
|
1926
1968
|
if (hasAspectYaml) {
|
|
1927
|
-
const id =
|
|
1928
|
-
const aspectYamlPath =
|
|
1969
|
+
const id = path10.relative(aspectsRoot, dirPath).split(path10.sep).join("/");
|
|
1970
|
+
const aspectYamlPath = path10.join(dirPath, "yg-aspect.yaml");
|
|
1929
1971
|
const aspect = await parseAspect(dirPath, aspectYamlPath, id);
|
|
1930
1972
|
aspects.push(aspect);
|
|
1931
1973
|
}
|
|
1932
1974
|
for (const entry of entries) {
|
|
1933
1975
|
if (!entry.isDirectory()) continue;
|
|
1934
1976
|
if (entry.name.startsWith(".")) continue;
|
|
1935
|
-
await scanAspectsDirectory(
|
|
1977
|
+
await scanAspectsDirectory(path10.join(dirPath, entry.name), aspectsRoot, aspects);
|
|
1936
1978
|
}
|
|
1937
1979
|
}
|
|
1938
1980
|
async function loadFlows(flowsDir) {
|
|
@@ -1945,8 +1987,8 @@ async function loadFlows(flowsDir) {
|
|
|
1945
1987
|
const flows = [];
|
|
1946
1988
|
for (const entry of entries) {
|
|
1947
1989
|
if (!entry.isDirectory()) continue;
|
|
1948
|
-
const flowYamlPath =
|
|
1949
|
-
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);
|
|
1950
1992
|
flows.push(flow);
|
|
1951
1993
|
}
|
|
1952
1994
|
return flows;
|
|
@@ -1958,7 +2000,7 @@ async function loadSchemas(schemasDir) {
|
|
|
1958
2000
|
for (const entry of entries) {
|
|
1959
2001
|
if (!entry.isFile()) continue;
|
|
1960
2002
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1961
|
-
const s = await parseSchema(
|
|
2003
|
+
const s = await parseSchema(path10.join(schemasDir, entry.name));
|
|
1962
2004
|
schemas.push(s);
|
|
1963
2005
|
}
|
|
1964
2006
|
return schemas;
|
|
@@ -1968,8 +2010,8 @@ async function loadSchemas(schemasDir) {
|
|
|
1968
2010
|
}
|
|
1969
2011
|
|
|
1970
2012
|
// src/core/context-builder.ts
|
|
1971
|
-
import { readFile as
|
|
1972
|
-
import
|
|
2013
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2014
|
+
import path11 from "path";
|
|
1973
2015
|
|
|
1974
2016
|
// src/utils/tokens.ts
|
|
1975
2017
|
function estimateTokens(text) {
|
|
@@ -1980,48 +2022,53 @@ function estimateTokens(text) {
|
|
|
1980
2022
|
var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
1981
2023
|
var EVENT_RELATION_TYPES = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
1982
2024
|
var YG_YAML_FILES = /* @__PURE__ */ new Set(["yg-node.yaml", "yg-aspect.yaml", "yg-flow.yaml"]);
|
|
1983
|
-
async function buildContext(graph, nodePath) {
|
|
2025
|
+
async function buildContext(graph, nodePath, options) {
|
|
1984
2026
|
const node = graph.nodes.get(nodePath);
|
|
1985
2027
|
if (!node) {
|
|
1986
2028
|
throw new Error(`Node not found: ${nodePath}`);
|
|
1987
2029
|
}
|
|
2030
|
+
const selfOnly = options?.selfOnly ?? false;
|
|
1988
2031
|
const layers = [];
|
|
1989
2032
|
layers.push(buildGlobalLayer(graph.config));
|
|
1990
2033
|
const ancestors = collectAncestors(node);
|
|
1991
|
-
|
|
1992
|
-
|
|
2034
|
+
if (!selfOnly) {
|
|
2035
|
+
for (const ancestor of ancestors) {
|
|
2036
|
+
layers.push(buildHierarchyLayer(ancestor, graph.config, graph));
|
|
2037
|
+
}
|
|
1993
2038
|
}
|
|
1994
2039
|
layers.push(await buildOwnLayer(node, graph.config, graph.rootPath, graph));
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
const
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
+
}
|
|
2000
2053
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
layers.push(buildStructuralRelationLayer(target, relation, graph.config));
|
|
2004
|
-
} else if (EVENT_RELATION_TYPES.has(relation.type)) {
|
|
2005
|
-
layers.push(buildEventRelationLayer(target, relation));
|
|
2054
|
+
for (const flow of collectParticipatingFlows(graph, node)) {
|
|
2055
|
+
layers.push(buildFlowLayer(flow, graph));
|
|
2006
2056
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
if (aspects) {
|
|
2015
|
-
for (const id of aspects.split(",").map((t) => t.trim()).filter(Boolean)) {
|
|
2016
|
-
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
|
+
}
|
|
2017
2064
|
}
|
|
2018
2065
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
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
|
+
}
|
|
2025
2072
|
}
|
|
2026
2073
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
2027
2074
|
const tokenCount = estimateTokens(fullText);
|
|
@@ -2082,12 +2129,12 @@ function buildGlobalLayer(config) {
|
|
|
2082
2129
|
`;
|
|
2083
2130
|
return { type: "global", label: "Global Context", content };
|
|
2084
2131
|
}
|
|
2085
|
-
function
|
|
2086
|
-
const allowed = new Set(Object.keys(
|
|
2132
|
+
function filterByStandardArtifacts(artifacts) {
|
|
2133
|
+
const allowed = new Set(Object.keys(STANDARD_ARTIFACTS2));
|
|
2087
2134
|
return artifacts.filter((a) => allowed.has(a.filename));
|
|
2088
2135
|
}
|
|
2089
2136
|
function buildHierarchyLayer(ancestor, config, graph) {
|
|
2090
|
-
const filtered =
|
|
2137
|
+
const filtered = filterByStandardArtifacts(ancestor.artifacts);
|
|
2091
2138
|
const content = filtered.map((a) => `### ${a.filename}
|
|
2092
2139
|
${a.content}`).join("\n\n");
|
|
2093
2140
|
const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
|
|
@@ -2106,9 +2153,9 @@ async function buildOwnLayer(node, config, graphRootPath, graph) {
|
|
|
2106
2153
|
parts.push(`### yg-node.yaml
|
|
2107
2154
|
${node.nodeYamlRaw.trim()}`);
|
|
2108
2155
|
} else {
|
|
2109
|
-
const nodeYamlPath =
|
|
2156
|
+
const nodeYamlPath = path11.join(graphRootPath, "model", node.path, "yg-node.yaml");
|
|
2110
2157
|
try {
|
|
2111
|
-
const nodeYamlContent = await
|
|
2158
|
+
const nodeYamlContent = await readFile13(nodeYamlPath, "utf-8");
|
|
2112
2159
|
parts.push(`### yg-node.yaml
|
|
2113
2160
|
${nodeYamlContent.trim()}`);
|
|
2114
2161
|
} catch {
|
|
@@ -2116,7 +2163,7 @@ ${nodeYamlContent.trim()}`);
|
|
|
2116
2163
|
(not found)`);
|
|
2117
2164
|
}
|
|
2118
2165
|
}
|
|
2119
|
-
const filtered =
|
|
2166
|
+
const filtered = filterByStandardArtifacts(node.artifacts);
|
|
2120
2167
|
for (const a of filtered) {
|
|
2121
2168
|
parts.push(`### ${a.filename}
|
|
2122
2169
|
${a.content}`);
|
|
@@ -2132,7 +2179,7 @@ ${a.content}`);
|
|
|
2132
2179
|
attrs
|
|
2133
2180
|
};
|
|
2134
2181
|
}
|
|
2135
|
-
function buildStructuralRelationLayer(target, relation
|
|
2182
|
+
function buildStructuralRelationLayer(target, relation) {
|
|
2136
2183
|
let content = "";
|
|
2137
2184
|
if (relation.consumes?.length) {
|
|
2138
2185
|
content += `Consumes: ${relation.consumes.join(", ")}
|
|
@@ -2144,7 +2191,7 @@ function buildStructuralRelationLayer(target, relation, config) {
|
|
|
2144
2191
|
|
|
2145
2192
|
`;
|
|
2146
2193
|
}
|
|
2147
|
-
const structuralArtifactFilenames = Object.entries(
|
|
2194
|
+
const structuralArtifactFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
2148
2195
|
const structuralArts = structuralArtifactFilenames.map((filename) => {
|
|
2149
2196
|
const art = target.artifacts.find((a) => a.filename === filename);
|
|
2150
2197
|
return art ? { filename: art.filename, content: art.content } : null;
|
|
@@ -2153,7 +2200,7 @@ function buildStructuralRelationLayer(target, relation, config) {
|
|
|
2153
2200
|
content += structuralArts.map((a) => `### ${a.filename}
|
|
2154
2201
|
${a.content}`).join("\n\n");
|
|
2155
2202
|
} else {
|
|
2156
|
-
const filtered =
|
|
2203
|
+
const filtered = filterByStandardArtifacts(target.artifacts);
|
|
2157
2204
|
content += filtered.map((a) => `### ${a.filename}
|
|
2158
2205
|
${a.content}`).join("\n\n");
|
|
2159
2206
|
}
|
|
@@ -2258,8 +2305,8 @@ function collectAncestors(node) {
|
|
|
2258
2305
|
}
|
|
2259
2306
|
function collectDependencyAncestors(target, config, graph) {
|
|
2260
2307
|
const ancestors = collectAncestors(target);
|
|
2261
|
-
const structuralFilenames = Object.entries(
|
|
2262
|
-
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)];
|
|
2263
2310
|
return ancestors.map((ancestor) => {
|
|
2264
2311
|
const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
|
|
2265
2312
|
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
@@ -2327,7 +2374,7 @@ function computeBudgetBreakdown(pkg2, graph) {
|
|
|
2327
2374
|
const total = own + hierarchy + aspects + flows + dependencies;
|
|
2328
2375
|
return { own, hierarchy, aspects, flows, dependencies, total };
|
|
2329
2376
|
}
|
|
2330
|
-
function toContextMapOutput(pkg2, graph) {
|
|
2377
|
+
function toContextMapOutput(pkg2, graph, options) {
|
|
2331
2378
|
const node = graph.nodes.get(pkg2.nodePath);
|
|
2332
2379
|
const config = graph.config;
|
|
2333
2380
|
const nodeAspects = (node.meta.aspects ?? []).map((entry) => {
|
|
@@ -2336,48 +2383,51 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2336
2383
|
if (entry.exceptions?.length) ref.exceptions = entry.exceptions;
|
|
2337
2384
|
return ref;
|
|
2338
2385
|
});
|
|
2339
|
-
const
|
|
2386
|
+
const selfOnly = options?.selfOnly ?? false;
|
|
2387
|
+
const participatingFlows = selfOnly ? [] : collectParticipatingFlows(graph, node);
|
|
2340
2388
|
const flowRefs = participatingFlows.map((f) => {
|
|
2341
2389
|
const ref = { path: f.path };
|
|
2342
2390
|
if (f.aspects?.length) ref.aspects = f.aspects;
|
|
2343
2391
|
return ref;
|
|
2344
2392
|
});
|
|
2345
2393
|
const ancestors = collectAncestors(node);
|
|
2346
|
-
const hierarchyRefs = ancestors.map((a) => {
|
|
2394
|
+
const hierarchyRefs = selfOnly ? [] : ancestors.map((a) => {
|
|
2347
2395
|
const nodeAspectIds = (a.meta.aspects ?? []).map((e) => e.aspect);
|
|
2348
2396
|
const expanded = expandAspects(nodeAspectIds, graph.aspects);
|
|
2349
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}`) };
|
|
2350
2398
|
});
|
|
2351
|
-
const ancestorPaths = new Set(ancestors.map((a) => a.path));
|
|
2352
2399
|
const depRefs = [];
|
|
2353
|
-
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
+
}
|
|
2379
2429
|
}
|
|
2380
|
-
const glossary = buildGlossary(node, depRefs, graph);
|
|
2430
|
+
const glossary = selfOnly ? { aspects: {}, flows: {} } : buildGlossary(node, depRefs, graph);
|
|
2381
2431
|
const breakdown = computeBudgetBreakdown(pkg2, graph);
|
|
2382
2432
|
const warningThreshold = config.quality?.context_budget?.warning ?? 1e4;
|
|
2383
2433
|
const errorThreshold = config.quality?.context_budget?.error ?? 2e4;
|
|
@@ -2400,13 +2450,13 @@ function toContextMapOutput(pkg2, graph) {
|
|
|
2400
2450
|
glossary
|
|
2401
2451
|
};
|
|
2402
2452
|
}
|
|
2403
|
-
function buildNodeFiles(node,
|
|
2404
|
-
const configKeys = Object.keys(
|
|
2453
|
+
function buildNodeFiles(node, _config, prefix) {
|
|
2454
|
+
const configKeys = Object.keys(STANDARD_ARTIFACTS2);
|
|
2405
2455
|
return configKeys.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
|
|
2406
2456
|
}
|
|
2407
|
-
function buildDepNodeFiles(node,
|
|
2408
|
-
const structural = Object.entries(
|
|
2409
|
-
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);
|
|
2410
2460
|
return filenames.filter((f) => !YG_YAML_FILES.has(f) && node.artifacts.some((a) => a.filename === f)).map((f) => `${prefix}/${f}`);
|
|
2411
2461
|
}
|
|
2412
2462
|
function buildGlossary(node, dependencies, graph) {
|
|
@@ -2511,8 +2561,8 @@ ${file.content}
|
|
|
2511
2561
|
}
|
|
2512
2562
|
|
|
2513
2563
|
// src/core/validator.ts
|
|
2514
|
-
import { readdir as readdir5, readFile as
|
|
2515
|
-
import
|
|
2564
|
+
import { readdir as readdir5, readFile as readFile14, stat as stat4 } from "fs/promises";
|
|
2565
|
+
import path12 from "path";
|
|
2516
2566
|
function getAspectIds(aspects) {
|
|
2517
2567
|
return (aspects ?? []).map((a) => a.aspect);
|
|
2518
2568
|
}
|
|
@@ -2546,7 +2596,6 @@ async function validate(graph, scope = "all") {
|
|
|
2546
2596
|
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
2547
2597
|
issues.push(...await checkAnchorPresence(graph));
|
|
2548
2598
|
issues.push(...checkRequiredArtifacts(graph));
|
|
2549
|
-
issues.push(...checkInvalidArtifactConditions(graph));
|
|
2550
2599
|
issues.push(...await checkContextBudget(graph));
|
|
2551
2600
|
issues.push(...checkHighFanOut(graph));
|
|
2552
2601
|
issues.push(...checkMissingDescriptions(graph));
|
|
@@ -2879,12 +2928,12 @@ function checkMappingOverlap(graph) {
|
|
|
2879
2928
|
}
|
|
2880
2929
|
async function checkMappingPathsExist(graph) {
|
|
2881
2930
|
const issues = [];
|
|
2882
|
-
const projectRoot =
|
|
2931
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
2883
2932
|
const { access: access4 } = await import("fs/promises");
|
|
2884
2933
|
for (const [nodePath, node] of graph.nodes) {
|
|
2885
2934
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2886
2935
|
for (const mp of mappingPaths) {
|
|
2887
|
-
const absPath =
|
|
2936
|
+
const absPath = path12.join(projectRoot, mp);
|
|
2888
2937
|
try {
|
|
2889
2938
|
await access4(absPath);
|
|
2890
2939
|
} catch {
|
|
@@ -2919,15 +2968,6 @@ function artifactRequiredReason(graph, nodePath, node, required) {
|
|
|
2919
2968
|
const sources = getIncomingRelationSources(graph, nodePath);
|
|
2920
2969
|
return sources.length > 0 ? `${sources.length} incoming relation(s): ${sources.join(", ")}` : null;
|
|
2921
2970
|
}
|
|
2922
|
-
if (when === "has_outgoing_relations") {
|
|
2923
|
-
const count = node.meta.relations?.length ?? 0;
|
|
2924
|
-
return count > 0 ? `${count} outgoing relation(s)` : null;
|
|
2925
|
-
}
|
|
2926
|
-
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
2927
|
-
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
2928
|
-
const aspectId = when.slice(prefix.length);
|
|
2929
|
-
return (node.meta.aspects ?? []).some((a) => a.aspect === aspectId) ? `node has aspect '${aspectId}'` : null;
|
|
2930
|
-
}
|
|
2931
2971
|
return null;
|
|
2932
2972
|
}
|
|
2933
2973
|
function getIncomingRelations(graph, nodePath) {
|
|
@@ -2944,7 +2984,7 @@ function getIncomingRelations(graph, nodePath) {
|
|
|
2944
2984
|
}
|
|
2945
2985
|
function checkRequiredArtifacts(graph) {
|
|
2946
2986
|
const issues = [];
|
|
2947
|
-
const artifacts =
|
|
2987
|
+
const artifacts = STANDARD_ARTIFACTS2;
|
|
2948
2988
|
for (const [nodePath, node] of graph.nodes) {
|
|
2949
2989
|
for (const [filename, config] of Object.entries(artifacts)) {
|
|
2950
2990
|
const hasArtifact = node.artifacts.some((a) => a.filename === filename);
|
|
@@ -3000,30 +3040,6 @@ function checkFlowAspectIds(graph) {
|
|
|
3000
3040
|
}
|
|
3001
3041
|
return issues;
|
|
3002
3042
|
}
|
|
3003
|
-
function checkInvalidArtifactConditions(graph) {
|
|
3004
|
-
const issues = [];
|
|
3005
|
-
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
3006
|
-
const artifacts = graph.config.artifacts ?? {};
|
|
3007
|
-
for (const [artifactName, config] of Object.entries(artifacts)) {
|
|
3008
|
-
const required = config.required;
|
|
3009
|
-
if (typeof required === "object" && required && "when" in required) {
|
|
3010
|
-
const when = required.when;
|
|
3011
|
-
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
3012
|
-
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
3013
|
-
const aspectId = when.slice(prefix.length);
|
|
3014
|
-
if (!validAspectIds.has(aspectId)) {
|
|
3015
|
-
issues.push({
|
|
3016
|
-
severity: "error",
|
|
3017
|
-
code: "E013",
|
|
3018
|
-
rule: "invalid-artifact-condition",
|
|
3019
|
-
message: `Artifact '${artifactName}' condition has_aspect:${aspectId} has no corresponding aspect in aspects/`
|
|
3020
|
-
});
|
|
3021
|
-
}
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
return issues;
|
|
3026
|
-
}
|
|
3027
3043
|
async function checkShallowArtifacts(graph) {
|
|
3028
3044
|
const issues = [];
|
|
3029
3045
|
const minLen = graph.config.quality?.min_artifact_length ?? 50;
|
|
@@ -3045,7 +3061,7 @@ async function checkShallowArtifacts(graph) {
|
|
|
3045
3061
|
async function checkWideNodes(graph) {
|
|
3046
3062
|
const issues = [];
|
|
3047
3063
|
const maxFiles = graph.config.quality?.max_mapping_source_files ?? 10;
|
|
3048
|
-
const projectRoot =
|
|
3064
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
3049
3065
|
for (const [nodePath, node] of graph.nodes) {
|
|
3050
3066
|
if (node.meta.blackbox) continue;
|
|
3051
3067
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
@@ -3148,11 +3164,11 @@ function checkSchemas(graph) {
|
|
|
3148
3164
|
}
|
|
3149
3165
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
3150
3166
|
const issues = [];
|
|
3151
|
-
const modelDir =
|
|
3167
|
+
const modelDir = path12.join(graph.rootPath, "model");
|
|
3152
3168
|
async function scanDir(dirPath, segments) {
|
|
3153
3169
|
const entries = await readdir5(dirPath, { withFileTypes: true });
|
|
3154
3170
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
3155
|
-
const dirName =
|
|
3171
|
+
const dirName = path12.basename(dirPath);
|
|
3156
3172
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
3157
3173
|
const hasFiles = entries.some((e) => e.isFile());
|
|
3158
3174
|
const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
|
|
@@ -3180,7 +3196,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
3180
3196
|
if (!entry.isDirectory()) continue;
|
|
3181
3197
|
if (RESERVED_DIRS.has(entry.name)) continue;
|
|
3182
3198
|
if (entry.name.startsWith(".")) continue;
|
|
3183
|
-
await scanDir(
|
|
3199
|
+
await scanDir(path12.join(dirPath, entry.name), [...segments, entry.name]);
|
|
3184
3200
|
}
|
|
3185
3201
|
}
|
|
3186
3202
|
try {
|
|
@@ -3188,7 +3204,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
3188
3204
|
for (const entry of rootEntries) {
|
|
3189
3205
|
if (!entry.isDirectory()) continue;
|
|
3190
3206
|
if (entry.name.startsWith(".")) continue;
|
|
3191
|
-
await scanDir(
|
|
3207
|
+
await scanDir(path12.join(modelDir, entry.name), [entry.name]);
|
|
3192
3208
|
}
|
|
3193
3209
|
} catch {
|
|
3194
3210
|
}
|
|
@@ -3205,7 +3221,7 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
3205
3221
|
const entries = await readdir5(absPath, { withFileTypes: true });
|
|
3206
3222
|
for (const entry of entries) {
|
|
3207
3223
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3208
|
-
const entryPath =
|
|
3224
|
+
const entryPath = path12.join(absPath, entry.name);
|
|
3209
3225
|
if (entry.isFile()) {
|
|
3210
3226
|
files.push(entryPath);
|
|
3211
3227
|
} else if (entry.isDirectory()) {
|
|
@@ -3217,13 +3233,13 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
3217
3233
|
}
|
|
3218
3234
|
}
|
|
3219
3235
|
for (const mp of mappingPaths) {
|
|
3220
|
-
await collectFiles(
|
|
3236
|
+
await collectFiles(path12.join(projectRoot, mp));
|
|
3221
3237
|
}
|
|
3222
3238
|
return files;
|
|
3223
3239
|
}
|
|
3224
3240
|
async function checkAnchorPresence(graph) {
|
|
3225
3241
|
const issues = [];
|
|
3226
|
-
const projectRoot =
|
|
3242
|
+
const projectRoot = path12.dirname(graph.rootPath);
|
|
3227
3243
|
for (const [nodePath, node] of graph.nodes) {
|
|
3228
3244
|
const aspectsWithAnchors = (node.meta.aspects ?? []).filter((a) => a.anchors && a.anchors.length > 0);
|
|
3229
3245
|
if (aspectsWithAnchors.length === 0) continue;
|
|
@@ -3234,7 +3250,7 @@ async function checkAnchorPresence(graph) {
|
|
|
3234
3250
|
const fileContents = [];
|
|
3235
3251
|
for (const filePath of sourceFiles) {
|
|
3236
3252
|
try {
|
|
3237
|
-
const content = await
|
|
3253
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3238
3254
|
fileContents.push(content);
|
|
3239
3255
|
} catch {
|
|
3240
3256
|
}
|
|
@@ -3343,7 +3359,7 @@ function checkMissingDescriptions(graph) {
|
|
|
3343
3359
|
}
|
|
3344
3360
|
|
|
3345
3361
|
// src/cli/owner.ts
|
|
3346
|
-
import
|
|
3362
|
+
import path13 from "path";
|
|
3347
3363
|
import { access as access2 } from "fs/promises";
|
|
3348
3364
|
function normalizeForMatch(inputPath) {
|
|
3349
3365
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -3373,11 +3389,11 @@ function registerOwnerCommand(program2) {
|
|
|
3373
3389
|
const graph = await loadGraph(cwd);
|
|
3374
3390
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3375
3391
|
const rawPath = options.file.trim();
|
|
3376
|
-
const absolute =
|
|
3377
|
-
const repoRelative =
|
|
3392
|
+
const absolute = path13.resolve(cwd, rawPath);
|
|
3393
|
+
const repoRelative = path13.relative(repoRoot, absolute).split(path13.sep).join("/");
|
|
3378
3394
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3379
3395
|
if (!result.nodePath) {
|
|
3380
|
-
const absPath =
|
|
3396
|
+
const absPath = path13.resolve(repoRoot, result.file);
|
|
3381
3397
|
let exists = true;
|
|
3382
3398
|
try {
|
|
3383
3399
|
await access2(absPath);
|
|
@@ -3430,7 +3446,7 @@ function collectRelevantNodePaths(graph, nodePath) {
|
|
|
3430
3446
|
return relevant;
|
|
3431
3447
|
}
|
|
3432
3448
|
function registerBuildCommand(program2) {
|
|
3433
|
-
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) => {
|
|
3434
3450
|
try {
|
|
3435
3451
|
if (!options.node && !options.file) {
|
|
3436
3452
|
process.stderr.write("Error: either '--node <path>' or '--file <path>' is required\n");
|
|
@@ -3478,8 +3494,8 @@ function registerBuildCommand(program2) {
|
|
|
3478
3494
|
process.stderr.write(msg);
|
|
3479
3495
|
process.exit(1);
|
|
3480
3496
|
}
|
|
3481
|
-
const pkg2 = await buildContext(graph, nodePath);
|
|
3482
|
-
const mapOutput = toContextMapOutput(pkg2, graph);
|
|
3497
|
+
const pkg2 = await buildContext(graph, nodePath, { selfOnly: options.self });
|
|
3498
|
+
const mapOutput = toContextMapOutput(pkg2, graph, { selfOnly: options.self });
|
|
3483
3499
|
let output = formatContextYaml(mapOutput);
|
|
3484
3500
|
if (options.full) {
|
|
3485
3501
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3601,12 +3617,12 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
3601
3617
|
import chalk2 from "chalk";
|
|
3602
3618
|
|
|
3603
3619
|
// src/io/drift-state-store.ts
|
|
3604
|
-
import { readFile as
|
|
3605
|
-
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";
|
|
3606
3622
|
import { parse as yamlParse } from "yaml";
|
|
3607
3623
|
var DRIFT_STATE_DIR = ".drift-state";
|
|
3608
3624
|
function nodeStatePath(yggRoot, nodePath) {
|
|
3609
|
-
return
|
|
3625
|
+
return path14.join(yggRoot, DRIFT_STATE_DIR, `${nodePath}.json`);
|
|
3610
3626
|
}
|
|
3611
3627
|
async function scanJsonFiles(dir, baseDir) {
|
|
3612
3628
|
const results = [];
|
|
@@ -3617,12 +3633,12 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3617
3633
|
return results;
|
|
3618
3634
|
}
|
|
3619
3635
|
for (const entry of entries) {
|
|
3620
|
-
const fullPath =
|
|
3636
|
+
const fullPath = path14.join(dir, entry.name);
|
|
3621
3637
|
if (entry.isDirectory()) {
|
|
3622
3638
|
const nested = await scanJsonFiles(fullPath, baseDir);
|
|
3623
3639
|
results.push(...nested);
|
|
3624
3640
|
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
3625
|
-
const relPath =
|
|
3641
|
+
const relPath = path14.relative(baseDir, fullPath);
|
|
3626
3642
|
const nodePath = relPath.replace(/\\/g, "/").replace(/\.json$/, "");
|
|
3627
3643
|
results.push(nodePath);
|
|
3628
3644
|
}
|
|
@@ -3630,13 +3646,13 @@ async function scanJsonFiles(dir, baseDir) {
|
|
|
3630
3646
|
return results;
|
|
3631
3647
|
}
|
|
3632
3648
|
async function removeEmptyParents(filePath, stopDir) {
|
|
3633
|
-
let dir =
|
|
3649
|
+
let dir = path14.dirname(filePath);
|
|
3634
3650
|
while (dir !== stopDir && dir.startsWith(stopDir)) {
|
|
3635
3651
|
try {
|
|
3636
3652
|
const entries = await readdir6(dir);
|
|
3637
3653
|
if (entries.length === 0) {
|
|
3638
3654
|
await rm2(dir, { recursive: true });
|
|
3639
|
-
dir =
|
|
3655
|
+
dir = path14.dirname(dir);
|
|
3640
3656
|
} else {
|
|
3641
3657
|
break;
|
|
3642
3658
|
}
|
|
@@ -3648,7 +3664,7 @@ async function removeEmptyParents(filePath, stopDir) {
|
|
|
3648
3664
|
async function readNodeDriftState(yggRoot, nodePath) {
|
|
3649
3665
|
try {
|
|
3650
3666
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3651
|
-
const content = await
|
|
3667
|
+
const content = await readFile15(filePath, "utf-8");
|
|
3652
3668
|
const parsed = JSON.parse(content);
|
|
3653
3669
|
return parsed;
|
|
3654
3670
|
} catch {
|
|
@@ -3657,12 +3673,12 @@ async function readNodeDriftState(yggRoot, nodePath) {
|
|
|
3657
3673
|
}
|
|
3658
3674
|
async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
3659
3675
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
3660
|
-
await mkdir3(
|
|
3676
|
+
await mkdir3(path14.dirname(filePath), { recursive: true });
|
|
3661
3677
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
3662
|
-
await
|
|
3678
|
+
await writeFile6(filePath, content, "utf-8");
|
|
3663
3679
|
}
|
|
3664
3680
|
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
3665
|
-
const driftDir =
|
|
3681
|
+
const driftDir = path14.join(yggRoot, DRIFT_STATE_DIR);
|
|
3666
3682
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
3667
3683
|
const removed = [];
|
|
3668
3684
|
for (const nodePath of allNodePaths) {
|
|
@@ -3676,7 +3692,7 @@ async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
|
3676
3692
|
return removed.sort();
|
|
3677
3693
|
}
|
|
3678
3694
|
async function readDriftState(yggRoot) {
|
|
3679
|
-
const driftPath =
|
|
3695
|
+
const driftPath = path14.join(yggRoot, DRIFT_STATE_DIR);
|
|
3680
3696
|
let driftStat;
|
|
3681
3697
|
try {
|
|
3682
3698
|
driftStat = await stat5(driftPath);
|
|
@@ -3684,7 +3700,7 @@ async function readDriftState(yggRoot) {
|
|
|
3684
3700
|
return {};
|
|
3685
3701
|
}
|
|
3686
3702
|
if (driftStat.isFile()) {
|
|
3687
|
-
const content = await
|
|
3703
|
+
const content = await readFile15(driftPath, "utf-8");
|
|
3688
3704
|
let raw;
|
|
3689
3705
|
try {
|
|
3690
3706
|
raw = JSON.parse(content);
|
|
@@ -3716,20 +3732,20 @@ async function readDriftState(yggRoot) {
|
|
|
3716
3732
|
}
|
|
3717
3733
|
|
|
3718
3734
|
// src/utils/hash.ts
|
|
3719
|
-
import { readFile as
|
|
3720
|
-
import
|
|
3735
|
+
import { readFile as readFile16, readdir as readdir7, stat as stat6 } from "fs/promises";
|
|
3736
|
+
import path15 from "path";
|
|
3721
3737
|
import { createHash } from "crypto";
|
|
3722
3738
|
import { createRequire } from "module";
|
|
3723
3739
|
var require2 = createRequire(import.meta.url);
|
|
3724
3740
|
var ignoreFactory = require2("ignore");
|
|
3725
3741
|
async function hashFile(filePath) {
|
|
3726
|
-
const content = await
|
|
3742
|
+
const content = await readFile16(filePath);
|
|
3727
3743
|
return createHash("sha256").update(content).digest("hex");
|
|
3728
3744
|
}
|
|
3729
3745
|
async function loadRootGitignoreStack(projectRoot) {
|
|
3730
3746
|
if (!projectRoot) return [];
|
|
3731
3747
|
try {
|
|
3732
|
-
const content = await
|
|
3748
|
+
const content = await readFile16(path15.join(projectRoot, ".gitignore"), "utf-8");
|
|
3733
3749
|
const matcher = ignoreFactory();
|
|
3734
3750
|
matcher.add(content);
|
|
3735
3751
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -3739,7 +3755,7 @@ async function loadRootGitignoreStack(projectRoot) {
|
|
|
3739
3755
|
}
|
|
3740
3756
|
function isIgnoredByStack(candidatePath, stack) {
|
|
3741
3757
|
for (const { basePath, matcher } of stack) {
|
|
3742
|
-
const relativePath =
|
|
3758
|
+
const relativePath = path15.relative(basePath, candidatePath);
|
|
3743
3759
|
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
3744
3760
|
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
3745
3761
|
}
|
|
@@ -3754,7 +3770,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3754
3770
|
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
3755
3771
|
const allFiles = [];
|
|
3756
3772
|
for (const tf of trackedFiles) {
|
|
3757
|
-
const absPath =
|
|
3773
|
+
const absPath = path15.join(projectRoot, tf.path);
|
|
3758
3774
|
try {
|
|
3759
3775
|
const st = await stat6(absPath);
|
|
3760
3776
|
if (st.isDirectory()) {
|
|
@@ -3764,7 +3780,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3764
3780
|
});
|
|
3765
3781
|
for (const entry of dirEntries) {
|
|
3766
3782
|
allFiles.push({
|
|
3767
|
-
relPath:
|
|
3783
|
+
relPath: path15.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
3768
3784
|
absPath: entry.absPath,
|
|
3769
3785
|
mtimeMs: entry.mtimeMs
|
|
3770
3786
|
});
|
|
@@ -3804,7 +3820,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
3804
3820
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
3805
3821
|
let stack = options.gitignoreStack ?? [];
|
|
3806
3822
|
try {
|
|
3807
|
-
const localContent = await
|
|
3823
|
+
const localContent = await readFile16(path15.join(directoryPath, ".gitignore"), "utf-8");
|
|
3808
3824
|
const localMatcher = ignoreFactory();
|
|
3809
3825
|
localMatcher.add(localContent);
|
|
3810
3826
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
@@ -3814,7 +3830,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3814
3830
|
const dirs = [];
|
|
3815
3831
|
const files = [];
|
|
3816
3832
|
for (const entry of entries) {
|
|
3817
|
-
const absoluteChildPath =
|
|
3833
|
+
const absoluteChildPath = path15.join(directoryPath, entry.name);
|
|
3818
3834
|
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
3819
3835
|
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
3820
3836
|
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
@@ -3827,7 +3843,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3827
3843
|
Promise.all(files.map(async (f) => {
|
|
3828
3844
|
const fileStat = await stat6(f);
|
|
3829
3845
|
return {
|
|
3830
|
-
relPath:
|
|
3846
|
+
relPath: path15.relative(rootDirectoryPath, f),
|
|
3831
3847
|
absPath: f,
|
|
3832
3848
|
mtimeMs: fileStat.mtimeMs
|
|
3833
3849
|
};
|
|
@@ -3840,15 +3856,15 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
3840
3856
|
}
|
|
3841
3857
|
|
|
3842
3858
|
// src/core/context-files.ts
|
|
3843
|
-
import
|
|
3859
|
+
import path16 from "path";
|
|
3844
3860
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3845
3861
|
function collectTrackedFiles(node, graph) {
|
|
3846
3862
|
const seen = /* @__PURE__ */ new Set();
|
|
3847
3863
|
const result = [];
|
|
3848
|
-
const projectRoot =
|
|
3849
|
-
const yggPrefix =
|
|
3850
|
-
const yggPrefixNormalized = yggPrefix.split(
|
|
3851
|
-
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));
|
|
3852
3868
|
function addFile(filePath, category) {
|
|
3853
3869
|
if (seen.has(filePath)) return;
|
|
3854
3870
|
seen.add(filePath);
|
|
@@ -3896,7 +3912,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
3896
3912
|
if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
|
|
3897
3913
|
const target = graph.nodes.get(relation.target);
|
|
3898
3914
|
if (!target) continue;
|
|
3899
|
-
const structuralFilenames = Object.entries(
|
|
3915
|
+
const structuralFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
3900
3916
|
const structuralArts = structuralFilenames.filter(
|
|
3901
3917
|
(filename) => target.artifacts.some((a) => a.filename === filename)
|
|
3902
3918
|
);
|
|
@@ -3925,7 +3941,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
3925
3941
|
if (relation.type !== "emits" && relation.type !== "listens") continue;
|
|
3926
3942
|
const target = graph.nodes.get(relation.target);
|
|
3927
3943
|
if (!target) continue;
|
|
3928
|
-
const structuralFilenames = Object.entries(
|
|
3944
|
+
const structuralFilenames = Object.entries(STANDARD_ARTIFACTS2).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
3929
3945
|
const filterFilenames = structuralFilenames.length > 0 ? structuralFilenames : [...configArtifactKeys];
|
|
3930
3946
|
for (const filename of filterFilenames) {
|
|
3931
3947
|
if (target.artifacts.some((a) => a.filename === filename)) {
|
|
@@ -3960,7 +3976,7 @@ function collectParticipatingFlows2(graph, node, ancestors) {
|
|
|
3960
3976
|
|
|
3961
3977
|
// src/core/drift-detector.ts
|
|
3962
3978
|
import { access as access3 } from "fs/promises";
|
|
3963
|
-
import
|
|
3979
|
+
import path17 from "path";
|
|
3964
3980
|
function getChildMappingExclusions(graph, nodePath) {
|
|
3965
3981
|
const node = graph.nodes.get(nodePath);
|
|
3966
3982
|
if (!node) return [];
|
|
@@ -3982,7 +3998,7 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
3982
3998
|
return exclusions;
|
|
3983
3999
|
}
|
|
3984
4000
|
async function detectDrift(graph, filterNodePath) {
|
|
3985
|
-
const projectRoot =
|
|
4001
|
+
const projectRoot = path17.dirname(graph.rootPath);
|
|
3986
4002
|
const driftState = await readDriftState(graph.rootPath);
|
|
3987
4003
|
const entries = [];
|
|
3988
4004
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -4065,14 +4081,14 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
4065
4081
|
};
|
|
4066
4082
|
}
|
|
4067
4083
|
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
4068
|
-
const yggPrefix =
|
|
4069
|
-
const normalizedPrefix = yggPrefix.split(
|
|
4084
|
+
const yggPrefix = path17.relative(projectRoot, _rootPath);
|
|
4085
|
+
const normalizedPrefix = yggPrefix.split(path17.sep).join("/");
|
|
4070
4086
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
4071
4087
|
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
4072
4088
|
}
|
|
4073
4089
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
4074
4090
|
for (const mp of mappingPaths) {
|
|
4075
|
-
const absPath =
|
|
4091
|
+
const absPath = path17.join(projectRoot, mp);
|
|
4076
4092
|
try {
|
|
4077
4093
|
await access3(absPath);
|
|
4078
4094
|
return false;
|
|
@@ -4082,7 +4098,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
4082
4098
|
return true;
|
|
4083
4099
|
}
|
|
4084
4100
|
async function syncDriftState(graph, nodePath) {
|
|
4085
|
-
const projectRoot =
|
|
4101
|
+
const projectRoot = path17.dirname(graph.rootPath);
|
|
4086
4102
|
const node = graph.nodes.get(nodePath);
|
|
4087
4103
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
4088
4104
|
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
@@ -4097,7 +4113,7 @@ async function syncDriftState(graph, nodePath) {
|
|
|
4097
4113
|
if (previousHash && previousHash !== canonicalHash && existingEntry?.files) {
|
|
4098
4114
|
let hasSourceChange = false;
|
|
4099
4115
|
let hasGraphChange = false;
|
|
4100
|
-
const yggPrefix =
|
|
4116
|
+
const yggPrefix = path17.relative(projectRoot, graph.rootPath).split(path17.sep).join("/");
|
|
4101
4117
|
for (const [filePath, hash] of Object.entries(fileHashes)) {
|
|
4102
4118
|
const storedHash = existingEntry.files[filePath];
|
|
4103
4119
|
if (storedHash && storedHash === hash) continue;
|
|
@@ -4363,7 +4379,7 @@ function registerStatusCommand(program2) {
|
|
|
4363
4379
|
const warningCount = validation.issues.filter(
|
|
4364
4380
|
(issue) => issue.severity === "warning"
|
|
4365
4381
|
).length;
|
|
4366
|
-
const configuredArtifactTypes = Object.keys(
|
|
4382
|
+
const configuredArtifactTypes = Object.keys(STANDARD_ARTIFACTS2);
|
|
4367
4383
|
const totalSlots = graph.nodes.size * configuredArtifactTypes.length;
|
|
4368
4384
|
let filledSlots = 0;
|
|
4369
4385
|
let mappedNodeCount = 0;
|
|
@@ -4437,10 +4453,10 @@ function registerTreeCommand(program2) {
|
|
|
4437
4453
|
let roots;
|
|
4438
4454
|
let showProjectName;
|
|
4439
4455
|
if (options.root?.trim()) {
|
|
4440
|
-
const
|
|
4441
|
-
const node = graph.nodes.get(
|
|
4456
|
+
const path20 = options.root.trim().replace(/\/$/, "");
|
|
4457
|
+
const node = graph.nodes.get(path20);
|
|
4442
4458
|
if (!node) {
|
|
4443
|
-
process.stderr.write(`Error: path '${
|
|
4459
|
+
process.stderr.write(`Error: path '${path20}' not found
|
|
4444
4460
|
`);
|
|
4445
4461
|
process.exit(1);
|
|
4446
4462
|
}
|
|
@@ -4485,7 +4501,7 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
4485
4501
|
|
|
4486
4502
|
// src/core/dependency-resolver.ts
|
|
4487
4503
|
import { execSync } from "child_process";
|
|
4488
|
-
import
|
|
4504
|
+
import path18 from "path";
|
|
4489
4505
|
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
4490
4506
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
4491
4507
|
function filterRelationType(relType, filter) {
|
|
@@ -4562,7 +4578,7 @@ function registerDepsCommand(program2) {
|
|
|
4562
4578
|
// src/core/graph-from-git.ts
|
|
4563
4579
|
import { mkdtemp, rm as rm3 } from "fs/promises";
|
|
4564
4580
|
import { tmpdir } from "os";
|
|
4565
|
-
import
|
|
4581
|
+
import path19 from "path";
|
|
4566
4582
|
import { execSync as execSync2 } from "child_process";
|
|
4567
4583
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
4568
4584
|
const yggPath = ".yggdrasil";
|
|
@@ -4573,8 +4589,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
4573
4589
|
return null;
|
|
4574
4590
|
}
|
|
4575
4591
|
try {
|
|
4576
|
-
tmpDir = await mkdtemp(
|
|
4577
|
-
const archivePath =
|
|
4592
|
+
tmpDir = await mkdtemp(path19.join(tmpdir(), "ygg-git-"));
|
|
4593
|
+
const archivePath = path19.join(tmpDir, "archive.tar");
|
|
4578
4594
|
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
4579
4595
|
cwd: projectRoot,
|
|
4580
4596
|
stdio: "pipe"
|
|
@@ -4644,14 +4660,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
4644
4660
|
}
|
|
4645
4661
|
const chains = [];
|
|
4646
4662
|
for (const node of transitiveOnly) {
|
|
4647
|
-
const
|
|
4663
|
+
const path20 = [];
|
|
4648
4664
|
let current = node;
|
|
4649
4665
|
while (current) {
|
|
4650
|
-
|
|
4666
|
+
path20.unshift(current);
|
|
4651
4667
|
current = parent.get(current);
|
|
4652
4668
|
}
|
|
4653
|
-
if (
|
|
4654
|
-
chains.push(
|
|
4669
|
+
if (path20.length >= 3) {
|
|
4670
|
+
chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
4655
4671
|
}
|
|
4656
4672
|
}
|
|
4657
4673
|
return chains.sort();
|
|
@@ -4695,14 +4711,14 @@ function collectIndirectDependents(graph, directlyAffected) {
|
|
|
4695
4711
|
}
|
|
4696
4712
|
for (const [node] of parent) {
|
|
4697
4713
|
if (directSet.has(node)) continue;
|
|
4698
|
-
const
|
|
4714
|
+
const path20 = [node];
|
|
4699
4715
|
let current = node;
|
|
4700
4716
|
while (parent.has(current)) {
|
|
4701
4717
|
current = parent.get(current);
|
|
4702
|
-
|
|
4718
|
+
path20.push(current);
|
|
4703
4719
|
}
|
|
4704
|
-
const chain =
|
|
4705
|
-
const depth =
|
|
4720
|
+
const chain = path20.map((p) => `<- ${p}`).join(" ");
|
|
4721
|
+
const depth = path20.length;
|
|
4706
4722
|
const existing = bestChain.get(node);
|
|
4707
4723
|
if (!existing || depth < existing.depth) {
|
|
4708
4724
|
bestChain.set(node, { chain, depth });
|