@chrisdudek/yg 1.2.0 → 1.4.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 +359 -71
- package/dist/bin.js.map +1 -1
- package/dist/templates/default-config.ts +4 -21
- package/dist/templates/rules.ts +96 -27
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -21,6 +21,7 @@ node_types:
|
|
|
21
21
|
- name: module
|
|
22
22
|
- name: service
|
|
23
23
|
- name: library
|
|
24
|
+
- name: infrastructure
|
|
24
25
|
|
|
25
26
|
artifacts:
|
|
26
27
|
responsibility.md:
|
|
@@ -30,29 +31,11 @@ artifacts:
|
|
|
30
31
|
interface.md:
|
|
31
32
|
required:
|
|
32
33
|
when: has_incoming_relations
|
|
33
|
-
description: "Public API \u2014 methods, parameters, return types, contracts"
|
|
34
|
+
description: "Public API \u2014 methods, parameters, return types, contracts, failure modes, exposed data structures"
|
|
34
35
|
structural_context: true
|
|
35
|
-
|
|
36
|
+
internals.md:
|
|
36
37
|
required: never
|
|
37
|
-
description: "
|
|
38
|
-
constraints.md:
|
|
39
|
-
required: never
|
|
40
|
-
description: "Validation rules, business rules, invariants"
|
|
41
|
-
structural_context: true
|
|
42
|
-
errors.md:
|
|
43
|
-
required:
|
|
44
|
-
when: has_incoming_relations
|
|
45
|
-
description: "Failure modes, edge cases, error conditions, recovery behavior"
|
|
46
|
-
structural_context: true
|
|
47
|
-
model.md:
|
|
48
|
-
required: never
|
|
49
|
-
description: "Data structures, schemas, entities, type definitions \u2014 the shape of data this node owns or manages"
|
|
50
|
-
state.md:
|
|
51
|
-
required: never
|
|
52
|
-
description: "State machines, lifecycle, transitions"
|
|
53
|
-
decisions.md:
|
|
54
|
-
required: never
|
|
55
|
-
description: "Local design decisions and rationale \u2014 choices specific to this node, not system-wide"
|
|
38
|
+
description: "How the node works and why \u2014 algorithms, business rules, state machines, design decisions with rejected alternatives"
|
|
56
39
|
|
|
57
40
|
quality:
|
|
58
41
|
min_artifact_length: 50
|
|
@@ -77,11 +60,12 @@ Yggdrasil is persistent semantic memory stored in \`.yggdrasil/\`. It maps the r
|
|
|
77
60
|
BEFORE reading, researching, planning, OR modifying ANY mapped file:
|
|
78
61
|
1. yg owner --file <path>
|
|
79
62
|
2. yg build-context --node <owner>
|
|
80
|
-
The context package is your primary source of understanding
|
|
81
|
-
|
|
82
|
-
|
|
63
|
+
The context package is your primary source of ARCHITECTURAL understanding:
|
|
64
|
+
intent, constraints, relations, rationale. For IMPLEMENTATION precision
|
|
65
|
+
(exact behavior, error handling, await patterns, edge cases) \u2014 verify
|
|
66
|
+
against source code. Aspects describe intended patterns; individual
|
|
67
|
+
implementations may deviate.
|
|
83
68
|
If the context package seems insufficient \u2014 enrich the graph.
|
|
84
|
-
Do not bypass it.
|
|
85
69
|
|
|
86
70
|
AFTER modifying:
|
|
87
71
|
3. Update graph artifacts to reflect changes
|
|
@@ -102,31 +86,42 @@ WHEN UNSURE: ask the user. Never guess. Never assume.
|
|
|
102
86
|
|
|
103
87
|
### Five Core Rules
|
|
104
88
|
|
|
105
|
-
1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and \`yg build-context\`. Always. The context package
|
|
89
|
+
1. **Graph first.** Before reading, researching, planning, or modifying mapped files, run \`yg owner\` and \`yg build-context\`. Always. The context package is your primary source of architectural understanding. For implementation-level precision (exact behavior, error paths, edge cases) \u2014 verify against source code after loading the context package.
|
|
106
90
|
2. **Code and graph are one.** Code changed \u2192 graph updated in the same response. Graph changed \u2192 source verified in the same response. No exceptions.
|
|
107
91
|
3. **Never invent why.** The graph captures human intent. If you don't know why something was decided, ask. Never hallucinate rationale.
|
|
108
92
|
4. **Always capture why \u2014 especially why NOT.** When the user explains a reason, record it in the graph immediately. When a design choice is made, also record rejected alternatives: "Chose X over Y because Z." Rejected alternatives are the highest-value information \u2014 invisible in code and irrecoverable once forgotten. Conversation evaporates; graph persists.
|
|
109
93
|
5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
|
|
110
94
|
|
|
95
|
+
### Recognizing Graph-Required Actions
|
|
96
|
+
|
|
97
|
+
What matters is the ACTION you are performing, not what instructed it. If the action involves understanding mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a workflow step, or your own initiative.
|
|
98
|
+
|
|
99
|
+
**Actions that require \`yg owner\` + \`yg build-context\` first:**
|
|
100
|
+
|
|
101
|
+
- Reading or exploring source files to understand a component
|
|
102
|
+
- Proposing approaches, designs, or plans for changing code
|
|
103
|
+
- Reviewing or debugging code
|
|
104
|
+
- Any form of reasoning about how mapped code works or should change
|
|
105
|
+
|
|
106
|
+
**Actions that do NOT require yg:**
|
|
107
|
+
|
|
108
|
+
- Git operations (log, diff, status, blame)
|
|
109
|
+
- Reading documentation, READMEs, or config files outside \`.yggdrasil/\`
|
|
110
|
+
- Running tests, builds, or linters
|
|
111
|
+
- Working with files that \`yg owner\` reports as unmapped
|
|
112
|
+
|
|
111
113
|
### Failure States
|
|
112
114
|
|
|
113
115
|
You have broken Yggdrasil if you do any of the following:
|
|
114
116
|
|
|
115
|
-
- \u274C
|
|
116
|
-
- \u274C Modified source code without updating graph artifacts in the same response.
|
|
117
|
-
- \u274C
|
|
118
|
-
- \u274C Resolved a code-graph inconsistency without asking the user first.
|
|
117
|
+
- \u274C Worked on a mapped file without running \`yg owner\` + \`yg build-context\` first \u2014 regardless of what instructed the action (skill, plan, user request, workflow step).
|
|
118
|
+
- \u274C Modified source code without updating graph artifacts in the same response, or vice versa.
|
|
119
|
+
- \u274C Resolved a code-graph inconsistency or ambiguity without asking the user first.
|
|
119
120
|
- \u274C Created or edited a graph element without reading its schema in \`schemas/\` first.
|
|
120
|
-
- \u274C Ran \`yg drift-sync\` before
|
|
121
|
-
- \u274C
|
|
122
|
-
- \u274C
|
|
123
|
-
- \u274C Placed a cross-cutting requirement in a local node artifact instead of an aspect.
|
|
124
|
-
- \u274C Invented a rationale, business rule, or architectural decision.
|
|
121
|
+
- \u274C Ran \`yg drift-sync\` before both graph artifacts and source code are current.
|
|
122
|
+
- \u274C Placed a cross-cutting requirement in a local artifact instead of an aspect, or used an aspect id with no \`aspects/\` directory.
|
|
123
|
+
- \u274C Invented a rationale, business rule, or decision \u2014 or recorded a decision without documenting rejected alternatives and rationale (use "rationale: unknown" if unknown).
|
|
125
124
|
- \u274C Used blackbox coverage for greenfield (new) code.
|
|
126
|
-
- \u274C Answered a question about a mapped file without running \`yg build-context\` first.
|
|
127
|
-
- \u274C Read mapped source files to plan or research changes without running \`yg build-context\` first.
|
|
128
|
-
- \u274C Deferred \`yg drift-sync\` to the end of a multi-step task instead of running it incrementally after each logical group of changes.
|
|
129
|
-
- \u274C Recorded a design decision without documenting which alternatives were rejected and why.
|
|
130
125
|
|
|
131
126
|
### Escape Hatch
|
|
132
127
|
|
|
@@ -201,7 +196,7 @@ You are not allowed to edit or create source code without establishing graph cov
|
|
|
201
196
|
|
|
202
197
|
1. Create aspects first (cross-cutting requirements the new code must satisfy)
|
|
203
198
|
2. Create flows if the code participates in a business process
|
|
204
|
-
3. Create nodes with full artifacts \u2014 responsibility,
|
|
199
|
+
3. Create nodes with full artifacts \u2014 responsibility, interface, internals
|
|
205
200
|
4. Review the context package (\`yg build-context\`) \u2014 it is now the behavioral specification
|
|
206
201
|
5. Implement code that satisfies the specification
|
|
207
202
|
6. The graph specifies WHAT and WHY; the code implements HOW (framework APIs, library choices)
|
|
@@ -229,6 +224,7 @@ Per area checklist:
|
|
|
229
224
|
- [ ] 4. Analyze source \u2014 for each artifact type in \`config.artifacts\`: extract content, do not invent
|
|
230
225
|
- [ ] 5. Identify relations \u2014 add to \`node.yaml\`
|
|
231
226
|
- [ ] 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 to \`node.yaml\` \`anchors\` field
|
|
232
228
|
- [ ] 7. Identify business process participation \u2014 add to flow, ask user if process unclear
|
|
233
229
|
- [ ] 8. \`yg validate\` \u2014 fix errors
|
|
234
230
|
- [ ] 9. \`yg drift-sync --node <path>\`
|
|
@@ -238,7 +234,8 @@ Per area checklist:
|
|
|
238
234
|
- Business process unclear: "This code appears to be part of a larger process. Can you describe what it means from a business perspective?"
|
|
239
235
|
- Constraint without rationale: "I see [constraint X]. Do you know why this exists? I want to record the reason, not just the rule."
|
|
240
236
|
- Unexplained architectural choice: "I see [approach X]. What was the reason for this choice?"
|
|
241
|
-
- Decision without alternatives: "You chose [X]. What alternatives did you consider, and why did you reject them?" Record the answer in \`
|
|
237
|
+
- Decision without alternatives: "You chose [X]. What alternatives did you consider, and why did you reject them?" Record the answer in the Decisions section of \`internals.md\`.
|
|
238
|
+
- Decision without known rationale: Record the decision in \`internals.md\` with "rationale: unknown \u2014 inferred from code, not confirmed by developer." A recorded decision with unknown rationale is infinitely more valuable than no record at all, and safer than an invented rationale.
|
|
242
239
|
|
|
243
240
|
### Bootstrap Mode
|
|
244
241
|
|
|
@@ -266,6 +263,27 @@ Always ask the user before resolving drift. Never auto-resolve.
|
|
|
266
263
|
|
|
267
264
|
Threshold: >10 drifted nodes \u2192 ask user which area to prioritize. Do not resolve all at once.
|
|
268
265
|
|
|
266
|
+
**Drift triage:** Prioritize aspects and \`internals.md\` (highest decay rate), then \`responsibility.md\` and \`interface.md\` (most stable).
|
|
267
|
+
|
|
268
|
+
### Graph Audit
|
|
269
|
+
|
|
270
|
+
When reviewing graph quality (triggered by user or quality improvement):
|
|
271
|
+
|
|
272
|
+
**Step 1 \u2014 Consistency** (catches WRONG information):
|
|
273
|
+
|
|
274
|
+
- [ ] 1. \`yg build-context --node <path>\`
|
|
275
|
+
- [ ] 2. Read mapped source files
|
|
276
|
+
- [ ] 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 \`aspect_exceptions\` in \`node.yaml\`
|
|
278
|
+
- [ ] 5. Report inconsistencies
|
|
279
|
+
|
|
280
|
+
**Step 2 \u2014 Completeness** (catches MISSING information):
|
|
281
|
+
|
|
282
|
+
- [ ] 1. For each public method: is it in \`interface.md\`?
|
|
283
|
+
- [ ] 2. For each error path: is it in \`interface.md\` (Failure Modes section)?
|
|
284
|
+
- [ ] 3. For each behavioral invariant: is it in the graph?
|
|
285
|
+
- [ ] 4. Report omissions separately from inconsistencies
|
|
286
|
+
|
|
269
287
|
### Error Recovery
|
|
270
288
|
|
|
271
289
|
- **\`yg\` not found** \u2192 inform user: "yg CLI is not installed or not in PATH." Stop.
|
|
@@ -294,9 +312,26 @@ Key facts:
|
|
|
294
312
|
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`aspect.yaml\` + content \`.md\` files. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
295
313
|
- **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
|
|
296
314
|
|
|
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).
|
|
321
|
+
|
|
322
|
+
### Artifact Structure
|
|
323
|
+
|
|
324
|
+
Three artifacts capture node knowledge at three levels:
|
|
325
|
+
|
|
326
|
+
- **responsibility.md** (always required) \u2014 WHAT: identity, boundaries, what the node is NOT responsible for.
|
|
327
|
+
- **interface.md** (required when node has consumers) \u2014 HOW TO USE: public methods, parameters, return types, contracts, failure modes, exposed data structures. Everything another node needs to interact with this one.
|
|
328
|
+
- **internals.md** (optional, highest value for cross-module nodes) \u2014 HOW IT WORKS + WHY: algorithms, control flow, business rules, invariants, state machines, lifecycle, and design decisions with rejected alternatives. Use sections within the file: ## Logic, ## Constraints, ## State, ## Decisions (with "Chose X over Y because Z" format).
|
|
329
|
+
|
|
330
|
+
**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
|
+
|
|
297
332
|
### Context Assembly
|
|
298
333
|
|
|
299
|
-
Run \`yg build-context --node <path>\` to get the deterministic context package for a node.
|
|
334
|
+
Run \`yg build-context --node <path>\` to get the deterministic context package for a node. The package assembles global config, 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.
|
|
300
335
|
|
|
301
336
|
### Information Routing
|
|
302
337
|
|
|
@@ -307,7 +342,7 @@ When you encounter information, route it to the correct location:
|
|
|
307
342
|
- **Business process** \u2192 flow (\`flows/<name>/\` with \`flow.yaml\` + \`description.md\`). Ask user if process unclear.
|
|
308
343
|
- **Shared across a domain** \u2192 parent node artifact. Children receive it through hierarchy.
|
|
309
344
|
- **Technology stack or standard** \u2192 \`config.yaml\` under \`stack\` or \`standards\` (+ \`rationale\` field)
|
|
310
|
-
- **Decision (why + why NOT):** one node \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 \`config.yaml\` rationale field. 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.
|
|
311
346
|
|
|
312
347
|
### Creating Aspects
|
|
313
348
|
|
|
@@ -325,6 +360,18 @@ Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No
|
|
|
325
360
|
- **Architectural:** Structural patterns with rationale (e.g., dual-rollback on provider failure, idempotency via key generation, fire-and-forget dispatch)
|
|
326
361
|
- **Concurrency:** Shared concurrency strategies (e.g., pessimistic locking, retry-on-deadlock, optimistic versioning)
|
|
327
362
|
|
|
363
|
+
When a node follows an aspect's pattern with exceptions, record exceptions in \`node.yaml\` under \`aspect_exceptions\`. Example: aspect says "fire-and-forget" but this node awaits the publish call. The exception appears in the context package next to the aspect content, preventing abstractions from masking implementation details.
|
|
364
|
+
|
|
365
|
+
**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
|
+
|
|
367
|
+
**Aspect stability tiers.** If an aspect has a \`stability\` field in \`aspect.yaml\`, use it to calibrate review urgency:
|
|
368
|
+
|
|
369
|
+
- \`schema\` \u2014 enforced by data model; review only when data model changes (most stable)
|
|
370
|
+
- \`protocol\` \u2014 contractual pattern; review when contracts or interfaces change
|
|
371
|
+
- \`implementation\` \u2014 specific mechanism; review after ANY significant code change (least stable)
|
|
372
|
+
|
|
373
|
+
When code anchors (\`anchors\` field in \`node.yaml\`) are present for an aspect, 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
|
+
|
|
328
375
|
### Creating Flows
|
|
329
376
|
|
|
330
377
|
- [ ] 1. Read \`schemas/flow.yaml\`
|
|
@@ -335,13 +382,18 @@ Test: "Does this requirement apply to more than one node?" Yes \u2192 aspect. No
|
|
|
335
382
|
|
|
336
383
|
Test: "Does this describe what happens in the world, or only in the software?" If only software \u2014 rewrite.
|
|
337
384
|
|
|
385
|
+
**Warning:** Flow descriptions must describe business processes, not code sequences. "The OrderService calls PaymentGateway.charge()" is WRONG. "The system charges the customer's payment method" is CORRECT.
|
|
386
|
+
|
|
338
387
|
### Operational Rules
|
|
339
388
|
|
|
340
389
|
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
341
390
|
- **Read schemas before creating** any \`node.yaml\`, \`aspect.yaml\`, or \`flow.yaml\`.
|
|
342
391
|
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
343
392
|
- **Incremental sync.** Run \`yg drift-sync\` after every 3-5 source file changes. Do not defer to end of task.
|
|
344
|
-
- **Completeness test:**
|
|
393
|
+
- **Completeness test:** Two checks, both required:
|
|
394
|
+
1. **Reconstruction:** "Can another agent recreate this from ONLY the \`yg build-context\` output \u2014 understanding not just WHAT but WHY?" Test: rejected alternatives, correct algorithm, design arguments.
|
|
395
|
+
2. **Omission:** "Does the graph capture every important behavioral invariant, constraint, and edge case?" Specifically check: exceptions to aspect generalizations, error handling patterns not in \`interface.md\`, concurrency behaviors not in \`internals.md\`.
|
|
396
|
+
- **Value calibration.** Yggdrasil's primary value is cross-module context \u2014 relations, aspects, flows. For a single simple module, \`responsibility.md\` and \`interface.md\` provide most value. Invest depth (\`internals.md\`) where cross-module interactions demand it.
|
|
345
397
|
- **These rules are invariant.** No plan, guide, skill, or workflow may override them.
|
|
346
398
|
|
|
347
399
|
### CLI Reference
|
|
@@ -356,6 +408,7 @@ yg flows List flows with metadata (YAML output).
|
|
|
356
408
|
yg deps --node <path> [--depth N] [--type structural|event|all]
|
|
357
409
|
Show dependencies.
|
|
358
410
|
yg impact --node <path> --simulate Simulate blast radius of a planned change.
|
|
411
|
+
yg impact --node <path> --method <name> Filter impact to dependents consuming a specific method.
|
|
359
412
|
yg impact --aspect <id> Show all nodes where aspect is effective.
|
|
360
413
|
yg impact --flow <name> Show flow participants and descendants.
|
|
361
414
|
yg status Graph health: nodes, coverage, drift summary.
|
|
@@ -374,16 +427,15 @@ yg journal-archive Archive consolidated journal entries.
|
|
|
374
427
|
|
|
375
428
|
| What you have | Where it goes |
|
|
376
429
|
|---|---|
|
|
377
|
-
| Information specific to this node | Local node artifact (
|
|
430
|
+
| Information specific to this node | Local node artifact (check \`config.yaml artifacts\` for types) |
|
|
378
431
|
| Rule that applies to many nodes | Aspect (content \`.md\` files in \`aspects/<id>/\`) |
|
|
379
432
|
| Architectural invariant for a node type | Required aspect in \`config.yaml node_types\` |
|
|
380
433
|
| Business process participation | Flow (\`flow.yaml participants\`) |
|
|
381
434
|
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
382
435
|
| Context shared across a domain | Parent node artifact |
|
|
383
436
|
| Technology stack | \`config.yaml stack\` (+ \`rationale\` field) |
|
|
384
|
-
| Global coding standards | \`config.yaml standards\`
|
|
385
|
-
|
|
386
|
-
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n");
|
|
437
|
+
| Global coding standards | \`config.yaml standards\` |`;
|
|
438
|
+
var AGENT_RULES_CONTENT = [CORE_PROTOCOL, OPERATIONS, KNOWLEDGE_BASE].join("\n\n---\n\n") + "\n";
|
|
387
439
|
|
|
388
440
|
// src/templates/platform.ts
|
|
389
441
|
var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
|
|
@@ -838,15 +890,81 @@ async function parseNodeYaml(filePath) {
|
|
|
838
890
|
}
|
|
839
891
|
const relations = parseRelations(raw.relations, filePath);
|
|
840
892
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
893
|
+
const aspects = parseStringArray(raw.aspects) ?? parseStringArray(raw.tags);
|
|
894
|
+
const aspectExceptions = parseAspectExceptions(raw.aspect_exceptions, aspects, filePath);
|
|
895
|
+
const anchors = parseAnchors(raw.anchors, filePath);
|
|
841
896
|
return {
|
|
842
897
|
name: raw.name.trim(),
|
|
843
898
|
type: raw.type.trim(),
|
|
844
|
-
aspects
|
|
899
|
+
aspects,
|
|
900
|
+
aspect_exceptions: aspectExceptions,
|
|
845
901
|
blackbox: raw.blackbox ?? false,
|
|
846
902
|
relations: relations.length > 0 ? relations : void 0,
|
|
847
|
-
mapping
|
|
903
|
+
mapping,
|
|
904
|
+
anchors
|
|
848
905
|
};
|
|
849
906
|
}
|
|
907
|
+
function parseAnchors(raw, filePath) {
|
|
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) {
|
|
935
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
936
|
+
if (!Array.isArray(raw)) {
|
|
937
|
+
throw new Error(`node.yaml at ${filePath}: 'aspect_exceptions' must be an array`);
|
|
938
|
+
}
|
|
939
|
+
if (raw.length === 0) return void 0;
|
|
940
|
+
const aspectSet = new Set(aspects ?? []);
|
|
941
|
+
const result = [];
|
|
942
|
+
for (let i = 0; i < raw.length; i++) {
|
|
943
|
+
const item = raw[i];
|
|
944
|
+
if (typeof item !== "object" || item === null) {
|
|
945
|
+
throw new Error(`node.yaml at ${filePath}: aspect_exceptions[${i}] must be an object`);
|
|
946
|
+
}
|
|
947
|
+
const obj = item;
|
|
948
|
+
if (typeof obj.aspect !== "string" || obj.aspect.trim() === "") {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`node.yaml at ${filePath}: aspect_exceptions[${i}].aspect must be a non-empty string`
|
|
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`
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const aspectId = obj.aspect.trim();
|
|
959
|
+
if (!aspectSet.has(aspectId)) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
`node.yaml at ${filePath}: aspect_exceptions[${i}].aspect "${aspectId}" is not in this node's aspects list`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
result.push({ aspect: aspectId, note: obj.note.trim() });
|
|
965
|
+
}
|
|
966
|
+
return result.length > 0 ? result : void 0;
|
|
967
|
+
}
|
|
850
968
|
function parseStringArray(val) {
|
|
851
969
|
if (!Array.isArray(val)) return void 0;
|
|
852
970
|
const arr = val.filter((v) => typeof v === "string");
|
|
@@ -943,6 +1061,7 @@ async function readArtifacts(dirPath, excludeFiles = ["node.yaml"], includeFiles
|
|
|
943
1061
|
}
|
|
944
1062
|
|
|
945
1063
|
// src/io/aspect-parser.ts
|
|
1064
|
+
var VALID_STABILITY_VALUES = ["schema", "protocol", "implementation"];
|
|
946
1065
|
async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
947
1066
|
const idTrimmed = id?.trim() ?? "";
|
|
948
1067
|
if (!idTrimmed) {
|
|
@@ -965,11 +1084,21 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
965
1084
|
}
|
|
966
1085
|
implies = raw.implies.filter((t) => typeof t === "string");
|
|
967
1086
|
}
|
|
1087
|
+
let stability;
|
|
1088
|
+
if (raw.stability !== void 0) {
|
|
1089
|
+
if (typeof raw.stability !== "string" || !VALID_STABILITY_VALUES.includes(raw.stability)) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`Aspect file ${aspectYamlPath}: 'stability' must be one of: ${VALID_STABILITY_VALUES.join(", ")}`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
stability = raw.stability;
|
|
1095
|
+
}
|
|
968
1096
|
return {
|
|
969
1097
|
name: raw.name.trim(),
|
|
970
1098
|
id: idTrimmed,
|
|
971
1099
|
description,
|
|
972
1100
|
implies,
|
|
1101
|
+
stability,
|
|
973
1102
|
artifacts
|
|
974
1103
|
};
|
|
975
1104
|
}
|
|
@@ -1292,7 +1421,8 @@ async function buildContext(graph, nodePath) {
|
|
|
1292
1421
|
}
|
|
1293
1422
|
const aspectsToInclude = resolveAspects(allAspectIds, graph.aspects);
|
|
1294
1423
|
for (const aspect of aspectsToInclude) {
|
|
1295
|
-
|
|
1424
|
+
const exception = node.meta.aspect_exceptions?.find((e) => e.aspect === aspect.id);
|
|
1425
|
+
layers.push(buildAspectLayer(aspect, exception?.note));
|
|
1296
1426
|
}
|
|
1297
1427
|
const fullText = layers.map((l) => l.content).join("\n\n");
|
|
1298
1428
|
const tokenCount = estimateTokens(fullText);
|
|
@@ -1475,9 +1605,18 @@ Consumes: ${relation.consumes.join(", ")}`;
|
|
|
1475
1605
|
attrs
|
|
1476
1606
|
};
|
|
1477
1607
|
}
|
|
1478
|
-
function buildAspectLayer(aspect) {
|
|
1479
|
-
|
|
1608
|
+
function buildAspectLayer(aspect, exceptionNote) {
|
|
1609
|
+
let content = aspect.artifacts.map((a) => `### ${a.filename}
|
|
1480
1610
|
${a.content}`).join("\n\n");
|
|
1611
|
+
if (aspect.stability) {
|
|
1612
|
+
content += `
|
|
1613
|
+
**Stability tier:** ${aspect.stability}`;
|
|
1614
|
+
}
|
|
1615
|
+
if (exceptionNote) {
|
|
1616
|
+
content += `
|
|
1617
|
+
|
|
1618
|
+
\u26A0 **Exception for this node:** ${exceptionNote}`;
|
|
1619
|
+
}
|
|
1481
1620
|
return {
|
|
1482
1621
|
type: "aspects",
|
|
1483
1622
|
label: `${aspect.name} (aspect: ${aspect.id})`,
|
|
@@ -1548,7 +1687,7 @@ function collectEffectiveAspectIds(graph, nodePath) {
|
|
|
1548
1687
|
}
|
|
1549
1688
|
|
|
1550
1689
|
// src/core/validator.ts
|
|
1551
|
-
import { readdir as readdir4 } from "fs/promises";
|
|
1690
|
+
import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
|
|
1552
1691
|
import path9 from "path";
|
|
1553
1692
|
var RESERVED_DIRS = /* @__PURE__ */ new Set();
|
|
1554
1693
|
async function validate(graph, scope = "all") {
|
|
@@ -1578,6 +1717,8 @@ async function validate(graph, scope = "all") {
|
|
|
1578
1717
|
issues.push(...checkImpliedAspectsExist(graph));
|
|
1579
1718
|
issues.push(...checkImpliesNoCycles(graph));
|
|
1580
1719
|
issues.push(...checkRequiredAspectsCoverage(graph));
|
|
1720
|
+
issues.push(...checkAspectExceptions(graph));
|
|
1721
|
+
issues.push(...await checkAnchorPresence(graph));
|
|
1581
1722
|
issues.push(...checkRequiredArtifacts(graph));
|
|
1582
1723
|
issues.push(...checkInvalidArtifactConditions(graph));
|
|
1583
1724
|
issues.push(...await checkContextBudget(graph));
|
|
@@ -1824,6 +1965,24 @@ function checkRequiredAspectsCoverage(graph) {
|
|
|
1824
1965
|
}
|
|
1825
1966
|
return issues;
|
|
1826
1967
|
}
|
|
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
|
+
}
|
|
1827
1986
|
function checkNoCycles(graph) {
|
|
1828
1987
|
const WHITE = 0;
|
|
1829
1988
|
const GRAY = 1;
|
|
@@ -2202,6 +2361,81 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
2202
2361
|
}
|
|
2203
2362
|
return issues;
|
|
2204
2363
|
}
|
|
2364
|
+
async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
2365
|
+
const files = [];
|
|
2366
|
+
async function collectFiles(absPath) {
|
|
2367
|
+
try {
|
|
2368
|
+
const s = await stat3(absPath);
|
|
2369
|
+
if (s.isFile()) {
|
|
2370
|
+
files.push(absPath);
|
|
2371
|
+
} else if (s.isDirectory()) {
|
|
2372
|
+
const entries = await readdir4(absPath, { withFileTypes: true });
|
|
2373
|
+
for (const entry of entries) {
|
|
2374
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2375
|
+
const entryPath = path9.join(absPath, entry.name);
|
|
2376
|
+
if (entry.isFile()) {
|
|
2377
|
+
files.push(entryPath);
|
|
2378
|
+
} else if (entry.isDirectory()) {
|
|
2379
|
+
await collectFiles(entryPath);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
} catch {
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
for (const mp of mappingPaths) {
|
|
2387
|
+
await collectFiles(path9.join(projectRoot, mp));
|
|
2388
|
+
}
|
|
2389
|
+
return files;
|
|
2390
|
+
}
|
|
2391
|
+
async function checkAnchorPresence(graph) {
|
|
2392
|
+
const issues = [];
|
|
2393
|
+
const projectRoot = path9.dirname(graph.rootPath);
|
|
2394
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
2395
|
+
const anchors = node.meta.anchors;
|
|
2396
|
+
if (!anchors) continue;
|
|
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
|
+
}
|
|
2409
|
+
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
2410
|
+
if (mappingPaths.length === 0) continue;
|
|
2411
|
+
const sourceFiles = await expandMappingToFiles(projectRoot, mappingPaths);
|
|
2412
|
+
if (sourceFiles.length === 0) continue;
|
|
2413
|
+
const fileContents = [];
|
|
2414
|
+
for (const filePath of sourceFiles) {
|
|
2415
|
+
try {
|
|
2416
|
+
const content = await readFile11(filePath, "utf-8");
|
|
2417
|
+
fileContents.push(content);
|
|
2418
|
+
} catch {
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
for (const [aspectId, anchorList] of Object.entries(anchors)) {
|
|
2422
|
+
if (!nodeAspects.has(aspectId)) continue;
|
|
2423
|
+
for (const anchor of anchorList) {
|
|
2424
|
+
const found = fileContents.some((content) => content.includes(anchor));
|
|
2425
|
+
if (!found) {
|
|
2426
|
+
issues.push({
|
|
2427
|
+
severity: "warning",
|
|
2428
|
+
code: "W014",
|
|
2429
|
+
rule: "anchor-not-found",
|
|
2430
|
+
message: `Anchor '${anchor}' for aspect '${aspectId}' not found in mapped source files`,
|
|
2431
|
+
nodePath
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
return issues;
|
|
2438
|
+
}
|
|
2205
2439
|
async function checkContextBudget(graph) {
|
|
2206
2440
|
const issues = [];
|
|
2207
2441
|
const warningThreshold = graph.config.quality?.context_budget.warning ?? 1e4;
|
|
@@ -2418,13 +2652,13 @@ ${errors.length} errors, ${warnings.length} warnings.
|
|
|
2418
2652
|
import chalk2 from "chalk";
|
|
2419
2653
|
|
|
2420
2654
|
// src/io/drift-state-store.ts
|
|
2421
|
-
import { readFile as
|
|
2655
|
+
import { readFile as readFile12, writeFile as writeFile3 } from "fs/promises";
|
|
2422
2656
|
import path10 from "path";
|
|
2423
2657
|
import { parse as yamlParse } from "yaml";
|
|
2424
2658
|
var DRIFT_STATE_FILE = ".drift-state";
|
|
2425
2659
|
async function readDriftState(yggRoot) {
|
|
2426
2660
|
try {
|
|
2427
|
-
const content = await
|
|
2661
|
+
const content = await readFile12(path10.join(yggRoot, DRIFT_STATE_FILE), "utf-8");
|
|
2428
2662
|
let raw;
|
|
2429
2663
|
try {
|
|
2430
2664
|
raw = JSON.parse(content);
|
|
@@ -2449,20 +2683,20 @@ async function writeDriftState(yggRoot, state) {
|
|
|
2449
2683
|
}
|
|
2450
2684
|
|
|
2451
2685
|
// src/utils/hash.ts
|
|
2452
|
-
import { readFile as
|
|
2686
|
+
import { readFile as readFile13, readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
2453
2687
|
import path11 from "path";
|
|
2454
2688
|
import { createHash } from "crypto";
|
|
2455
2689
|
import { createRequire } from "module";
|
|
2456
2690
|
var require2 = createRequire(import.meta.url);
|
|
2457
2691
|
var ignoreFactory = require2("ignore");
|
|
2458
2692
|
async function hashFile(filePath) {
|
|
2459
|
-
const content = await
|
|
2693
|
+
const content = await readFile13(filePath);
|
|
2460
2694
|
return createHash("sha256").update(content).digest("hex");
|
|
2461
2695
|
}
|
|
2462
2696
|
async function loadRootGitignoreStack(projectRoot) {
|
|
2463
2697
|
if (!projectRoot) return [];
|
|
2464
2698
|
try {
|
|
2465
|
-
const content = await
|
|
2699
|
+
const content = await readFile13(path11.join(projectRoot, ".gitignore"), "utf-8");
|
|
2466
2700
|
const matcher = ignoreFactory();
|
|
2467
2701
|
matcher.add(content);
|
|
2468
2702
|
return [{ basePath: projectRoot, matcher }];
|
|
@@ -2489,7 +2723,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2489
2723
|
for (const tf of trackedFiles) {
|
|
2490
2724
|
const absPath = path11.join(projectRoot, tf.path);
|
|
2491
2725
|
try {
|
|
2492
|
-
const st = await
|
|
2726
|
+
const st = await stat4(absPath);
|
|
2493
2727
|
if (st.isDirectory()) {
|
|
2494
2728
|
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2495
2729
|
projectRoot,
|
|
@@ -2537,7 +2771,7 @@ async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, exclu
|
|
|
2537
2771
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
2538
2772
|
let stack = options.gitignoreStack ?? [];
|
|
2539
2773
|
try {
|
|
2540
|
-
const localContent = await
|
|
2774
|
+
const localContent = await readFile13(path11.join(directoryPath, ".gitignore"), "utf-8");
|
|
2541
2775
|
const localMatcher = ignoreFactory();
|
|
2542
2776
|
localMatcher.add(localContent);
|
|
2543
2777
|
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
@@ -2558,7 +2792,7 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
2558
2792
|
gitignoreStack: stack
|
|
2559
2793
|
}))),
|
|
2560
2794
|
Promise.all(files.map(async (f) => {
|
|
2561
|
-
const fileStat = await
|
|
2795
|
+
const fileStat = await stat4(f);
|
|
2562
2796
|
return {
|
|
2563
2797
|
relPath: path11.relative(rootDirectoryPath, f),
|
|
2564
2798
|
absPath: f,
|
|
@@ -3558,7 +3792,7 @@ Total scope: ${sorted.length} nodes
|
|
|
3558
3792
|
}
|
|
3559
3793
|
}
|
|
3560
3794
|
function registerImpactCommand(program2) {
|
|
3561
|
-
program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
|
|
3795
|
+
program2.command("impact").description("Show reverse dependency impact for a node, aspect, or flow").option("--node <path>", "Node path relative to .yggdrasil/model/").option("--aspect <id>", "Aspect id (directory path under aspects/)").option("--flow <name>", "Flow name (directory name under flows/)").option("--method <name>", "Filter impact to dependents consuming a specific method (requires --node)").option("--simulate", "Simulate context package impact (compare HEAD vs current)").action(
|
|
3562
3796
|
async (options) => {
|
|
3563
3797
|
try {
|
|
3564
3798
|
const modeCount = [options.node, options.aspect, options.flow].filter(Boolean).length;
|
|
@@ -3589,11 +3823,56 @@ function registerImpactCommand(program2) {
|
|
|
3589
3823
|
`);
|
|
3590
3824
|
process.exit(1);
|
|
3591
3825
|
}
|
|
3826
|
+
if (options.method && !options.node) {
|
|
3827
|
+
process.stderr.write("Error: --method requires --node\n");
|
|
3828
|
+
process.exit(1);
|
|
3829
|
+
}
|
|
3592
3830
|
const { direct, allDependents, reverse, relationFrom } = collectReverseDependents(
|
|
3593
3831
|
graph,
|
|
3594
3832
|
nodePath
|
|
3595
3833
|
);
|
|
3596
|
-
const
|
|
3834
|
+
const methodFilter = options.method?.trim();
|
|
3835
|
+
let filteredDirect = direct;
|
|
3836
|
+
let filteredAllDependents = allDependents;
|
|
3837
|
+
if (methodFilter) {
|
|
3838
|
+
filteredDirect = direct.filter((dep) => {
|
|
3839
|
+
const rel = relationFrom.get(`${dep}->${nodePath}`);
|
|
3840
|
+
return rel?.consumes?.includes(methodFilter) || !rel?.consumes?.length;
|
|
3841
|
+
});
|
|
3842
|
+
const filteredSet = new Set(filteredDirect);
|
|
3843
|
+
filteredAllDependents = allDependents.filter((dep) => filteredSet.has(dep));
|
|
3844
|
+
}
|
|
3845
|
+
const chains = buildTransitiveChains(nodePath, filteredDirect, filteredAllDependents, reverse);
|
|
3846
|
+
const eventDependents = [];
|
|
3847
|
+
for (const [np, n] of graph.nodes) {
|
|
3848
|
+
for (const rel of n.meta.relations ?? []) {
|
|
3849
|
+
if (rel.target === nodePath && (rel.type === "emits" || rel.type === "listens")) {
|
|
3850
|
+
eventDependents.push({
|
|
3851
|
+
path: np,
|
|
3852
|
+
type: rel.type,
|
|
3853
|
+
eventName: rel.event_name ?? n.meta.name
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
const targetNode = graph.nodes.get(nodePath);
|
|
3859
|
+
for (const rel of targetNode.meta.relations ?? []) {
|
|
3860
|
+
if (rel.type === "emits") {
|
|
3861
|
+
const eventName = rel.event_name ?? rel.target;
|
|
3862
|
+
for (const [np, n] of graph.nodes) {
|
|
3863
|
+
if (np === nodePath) continue;
|
|
3864
|
+
for (const r of n.meta.relations ?? []) {
|
|
3865
|
+
if (r.type === "listens" && r.target === rel.target) {
|
|
3866
|
+
eventDependents.push({
|
|
3867
|
+
path: np,
|
|
3868
|
+
type: "listens",
|
|
3869
|
+
eventName: r.event_name ?? eventName
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3597
3876
|
const flows = [];
|
|
3598
3877
|
for (const flow of graph.flows) {
|
|
3599
3878
|
if (flow.nodes.includes(nodePath)) {
|
|
@@ -3607,17 +3886,25 @@ function registerImpactCommand(program2) {
|
|
|
3607
3886
|
aspectsInScope.push(aspect.name);
|
|
3608
3887
|
}
|
|
3609
3888
|
}
|
|
3610
|
-
|
|
3889
|
+
const methodLabel = methodFilter ? ` (method: ${methodFilter})` : "";
|
|
3890
|
+
process.stdout.write(`Impact of changes in ${nodePath}${methodLabel}:
|
|
3611
3891
|
|
|
3612
3892
|
`);
|
|
3613
3893
|
process.stdout.write("Directly dependent:\n");
|
|
3614
|
-
if (
|
|
3894
|
+
if (filteredDirect.length === 0) {
|
|
3615
3895
|
process.stdout.write(" (none)\n");
|
|
3616
3896
|
} else {
|
|
3617
|
-
for (const dep of
|
|
3897
|
+
for (const dep of filteredDirect) {
|
|
3618
3898
|
const rel = relationFrom.get(`${dep}->${nodePath}`);
|
|
3619
|
-
const annot = rel?.consumes?.length ? ` (${rel.type},
|
|
3899
|
+
const annot = rel?.consumes?.length ? ` (${rel.type}, consumes: ${rel.consumes.join(", ")})` : rel ? ` (${rel.type})` : "";
|
|
3620
3900
|
process.stdout.write(` <- ${dep}${annot}
|
|
3901
|
+
`);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
if (eventDependents.length > 0 && !methodFilter) {
|
|
3905
|
+
process.stdout.write("\nEvent-connected:\n");
|
|
3906
|
+
for (const { path: p, type, eventName } of eventDependents.sort((a, b) => a.path.localeCompare(b.path))) {
|
|
3907
|
+
process.stdout.write(` ${p} (${type}: ${eventName})
|
|
3621
3908
|
`);
|
|
3622
3909
|
}
|
|
3623
3910
|
}
|
|
@@ -3667,7 +3954,7 @@ Flows: ${flows.length > 0 ? flows.join(", ") : "(none)"}
|
|
|
3667
3954
|
`);
|
|
3668
3955
|
}
|
|
3669
3956
|
}
|
|
3670
|
-
const allAffected = /* @__PURE__ */ new Set([...
|
|
3957
|
+
const allAffected = /* @__PURE__ */ new Set([...filteredAllDependents, ...descendants, ...eventDependents.map((e) => e.path)]);
|
|
3671
3958
|
process.stdout.write(
|
|
3672
3959
|
`
|
|
3673
3960
|
Total scope: ${allAffected.size} nodes, ${flows.length} flows, ${aspectsInScope.length} aspects
|
|
@@ -3696,6 +3983,7 @@ function registerAspectsCommand(program2) {
|
|
|
3696
3983
|
const entry = { id: aspect.id, name: aspect.name };
|
|
3697
3984
|
if (aspect.description) entry.description = aspect.description;
|
|
3698
3985
|
if (aspect.implies && aspect.implies.length > 0) entry.implies = aspect.implies;
|
|
3986
|
+
if (aspect.stability) entry.stability = aspect.stability;
|
|
3699
3987
|
return entry;
|
|
3700
3988
|
});
|
|
3701
3989
|
process.stdout.write(yamlStringify(output));
|
|
@@ -3749,7 +4037,7 @@ function registerFlowsCommand(program2) {
|
|
|
3749
4037
|
}
|
|
3750
4038
|
|
|
3751
4039
|
// src/io/journal-store.ts
|
|
3752
|
-
import { readFile as
|
|
4040
|
+
import { readFile as readFile14, writeFile as writeFile4, mkdir as mkdir3, rename, access as access3 } from "fs/promises";
|
|
3753
4041
|
import { parse as parseYaml6, stringify as stringifyYaml } from "yaml";
|
|
3754
4042
|
import path17 from "path";
|
|
3755
4043
|
var JOURNAL_FILE = ".journal.yaml";
|
|
@@ -3757,7 +4045,7 @@ var ARCHIVE_DIR = "journals-archive";
|
|
|
3757
4045
|
async function readJournal(yggRoot) {
|
|
3758
4046
|
const filePath = path17.join(yggRoot, JOURNAL_FILE);
|
|
3759
4047
|
try {
|
|
3760
|
-
const content = await
|
|
4048
|
+
const content = await readFile14(filePath, "utf-8");
|
|
3761
4049
|
const raw = parseYaml6(content);
|
|
3762
4050
|
const entries = raw.entries ?? [];
|
|
3763
4051
|
return Array.isArray(entries) ? entries : [];
|