@chrisdudek/yg 1.4.3 → 2.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 +691 -400
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +12 -12
- package/dist/templates/rules.ts +45 -33
- package/graph-schemas/{aspect.yaml → yg-aspect.yaml} +5 -1
- package/graph-schemas/{config.yaml → yg-config.yaml} +15 -8
- package/graph-schemas/{flow.yaml → yg-flow.yaml} +1 -1
- package/graph-schemas/{node.yaml → yg-node.yaml} +10 -3
- package/package.json +3 -1
package/dist/bin.js
CHANGED
|
@@ -4,35 +4,37 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/cli/init.ts
|
|
7
|
-
import { mkdir as mkdir2, writeFile as
|
|
8
|
-
import
|
|
7
|
+
import { mkdir as mkdir2, writeFile as writeFile3, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
8
|
+
import path4 from "path";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { gt as gt2, valid as valid2 } from "semver";
|
|
10
12
|
|
|
11
13
|
// src/templates/default-config.ts
|
|
12
|
-
var DEFAULT_CONFIG = `
|
|
13
|
-
|
|
14
|
-
stack:
|
|
15
|
-
language: ""
|
|
16
|
-
runtime: ""
|
|
14
|
+
var DEFAULT_CONFIG = `version: "2.0.0"
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
name: ""
|
|
19
17
|
|
|
20
18
|
node_types:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
module:
|
|
20
|
+
description: "Business logic unit with clear domain responsibility"
|
|
21
|
+
service:
|
|
22
|
+
description: "Component providing functionality to other nodes"
|
|
23
|
+
library:
|
|
24
|
+
description: "Shared utility code with no domain knowledge"
|
|
25
|
+
infrastructure:
|
|
26
|
+
description: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
|
|
25
27
|
|
|
26
28
|
artifacts:
|
|
27
29
|
responsibility.md:
|
|
28
30
|
required: always
|
|
29
31
|
description: "What this node is responsible for, and what it is not"
|
|
30
|
-
|
|
32
|
+
included_in_relations: true
|
|
31
33
|
interface.md:
|
|
32
34
|
required:
|
|
33
35
|
when: has_incoming_relations
|
|
34
36
|
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures"
|
|
35
|
-
|
|
37
|
+
included_in_relations: true
|
|
36
38
|
internals.md:
|
|
37
39
|
required: never
|
|
38
40
|
description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
|
|
@@ -136,7 +138,22 @@ Before preflight:
|
|
|
136
138
|
|
|
137
139
|
- Verify \`yg\` CLI is available. If not found, inform user and stop.
|
|
138
140
|
- If \`yg preflight\` shows 0 nodes \u2192 enter BOOTSTRAP MODE (see Operations).
|
|
139
|
-
- If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once
|
|
141
|
+
- If drift report shows >10 drifted nodes \u2192 report scope to user, ask which area to prioritize. Do not resolve all at once.
|
|
142
|
+
|
|
143
|
+
### Delegating to Subagents
|
|
144
|
+
|
|
145
|
+
When you delegate work to a subagent (any subprocess, tool agent, or spawned assistant), the subagent does NOT inherit your Yggdrasil knowledge. Before any other instruction, the subagent MUST:
|
|
146
|
+
|
|
147
|
+
1. Read \`.yggdrasil/agent-rules.md\` \u2014 this is the complete operating manual
|
|
148
|
+
2. Follow the Quick Start Protocol from that file before touching any mapped code
|
|
149
|
+
|
|
150
|
+
Include this as the FIRST instruction in every subagent prompt:
|
|
151
|
+
|
|
152
|
+
\`\`\`
|
|
153
|
+
BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
|
|
154
|
+
\`\`\`
|
|
155
|
+
|
|
156
|
+
A subagent that skips this step will read code without graph context, miss architectural constraints, and produce changes that break graph-code consistency.`;
|
|
140
157
|
var OPERATIONS = `## OPERATIONS
|
|
141
158
|
|
|
142
159
|
### Conversation Lifecycle
|
|
@@ -220,11 +237,11 @@ Per area checklist:
|
|
|
220
237
|
|
|
221
238
|
- [ ] 1. \`yg owner --file <path>\` \u2014 confirm no coverage
|
|
222
239
|
- [ ] 2. Determine node granularity \u2014 propose to user if unclear
|
|
223
|
-
- [ ] 3. Create node directory, read \`schemas/node.yaml\`, create \`node.yaml\`
|
|
224
|
-
- [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
|
|
225
|
-
- [ ] 5. Identify relations \u2014 add to \`node.yaml\`
|
|
240
|
+
- [ ] 3. Create node directory, read \`schemas/yg-node.yaml\`, create \`yg-node.yaml\`
|
|
241
|
+
- [ ] 4. Analyze source \u2014 for each artifact type in \`yg-config.yaml artifacts\`: extract content, do not invent
|
|
242
|
+
- [ ] 5. Identify relations \u2014 add to \`yg-node.yaml\`
|
|
226
243
|
- [ ] 6. Identify cross-cutting requirements \u2014 add matching aspects, create if needed
|
|
227
|
-
- [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add
|
|
244
|
+
- [ ] 6b. For each aspect on the node: identify 2-5 code anchors (function names, constants) that evidence the pattern \u2192 add as \`anchors\` in the aspect entry in \`yg-node.yaml\`
|
|
228
245
|
- [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
|
|
229
246
|
- [ ] 8. \`yg validate\` \u2014 fix errors
|
|
230
247
|
- [ ] 9. \`yg drift-sync --node <path>\`
|
|
@@ -274,7 +291,7 @@ When reviewing graph quality (triggered by user or quality improvement):
|
|
|
274
291
|
- [ ] 1. \`yg build-context --node <path>\`
|
|
275
292
|
- [ ] 2. Read mapped source files
|
|
276
293
|
- [ ] 3. For each claim in graph: verify against source code
|
|
277
|
-
- [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`
|
|
294
|
+
- [ ] 4. For each aspect: verify the pattern holds in THIS node. If it deviates, add \`exceptions\` to the aspect entry in \`yg-node.yaml\`
|
|
278
295
|
- [ ] 5. Report inconsistencies
|
|
279
296
|
|
|
280
297
|
**Step 2 \u2014 Completeness** (catches MISSING information):
|
|
@@ -297,7 +314,7 @@ var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
|
|
|
297
314
|
|
|
298
315
|
\`\`\`
|
|
299
316
|
.yggdrasil/
|
|
300
|
-
config.yaml
|
|
317
|
+
yg-config.yaml \u2190 version, vocabulary, node types, artifact rules, required aspects
|
|
301
318
|
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
302
319
|
aspects/ \u2190 what must: cross-cutting requirements with rationale and guidance
|
|
303
320
|
flows/ \u2190 why and in what process: business processes with node participation
|
|
@@ -309,15 +326,10 @@ var KNOWLEDGE_BASE = `## KNOWLEDGE BASE
|
|
|
309
326
|
Key facts:
|
|
310
327
|
|
|
311
328
|
- **Hierarchy:** nodes nest in \`model/\`. Children inherit parent context. Do not repeat parent content in children.
|
|
312
|
-
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
329
|
+
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`yg-aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
313
330
|
- **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
|
|
314
331
|
|
|
315
|
-
**Node type guidance:**
|
|
316
|
-
|
|
317
|
-
- \`module\` \u2014 business logic unit with clear domain responsibility
|
|
318
|
-
- \`service\` \u2014 component providing functionality to other nodes
|
|
319
|
-
- \`library\` \u2014 shared utility code with no domain knowledge
|
|
320
|
-
- \`infrastructure\` \u2014 guards, resolvers, middleware, interceptors, validators that intercept or modify request flow. These affect blast radius of changes but are invisible in call graphs. Map them to make blast radius analysis accurate. Key signal: code that runs WITHOUT being explicitly called by business logic (e.g., NestJS guards, Express middleware, GraphQL resolvers).
|
|
332
|
+
**Node type guidance:** Each type in \`yg-config.yaml node_types\` has a \`description\` that tells you when to use it. Check the project's config for the full list and descriptions. Common types: \`module\` (business logic), \`service\` (providing functionality), \`library\` (shared utilities), \`infrastructure\` (guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius).
|
|
321
333
|
|
|
322
334
|
### Artifact Structure
|
|
323
335
|
|
|
@@ -329,26 +341,28 @@ Three artifacts capture node knowledge at three levels:
|
|
|
329
341
|
|
|
330
342
|
**Enrichment priority (when adding incrementally):** \`interface.md\` first (highest cross-module ROI \u2014 contracts enable other nodes to reason about interactions), then \`responsibility.md\` (identity and boundaries), then \`internals.md\` (depth for complex nodes). A node with only \`interface.md\` provides more cross-module value than one with only \`internals.md\`.
|
|
331
343
|
|
|
344
|
+
Projects can define additional artifact types in \`yg-config.yaml\` under \`artifacts\`. Each custom artifact has a \`description\` (tells you what to write), a \`required\` condition (\`always\`, \`never\`, \`when: has_incoming_relations\`, \`when: has_aspect:<id>\`), and an \`included_in_relations\` flag (if true, included in dependency context packages for structural relations). The three standard artifacts are always present in config. Check \`yg-config.yaml\` to see all defined artifacts for the project.
|
|
345
|
+
|
|
332
346
|
### Context Assembly
|
|
333
347
|
|
|
334
|
-
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global
|
|
348
|
+
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global project identity, hierarchy, own artifacts, aspects, and relational context. It is your architectural map. For implementation-level claims (exact call patterns, error handling, await vs fire-and-forget) \u2014 verify against source code. If the package is insufficient, enrich the graph.
|
|
335
349
|
|
|
336
350
|
### Information Routing
|
|
337
351
|
|
|
338
352
|
When you encounter information, route it to the correct location:
|
|
339
353
|
|
|
340
|
-
- **Specific to this node** \u2192 local node artifact (check \`config.yaml artifacts\` for available types)
|
|
341
|
-
- **Rule for many nodes** \u2192 aspect (\`aspects/<id>/\` with \`aspect.yaml\` + content \`.md\` files). If applies to ALL nodes of a type \u2192 \`node_types
|
|
342
|
-
- **Business process** \u2192 flow (\`flows/<name>/\` with \`flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
354
|
+
- **Specific to this node** \u2192 local node artifact (check \`yg-config.yaml artifacts\` for available types)
|
|
355
|
+
- **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\`
|
|
356
|
+
- **Business process** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
343
357
|
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
344
|
-
- **Technology stack or standard** \u2192 \`
|
|
345
|
-
- **Decision (why + why NOT):** one node \u2192 Decisions section of \`internals.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192
|
|
358
|
+
- **Technology stack or standard** \u2192 node artifact at the appropriate hierarchy level (e.g., root node's \`responsibility.md\` for single-stack repos, or deployment unit node for monorepos)
|
|
359
|
+
- **Decision (why + why NOT):** one node \u2192 Decisions section of \`internals.md\` with format "Chose X over Y because Z"; category of nodes \u2192 aspect content files; tech choice \u2192 node artifact at the level where the technology applies. Always include rejected alternatives \u2014 they are the highest-value graph content. If the rationale is unknown: record the decision with "rationale: unknown" and note what CAN be observed from the code. Never invent a plausible-sounding rationale.
|
|
346
360
|
|
|
347
361
|
### Creating Aspects
|
|
348
362
|
|
|
349
|
-
- [ ] 1. Read \`schemas/aspect.yaml\`
|
|
363
|
+
- [ ] 1. Read \`schemas/yg-aspect.yaml\`
|
|
350
364
|
- [ ] 2. Create \`aspects/<id>/\` directory
|
|
351
|
-
- [ ] 3. Write \`aspect.yaml\` \u2014 name, optional description, optional implies
|
|
365
|
+
- [ ] 3. Write \`yg-aspect.yaml\` \u2014 name, optional description, optional implies
|
|
352
366
|
- [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
|
|
353
367
|
- [ ] 5. \`yg validate\`
|
|
354
368
|
|
|
@@ -360,23 +374,23 @@ Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No
|
|
|
360
374
|
- **Architectural:** Structural patterns with rationale (e.g., dual-rollback on provider failure, idempotency via key generation, fire-and-forget dispatch)
|
|
361
375
|
- **Concurrency:** Shared concurrency strategies (e.g., pessimistic locking, retry-on-deadlock, optimistic versioning)
|
|
362
376
|
|
|
363
|
-
When a node follows an aspect's pattern with exceptions, record exceptions in \`node.yaml
|
|
377
|
+
When a node follows an aspect's pattern with exceptions, record them in the \`exceptions\` field of the aspect entry in \`yg-node.yaml\`. Example: aspect says "fire-and-forget" but this node awaits the publish call \u2014 add \`exceptions: ["awaits publish call instead of fire-and-forget because..."]\`. Exceptions appear in the context package next to the aspect content, preventing abstractions from masking implementation details.
|
|
364
378
|
|
|
365
379
|
**Aspect lifecycle warning.** Aspects decay CATASTROPHICALLY \u2014 a pattern either exists or it doesn't. When a pattern changes, ALL aspect claims become wrong at once. This differs from other artifacts: \`interface.md\` and \`responsibility.md\` are most stable (~9-year half-life); \`internals.md\` has moderate stability (~2.5-year half-life); aspects are least stable (~2.4-year half-life, binary decay). After any significant feature addition, review ALL aspects touching the affected area. Don't wait for drift \u2014 aspects can be 100% wrong without any mapped file changing.
|
|
366
380
|
|
|
367
|
-
**Aspect stability tiers.** If an aspect has a \`stability\` field in \`aspect.yaml\`, use it to calibrate review urgency:
|
|
381
|
+
**Aspect stability tiers.** If an aspect has a \`stability\` field in \`yg-aspect.yaml\`, use it to calibrate review urgency:
|
|
368
382
|
|
|
369
383
|
- \`schema\` \u2014 enforced by data model; review only when data model changes (most stable)
|
|
370
384
|
- \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
|
|
371
385
|
- \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
|
|
372
386
|
|
|
373
|
-
When code anchors (\`anchors\`
|
|
387
|
+
When code anchors (\`anchors\` in an aspect entry in \`yg-node.yaml\`) are present, they list code patterns (function names, constants, SQL fragments) evidencing the aspect's implementation in this node. \`yg validate\` checks that each anchor exists in the node's mapped source files \u2014 a missing anchor (W014) signals the aspect may be stale for this node.
|
|
374
388
|
|
|
375
389
|
### Creating Flows
|
|
376
390
|
|
|
377
|
-
- [ ] 1. Read \`schemas/flow.yaml\`
|
|
391
|
+
- [ ] 1. Read \`schemas/yg-flow.yaml\`
|
|
378
392
|
- [ ] 2. Create \`flows/<name>/\` directory
|
|
379
|
-
- [ ] 3. Write \`flow.yaml\` \u2014 declare participants and flow-level aspects
|
|
393
|
+
- [ ] 3. Write \`yg-flow.yaml\` \u2014 declare participants and flow-level aspects
|
|
380
394
|
- [ ] 4. Write \`description.md\` with required sections: Business context, Trigger, Goal, Participants, Paths (at least Happy path), Invariants across all paths
|
|
381
395
|
- [ ] 5. \`yg validate\`
|
|
382
396
|
|
|
@@ -387,7 +401,7 @@ Test: "Does this describe what happens in the world, or only in the software?" I
|
|
|
387
401
|
### Operational Rules
|
|
388
402
|
|
|
389
403
|
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
390
|
-
- **Read schemas before creating** any \`node.yaml\`, \`aspect.yaml\`, or \`flow.yaml\`.
|
|
404
|
+
- **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
|
|
391
405
|
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
392
406
|
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
|
|
393
407
|
- **Completeness test:** Two checks, both required:
|
|
@@ -427,14 +441,14 @@ yg journal-archive Archive consolidated journal entries.
|
|
|
427
441
|
|
|
428
442
|
| What you have | Where it goes |
|
|
429
443
|
|---|---|
|
|
430
|
-
| Information specific to this node | Local node artifact (check \`config.yaml artifacts\` for types) |
|
|
444
|
+
| Information specific to this node | Local node artifact (check \`yg-config.yaml artifacts\` for types) |
|
|
431
445
|
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
432
|
-
| Architectural invariant for a node type | Required aspect in \`config.yaml node_types\` |
|
|
433
|
-
| Business process participation | Flow (\`flow.yaml participants\`) |
|
|
446
|
+
| Architectural invariant for a node type | Required aspect in \`yg-config.yaml node_types\` |
|
|
447
|
+
| Business process participation | Flow (\`yg-flow.yaml participants\`) |
|
|
434
448
|
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
435
449
|
| Context shared across a domain | Parent node artifact |
|
|
436
|
-
| Technology stack |
|
|
437
|
-
|
|
|
450
|
+
| Technology stack | Node artifact at appropriate hierarchy level |
|
|
451
|
+
| Coding standards | Node artifact at appropriate hierarchy level |`;
|
|
438
452
|
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
|
|
439
453
|
|
|
440
454
|
// src/templates/platform.ts
|
|
@@ -677,11 +691,314 @@ function escapeRegex(s) {
|
|
|
677
691
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
678
692
|
}
|
|
679
693
|
|
|
694
|
+
// src/core/migrator.ts
|
|
695
|
+
import { readFile as readFile2, access } from "fs/promises";
|
|
696
|
+
import path2 from "path";
|
|
697
|
+
import { parse as parseYaml } from "yaml";
|
|
698
|
+
import { gt, valid, compare } from "semver";
|
|
699
|
+
async function detectVersion(yggRoot) {
|
|
700
|
+
const newConfigPath = path2.join(yggRoot, "yg-config.yaml");
|
|
701
|
+
try {
|
|
702
|
+
const content = await readFile2(newConfigPath, "utf-8");
|
|
703
|
+
const raw = parseYaml(content);
|
|
704
|
+
if (raw && typeof raw === "object" && typeof raw.version === "string") {
|
|
705
|
+
return raw.version.trim();
|
|
706
|
+
}
|
|
707
|
+
return "1.4.3";
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
const oldConfigPath = path2.join(yggRoot, "config.yaml");
|
|
711
|
+
try {
|
|
712
|
+
await access(oldConfigPath);
|
|
713
|
+
return "1.4.3";
|
|
714
|
+
} catch {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function runMigrations(currentVersion, migrations, yggRoot) {
|
|
719
|
+
const cVer = valid(currentVersion);
|
|
720
|
+
if (!cVer) return [];
|
|
721
|
+
const applicable = migrations.filter((m) => {
|
|
722
|
+
const mVer = valid(m.to);
|
|
723
|
+
if (!mVer) return false;
|
|
724
|
+
return gt(mVer, cVer);
|
|
725
|
+
}).sort((a, b) => compare(valid(a.to), valid(b.to)));
|
|
726
|
+
const results = [];
|
|
727
|
+
for (const migration of applicable) {
|
|
728
|
+
const result = await migration.run(yggRoot);
|
|
729
|
+
results.push(result);
|
|
730
|
+
}
|
|
731
|
+
return results;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/migrations/to-2.0.0.ts
|
|
735
|
+
import { readFile as readFile3, writeFile as writeFile2, rename, readdir, rm, stat } from "fs/promises";
|
|
736
|
+
import path3 from "path";
|
|
737
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
738
|
+
var KNOWN_TYPE_DESCRIPTIONS = {
|
|
739
|
+
module: "Business logic unit with clear domain responsibility",
|
|
740
|
+
service: "Component providing functionality to other nodes",
|
|
741
|
+
library: "Shared utility code with no domain knowledge",
|
|
742
|
+
infrastructure: "Guards, middleware, interceptors \u2014 invisible in call graphs but affect blast radius"
|
|
743
|
+
};
|
|
744
|
+
var STANDARD_ARTIFACTS = {
|
|
745
|
+
"responsibility.md": {
|
|
746
|
+
required: "always",
|
|
747
|
+
description: "What this node is responsible for, and what it is not",
|
|
748
|
+
included_in_relations: true
|
|
749
|
+
},
|
|
750
|
+
"interface.md": {
|
|
751
|
+
required: { when: "has_incoming_relations" },
|
|
752
|
+
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures",
|
|
753
|
+
included_in_relations: true
|
|
754
|
+
},
|
|
755
|
+
"internals.md": {
|
|
756
|
+
required: "never",
|
|
757
|
+
description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
async function migrateTo2(yggRoot) {
|
|
761
|
+
const actions = [];
|
|
762
|
+
const warnings = [];
|
|
763
|
+
const oldConfigPath = path3.join(yggRoot, "config.yaml");
|
|
764
|
+
const newConfigPath = path3.join(yggRoot, "yg-config.yaml");
|
|
765
|
+
let configContent;
|
|
766
|
+
const oldConfigExists = await fileExists(oldConfigPath);
|
|
767
|
+
if (oldConfigExists) {
|
|
768
|
+
configContent = await readFile3(oldConfigPath, "utf-8");
|
|
769
|
+
await rename(oldConfigPath, newConfigPath);
|
|
770
|
+
actions.push("Renamed config.yaml \u2192 yg-config.yaml");
|
|
771
|
+
} else {
|
|
772
|
+
configContent = await readFile3(newConfigPath, "utf-8");
|
|
773
|
+
}
|
|
774
|
+
const raw = parseYaml2(configContent);
|
|
775
|
+
const nodeTypesRaw = raw.node_types;
|
|
776
|
+
const nodeTypes = {};
|
|
777
|
+
if (Array.isArray(nodeTypesRaw)) {
|
|
778
|
+
for (const typeName of nodeTypesRaw) {
|
|
779
|
+
if (typeof typeName === "string") {
|
|
780
|
+
const desc = KNOWN_TYPE_DESCRIPTIONS[typeName];
|
|
781
|
+
if (desc) {
|
|
782
|
+
nodeTypes[typeName] = { description: desc };
|
|
783
|
+
} else {
|
|
784
|
+
nodeTypes[typeName] = { description: "TODO: add description" };
|
|
785
|
+
warnings.push(`Unknown node type '${typeName}' \u2014 needs a description`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
actions.push("Converted node_types from array to object format");
|
|
790
|
+
} else if (nodeTypesRaw && typeof nodeTypesRaw === "object") {
|
|
791
|
+
for (const [name, val] of Object.entries(nodeTypesRaw)) {
|
|
792
|
+
const entry = val;
|
|
793
|
+
const desc = KNOWN_TYPE_DESCRIPTIONS[name] ?? (typeof entry?.description === "string" ? entry.description : "TODO: add description");
|
|
794
|
+
nodeTypes[name] = { description: desc };
|
|
795
|
+
if (entry?.required_aspects) {
|
|
796
|
+
nodeTypes[name].required_aspects = entry.required_aspects;
|
|
797
|
+
}
|
|
798
|
+
if (!KNOWN_TYPE_DESCRIPTIONS[name] && (!entry?.description || entry.description === "TODO: add description")) {
|
|
799
|
+
warnings.push(`Unknown node type '${name}' \u2014 needs a description`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!nodeTypes.infrastructure) {
|
|
804
|
+
nodeTypes.infrastructure = { description: KNOWN_TYPE_DESCRIPTIONS.infrastructure };
|
|
805
|
+
actions.push("Added infrastructure node type");
|
|
806
|
+
}
|
|
807
|
+
const stackRaw = raw.stack;
|
|
808
|
+
const standardsRaw = raw.standards;
|
|
809
|
+
if (stackRaw || standardsRaw) {
|
|
810
|
+
await migrateStackStandards(yggRoot, stackRaw, standardsRaw, actions);
|
|
811
|
+
}
|
|
812
|
+
const newConfig = {
|
|
813
|
+
version: "2.0.0",
|
|
814
|
+
name: raw.name,
|
|
815
|
+
node_types: nodeTypes,
|
|
816
|
+
artifacts: STANDARD_ARTIFACTS
|
|
817
|
+
};
|
|
818
|
+
if (raw.quality) {
|
|
819
|
+
newConfig.quality = raw.quality;
|
|
820
|
+
}
|
|
821
|
+
await writeFile2(newConfigPath, stringifyYaml(newConfig, { lineWidth: 120 }), "utf-8");
|
|
822
|
+
actions.push("Updated config: version, artifacts, removed stack/standards");
|
|
823
|
+
const modelDir = path3.join(yggRoot, "model");
|
|
824
|
+
if (await fileExists(modelDir)) {
|
|
825
|
+
await renameFilesRecursively(modelDir, "node.yaml", "yg-node.yaml", actions);
|
|
826
|
+
await transformNodeFiles(modelDir, actions, warnings);
|
|
827
|
+
}
|
|
828
|
+
const aspectsDir = path3.join(yggRoot, "aspects");
|
|
829
|
+
if (await fileExists(aspectsDir)) {
|
|
830
|
+
await renameFilesRecursively(aspectsDir, "aspect.yaml", "yg-aspect.yaml", actions);
|
|
831
|
+
}
|
|
832
|
+
const flowsDir = path3.join(yggRoot, "flows");
|
|
833
|
+
if (await fileExists(flowsDir)) {
|
|
834
|
+
await renameFilesRecursively(flowsDir, "flow.yaml", "yg-flow.yaml", actions);
|
|
835
|
+
}
|
|
836
|
+
const schemasDir = path3.join(yggRoot, "schemas");
|
|
837
|
+
if (await fileExists(schemasDir)) {
|
|
838
|
+
for (const name of ["config.yaml", "node.yaml", "aspect.yaml", "flow.yaml"]) {
|
|
839
|
+
const oldPath = path3.join(schemasDir, name);
|
|
840
|
+
const newPath = path3.join(schemasDir, `yg-${name}`);
|
|
841
|
+
if (await fileExists(oldPath) && !await fileExists(newPath)) {
|
|
842
|
+
await rename(oldPath, newPath);
|
|
843
|
+
actions.push(`Renamed schemas/${name} \u2192 yg-${name}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const driftStatePath = path3.join(yggRoot, ".drift-state");
|
|
848
|
+
if (await fileExists(driftStatePath)) {
|
|
849
|
+
await rm(driftStatePath);
|
|
850
|
+
actions.push("Deleted .drift-state (requires fresh yg drift-sync --all)");
|
|
851
|
+
}
|
|
852
|
+
return { actions, warnings };
|
|
853
|
+
}
|
|
854
|
+
async function fileExists(p) {
|
|
855
|
+
try {
|
|
856
|
+
await stat(p);
|
|
857
|
+
return true;
|
|
858
|
+
} catch {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
async function migrateStackStandards(yggRoot, stack, standards, actions) {
|
|
863
|
+
const modelDir = path3.join(yggRoot, "model");
|
|
864
|
+
if (!await fileExists(modelDir)) return;
|
|
865
|
+
const lines = [];
|
|
866
|
+
if (stack && Object.keys(stack).length > 0) {
|
|
867
|
+
lines.push("## Technology Stack");
|
|
868
|
+
lines.push("");
|
|
869
|
+
for (const [key, value] of Object.entries(stack)) {
|
|
870
|
+
lines.push(`- **${key}:** ${value}`);
|
|
871
|
+
}
|
|
872
|
+
lines.push("");
|
|
873
|
+
}
|
|
874
|
+
if (standards) {
|
|
875
|
+
lines.push("## Standards");
|
|
876
|
+
lines.push("");
|
|
877
|
+
lines.push(standards);
|
|
878
|
+
lines.push("");
|
|
879
|
+
}
|
|
880
|
+
if (lines.length === 0) return;
|
|
881
|
+
const rootNodeYgPath = path3.join(modelDir, "yg-node.yaml");
|
|
882
|
+
const rootNodeOldPath = path3.join(modelDir, "node.yaml");
|
|
883
|
+
const hasRootNode = await fileExists(rootNodeYgPath) || await fileExists(rootNodeOldPath);
|
|
884
|
+
if (!hasRootNode) {
|
|
885
|
+
await writeFile2(rootNodeYgPath, stringifyYaml({ name: "Root", type: "module" }), "utf-8");
|
|
886
|
+
await writeFile2(path3.join(modelDir, "responsibility.md"), "TBD\n", "utf-8");
|
|
887
|
+
actions.push("Created root node in model/ for stack/standards migration");
|
|
888
|
+
}
|
|
889
|
+
const internalsPath = path3.join(modelDir, "internals.md");
|
|
890
|
+
const existingInternals = await fileExists(internalsPath) ? await readFile3(internalsPath, "utf-8") : "";
|
|
891
|
+
const MIGRATION_MARKER = "<!-- migrated-stack-standards-v2 -->";
|
|
892
|
+
if (existingInternals.includes(MIGRATION_MARKER)) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const markerLine = MIGRATION_MARKER + "\n";
|
|
896
|
+
const newContent = existingInternals ? existingInternals.trimEnd() + "\n\n" + markerLine + lines.join("\n") : markerLine + lines.join("\n");
|
|
897
|
+
await writeFile2(internalsPath, newContent, "utf-8");
|
|
898
|
+
actions.push("Migrated stack/standards to model/internals.md");
|
|
899
|
+
}
|
|
900
|
+
async function renameFilesRecursively(dir, oldName, newName, actions) {
|
|
901
|
+
let entries;
|
|
902
|
+
try {
|
|
903
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
904
|
+
} catch {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
for (const entry of entries) {
|
|
908
|
+
const fullPath = path3.join(dir, entry.name);
|
|
909
|
+
if (entry.isDirectory()) {
|
|
910
|
+
await renameFilesRecursively(fullPath, oldName, newName, actions);
|
|
911
|
+
} else if (entry.name === oldName) {
|
|
912
|
+
const destPath = path3.join(dir, newName);
|
|
913
|
+
if (!await fileExists(destPath)) {
|
|
914
|
+
await rename(fullPath, destPath);
|
|
915
|
+
actions.push(`Renamed ${oldName} \u2192 ${newName} in ${dir}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async function transformNodeFiles(dir, actions, warnings) {
|
|
921
|
+
let entries;
|
|
922
|
+
try {
|
|
923
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
924
|
+
} catch {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
for (const entry of entries) {
|
|
928
|
+
const fullPath = path3.join(dir, entry.name);
|
|
929
|
+
if (entry.isDirectory()) {
|
|
930
|
+
await transformNodeFiles(fullPath, actions, warnings);
|
|
931
|
+
} else if (entry.name === "yg-node.yaml") {
|
|
932
|
+
await transformSingleNode(fullPath, actions, warnings);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
async function transformSingleNode(filePath, actions, warnings) {
|
|
937
|
+
const content = await readFile3(filePath, "utf-8");
|
|
938
|
+
const raw = parseYaml2(content);
|
|
939
|
+
if (!raw || typeof raw !== "object") {
|
|
940
|
+
warnings.push(`Skipped ${filePath}: not a valid YAML object`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
let changed = false;
|
|
944
|
+
if (Array.isArray(raw.aspects) && raw.aspects.length > 0 && typeof raw.aspects[0] === "string") {
|
|
945
|
+
const aspectExceptions = raw.aspect_exceptions ?? {};
|
|
946
|
+
const anchors = raw.anchors ?? {};
|
|
947
|
+
raw.aspects = raw.aspects.map((id) => {
|
|
948
|
+
const entry = { aspect: id };
|
|
949
|
+
if (aspectExceptions[id]) entry.exceptions = aspectExceptions[id];
|
|
950
|
+
if (anchors[id]) entry.anchors = anchors[id];
|
|
951
|
+
return entry;
|
|
952
|
+
});
|
|
953
|
+
delete raw.aspect_exceptions;
|
|
954
|
+
delete raw.anchors;
|
|
955
|
+
changed = true;
|
|
956
|
+
}
|
|
957
|
+
if (raw.tags !== void 0) {
|
|
958
|
+
delete raw.tags;
|
|
959
|
+
changed = true;
|
|
960
|
+
}
|
|
961
|
+
if (changed) {
|
|
962
|
+
await writeFile2(filePath, stringifyYaml(raw, { lineWidth: 120 }), "utf-8");
|
|
963
|
+
actions.push(`Transformed ${path3.basename(path3.dirname(filePath))}/yg-node.yaml`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/migrations/index.ts
|
|
968
|
+
var MIGRATIONS = [
|
|
969
|
+
{
|
|
970
|
+
to: "2.0.0",
|
|
971
|
+
description: "Rename YAML files to yg-* prefix, restructure config, convert aspects format",
|
|
972
|
+
run: migrateTo2
|
|
973
|
+
}
|
|
974
|
+
];
|
|
975
|
+
|
|
680
976
|
// src/cli/init.ts
|
|
681
977
|
function getGraphSchemasDir() {
|
|
682
|
-
const currentDir =
|
|
683
|
-
const packageRoot =
|
|
684
|
-
return
|
|
978
|
+
const currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
979
|
+
const packageRoot = path4.join(currentDir, "..");
|
|
980
|
+
return path4.join(packageRoot, "graph-schemas");
|
|
981
|
+
}
|
|
982
|
+
function getCliVersion() {
|
|
983
|
+
const currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
984
|
+
const packageRoot = path4.join(currentDir, "..");
|
|
985
|
+
const pkg2 = JSON.parse(readFileSync(path4.join(packageRoot, "package.json"), "utf-8"));
|
|
986
|
+
return pkg2.version;
|
|
987
|
+
}
|
|
988
|
+
async function refreshSchemas(yggRoot) {
|
|
989
|
+
const schemasDir = path4.join(yggRoot, "schemas");
|
|
990
|
+
await mkdir2(schemasDir, { recursive: true });
|
|
991
|
+
const graphSchemasDir = getGraphSchemasDir();
|
|
992
|
+
try {
|
|
993
|
+
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
994
|
+
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
995
|
+
for (const file of schemaFiles) {
|
|
996
|
+
const srcPath = path4.join(graphSchemasDir, file);
|
|
997
|
+
const content = await readFile4(srcPath, "utf-8");
|
|
998
|
+
await writeFile3(path4.join(schemasDir, file), content, "utf-8");
|
|
999
|
+
}
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
685
1002
|
}
|
|
686
1003
|
var GITIGNORE_CONTENT = `.journal.yaml
|
|
687
1004
|
.drift-state
|
|
@@ -694,10 +1011,10 @@ function registerInitCommand(program2) {
|
|
|
694
1011
|
"generic"
|
|
695
1012
|
).option("--upgrade", "Refresh rules only (when .yggdrasil/ already exists)").action(async (options) => {
|
|
696
1013
|
const projectRoot = process.cwd();
|
|
697
|
-
const yggRoot =
|
|
1014
|
+
const yggRoot = path4.join(projectRoot, ".yggdrasil");
|
|
698
1015
|
let upgradeMode = false;
|
|
699
1016
|
try {
|
|
700
|
-
const statResult = await
|
|
1017
|
+
const statResult = await stat2(yggRoot);
|
|
701
1018
|
if (!statResult.isDirectory()) {
|
|
702
1019
|
process.stderr.write("Error: .yggdrasil exists but is not a directory.\n");
|
|
703
1020
|
process.exit(1);
|
|
@@ -721,25 +1038,58 @@ function registerInitCommand(program2) {
|
|
|
721
1038
|
process.exit(1);
|
|
722
1039
|
}
|
|
723
1040
|
if (upgradeMode) {
|
|
1041
|
+
const projectVersion = await detectVersion(yggRoot);
|
|
1042
|
+
if (!projectVersion) {
|
|
1043
|
+
process.stderr.write("Error: No Yggdrasil project found. Run `yg init` first.\n");
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
1046
|
+
const cliVersion = getCliVersion();
|
|
1047
|
+
if (valid2(projectVersion) && valid2(cliVersion) && gt2(projectVersion, cliVersion)) {
|
|
1048
|
+
process.stderr.write(
|
|
1049
|
+
`Warning: Project version (${projectVersion}) is newer than CLI (${cliVersion}). Upgrade your CLI.
|
|
1050
|
+
`
|
|
1051
|
+
);
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
if (valid2(projectVersion) && valid2(cliVersion) && gt2(cliVersion, projectVersion)) {
|
|
1055
|
+
process.stdout.write(`Migrating from ${projectVersion} to ${cliVersion}...
|
|
1056
|
+
|
|
1057
|
+
`);
|
|
1058
|
+
const results = await runMigrations(projectVersion, MIGRATIONS, yggRoot);
|
|
1059
|
+
for (const result of results) {
|
|
1060
|
+
for (const action of result.actions) {
|
|
1061
|
+
process.stdout.write(` \u2713 ${action}
|
|
1062
|
+
`);
|
|
1063
|
+
}
|
|
1064
|
+
for (const warning of result.warnings) {
|
|
1065
|
+
process.stdout.write(` \u26A0 ${warning}
|
|
1066
|
+
`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (results.length > 0) {
|
|
1070
|
+
process.stdout.write("\n");
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
await refreshSchemas(yggRoot);
|
|
724
1074
|
const rulesPath2 = await installRulesForPlatform(projectRoot, platform);
|
|
725
1075
|
process.stdout.write("\u2713 Rules refreshed.\n");
|
|
726
|
-
process.stdout.write(` ${
|
|
1076
|
+
process.stdout.write(` ${path4.relative(projectRoot, rulesPath2)}
|
|
727
1077
|
`);
|
|
728
1078
|
return;
|
|
729
1079
|
}
|
|
730
|
-
await mkdir2(
|
|
731
|
-
await mkdir2(
|
|
732
|
-
await mkdir2(
|
|
733
|
-
const schemasDir =
|
|
1080
|
+
await mkdir2(path4.join(yggRoot, "model"), { recursive: true });
|
|
1081
|
+
await mkdir2(path4.join(yggRoot, "aspects"), { recursive: true });
|
|
1082
|
+
await mkdir2(path4.join(yggRoot, "flows"), { recursive: true });
|
|
1083
|
+
const schemasDir = path4.join(yggRoot, "schemas");
|
|
734
1084
|
await mkdir2(schemasDir, { recursive: true });
|
|
735
1085
|
const graphSchemasDir = getGraphSchemasDir();
|
|
736
1086
|
try {
|
|
737
|
-
const entries = await
|
|
1087
|
+
const entries = await readdir2(graphSchemasDir, { withFileTypes: true });
|
|
738
1088
|
const schemaFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
739
1089
|
for (const file of schemaFiles) {
|
|
740
|
-
const srcPath =
|
|
741
|
-
const content = await
|
|
742
|
-
await
|
|
1090
|
+
const srcPath = path4.join(graphSchemasDir, file);
|
|
1091
|
+
const content = await readFile4(srcPath, "utf-8");
|
|
1092
|
+
await writeFile3(path4.join(schemasDir, file), content, "utf-8");
|
|
743
1093
|
}
|
|
744
1094
|
} catch (err) {
|
|
745
1095
|
process.stderr.write(
|
|
@@ -747,95 +1097,94 @@ function registerInitCommand(program2) {
|
|
|
747
1097
|
`
|
|
748
1098
|
);
|
|
749
1099
|
}
|
|
750
|
-
await
|
|
751
|
-
await
|
|
1100
|
+
await writeFile3(path4.join(yggRoot, "yg-config.yaml"), DEFAULT_CONFIG, "utf-8");
|
|
1101
|
+
await writeFile3(path4.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
|
|
752
1102
|
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
753
1103
|
process.stdout.write("\u2713 Yggdrasil initialized.\n\n");
|
|
754
1104
|
process.stdout.write("Created:\n");
|
|
755
|
-
process.stdout.write(" .yggdrasil/config.yaml\n");
|
|
1105
|
+
process.stdout.write(" .yggdrasil/yg-config.yaml\n");
|
|
756
1106
|
process.stdout.write(" .yggdrasil/.gitignore\n");
|
|
757
1107
|
process.stdout.write(" .yggdrasil/model/\n");
|
|
758
1108
|
process.stdout.write(" .yggdrasil/aspects/\n");
|
|
759
1109
|
process.stdout.write(" .yggdrasil/flows/\n");
|
|
760
|
-
process.stdout.write(" .yggdrasil/schemas/ (config, node, aspect, flow)\n");
|
|
761
|
-
process.stdout.write(` ${
|
|
1110
|
+
process.stdout.write(" .yggdrasil/schemas/ (yg-config, yg-node, yg-aspect, yg-flow)\n");
|
|
1111
|
+
process.stdout.write(` ${path4.relative(projectRoot, rulesPath)} (rules)
|
|
762
1112
|
|
|
763
1113
|
`);
|
|
764
1114
|
process.stdout.write("Next steps:\n");
|
|
765
|
-
process.stdout.write(" 1. Edit .yggdrasil/config.yaml \u2014 set name
|
|
1115
|
+
process.stdout.write(" 1. Edit .yggdrasil/yg-config.yaml \u2014 set name and configure node types\n");
|
|
766
1116
|
process.stdout.write(" 2. Create nodes under .yggdrasil/model/\n");
|
|
767
1117
|
process.stdout.write(" 3. Run: yg validate\n");
|
|
768
1118
|
});
|
|
769
1119
|
}
|
|
770
1120
|
|
|
771
1121
|
// src/core/graph-loader.ts
|
|
772
|
-
import { readdir as
|
|
773
|
-
import
|
|
1122
|
+
import { readdir as readdir4, readFile as readFile11 } from "fs/promises";
|
|
1123
|
+
import path9 from "path";
|
|
774
1124
|
|
|
775
1125
|
// src/io/config-parser.ts
|
|
776
|
-
import { readFile as
|
|
777
|
-
import { parse as
|
|
1126
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1127
|
+
import { parse as parseYaml3 } from "yaml";
|
|
778
1128
|
var DEFAULT_QUALITY = {
|
|
779
1129
|
min_artifact_length: 50,
|
|
780
1130
|
max_direct_relations: 10,
|
|
781
1131
|
context_budget: { warning: 1e4, error: 2e4 }
|
|
782
1132
|
};
|
|
783
1133
|
async function parseConfig(filePath) {
|
|
784
|
-
const content = await
|
|
785
|
-
const raw =
|
|
1134
|
+
const content = await readFile5(filePath, "utf-8");
|
|
1135
|
+
const raw = parseYaml3(content);
|
|
786
1136
|
if (!raw || typeof raw !== "object") {
|
|
787
|
-
throw new Error(`config.yaml: file is empty or not a valid YAML mapping`);
|
|
1137
|
+
throw new Error(`yg-config.yaml: file is empty or not a valid YAML mapping`);
|
|
788
1138
|
}
|
|
1139
|
+
const version = typeof raw.version === "string" ? raw.version.trim() : void 0;
|
|
789
1140
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
790
|
-
throw new Error(`config.yaml: missing or invalid 'name' field`);
|
|
1141
|
+
throw new Error(`yg-config.yaml: missing or invalid 'name' field`);
|
|
791
1142
|
}
|
|
792
1143
|
const nodeTypesRaw = raw.node_types;
|
|
793
|
-
if (!Array.isArray(nodeTypesRaw) || nodeTypesRaw.length === 0) {
|
|
794
|
-
throw new Error(`config.yaml: 'node_types' must be a non-empty
|
|
1144
|
+
if (!nodeTypesRaw || typeof nodeTypesRaw !== "object" || Array.isArray(nodeTypesRaw) || Object.keys(nodeTypesRaw).length === 0) {
|
|
1145
|
+
throw new Error(`yg-config.yaml: 'node_types' must be a non-empty object`);
|
|
795
1146
|
}
|
|
796
|
-
const nodeTypes =
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
name: obj.name,
|
|
805
|
-
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
806
|
-
};
|
|
1147
|
+
const nodeTypes = {};
|
|
1148
|
+
for (const [typeName, val] of Object.entries(nodeTypesRaw)) {
|
|
1149
|
+
const entry = val;
|
|
1150
|
+
if (!entry || typeof entry !== "object" || typeof entry.description !== "string" || entry.description.trim() === "") {
|
|
1151
|
+
throw new Error(
|
|
1152
|
+
`yg-config.yaml: node_types.${typeName} must have a non-empty 'description' string`
|
|
1153
|
+
);
|
|
807
1154
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1155
|
+
const requiredAspects = Array.isArray(entry.required_aspects) ? entry.required_aspects.filter((t) => typeof t === "string") : void 0;
|
|
1156
|
+
nodeTypes[typeName] = {
|
|
1157
|
+
description: entry.description,
|
|
1158
|
+
required_aspects: requiredAspects && requiredAspects.length > 0 ? requiredAspects : void 0
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
812
1161
|
const artifacts = raw.artifacts;
|
|
813
1162
|
if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts) || Object.keys(artifacts).length === 0) {
|
|
814
|
-
throw new Error(`config.yaml: 'artifacts' must be a non-empty object`);
|
|
1163
|
+
throw new Error(`yg-config.yaml: 'artifacts' must be a non-empty object`);
|
|
815
1164
|
}
|
|
816
1165
|
const artifactsMap = {};
|
|
817
1166
|
for (const [key, val] of Object.entries(artifacts)) {
|
|
818
|
-
if (key === "node.yaml") {
|
|
819
|
-
throw new Error(`config.yaml: artifact name 'node.yaml' is reserved`);
|
|
1167
|
+
if (key === "yg-node.yaml") {
|
|
1168
|
+
throw new Error(`yg-config.yaml: artifact name 'yg-node.yaml' is reserved`);
|
|
820
1169
|
}
|
|
821
1170
|
const a = val;
|
|
822
1171
|
const required = a.required;
|
|
823
1172
|
if (required !== "always" && required !== "never" && (typeof required !== "object" || !required || !("when" in required))) {
|
|
824
|
-
throw new Error(`config.yaml: artifact '${key}' has invalid 'required' field`);
|
|
1173
|
+
throw new Error(`yg-config.yaml: artifact '${key}' has invalid 'required' field`);
|
|
825
1174
|
}
|
|
826
1175
|
if (typeof required === "object" && required && "when" in required) {
|
|
827
1176
|
const when = required.when;
|
|
828
1177
|
const validWhen = when === "has_incoming_relations" || when === "has_outgoing_relations" || typeof when === "string" && (when.startsWith("has_aspect:") || when.startsWith("has_tag:"));
|
|
829
1178
|
if (!validWhen) {
|
|
830
1179
|
throw new Error(
|
|
831
|
-
`config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
1180
|
+
`yg-config.yaml: artifact '${key}' has invalid 'required.when': must be has_incoming_relations, has_outgoing_relations, or has_aspect:<name>`
|
|
832
1181
|
);
|
|
833
1182
|
}
|
|
834
1183
|
}
|
|
835
1184
|
artifactsMap[key] = {
|
|
836
1185
|
required,
|
|
837
1186
|
description: a.description ?? "",
|
|
838
|
-
|
|
1187
|
+
included_in_relations: a.included_in_relations ?? false
|
|
839
1188
|
};
|
|
840
1189
|
}
|
|
841
1190
|
const qualityRaw = raw.quality;
|
|
@@ -849,13 +1198,12 @@ async function parseConfig(filePath) {
|
|
|
849
1198
|
} : DEFAULT_QUALITY;
|
|
850
1199
|
if (quality.context_budget.error < quality.context_budget.warning) {
|
|
851
1200
|
throw new Error(
|
|
852
|
-
`config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
|
|
1201
|
+
`yg-config.yaml: quality.context_budget.error (${quality.context_budget.error}) must be >= warning (${quality.context_budget.warning})`
|
|
853
1202
|
);
|
|
854
1203
|
}
|
|
855
1204
|
return {
|
|
1205
|
+
version,
|
|
856
1206
|
name: raw.name.trim(),
|
|
857
|
-
stack: raw.stack ?? {},
|
|
858
|
-
standards: typeof raw.standards === "string" ? raw.standards : "",
|
|
859
1207
|
node_types: nodeTypes,
|
|
860
1208
|
artifacts: artifactsMap,
|
|
861
1209
|
quality
|
|
@@ -863,8 +1211,8 @@ async function parseConfig(filePath) {
|
|
|
863
1211
|
}
|
|
864
1212
|
|
|
865
1213
|
// src/io/node-parser.ts
|
|
866
|
-
import { readFile as
|
|
867
|
-
import { parse as
|
|
1214
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1215
|
+
import { parse as parseYaml4 } from "yaml";
|
|
868
1216
|
var RELATION_TYPES = [
|
|
869
1217
|
"uses",
|
|
870
1218
|
"calls",
|
|
@@ -877,120 +1225,103 @@ function isValidRelationType(t) {
|
|
|
877
1225
|
return typeof t === "string" && RELATION_TYPES.includes(t);
|
|
878
1226
|
}
|
|
879
1227
|
async function parseNodeYaml(filePath) {
|
|
880
|
-
const content = await
|
|
881
|
-
const raw =
|
|
1228
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1229
|
+
const raw = parseYaml4(content);
|
|
882
1230
|
if (!raw || typeof raw !== "object") {
|
|
883
|
-
throw new Error(`node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
1231
|
+
throw new Error(`yg-node.yaml at ${filePath}: file is empty or not a valid YAML mapping`);
|
|
884
1232
|
}
|
|
885
1233
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
886
|
-
throw new Error(`node.yaml at ${filePath}: missing or empty 'name'`);
|
|
1234
|
+
throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'name'`);
|
|
887
1235
|
}
|
|
888
1236
|
if (!raw.type || typeof raw.type !== "string" || raw.type.trim() === "") {
|
|
889
|
-
throw new Error(`node.yaml at ${filePath}: missing or empty 'type'`);
|
|
1237
|
+
throw new Error(`yg-node.yaml at ${filePath}: missing or empty 'type'`);
|
|
890
1238
|
}
|
|
891
1239
|
const relations = parseRelations(raw.relations, filePath);
|
|
892
1240
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
893
|
-
const aspects =
|
|
894
|
-
const aspectExceptions = parseAspectExceptions(raw.aspect_exceptions, aspects, filePath);
|
|
895
|
-
const anchors = parseAnchors(raw.anchors, filePath);
|
|
1241
|
+
const aspects = parseAspects(raw.aspects, filePath);
|
|
896
1242
|
return {
|
|
897
1243
|
name: raw.name.trim(),
|
|
898
1244
|
type: raw.type.trim(),
|
|
899
1245
|
aspects,
|
|
900
|
-
aspect_exceptions: aspectExceptions,
|
|
901
1246
|
blackbox: raw.blackbox ?? false,
|
|
902
1247
|
relations: relations.length > 0 ? relations : void 0,
|
|
903
|
-
mapping
|
|
904
|
-
anchors
|
|
1248
|
+
mapping
|
|
905
1249
|
};
|
|
906
1250
|
}
|
|
907
|
-
function
|
|
908
|
-
if (raw === void 0 || raw === null) return void 0;
|
|
909
|
-
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
910
|
-
throw new Error(
|
|
911
|
-
`node.yaml at ${filePath}: 'anchors' must be an object mapping aspect ids to arrays of strings`
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
const obj = raw;
|
|
915
|
-
const entries = Object.entries(obj);
|
|
916
|
-
if (entries.length === 0) return void 0;
|
|
917
|
-
const result = {};
|
|
918
|
-
for (const [key, value] of entries) {
|
|
919
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
920
|
-
throw new Error(
|
|
921
|
-
`node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
const strings = value.filter((v) => typeof v === "string");
|
|
925
|
-
if (strings.length === 0) {
|
|
926
|
-
throw new Error(
|
|
927
|
-
`node.yaml at ${filePath}: 'anchors.${key}' must be a non-empty array of strings`
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
result[key] = strings;
|
|
931
|
-
}
|
|
932
|
-
return result;
|
|
933
|
-
}
|
|
934
|
-
function parseAspectExceptions(raw, aspects, filePath) {
|
|
1251
|
+
function parseAspects(raw, filePath) {
|
|
935
1252
|
if (raw === void 0 || raw === null) return void 0;
|
|
936
1253
|
if (!Array.isArray(raw)) {
|
|
937
|
-
throw new Error(`node.yaml at ${filePath}: '
|
|
1254
|
+
throw new Error(`yg-node.yaml at ${filePath}: 'aspects' must be an array`);
|
|
938
1255
|
}
|
|
939
1256
|
if (raw.length === 0) return void 0;
|
|
940
|
-
const aspectSet = new Set(aspects ?? []);
|
|
941
1257
|
const result = [];
|
|
1258
|
+
const seenAspects = /* @__PURE__ */ new Set();
|
|
942
1259
|
for (let i = 0; i < raw.length; i++) {
|
|
943
1260
|
const item = raw[i];
|
|
944
1261
|
if (typeof item !== "object" || item === null) {
|
|
945
|
-
throw new Error(`node.yaml at ${filePath}:
|
|
1262
|
+
throw new Error(`yg-node.yaml at ${filePath}: aspects[${i}] must be an object with 'aspect' key`);
|
|
946
1263
|
}
|
|
947
1264
|
const obj = item;
|
|
948
1265
|
if (typeof obj.aspect !== "string" || obj.aspect.trim() === "") {
|
|
949
1266
|
throw new Error(
|
|
950
|
-
`node.yaml at ${filePath}:
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
if (typeof obj.note !== "string" || obj.note.trim() === "") {
|
|
954
|
-
throw new Error(
|
|
955
|
-
`node.yaml at ${filePath}: aspect_exceptions[${i}].note must be a non-empty string`
|
|
1267
|
+
`yg-node.yaml at ${filePath}: aspects[${i}].aspect must be a non-empty string`
|
|
956
1268
|
);
|
|
957
1269
|
}
|
|
958
1270
|
const aspectId = obj.aspect.trim();
|
|
959
|
-
if (
|
|
1271
|
+
if (seenAspects.has(aspectId)) {
|
|
960
1272
|
throw new Error(
|
|
961
|
-
`node.yaml at ${filePath}:
|
|
1273
|
+
`yg-node.yaml at ${filePath}: duplicate aspect '${aspectId}' in aspects list`
|
|
962
1274
|
);
|
|
963
1275
|
}
|
|
964
|
-
|
|
1276
|
+
seenAspects.add(aspectId);
|
|
1277
|
+
const entry = { aspect: aspectId };
|
|
1278
|
+
if (obj.exceptions !== void 0 && obj.exceptions !== null) {
|
|
1279
|
+
if (!Array.isArray(obj.exceptions)) {
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`yg-node.yaml at ${filePath}: aspects[${i}].exceptions must be an array of strings`
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const exceptions = obj.exceptions.filter((e) => typeof e === "string" && e.trim() !== "");
|
|
1285
|
+
if (exceptions.length > 0) {
|
|
1286
|
+
entry.exceptions = exceptions;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
if (obj.anchors !== void 0 && obj.anchors !== null) {
|
|
1290
|
+
if (!Array.isArray(obj.anchors)) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`yg-node.yaml at ${filePath}: aspects[${i}].anchors must be an array of strings`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const anchors = obj.anchors.filter((a) => typeof a === "string" && a.trim() !== "");
|
|
1296
|
+
if (anchors.length > 0) {
|
|
1297
|
+
entry.anchors = anchors;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
result.push(entry);
|
|
965
1301
|
}
|
|
966
1302
|
return result.length > 0 ? result : void 0;
|
|
967
1303
|
}
|
|
968
|
-
function parseStringArray(val) {
|
|
969
|
-
if (!Array.isArray(val)) return void 0;
|
|
970
|
-
const arr = val.filter((v) => typeof v === "string");
|
|
971
|
-
return arr.length > 0 ? arr : void 0;
|
|
972
|
-
}
|
|
973
1304
|
function parseRelations(raw, filePath) {
|
|
974
1305
|
if (raw === void 0) return [];
|
|
975
1306
|
if (!Array.isArray(raw)) {
|
|
976
|
-
throw new Error(`node.yaml at ${filePath}: 'relations' must be an array`);
|
|
1307
|
+
throw new Error(`yg-node.yaml at ${filePath}: 'relations' must be an array`);
|
|
977
1308
|
}
|
|
978
1309
|
const result = [];
|
|
979
1310
|
for (let index = 0; index < raw.length; index++) {
|
|
980
1311
|
const r = raw[index];
|
|
981
1312
|
if (typeof r !== "object" || r === null) {
|
|
982
|
-
throw new Error(`node.yaml at ${filePath}: relations[${index}] must be an object`);
|
|
1313
|
+
throw new Error(`yg-node.yaml at ${filePath}: relations[${index}] must be an object`);
|
|
983
1314
|
}
|
|
984
1315
|
const obj = r;
|
|
985
1316
|
const target = obj.target;
|
|
986
1317
|
const type = obj.type;
|
|
987
1318
|
if (typeof target !== "string" || target.trim() === "") {
|
|
988
1319
|
throw new Error(
|
|
989
|
-
`node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
|
|
1320
|
+
`yg-node.yaml at ${filePath}: relations[${index}].target must be a non-empty string`
|
|
990
1321
|
);
|
|
991
1322
|
}
|
|
992
1323
|
if (!isValidRelationType(type)) {
|
|
993
|
-
throw new Error(`node.yaml at ${filePath}: relations[${index}].type is invalid`);
|
|
1324
|
+
throw new Error(`yg-node.yaml at ${filePath}: relations[${index}].type is invalid`);
|
|
994
1325
|
}
|
|
995
1326
|
const rel = {
|
|
996
1327
|
target: target.trim(),
|
|
@@ -1012,10 +1343,10 @@ function parseRelations(raw, filePath) {
|
|
|
1012
1343
|
function validateRelativePath(pathValue, filePath, fieldName) {
|
|
1013
1344
|
const normalized = pathValue.trim();
|
|
1014
1345
|
if (normalized === "") {
|
|
1015
|
-
throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
|
|
1346
|
+
throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be non-empty`);
|
|
1016
1347
|
}
|
|
1017
1348
|
if (normalized.startsWith("/")) {
|
|
1018
|
-
throw new Error(`node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
|
|
1349
|
+
throw new Error(`yg-node.yaml at ${filePath}: '${fieldName}' must be relative to repository root`);
|
|
1019
1350
|
}
|
|
1020
1351
|
return normalized;
|
|
1021
1352
|
}
|
|
@@ -1025,35 +1356,35 @@ function parseMapping(rawMapping, filePath) {
|
|
|
1025
1356
|
if (Array.isArray(obj.paths) && obj.paths.length > 0) {
|
|
1026
1357
|
const paths = obj.paths.filter((p) => typeof p === "string").map((p) => validateRelativePath(p, filePath, "mapping.paths[]"));
|
|
1027
1358
|
if (paths.length === 0) {
|
|
1028
|
-
throw new Error(`node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
|
|
1359
|
+
throw new Error(`yg-node.yaml at ${filePath}: mapping.paths must be a non-empty array`);
|
|
1029
1360
|
}
|
|
1030
1361
|
return { paths };
|
|
1031
1362
|
}
|
|
1032
1363
|
if (obj.paths !== void 0 || obj.type !== void 0 || obj.path !== void 0) {
|
|
1033
1364
|
throw new Error(
|
|
1034
|
-
`node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
|
|
1365
|
+
`yg-node.yaml at ${filePath}: mapping must have paths (array of file/directory paths)`
|
|
1035
1366
|
);
|
|
1036
1367
|
}
|
|
1037
1368
|
return void 0;
|
|
1038
1369
|
}
|
|
1039
1370
|
|
|
1040
1371
|
// src/io/aspect-parser.ts
|
|
1041
|
-
import { readFile as
|
|
1042
|
-
import { parse as
|
|
1372
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1373
|
+
import { parse as parseYaml5 } from "yaml";
|
|
1043
1374
|
|
|
1044
1375
|
// src/io/artifact-reader.ts
|
|
1045
|
-
import { readFile as
|
|
1046
|
-
import
|
|
1047
|
-
async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles) {
|
|
1048
|
-
const entries = await
|
|
1376
|
+
import { readFile as readFile7, readdir as readdir3 } from "fs/promises";
|
|
1377
|
+
import path5 from "path";
|
|
1378
|
+
async function readArtifacts(dirPath, excludeFiles = ["yg-node.yaml"], includeFiles) {
|
|
1379
|
+
const entries = await readdir3(dirPath, { withFileTypes: true });
|
|
1049
1380
|
const artifacts = [];
|
|
1050
1381
|
const includeSet = includeFiles && includeFiles.length > 0 ? new Set(includeFiles) : null;
|
|
1051
1382
|
for (const entry of entries) {
|
|
1052
1383
|
if (!entry.isFile()) continue;
|
|
1053
1384
|
if (excludeFiles.includes(entry.name)) continue;
|
|
1054
1385
|
if (includeSet && !includeSet.has(entry.name)) continue;
|
|
1055
|
-
const filePath =
|
|
1056
|
-
const content = await
|
|
1386
|
+
const filePath = path5.join(dirPath, entry.name);
|
|
1387
|
+
const content = await readFile7(filePath, "utf-8");
|
|
1057
1388
|
artifacts.push({ filename: entry.name, content });
|
|
1058
1389
|
}
|
|
1059
1390
|
artifacts.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
@@ -1067,8 +1398,8 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1067
1398
|
if (!idTrimmed) {
|
|
1068
1399
|
throw new Error(`Aspect id must be non-empty (relative path in aspects/)`);
|
|
1069
1400
|
}
|
|
1070
|
-
const content = await
|
|
1071
|
-
const raw =
|
|
1401
|
+
const content = await readFile8(aspectYamlPath, "utf-8");
|
|
1402
|
+
const raw = parseYaml5(content);
|
|
1072
1403
|
if (!raw || typeof raw !== "object") {
|
|
1073
1404
|
throw new Error(`Aspect file ${aspectYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1074
1405
|
}
|
|
@@ -1076,7 +1407,7 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1076
1407
|
throw new Error(`Aspect file ${aspectYamlPath}: missing or empty 'name'`);
|
|
1077
1408
|
}
|
|
1078
1409
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1079
|
-
const artifacts = await readArtifacts(aspectDir, ["aspect.yaml"]);
|
|
1410
|
+
const artifacts = await readArtifacts(aspectDir, ["yg-aspect.yaml"]);
|
|
1080
1411
|
let implies;
|
|
1081
1412
|
if (raw.implies !== void 0) {
|
|
1082
1413
|
if (!Array.isArray(raw.implies)) {
|
|
@@ -1104,37 +1435,37 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
1104
1435
|
}
|
|
1105
1436
|
|
|
1106
1437
|
// src/io/flow-parser.ts
|
|
1107
|
-
import { readFile as
|
|
1108
|
-
import
|
|
1109
|
-
import { parse as
|
|
1438
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1439
|
+
import path6 from "path";
|
|
1440
|
+
import { parse as parseYaml6 } from "yaml";
|
|
1110
1441
|
async function parseFlow(flowDir, flowYamlPath) {
|
|
1111
|
-
const content = await
|
|
1112
|
-
const raw =
|
|
1442
|
+
const content = await readFile9(flowYamlPath, "utf-8");
|
|
1443
|
+
const raw = parseYaml6(content);
|
|
1113
1444
|
if (!raw || typeof raw !== "object") {
|
|
1114
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1445
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: file is empty or not a valid YAML mapping`);
|
|
1115
1446
|
}
|
|
1116
1447
|
if (!raw.name || typeof raw.name !== "string" || raw.name.trim() === "") {
|
|
1117
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
|
|
1448
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: missing or empty 'name'`);
|
|
1118
1449
|
}
|
|
1119
1450
|
const nodes = raw.nodes;
|
|
1120
1451
|
if (!Array.isArray(nodes) || nodes.length === 0) {
|
|
1121
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
|
|
1452
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must be a non-empty array`);
|
|
1122
1453
|
}
|
|
1123
1454
|
const nodePaths = nodes.filter((n) => typeof n === "string");
|
|
1124
1455
|
if (nodePaths.length === 0) {
|
|
1125
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
|
|
1456
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'nodes' must contain string node paths`);
|
|
1126
1457
|
}
|
|
1127
1458
|
let aspects;
|
|
1128
1459
|
if (raw.aspects !== void 0) {
|
|
1129
1460
|
if (!Array.isArray(raw.aspects)) {
|
|
1130
|
-
throw new Error(`flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
1461
|
+
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
1131
1462
|
}
|
|
1132
1463
|
const aspectTags = raw.aspects.filter((a) => typeof a === "string");
|
|
1133
1464
|
aspects = aspectTags.length > 0 ? aspectTags : [];
|
|
1134
1465
|
}
|
|
1135
|
-
const artifacts = await readArtifacts(flowDir, ["flow.yaml"]);
|
|
1466
|
+
const artifacts = await readArtifacts(flowDir, ["yg-flow.yaml"]);
|
|
1136
1467
|
return {
|
|
1137
|
-
path:
|
|
1468
|
+
path: path6.basename(flowDir),
|
|
1138
1469
|
name: raw.name.trim(),
|
|
1139
1470
|
nodes: nodePaths,
|
|
1140
1471
|
...aspects !== void 0 && { aspects },
|
|
@@ -1143,27 +1474,28 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
1143
1474
|
}
|
|
1144
1475
|
|
|
1145
1476
|
// src/io/schema-parser.ts
|
|
1146
|
-
import { readFile as
|
|
1147
|
-
import
|
|
1148
|
-
import { parse as
|
|
1477
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1478
|
+
import path7 from "path";
|
|
1479
|
+
import { parse as parseYaml7 } from "yaml";
|
|
1149
1480
|
async function parseSchema(filePath) {
|
|
1150
|
-
const content = await
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1481
|
+
const content = await readFile10(filePath, "utf-8");
|
|
1482
|
+
parseYaml7(content);
|
|
1483
|
+
const rawName = path7.basename(filePath, path7.extname(filePath));
|
|
1484
|
+
const schemaType = rawName.startsWith("yg-") ? rawName.slice(3) : rawName;
|
|
1153
1485
|
return { schemaType };
|
|
1154
1486
|
}
|
|
1155
1487
|
|
|
1156
1488
|
// src/utils/paths.ts
|
|
1157
|
-
import
|
|
1489
|
+
import path8 from "path";
|
|
1158
1490
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1159
|
-
import { stat as
|
|
1491
|
+
import { stat as stat3 } from "fs/promises";
|
|
1160
1492
|
async function findYggRoot(projectRoot) {
|
|
1161
|
-
let current =
|
|
1162
|
-
const root =
|
|
1493
|
+
let current = path8.resolve(projectRoot);
|
|
1494
|
+
const root = path8.parse(current).root;
|
|
1163
1495
|
while (true) {
|
|
1164
|
-
const yggPath =
|
|
1496
|
+
const yggPath = path8.join(current, ".yggdrasil");
|
|
1165
1497
|
try {
|
|
1166
|
-
const st = await
|
|
1498
|
+
const st = await stat3(yggPath);
|
|
1167
1499
|
if (!st.isDirectory()) {
|
|
1168
1500
|
throw new Error(
|
|
1169
1501
|
`.yggdrasil exists but is not a directory (${yggPath}). Run 'yg init' in a clean location.`
|
|
@@ -1175,7 +1507,7 @@ async function findYggRoot(projectRoot) {
|
|
|
1175
1507
|
if (current === root) {
|
|
1176
1508
|
throw new Error(`No .yggdrasil/ directory found. Run 'yg init' first.`, { cause: err });
|
|
1177
1509
|
}
|
|
1178
|
-
current =
|
|
1510
|
+
current = path8.dirname(current);
|
|
1179
1511
|
continue;
|
|
1180
1512
|
}
|
|
1181
1513
|
throw err;
|
|
@@ -1191,27 +1523,25 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
1191
1523
|
if (normalizedInput.length === 0) {
|
|
1192
1524
|
throw new Error("Path cannot be empty");
|
|
1193
1525
|
}
|
|
1194
|
-
const absolute =
|
|
1195
|
-
const relative =
|
|
1196
|
-
const isOutside = relative.startsWith("..") ||
|
|
1526
|
+
const absolute = path8.resolve(projectRoot, normalizedInput);
|
|
1527
|
+
const relative = path8.relative(projectRoot, absolute);
|
|
1528
|
+
const isOutside = relative.startsWith("..") || path8.isAbsolute(relative);
|
|
1197
1529
|
if (isOutside) {
|
|
1198
1530
|
throw new Error(`Path is outside project root: ${rawPath}`);
|
|
1199
1531
|
}
|
|
1200
|
-
return relative.split(
|
|
1532
|
+
return relative.split(path8.sep).join("/");
|
|
1201
1533
|
}
|
|
1202
1534
|
function projectRootFromGraph(yggRootPath) {
|
|
1203
|
-
return
|
|
1535
|
+
return path8.dirname(yggRootPath);
|
|
1204
1536
|
}
|
|
1205
1537
|
|
|
1206
1538
|
// src/core/graph-loader.ts
|
|
1207
1539
|
function toModelPath(absolutePath, modelDir) {
|
|
1208
|
-
return
|
|
1540
|
+
return path9.relative(modelDir, absolutePath).split(path9.sep).join("/");
|
|
1209
1541
|
}
|
|
1210
1542
|
var FALLBACK_CONFIG = {
|
|
1211
1543
|
name: "",
|
|
1212
|
-
|
|
1213
|
-
standards: "",
|
|
1214
|
-
node_types: [],
|
|
1544
|
+
node_types: {},
|
|
1215
1545
|
artifacts: {}
|
|
1216
1546
|
};
|
|
1217
1547
|
async function loadGraph(projectRoot, options = {}) {
|
|
@@ -1219,14 +1549,14 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1219
1549
|
let configError;
|
|
1220
1550
|
let config = FALLBACK_CONFIG;
|
|
1221
1551
|
try {
|
|
1222
|
-
config = await parseConfig(
|
|
1552
|
+
config = await parseConfig(path9.join(yggRoot, "yg-config.yaml"));
|
|
1223
1553
|
} catch (error) {
|
|
1224
1554
|
if (!options.tolerateInvalidConfig) {
|
|
1225
1555
|
throw error;
|
|
1226
1556
|
}
|
|
1227
1557
|
configError = error.message;
|
|
1228
1558
|
}
|
|
1229
|
-
const modelDir =
|
|
1559
|
+
const modelDir = path9.join(yggRoot, "model");
|
|
1230
1560
|
const nodes = /* @__PURE__ */ new Map();
|
|
1231
1561
|
const nodeParseErrors = [];
|
|
1232
1562
|
const artifactFilenames = Object.keys(config.artifacts ?? {});
|
|
@@ -1240,9 +1570,9 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1240
1570
|
}
|
|
1241
1571
|
throw err;
|
|
1242
1572
|
}
|
|
1243
|
-
const aspects = await loadAspects(
|
|
1244
|
-
const flows = await loadFlows(
|
|
1245
|
-
const schemas = await loadSchemas(
|
|
1573
|
+
const aspects = await loadAspects(path9.join(yggRoot, "aspects"));
|
|
1574
|
+
const flows = await loadFlows(path9.join(yggRoot, "flows"));
|
|
1575
|
+
const schemas = await loadSchemas(path9.join(yggRoot, "schemas"));
|
|
1246
1576
|
return {
|
|
1247
1577
|
config,
|
|
1248
1578
|
configError,
|
|
@@ -1255,18 +1585,18 @@ async function loadGraph(projectRoot, options = {}) {
|
|
|
1255
1585
|
};
|
|
1256
1586
|
}
|
|
1257
1587
|
async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErrors, artifactFilenames) {
|
|
1258
|
-
const entries = await
|
|
1259
|
-
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
1588
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
1589
|
+
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
1260
1590
|
if (!hasNodeYaml && dirPath !== modelDir) {
|
|
1261
1591
|
return;
|
|
1262
1592
|
}
|
|
1263
1593
|
if (hasNodeYaml) {
|
|
1264
1594
|
const graphPath = toModelPath(dirPath, modelDir);
|
|
1265
|
-
const nodeYamlPath =
|
|
1595
|
+
const nodeYamlPath = path9.join(dirPath, "yg-node.yaml");
|
|
1266
1596
|
let meta;
|
|
1267
1597
|
let nodeYamlRaw;
|
|
1268
1598
|
try {
|
|
1269
|
-
nodeYamlRaw = await
|
|
1599
|
+
nodeYamlRaw = await readFile11(nodeYamlPath, "utf-8");
|
|
1270
1600
|
meta = await parseNodeYaml(nodeYamlPath);
|
|
1271
1601
|
} catch (err) {
|
|
1272
1602
|
nodeParseErrors.push({
|
|
@@ -1275,7 +1605,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1275
1605
|
});
|
|
1276
1606
|
return;
|
|
1277
1607
|
}
|
|
1278
|
-
const artifacts = await readArtifacts(dirPath, ["node.yaml"], artifactFilenames);
|
|
1608
|
+
const artifacts = await readArtifacts(dirPath, ["yg-node.yaml"], artifactFilenames);
|
|
1279
1609
|
const node = {
|
|
1280
1610
|
path: graphPath,
|
|
1281
1611
|
meta,
|
|
@@ -1292,7 +1622,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1292
1622
|
if (!entry.isDirectory()) continue;
|
|
1293
1623
|
if (entry.name.startsWith(".")) continue;
|
|
1294
1624
|
await scanModelDirectory(
|
|
1295
|
-
|
|
1625
|
+
path9.join(dirPath, entry.name),
|
|
1296
1626
|
modelDir,
|
|
1297
1627
|
node,
|
|
1298
1628
|
nodes,
|
|
@@ -1305,7 +1635,7 @@ async function scanModelDirectory(dirPath, modelDir, parent, nodes, nodeParseErr
|
|
|
1305
1635
|
if (!entry.isDirectory()) continue;
|
|
1306
1636
|
if (entry.name.startsWith(".")) continue;
|
|
1307
1637
|
await scanModelDirectory(
|
|
1308
|
-
|
|
1638
|
+
path9.join(dirPath, entry.name),
|
|
1309
1639
|
modelDir,
|
|
1310
1640
|
null,
|
|
1311
1641
|
nodes,
|
|
@@ -1325,28 +1655,28 @@ async function loadAspects(aspectsDir) {
|
|
|
1325
1655
|
}
|
|
1326
1656
|
}
|
|
1327
1657
|
async function scanAspectsDirectory(dirPath, aspectsRoot, aspects) {
|
|
1328
|
-
const entries = await
|
|
1329
|
-
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "aspect.yaml");
|
|
1658
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
1659
|
+
const hasAspectYaml = entries.some((e) => e.isFile() && e.name === "yg-aspect.yaml");
|
|
1330
1660
|
if (hasAspectYaml) {
|
|
1331
|
-
const id =
|
|
1332
|
-
const aspectYamlPath =
|
|
1661
|
+
const id = path9.relative(aspectsRoot, dirPath).split(path9.sep).join("/");
|
|
1662
|
+
const aspectYamlPath = path9.join(dirPath, "yg-aspect.yaml");
|
|
1333
1663
|
const aspect = await parseAspect(dirPath, aspectYamlPath, id);
|
|
1334
1664
|
aspects.push(aspect);
|
|
1335
1665
|
}
|
|
1336
1666
|
for (const entry of entries) {
|
|
1337
1667
|
if (!entry.isDirectory()) continue;
|
|
1338
1668
|
if (entry.name.startsWith(".")) continue;
|
|
1339
|
-
await scanAspectsDirectory(
|
|
1669
|
+
await scanAspectsDirectory(path9.join(dirPath, entry.name), aspectsRoot, aspects);
|
|
1340
1670
|
}
|
|
1341
1671
|
}
|
|
1342
1672
|
async function loadFlows(flowsDir) {
|
|
1343
1673
|
try {
|
|
1344
|
-
const entries = await
|
|
1674
|
+
const entries = await readdir4(flowsDir, { withFileTypes: true });
|
|
1345
1675
|
const flows = [];
|
|
1346
1676
|
for (const entry of entries) {
|
|
1347
1677
|
if (!entry.isDirectory()) continue;
|
|
1348
|
-
const flowYamlPath =
|
|
1349
|
-
const flow = await parseFlow(
|
|
1678
|
+
const flowYamlPath = path9.join(flowsDir, entry.name, "yg-flow.yaml");
|
|
1679
|
+
const flow = await parseFlow(path9.join(flowsDir, entry.name), flowYamlPath);
|
|
1350
1680
|
flows.push(flow);
|
|
1351
1681
|
}
|
|
1352
1682
|
return flows;
|
|
@@ -1356,12 +1686,12 @@ async function loadFlows(flowsDir) {
|
|
|
1356
1686
|
}
|
|
1357
1687
|
async function loadSchemas(schemasDir) {
|
|
1358
1688
|
try {
|
|
1359
|
-
const entries = await
|
|
1689
|
+
const entries = await readdir4(schemasDir, { withFileTypes: true });
|
|
1360
1690
|
const schemas = [];
|
|
1361
1691
|
for (const entry of entries) {
|
|
1362
1692
|
if (!entry.isFile()) continue;
|
|
1363
1693
|
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
|
|
1364
|
-
const s = await parseSchema(
|
|
1694
|
+
const s = await parseSchema(path9.join(schemasDir, entry.name));
|
|
1365
1695
|
schemas.push(s);
|
|
1366
1696
|
}
|
|
1367
1697
|
return schemas;
|
|
@@ -1371,8 +1701,8 @@ async function loadSchemas(schemasDir) {
|
|
|
1371
1701
|
}
|
|
1372
1702
|
|
|
1373
1703
|
// src/core/context-builder.ts
|
|
1374
|
-
import { readFile as
|
|
1375
|
-
import
|
|
1704
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
1705
|
+
import path10 from "path";
|
|
1376
1706
|
|
|
1377
1707
|
// src/utils/tokens.ts
|
|
1378
1708
|
function estimateTokens(text) {
|
|
@@ -1421,8 +1751,9 @@ async function buildContext(graph, nodePath) {
|
|
|
1421
1751
|
}
|
|
1422
1752
|
const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
|
|
1423
1753
|
for (const aspect of aspectsToInclude) {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1754
|
+
const entry = node.meta.aspects?.find((a) => a.aspect === aspect.id);
|
|
1755
|
+
const exceptionNote = entry?.exceptions?.join("; ");
|
|
1756
|
+
layers.push(buildAspectLayer(aspect, exceptionNote));
|
|
1426
1757
|
}
|
|
1427
1758
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
1428
1759
|
const tokenCount = estimateTokens(fullText);
|
|
@@ -1479,18 +1810,7 @@ function resolveAspects(aspectIds, aspects) {
|
|
|
1479
1810
|
return expandedIds.map((id) => idToAspect.get(id)).filter((a) => a !== void 0);
|
|
1480
1811
|
}
|
|
1481
1812
|
function buildGlobalLayer(config) {
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
`;
|
|
1485
|
-
content += `**Stack:**
|
|
1486
|
-
`;
|
|
1487
|
-
for (const [key, value] of Object.entries(config.stack)) {
|
|
1488
|
-
content += `- ${key}: ${value}
|
|
1489
|
-
`;
|
|
1490
|
-
}
|
|
1491
|
-
content += `
|
|
1492
|
-
**Standards:**
|
|
1493
|
-
${config.standards || "(none)"}
|
|
1813
|
+
const content = `**Project:** ${config.name}
|
|
1494
1814
|
`;
|
|
1495
1815
|
return { type: "global", label: "Global Context", content };
|
|
1496
1816
|
}
|
|
@@ -1502,7 +1822,7 @@ function buildHierarchyLayer(ancestor, config, graph) {
|
|
|
1502
1822
|
const filtered = filterArtifactsByConfig(ancestor.artifacts, config);
|
|
1503
1823
|
const content = filtered.map((a) => `### ${a.filename}
|
|
1504
1824
|
${a.content}`).join("\n\n");
|
|
1505
|
-
const nodeAspects = ancestor.meta.aspects ?? [];
|
|
1825
|
+
const nodeAspects = (ancestor.meta.aspects ?? []).map((a) => a.aspect);
|
|
1506
1826
|
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1507
1827
|
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1508
1828
|
return {
|
|
@@ -1515,16 +1835,16 @@ ${a.content}`).join("\n\n");
|
|
|
1515
1835
|
async function buildOwnLayer(node, config, graphRootPath, graph) {
|
|
1516
1836
|
const parts = [];
|
|
1517
1837
|
if (node.nodeYamlRaw) {
|
|
1518
|
-
parts.push(`### node.yaml
|
|
1838
|
+
parts.push(`### yg-node.yaml
|
|
1519
1839
|
${node.nodeYamlRaw.trim()}`);
|
|
1520
1840
|
} else {
|
|
1521
|
-
const nodeYamlPath =
|
|
1841
|
+
const nodeYamlPath = path10.join(graphRootPath, "model", node.path, "yg-node.yaml");
|
|
1522
1842
|
try {
|
|
1523
|
-
const nodeYamlContent = await
|
|
1524
|
-
parts.push(`### node.yaml
|
|
1843
|
+
const nodeYamlContent = await readFile12(nodeYamlPath, "utf-8");
|
|
1844
|
+
parts.push(`### yg-node.yaml
|
|
1525
1845
|
${nodeYamlContent.trim()}`);
|
|
1526
1846
|
} catch {
|
|
1527
|
-
parts.push(`### node.yaml
|
|
1847
|
+
parts.push(`### yg-node.yaml
|
|
1528
1848
|
(not found)`);
|
|
1529
1849
|
}
|
|
1530
1850
|
}
|
|
@@ -1534,7 +1854,7 @@ ${nodeYamlContent.trim()}`);
|
|
|
1534
1854
|
${a.content}`);
|
|
1535
1855
|
}
|
|
1536
1856
|
const content = parts.join("\n\n");
|
|
1537
|
-
const nodeAspects = node.meta.aspects ?? [];
|
|
1857
|
+
const nodeAspects = (node.meta.aspects ?? []).map((a) => a.aspect);
|
|
1538
1858
|
const expanded = expandAspects(nodeAspects, graph.aspects);
|
|
1539
1859
|
const attrs = expanded.length > 0 ? { aspects: expanded.join(",") } : void 0;
|
|
1540
1860
|
return {
|
|
@@ -1556,7 +1876,7 @@ function buildStructuralRelationLayer(target, relation, config) {
|
|
|
1556
1876
|
|
|
1557
1877
|
`;
|
|
1558
1878
|
}
|
|
1559
|
-
const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.
|
|
1879
|
+
const structuralArtifactFilenames = Object.entries(config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
1560
1880
|
const structuralArts = structuralArtifactFilenames.map((filename) => {
|
|
1561
1881
|
const art = target.artifacts.find((a) => a.filename === filename);
|
|
1562
1882
|
return art ? { filename: art.filename, content: art.content } : null;
|
|
@@ -1671,10 +1991,10 @@ function collectAncestors(node) {
|
|
|
1671
1991
|
function collectEffectiveAspectIds(graph, nodePath) {
|
|
1672
1992
|
const node = graph.nodes.get(nodePath);
|
|
1673
1993
|
if (!node) return /* @__PURE__ */ new Set();
|
|
1674
|
-
const raw = new Set(node.meta.aspects ?? []);
|
|
1994
|
+
const raw = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
|
|
1675
1995
|
let ancestor = node.parent;
|
|
1676
1996
|
while (ancestor) {
|
|
1677
|
-
for (const
|
|
1997
|
+
for (const entry of ancestor.meta.aspects ?? []) raw.add(entry.aspect);
|
|
1678
1998
|
ancestor = ancestor.parent;
|
|
1679
1999
|
}
|
|
1680
2000
|
const ancestorPaths = /* @__PURE__ */ new Set([nodePath, ...collectAncestors(node).map((a) => a.path)]);
|
|
@@ -1687,8 +2007,11 @@ function collectEffectiveAspectIds(graph, nodePath) {
|
|
|
1687
2007
|
}
|
|
1688
2008
|
|
|
1689
2009
|
// src/core/validator.ts
|
|
1690
|
-
import { readdir as
|
|
1691
|
-
import
|
|
2010
|
+
import { readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
|
|
2011
|
+
import path11 from "path";
|
|
2012
|
+
function getAspectIds(aspects) {
|
|
2013
|
+
return (aspects ?? []).map((a) => a.aspect);
|
|
2014
|
+
}
|
|
1692
2015
|
var RESERVED_DIRS = /* @__PURE__ */ new Set();
|
|
1693
2016
|
async function validate(graph, scope = "all") {
|
|
1694
2017
|
const issues = [];
|
|
@@ -1717,7 +2040,6 @@ async function validate(graph, scope = "all") {
|
|
|
1717
2040
|
issues.push(...checkImpliedAspectsExist(graph));
|
|
1718
2041
|
issues.push(...checkImpliesNoCycles(graph));
|
|
1719
2042
|
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
1720
|
-
issues.push(...checkAspectExceptions(graph));
|
|
1721
2043
|
issues.push(...await checkAnchorPresence(graph));
|
|
1722
2044
|
issues.push(...checkRequiredArtifacts(graph));
|
|
1723
2045
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
@@ -1766,7 +2088,7 @@ async function validate(graph, scope = "all") {
|
|
|
1766
2088
|
}
|
|
1767
2089
|
function checkNodeTypes(graph) {
|
|
1768
2090
|
const issues = [];
|
|
1769
|
-
const allowedTypes = new Set((graph.config.node_types ??
|
|
2091
|
+
const allowedTypes = new Set(Object.keys(graph.config.node_types ?? {}));
|
|
1770
2092
|
for (const [nodePath, node] of graph.nodes) {
|
|
1771
2093
|
if (!allowedTypes.has(node.meta.type)) {
|
|
1772
2094
|
issues.push({
|
|
@@ -1833,7 +2155,7 @@ function checkAspectsDefined(graph) {
|
|
|
1833
2155
|
const issues = [];
|
|
1834
2156
|
const validAspectIds = new Set(graph.aspects.map((a) => a.id));
|
|
1835
2157
|
for (const [nodePath, node] of graph.nodes) {
|
|
1836
|
-
for (const aspectId of node.meta.aspects
|
|
2158
|
+
for (const aspectId of getAspectIds(node.meta.aspects)) {
|
|
1837
2159
|
if (!validAspectIds.has(aspectId)) {
|
|
1838
2160
|
issues.push({
|
|
1839
2161
|
severity: "error",
|
|
@@ -1937,16 +2259,16 @@ function checkImpliesNoCycles(graph) {
|
|
|
1937
2259
|
function checkRequiredAspectsCoverage(graph) {
|
|
1938
2260
|
const issues = [];
|
|
1939
2261
|
const typeConfig = new Map(
|
|
1940
|
-
(graph.config.node_types ??
|
|
2262
|
+
Object.entries(graph.config.node_types ?? {}).map(([name, cfg]) => [name, cfg.required_aspects ?? []])
|
|
1941
2263
|
);
|
|
1942
2264
|
for (const [nodePath, node] of graph.nodes) {
|
|
1943
2265
|
if (node.meta.blackbox) continue;
|
|
1944
2266
|
const requiredAspects = typeConfig.get(node.meta.type);
|
|
1945
2267
|
if (!requiredAspects || requiredAspects.length === 0) continue;
|
|
1946
|
-
const
|
|
2268
|
+
const nodeAspectIds = getAspectIds(node.meta.aspects);
|
|
1947
2269
|
let effectiveAspects;
|
|
1948
2270
|
try {
|
|
1949
|
-
effectiveAspects = resolveAspects(
|
|
2271
|
+
effectiveAspects = resolveAspects(nodeAspectIds, graph.aspects);
|
|
1950
2272
|
} catch {
|
|
1951
2273
|
continue;
|
|
1952
2274
|
}
|
|
@@ -1965,24 +2287,6 @@ function checkRequiredAspectsCoverage(graph) {
|
|
|
1965
2287
|
}
|
|
1966
2288
|
return issues;
|
|
1967
2289
|
}
|
|
1968
|
-
function checkAspectExceptions(graph) {
|
|
1969
|
-
const issues = [];
|
|
1970
|
-
for (const [nodePath, node] of graph.nodes) {
|
|
1971
|
-
for (const exception of node.meta.aspect_exceptions ?? []) {
|
|
1972
|
-
const nodeAspects = node.meta.aspects ?? [];
|
|
1973
|
-
if (!nodeAspects.includes(exception.aspect)) {
|
|
1974
|
-
issues.push({
|
|
1975
|
-
severity: "error",
|
|
1976
|
-
code: "E018",
|
|
1977
|
-
rule: "invalid-aspect-exception",
|
|
1978
|
-
message: `aspect_exceptions references aspect '${exception.aspect}' which is not in this node's aspects list (${nodeAspects.join(", ") || "none"})`,
|
|
1979
|
-
nodePath
|
|
1980
|
-
});
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
return issues;
|
|
1985
|
-
}
|
|
1986
2290
|
function checkNoCycles(graph) {
|
|
1987
2291
|
const WHITE = 0;
|
|
1988
2292
|
const GRAY = 1;
|
|
@@ -2069,14 +2373,14 @@ function checkMappingOverlap(graph) {
|
|
|
2069
2373
|
}
|
|
2070
2374
|
async function checkMappingPathsExist(graph) {
|
|
2071
2375
|
const issues = [];
|
|
2072
|
-
const projectRoot =
|
|
2073
|
-
const { access:
|
|
2376
|
+
const projectRoot = path11.dirname(graph.rootPath);
|
|
2377
|
+
const { access: access5 } = await import("fs/promises");
|
|
2074
2378
|
for (const [nodePath, node] of graph.nodes) {
|
|
2075
2379
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2076
2380
|
for (const mp of mappingPaths) {
|
|
2077
|
-
const absPath =
|
|
2381
|
+
const absPath = path11.join(projectRoot, mp);
|
|
2078
2382
|
try {
|
|
2079
|
-
await
|
|
2383
|
+
await access5(absPath);
|
|
2080
2384
|
} catch {
|
|
2081
2385
|
issues.push({
|
|
2082
2386
|
severity: "warning",
|
|
@@ -2116,7 +2420,7 @@ function artifactRequiredReason(graph, nodePath, node, required) {
|
|
|
2116
2420
|
if (when.startsWith("has_aspect:") || when.startsWith("has_tag:")) {
|
|
2117
2421
|
const prefix = when.startsWith("has_aspect:") ? "has_aspect:" : "has_tag:";
|
|
2118
2422
|
const aspectId = when.slice(prefix.length);
|
|
2119
|
-
return (node.meta.aspects ?? []).
|
|
2423
|
+
return (node.meta.aspects ?? []).some((a) => a.aspect === aspectId) ? `node has aspect '${aspectId}'` : null;
|
|
2120
2424
|
}
|
|
2121
2425
|
return null;
|
|
2122
2426
|
}
|
|
@@ -2307,7 +2611,7 @@ function checkSchemas(graph) {
|
|
|
2307
2611
|
severity: "warning",
|
|
2308
2612
|
code: "W010",
|
|
2309
2613
|
rule: "missing-schema",
|
|
2310
|
-
message: `Schema '
|
|
2614
|
+
message: `Schema 'yg-${required}.yaml' missing from .yggdrasil/schemas/`
|
|
2311
2615
|
});
|
|
2312
2616
|
}
|
|
2313
2617
|
}
|
|
@@ -2315,11 +2619,11 @@ function checkSchemas(graph) {
|
|
|
2315
2619
|
}
|
|
2316
2620
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
2317
2621
|
const issues = [];
|
|
2318
|
-
const modelDir =
|
|
2622
|
+
const modelDir = path11.join(graph.rootPath, "model");
|
|
2319
2623
|
async function scanDir(dirPath, segments) {
|
|
2320
|
-
const entries = await
|
|
2321
|
-
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "node.yaml");
|
|
2322
|
-
const dirName =
|
|
2624
|
+
const entries = await readdir5(dirPath, { withFileTypes: true });
|
|
2625
|
+
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
2626
|
+
const dirName = path11.basename(dirPath);
|
|
2323
2627
|
if (RESERVED_DIRS.has(dirName)) return;
|
|
2324
2628
|
const hasFiles = entries.some((e) => e.isFile());
|
|
2325
2629
|
const hasSubdirs = entries.some((e) => e.isDirectory() && !RESERVED_DIRS.has(e.name) && !e.name.startsWith("."));
|
|
@@ -2330,7 +2634,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2330
2634
|
severity: "error",
|
|
2331
2635
|
code: "E015",
|
|
2332
2636
|
rule: "missing-node-yaml",
|
|
2333
|
-
message: `Directory '${graphPath}' has files but no node.yaml`,
|
|
2637
|
+
message: `Directory '${graphPath}' has files but no yg-node.yaml`,
|
|
2334
2638
|
nodePath: graphPath
|
|
2335
2639
|
});
|
|
2336
2640
|
} else if (hasSubdirs) {
|
|
@@ -2338,7 +2642,7 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2338
2642
|
severity: "warning",
|
|
2339
2643
|
code: "W013",
|
|
2340
2644
|
rule: "directory-without-node",
|
|
2341
|
-
message: `Directory '${graphPath}' has subdirectories but no node.yaml \u2014 consider creating a node`,
|
|
2645
|
+
message: `Directory '${graphPath}' has subdirectories but no yg-node.yaml \u2014 consider creating a node`,
|
|
2342
2646
|
nodePath: graphPath
|
|
2343
2647
|
});
|
|
2344
2648
|
}
|
|
@@ -2347,15 +2651,15 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2347
2651
|
if (!entry.isDirectory()) continue;
|
|
2348
2652
|
if (RESERVED_DIRS.has(entry.name)) continue;
|
|
2349
2653
|
if (entry.name.startsWith(".")) continue;
|
|
2350
|
-
await scanDir(
|
|
2654
|
+
await scanDir(path11.join(dirPath, entry.name), [...segments, entry.name]);
|
|
2351
2655
|
}
|
|
2352
2656
|
}
|
|
2353
2657
|
try {
|
|
2354
|
-
const rootEntries = await
|
|
2658
|
+
const rootEntries = await readdir5(modelDir, { withFileTypes: true });
|
|
2355
2659
|
for (const entry of rootEntries) {
|
|
2356
2660
|
if (!entry.isDirectory()) continue;
|
|
2357
2661
|
if (entry.name.startsWith(".")) continue;
|
|
2358
|
-
await scanDir(
|
|
2662
|
+
await scanDir(path11.join(modelDir, entry.name), [entry.name]);
|
|
2359
2663
|
}
|
|
2360
2664
|
} catch {
|
|
2361
2665
|
}
|
|
@@ -2365,14 +2669,14 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
2365
2669
|
const files = [];
|
|
2366
2670
|
async function collectFiles(absPath) {
|
|
2367
2671
|
try {
|
|
2368
|
-
const s = await
|
|
2672
|
+
const s = await stat4(absPath);
|
|
2369
2673
|
if (s.isFile()) {
|
|
2370
2674
|
files.push(absPath);
|
|
2371
2675
|
} else if (s.isDirectory()) {
|
|
2372
|
-
const entries = await
|
|
2676
|
+
const entries = await readdir5(absPath, { withFileTypes: true });
|
|
2373
2677
|
for (const entry of entries) {
|
|
2374
2678
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2375
|
-
const entryPath =
|
|
2679
|
+
const entryPath = path11.join(absPath, entry.name);
|
|
2376
2680
|
if (entry.isFile()) {
|
|
2377
2681
|
files.push(entryPath);
|
|
2378
2682
|
} else if (entry.isDirectory()) {
|
|
@@ -2384,28 +2688,16 @@ async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
|
2384
2688
|
}
|
|
2385
2689
|
}
|
|
2386
2690
|
for (const mp of mappingPaths) {
|
|
2387
|
-
await collectFiles(
|
|
2691
|
+
await collectFiles(path11.join(projectRoot, mp));
|
|
2388
2692
|
}
|
|
2389
2693
|
return files;
|
|
2390
2694
|
}
|
|
2391
2695
|
async function checkAnchorPresence(graph) {
|
|
2392
2696
|
const issues = [];
|
|
2393
|
-
const projectRoot =
|
|
2697
|
+
const projectRoot = path11.dirname(graph.rootPath);
|
|
2394
2698
|
for (const [nodePath, node] of graph.nodes) {
|
|
2395
|
-
const
|
|
2396
|
-
if (
|
|
2397
|
-
const nodeAspects = new Set(node.meta.aspects ?? []);
|
|
2398
|
-
for (const aspectId of Object.keys(anchors)) {
|
|
2399
|
-
if (!nodeAspects.has(aspectId)) {
|
|
2400
|
-
issues.push({
|
|
2401
|
-
severity: "error",
|
|
2402
|
-
code: "E019",
|
|
2403
|
-
rule: "invalid-anchor-ref",
|
|
2404
|
-
message: `anchors references aspect '${aspectId}' which is not in this node's aspects list (${[...nodeAspects].join(", ") || "none"})`,
|
|
2405
|
-
nodePath
|
|
2406
|
-
});
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2699
|
+
const aspectsWithAnchors = (node.meta.aspects ?? []).filter((a) => a.anchors && a.anchors.length > 0);
|
|
2700
|
+
if (aspectsWithAnchors.length === 0) continue;
|
|
2409
2701
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2410
2702
|
if (mappingPaths.length === 0) continue;
|
|
2411
2703
|
const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
|
|
@@ -2413,21 +2705,20 @@ async function checkAnchorPresence(graph) {
|
|
|
2413
2705
|
const fileContents = [];
|
|
2414
2706
|
for (const filePath of sourceFiles) {
|
|
2415
2707
|
try {
|
|
2416
|
-
const content = await
|
|
2708
|
+
const content = await readFile13(filePath, "utf-8");
|
|
2417
2709
|
fileContents.push(content);
|
|
2418
2710
|
} catch {
|
|
2419
2711
|
}
|
|
2420
2712
|
}
|
|
2421
|
-
for (const
|
|
2422
|
-
|
|
2423
|
-
for (const anchor of anchorList) {
|
|
2713
|
+
for (const entry of aspectsWithAnchors) {
|
|
2714
|
+
for (const anchor of entry.anchors) {
|
|
2424
2715
|
const found = fileContents.some((content) => content.includes(anchor));
|
|
2425
2716
|
if (!found) {
|
|
2426
2717
|
issues.push({
|
|
2427
2718
|
severity: "warning",
|
|
2428
2719
|
code: "W014",
|
|
2429
2720
|
rule: "anchor-not-found",
|
|
2430
|
-
message: `Anchor '${anchor}' for aspect '${
|
|
2721
|
+
message: `Anchor '${anchor}' for aspect '${entry.aspect}' not found in mapped source files`,
|
|
2431
2722
|
nodePath
|
|
2432
2723
|
});
|
|
2433
2724
|
}
|
|
@@ -2652,13 +2943,13 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
2652
2943
|
import chalk2 from "chalk";
|
|
2653
2944
|
|
|
2654
2945
|
// src/io/drift-state-store.ts
|
|
2655
|
-
import { readFile as
|
|
2656
|
-
import
|
|
2946
|
+
import { readFile as readFile14, writeFile as writeFile4 } from "fs/promises";
|
|
2947
|
+
import path12 from "path";
|
|
2657
2948
|
import { parse as yamlParse } from "yaml";
|
|
2658
2949
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2659
2950
|
async function readDriftState(yggRoot) {
|
|
2660
2951
|
try {
|
|
2661
|
-
const content = await
|
|
2952
|
+
const content = await readFile14(path12.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
|
|
2662
2953
|
let raw;
|
|
2663
2954
|
try {
|
|
2664
2955
|
raw = JSON.parse(content);
|
|
@@ -2679,24 +2970,24 @@ async function readDriftState(yggRoot) {
|
|
|
2679
2970
|
}
|
|
2680
2971
|
async function writeDriftState(yggRoot, state) {
|
|
2681
2972
|
const content = JSON.stringify(state);
|
|
2682
|
-
await
|
|
2973
|
+
await writeFile4(path12.join(yggRoot, DRIFT_STATE_FILE), content, "utf-8");
|
|
2683
2974
|
}
|
|
2684
2975
|
|
|
2685
2976
|
// src/utils/hash.ts
|
|
2686
|
-
import { readFile as
|
|
2687
|
-
import
|
|
2977
|
+
import { readFile as readFile15, readdir as readdir6, stat as stat5 } from "fs/promises";
|
|
2978
|
+
import path13 from "path";
|
|
2688
2979
|
import { createHash } from "crypto";
|
|
2689
2980
|
import { createRequire } from "module";
|
|
2690
2981
|
var require2 = createRequire(import.meta.url);
|
|
2691
2982
|
var ignoreFactory = require2("ignore");
|
|
2692
2983
|
async function hashFile(filePath) {
|
|
2693
|
-
const content = await
|
|
2984
|
+
const content = await readFile15(filePath);
|
|
2694
2985
|
return createHash("sha256").update(content).digest("hex");
|
|
2695
2986
|
}
|
|
2696
2987
|
async function loadRootGitignoreStack(projectRoot) {
|
|
2697
2988
|
if (!projectRoot) return [];
|
|
2698
2989
|
try {
|
|
2699
|
-
const content = await
|
|
2990
|
+
const content = await readFile15(path13.join(projectRoot, ".gitignore"), "utf-8");
|
|
2700
2991
|
const matcher = ignoreFactory();
|
|
2701
2992
|
matcher.add(content);
|
|
2702
2993
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -2706,7 +2997,7 @@ async function loadRootGitignoreStack(projectRoot) {
|
|
|
2706
2997
|
}
|
|
2707
2998
|
function isIgnoredByStack(candidatePath, stack) {
|
|
2708
2999
|
for (const { basePath, matcher } of stack) {
|
|
2709
|
-
const relativePath =
|
|
3000
|
+
const relativePath = path13.relative(basePath, candidatePath);
|
|
2710
3001
|
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
2711
3002
|
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
2712
3003
|
}
|
|
@@ -2721,9 +3012,9 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2721
3012
|
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2722
3013
|
const allFiles = [];
|
|
2723
3014
|
for (const tf of trackedFiles) {
|
|
2724
|
-
const absPath =
|
|
3015
|
+
const absPath = path13.join(projectRoot, tf.path);
|
|
2725
3016
|
try {
|
|
2726
|
-
const st = await
|
|
3017
|
+
const st = await stat5(absPath);
|
|
2727
3018
|
if (st.isDirectory()) {
|
|
2728
3019
|
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2729
3020
|
projectRoot,
|
|
@@ -2731,7 +3022,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2731
3022
|
});
|
|
2732
3023
|
for (const entry of dirEntries) {
|
|
2733
3024
|
allFiles.push({
|
|
2734
|
-
relPath:
|
|
3025
|
+
relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/"),
|
|
2735
3026
|
absPath: entry.absPath,
|
|
2736
3027
|
mtimeMs: entry.mtimeMs
|
|
2737
3028
|
});
|
|
@@ -2771,17 +3062,17 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2771
3062
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
2772
3063
|
let stack = options.gitignoreStack ?? [];
|
|
2773
3064
|
try {
|
|
2774
|
-
const localContent = await
|
|
3065
|
+
const localContent = await readFile15(path13.join(directoryPath, ".gitignore"), "utf-8");
|
|
2775
3066
|
const localMatcher = ignoreFactory();
|
|
2776
3067
|
localMatcher.add(localContent);
|
|
2777
3068
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
2778
3069
|
} catch {
|
|
2779
3070
|
}
|
|
2780
|
-
const entries = await
|
|
3071
|
+
const entries = await readdir6(directoryPath, { withFileTypes: true });
|
|
2781
3072
|
const dirs = [];
|
|
2782
3073
|
const files = [];
|
|
2783
3074
|
for (const entry of entries) {
|
|
2784
|
-
const absoluteChildPath =
|
|
3075
|
+
const absoluteChildPath = path13.join(directoryPath, entry.name);
|
|
2785
3076
|
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
2786
3077
|
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
2787
3078
|
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
@@ -2792,9 +3083,9 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
2792
3083
|
gitignoreStack: stack
|
|
2793
3084
|
}))),
|
|
2794
3085
|
Promise.all(files.map(async (f) => {
|
|
2795
|
-
const fileStat = await
|
|
3086
|
+
const fileStat = await stat5(f);
|
|
2796
3087
|
return {
|
|
2797
|
-
relPath:
|
|
3088
|
+
relPath: path13.relative(rootDirectoryPath, f),
|
|
2798
3089
|
absPath: f,
|
|
2799
3090
|
mtimeMs: fileStat.mtimeMs
|
|
2800
3091
|
};
|
|
@@ -2807,14 +3098,14 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
2807
3098
|
}
|
|
2808
3099
|
|
|
2809
3100
|
// src/core/context-files.ts
|
|
2810
|
-
import
|
|
3101
|
+
import path14 from "path";
|
|
2811
3102
|
var STRUCTURAL_RELATION_TYPES2 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
2812
3103
|
function collectTrackedFiles(node, graph) {
|
|
2813
3104
|
const seen = /* @__PURE__ */ new Set();
|
|
2814
3105
|
const result = [];
|
|
2815
|
-
const projectRoot =
|
|
2816
|
-
const yggPrefix =
|
|
2817
|
-
const yggPrefixNormalized = yggPrefix.split(
|
|
3106
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3107
|
+
const yggPrefix = path14.relative(projectRoot, graph.rootPath);
|
|
3108
|
+
const yggPrefixNormalized = yggPrefix.split(path14.sep).join("/");
|
|
2818
3109
|
const configArtifactKeys = new Set(Object.keys(graph.config.artifacts ?? {}));
|
|
2819
3110
|
function addFile(filePath, category) {
|
|
2820
3111
|
if (seen.has(filePath)) return;
|
|
@@ -2825,7 +3116,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2825
3116
|
return [yggPrefixNormalized, ...segments].join("/");
|
|
2826
3117
|
}
|
|
2827
3118
|
function addNodeFiles(n) {
|
|
2828
|
-
addFile(graphPath("model", n.path, "node.yaml"), "graph");
|
|
3119
|
+
addFile(graphPath("model", n.path, "yg-node.yaml"), "graph");
|
|
2829
3120
|
for (const art of n.artifacts) {
|
|
2830
3121
|
if (configArtifactKeys.has(art.filename)) {
|
|
2831
3122
|
addFile(graphPath("model", n.path, art.filename), "graph");
|
|
@@ -2838,12 +3129,12 @@ function collectTrackedFiles(node, graph) {
|
|
|
2838
3129
|
addNodeFiles(ancestor);
|
|
2839
3130
|
}
|
|
2840
3131
|
const allAspectIds = /* @__PURE__ */ new Set();
|
|
2841
|
-
for (const
|
|
2842
|
-
allAspectIds.add(
|
|
3132
|
+
for (const entry of node.meta.aspects ?? []) {
|
|
3133
|
+
allAspectIds.add(entry.aspect);
|
|
2843
3134
|
}
|
|
2844
3135
|
for (const ancestor of ancestors) {
|
|
2845
|
-
for (const
|
|
2846
|
-
allAspectIds.add(
|
|
3136
|
+
for (const entry of ancestor.meta.aspects ?? []) {
|
|
3137
|
+
allAspectIds.add(entry.aspect);
|
|
2847
3138
|
}
|
|
2848
3139
|
}
|
|
2849
3140
|
const participatingFlows = collectParticipatingFlows2(graph, node, ancestors);
|
|
@@ -2854,7 +3145,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2854
3145
|
}
|
|
2855
3146
|
const resolvedAspects = resolveAspects(allAspectIds, graph.aspects);
|
|
2856
3147
|
for (const aspect of resolvedAspects) {
|
|
2857
|
-
addFile(graphPath("aspects", aspect.id, "aspect.yaml"), "graph");
|
|
3148
|
+
addFile(graphPath("aspects", aspect.id, "yg-aspect.yaml"), "graph");
|
|
2858
3149
|
for (const art of aspect.artifacts) {
|
|
2859
3150
|
addFile(graphPath("aspects", aspect.id, art.filename), "graph");
|
|
2860
3151
|
}
|
|
@@ -2863,7 +3154,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2863
3154
|
if (!STRUCTURAL_RELATION_TYPES2.has(relation.type)) continue;
|
|
2864
3155
|
const target = graph.nodes.get(relation.target);
|
|
2865
3156
|
if (!target) continue;
|
|
2866
|
-
const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.
|
|
3157
|
+
const structuralFilenames = Object.entries(graph.config.artifacts ?? {}).filter(([, c]) => c.included_in_relations).map(([filename]) => filename);
|
|
2867
3158
|
const structuralArts = structuralFilenames.filter(
|
|
2868
3159
|
(filename) => target.artifacts.some((a) => a.filename === filename)
|
|
2869
3160
|
);
|
|
@@ -2880,7 +3171,7 @@ function collectTrackedFiles(node, graph) {
|
|
|
2880
3171
|
}
|
|
2881
3172
|
}
|
|
2882
3173
|
for (const flow of participatingFlows) {
|
|
2883
|
-
addFile(graphPath("flows", flow.path, "flow.yaml"), "graph");
|
|
3174
|
+
addFile(graphPath("flows", flow.path, "yg-flow.yaml"), "graph");
|
|
2884
3175
|
for (const art of flow.artifacts) {
|
|
2885
3176
|
addFile(graphPath("flows", flow.path, art.filename), "graph");
|
|
2886
3177
|
}
|
|
@@ -2897,8 +3188,8 @@ function collectParticipatingFlows2(graph, node, ancestors) {
|
|
|
2897
3188
|
}
|
|
2898
3189
|
|
|
2899
3190
|
// src/core/drift-detector.ts
|
|
2900
|
-
import { access } from "fs/promises";
|
|
2901
|
-
import
|
|
3191
|
+
import { access as access2 } from "fs/promises";
|
|
3192
|
+
import path15 from "path";
|
|
2902
3193
|
function getChildMappingExclusions(graph, nodePath) {
|
|
2903
3194
|
const node = graph.nodes.get(nodePath);
|
|
2904
3195
|
if (!node) return [];
|
|
@@ -2920,7 +3211,7 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
2920
3211
|
return exclusions;
|
|
2921
3212
|
}
|
|
2922
3213
|
async function detectDrift(graph, filterNodePath) {
|
|
2923
|
-
const projectRoot =
|
|
3214
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
2924
3215
|
const driftState = await readDriftState(graph.rootPath);
|
|
2925
3216
|
const entries = [];
|
|
2926
3217
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -3002,16 +3293,16 @@ async function detectDrift(graph, filterNodePath) {
|
|
|
3002
3293
|
};
|
|
3003
3294
|
}
|
|
3004
3295
|
function categorizeFile(filePath, _rootPath, projectRoot) {
|
|
3005
|
-
const yggPrefix =
|
|
3006
|
-
const normalizedPrefix = yggPrefix.split(
|
|
3296
|
+
const yggPrefix = path15.relative(projectRoot, _rootPath);
|
|
3297
|
+
const normalizedPrefix = yggPrefix.split(path15.sep).join("/");
|
|
3007
3298
|
const normalizedFilePath = filePath.replace(/\\/g, "/");
|
|
3008
3299
|
return normalizedFilePath.startsWith(normalizedPrefix) ? "graph" : "source";
|
|
3009
3300
|
}
|
|
3010
3301
|
async function allPathsMissing(projectRoot, mappingPaths) {
|
|
3011
3302
|
for (const mp of mappingPaths) {
|
|
3012
|
-
const absPath =
|
|
3303
|
+
const absPath = path15.join(projectRoot, mp);
|
|
3013
3304
|
try {
|
|
3014
|
-
await
|
|
3305
|
+
await access2(absPath);
|
|
3015
3306
|
return false;
|
|
3016
3307
|
} catch {
|
|
3017
3308
|
}
|
|
@@ -3019,7 +3310,7 @@ async function allPathsMissing(projectRoot, mappingPaths) {
|
|
|
3019
3310
|
return true;
|
|
3020
3311
|
}
|
|
3021
3312
|
async function syncDriftState(graph, nodePath) {
|
|
3022
|
-
const projectRoot =
|
|
3313
|
+
const projectRoot = path15.dirname(graph.rootPath);
|
|
3023
3314
|
const node = graph.nodes.get(nodePath);
|
|
3024
3315
|
if (!node) throw new Error(`Node not found: ${nodePath}`);
|
|
3025
3316
|
if (!node.meta.mapping) throw new Error(`Node has no mapping: ${nodePath}`);
|
|
@@ -3334,10 +3625,10 @@ function registerTreeCommand(program2) {
|
|
|
3334
3625
|
let roots;
|
|
3335
3626
|
let showProjectName;
|
|
3336
3627
|
if (options.root?.trim()) {
|
|
3337
|
-
const
|
|
3338
|
-
const node = graph.nodes.get(
|
|
3628
|
+
const path20 = options.root.trim().replace(/\/$/, "");
|
|
3629
|
+
const node = graph.nodes.get(path20);
|
|
3339
3630
|
if (!node) {
|
|
3340
|
-
process.stderr.write(`Error: path '${
|
|
3631
|
+
process.stderr.write(`Error: path '${path20}' not found
|
|
3341
3632
|
`);
|
|
3342
3633
|
process.exit(1);
|
|
3343
3634
|
}
|
|
@@ -3381,8 +3672,8 @@ function printNode(node, prefix, isLast, depth, maxDepth) {
|
|
|
3381
3672
|
}
|
|
3382
3673
|
|
|
3383
3674
|
// src/cli/owner.ts
|
|
3384
|
-
import
|
|
3385
|
-
import { access as
|
|
3675
|
+
import path16 from "path";
|
|
3676
|
+
import { access as access3 } from "fs/promises";
|
|
3386
3677
|
function normalizeForMatch(inputPath) {
|
|
3387
3678
|
return inputPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3388
3679
|
}
|
|
@@ -3411,14 +3702,14 @@ function registerOwnerCommand(program2) {
|
|
|
3411
3702
|
const graph = await loadGraph(cwd);
|
|
3412
3703
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3413
3704
|
const rawPath = options.file.trim();
|
|
3414
|
-
const absolute =
|
|
3415
|
-
const repoRelative =
|
|
3705
|
+
const absolute = path16.resolve(cwd, rawPath);
|
|
3706
|
+
const repoRelative = path16.relative(repoRoot, absolute).split(path16.sep).join("/");
|
|
3416
3707
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3417
3708
|
if (!result.nodePath) {
|
|
3418
|
-
const absPath =
|
|
3709
|
+
const absPath = path16.resolve(repoRoot, result.file);
|
|
3419
3710
|
let exists = true;
|
|
3420
3711
|
try {
|
|
3421
|
-
await
|
|
3712
|
+
await access3(absPath);
|
|
3422
3713
|
} catch {
|
|
3423
3714
|
exists = false;
|
|
3424
3715
|
}
|
|
@@ -3449,7 +3740,7 @@ function registerOwnerCommand(program2) {
|
|
|
3449
3740
|
|
|
3450
3741
|
// src/core/dependency-resolver.ts
|
|
3451
3742
|
import { execSync } from "child_process";
|
|
3452
|
-
import
|
|
3743
|
+
import path17 from "path";
|
|
3453
3744
|
var STRUCTURAL_RELATION_TYPES3 = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
3454
3745
|
var EVENT_RELATION_TYPES2 = /* @__PURE__ */ new Set(["emits", "listens"]);
|
|
3455
3746
|
function filterRelationType(relType, filter) {
|
|
@@ -3524,9 +3815,9 @@ function registerDepsCommand(program2) {
|
|
|
3524
3815
|
}
|
|
3525
3816
|
|
|
3526
3817
|
// src/core/graph-from-git.ts
|
|
3527
|
-
import { mkdtemp, rm } from "fs/promises";
|
|
3818
|
+
import { mkdtemp, rm as rm2 } from "fs/promises";
|
|
3528
3819
|
import { tmpdir } from "os";
|
|
3529
|
-
import
|
|
3820
|
+
import path18 from "path";
|
|
3530
3821
|
import { execSync as execSync2 } from "child_process";
|
|
3531
3822
|
async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
3532
3823
|
const yggPath = ".yggdrasil";
|
|
@@ -3537,8 +3828,8 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
3537
3828
|
return null;
|
|
3538
3829
|
}
|
|
3539
3830
|
try {
|
|
3540
|
-
tmpDir = await mkdtemp(
|
|
3541
|
-
const archivePath =
|
|
3831
|
+
tmpDir = await mkdtemp(path18.join(tmpdir(), "ygg-git-"));
|
|
3832
|
+
const archivePath = path18.join(tmpDir, "archive.tar");
|
|
3542
3833
|
execSync2(`git archive ${ref} ${yggPath} -o "${archivePath}"`, {
|
|
3543
3834
|
cwd: projectRoot,
|
|
3544
3835
|
stdio: "pipe"
|
|
@@ -3550,7 +3841,7 @@ async function loadGraphFromRef(projectRoot, ref = "HEAD") {
|
|
|
3550
3841
|
return null;
|
|
3551
3842
|
} finally {
|
|
3552
3843
|
if (tmpDir) {
|
|
3553
|
-
await
|
|
3844
|
+
await rm2(tmpDir, { recursive: true, force: true });
|
|
3554
3845
|
}
|
|
3555
3846
|
}
|
|
3556
3847
|
}
|
|
@@ -3608,14 +3899,14 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
3608
3899
|
}
|
|
3609
3900
|
const chains = [];
|
|
3610
3901
|
for (const node of transitiveOnly) {
|
|
3611
|
-
const
|
|
3902
|
+
const path20 = [];
|
|
3612
3903
|
let current = node;
|
|
3613
3904
|
while (current) {
|
|
3614
|
-
|
|
3905
|
+
path20.unshift(current);
|
|
3615
3906
|
current = parent.get(current);
|
|
3616
3907
|
}
|
|
3617
|
-
if (
|
|
3618
|
-
chains.push(
|
|
3908
|
+
if (path20.length >= 3) {
|
|
3909
|
+
chains.push(path20.slice(1).map((p) => `<- ${p}`).join(" "));
|
|
3619
3910
|
}
|
|
3620
3911
|
}
|
|
3621
3912
|
return chains.sort();
|
|
@@ -3685,14 +3976,14 @@ async function handleAspectImpact(graph, aspectId, simulate) {
|
|
|
3685
3976
|
const effective = collectEffectiveAspectIds(graph, nodePath);
|
|
3686
3977
|
if (effective.has(aspectId)) {
|
|
3687
3978
|
const node = graph.nodes.get(nodePath);
|
|
3688
|
-
const
|
|
3689
|
-
if (
|
|
3979
|
+
const ownAspectIds = new Set((node.meta.aspects ?? []).map((a) => a.aspect));
|
|
3980
|
+
if (ownAspectIds.has(aspectId)) {
|
|
3690
3981
|
affected.push({ path: nodePath, source: "own" });
|
|
3691
3982
|
} else {
|
|
3692
3983
|
let fromHierarchy = false;
|
|
3693
3984
|
let anc = node.parent;
|
|
3694
3985
|
while (anc) {
|
|
3695
|
-
if ((anc.meta.aspects ?? []).
|
|
3986
|
+
if ((anc.meta.aspects ?? []).some((a) => a.aspect === aspectId)) {
|
|
3696
3987
|
fromHierarchy = true;
|
|
3697
3988
|
break;
|
|
3698
3989
|
}
|
|
@@ -4037,16 +4328,16 @@ function registerFlowsCommand(program2) {
|
|
|
4037
4328
|
}
|
|
4038
4329
|
|
|
4039
4330
|
// src/io/journal-store.ts
|
|
4040
|
-
import { readFile as
|
|
4041
|
-
import { parse as
|
|
4042
|
-
import
|
|
4331
|
+
import { readFile as readFile16, writeFile as writeFile5, mkdir as mkdir3, rename as rename2, access as access4 } from "fs/promises";
|
|
4332
|
+
import { parse as parseYaml8, stringify as stringifyYaml2 } from "yaml";
|
|
4333
|
+
import path19 from "path";
|
|
4043
4334
|
var JOURNAL_FILE = ".journal.yaml";
|
|
4044
4335
|
var ARCHIVE_DIR = "journals-archive";
|
|
4045
4336
|
async function readJournal(yggRoot) {
|
|
4046
|
-
const filePath =
|
|
4337
|
+
const filePath = path19.join(yggRoot, JOURNAL_FILE);
|
|
4047
4338
|
try {
|
|
4048
|
-
const content = await
|
|
4049
|
-
const raw =
|
|
4339
|
+
const content = await readFile16(filePath, "utf-8");
|
|
4340
|
+
const raw = parseYaml8(content);
|
|
4050
4341
|
const entries = raw.entries ?? [];
|
|
4051
4342
|
return Array.isArray(entries) ? entries : [];
|
|
4052
4343
|
} catch {
|
|
@@ -4058,27 +4349,27 @@ async function appendJournalEntry(yggRoot, note, target) {
|
|
|
4058
4349
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4059
4350
|
const entry = target ? { at, target, note } : { at, note };
|
|
4060
4351
|
entries.push(entry);
|
|
4061
|
-
const filePath =
|
|
4062
|
-
const content =
|
|
4063
|
-
await
|
|
4352
|
+
const filePath = path19.join(yggRoot, JOURNAL_FILE);
|
|
4353
|
+
const content = stringifyYaml2({ entries });
|
|
4354
|
+
await writeFile5(filePath, content, "utf-8");
|
|
4064
4355
|
return entry;
|
|
4065
4356
|
}
|
|
4066
4357
|
async function archiveJournal(yggRoot) {
|
|
4067
|
-
const journalPath =
|
|
4358
|
+
const journalPath = path19.join(yggRoot, JOURNAL_FILE);
|
|
4068
4359
|
try {
|
|
4069
|
-
await
|
|
4360
|
+
await access4(journalPath);
|
|
4070
4361
|
} catch {
|
|
4071
4362
|
return null;
|
|
4072
4363
|
}
|
|
4073
4364
|
const entries = await readJournal(yggRoot);
|
|
4074
4365
|
if (entries.length === 0) return null;
|
|
4075
|
-
const archiveDir =
|
|
4366
|
+
const archiveDir = path19.join(yggRoot, ARCHIVE_DIR);
|
|
4076
4367
|
await mkdir3(archiveDir, { recursive: true });
|
|
4077
4368
|
const now = /* @__PURE__ */ new Date();
|
|
4078
4369
|
const timestamp = `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}${String(now.getUTCDate()).padStart(2, "0")}-${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`;
|
|
4079
4370
|
const archiveName = `.journal.${timestamp}.yaml`;
|
|
4080
|
-
const archivePath =
|
|
4081
|
-
await
|
|
4371
|
+
const archivePath = path19.join(archiveDir, archiveName);
|
|
4372
|
+
await rename2(journalPath, archivePath);
|
|
4082
4373
|
return { archiveName, entryCount: entries.length };
|
|
4083
4374
|
}
|
|
4084
4375
|
|
|
@@ -4232,12 +4523,12 @@ function registerPreflightCommand(program2) {
|
|
|
4232
4523
|
}
|
|
4233
4524
|
|
|
4234
4525
|
// src/bin.ts
|
|
4235
|
-
import { readFileSync } from "fs";
|
|
4526
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
4236
4527
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4237
4528
|
import { dirname, join } from "path";
|
|
4238
4529
|
var __filename = fileURLToPath3(import.meta.url);
|
|
4239
4530
|
var __dirname = dirname(__filename);
|
|
4240
|
-
var pkg = JSON.parse(
|
|
4531
|
+
var pkg = JSON.parse(readFileSync2(join(__dirname, "..", "package.json"), "utf-8"));
|
|
4241
4532
|
var program = new Command();
|
|
4242
4533
|
program.name("yg").description("Yggdrasil \u2014 architectural knowledge infrastructure for AI agents").version(pkg.version);
|
|
4243
4534
|
registerInitCommand(program);
|